Repository navigation
[feat] One launcher for every AI call, from the tool descriptor #1389
Copy link
Copy link
Closed
Labels
enhancementNew feature or requestNew feature or request
Description
Example
Today teamai launches AI tools in two unrelated ways. Workflow steps and judges go through src/tool-launch/ for claude, codex and opencode (LAUNCH_TOOLS), with token reports. Nine other modules call src/utils/ai-client.ts, which picks the first CLI on PATH out of nine, builds its own arguments and returns bare stdout. After this issue, every AI call goes through one launcher, chosen from the tool's descriptor:
// src/tools.ts
export type ToolDescriptor = {
…
/** The adapter teamai launches this tool headless with, or 'none' when teamai never launches it. Required, as #1221's other facets are. */
readonly launch: LaunchAdapterId | 'none';
};
export const TOOLS = {
claude: { …, launch: 'claude' },
'claude-internal': { …, launch: 'claude' },
codex: { …, launch: 'codex' },
opencode: { …, launch: 'opencode' },
codebuddy: { …, launch: 'codebuddy' },
workbuddy: { …, launch: 'workbuddy' },
openclaw: { …, launch: 'openclaw' },
cursor: { …, launch: 'none' }, // sessions are read, the tool is never launched
…
} satisfies Record<ToolId, ToolDescriptor>;Each adapter builds some of three profiles. Every adapter builds call:
| Profile | In | Out | Used by |
|---|---|---|---|
call |
a prompt | text and tokens, as ai-client returns text today |
the migrated callers |
step |
a workflow step's prompt, working directory and tool settings | { output, model, session, tokens } |
workflow steps; a migrated caller whose prompt has the tool read its working directory |
judge |
a judge prompt in a confined profile | the answer and tokens | scorers |
src/tool-launch/
claude.ts call, step, judge stdout → { output, model, session, tokens }
codex.ts call, step, judge codex exec …
opencode.ts call, step, judge
codebuddy.ts call, judge
workbuddy.ts call
openclaw.ts call openclaw agent --session-id <fresh id> --message <prompt>
tools.ts LAUNCH_TOOLS = tools whose adapter builds step; the judge list = tools whose adapter builds judge
Commands keep their flags, output and delivery:
$ teamai recall promote pod-startup-timeouts-2026-10-24-k3x9q2 --category skills
✔ Promoted: pod-startup-timeouts-2026-10-24-k3x9q2 -> skills/pod-startup-timeouts-2026-10-24-k3x9q2/SKILL.md
$ teamai ci extract-mr --url https://github.com/acme/app/pull/42 --mode comment # in a bare CI checkout, no teamai init
…While the migration spans PRs, a new importer of ai-client fails the lint:
$ npm run lint # after adding `import { callClaude } from './utils/ai-client.js'` to a new file
src/new.ts:1:1: error eslint(no-restricted-imports): './utils/ai-client.js' import is restricted from being used by a pattern. help: Launch AI tools through src/tool-launch, not ai-client (see #1389).// .oxlintrc.json, added by the first PR and removed by the last
"rules": {
"no-restricted-imports": ["error", { "patterns": [{
"group": ["**/utils/ai-client*", "./ai-client*"],
"message": "Launch AI tools through src/tool-launch, not ai-client (see #1389)."
}]}]
},
"overrides": [
// today's importers; each leaves the list in the PR that migrates it
{ "files": ["src/codebase.ts", "src/deep-enrich.ts", "src/enrich-with-ai.ts", "src/import-local.ts",
"src/import-mr.ts", "src/iwiki-dual.ts", "src/maintenance/promote.ts",
"src/maintenance/quality-update.ts", "src/votes-judge.ts"],
"rules": { "no-restricted-imports": "off" } },
// merged into the existing src/__tests__/** override, not a second one
{ "files": ["src/__tests__/**"], "rules": { "no-restricted-imports": "off", "typescript/no-misused-promises": [ … as today … ] } }
]Behavior
Caller of ai-client today |
Reached from | After |
|---|---|---|
src/codebase.ts |
teamai import (repo), through src/import-repo.ts |
launcher |
src/deep-enrich.ts |
teamai codebase, teamai import (codebase), teamai ci extract-mr (through src/import-codebase.ts, src/ci/extract-mr.ts:189-190), the hidden deep-enrich command (src/index.ts:1486) |
launcher |
src/enrich-with-ai.ts |
codebase extraction (src/codebase-extract.ts:969) |
launcher |
src/import-local.ts |
teamai import (local, iwiki) |
launcher |
src/iwiki-dual.ts |
teamai import (iwiki) |
launcher |
src/import-mr.ts |
teamai import --from-mr, teamai ci extract-mr |
launcher; still runs in a bare checkout without teamai init |
src/maintenance/promote.ts (generatePromotedContent and inferCategory) |
teamai recall promote; teamai dashboard and its report generation, through buildVizData in src/viz.ts, which calls findPromotionCandidates |
launcher; both AI calls move |
src/maintenance/quality-update.ts |
teamai recall maintenance --update-quality |
launcher |
src/votes-judge.ts |
the Stop hook, with TEAMAI_UPVOTE_JUDGE=1 |
launcher |
| Question | Today | After |
|---|---|---|
| Which tool runs a call | first on PATH: claude, claude-internal, tclaude, codex, codex-internal, tcodex, codebuddy, workbuddy, openclaw | the same tools, in the same order, each through its adapter's call profile |
| Which tools a workflow step or a judge can run | LAUNCH_TOOLS (claude, codex and opencode families), judges add codebuddy |
unchanged: LAUNCH_TOOLS is the tools whose adapter builds step, the judge list those that build judge. No tool is added to either |
| Tokens of an AI call outside workflows | not read | read from stdout by the adapter and returned to the caller. Not published by this issue |
| The sessions those calls start | count as the member's sessions | carry TEAMAI_LAUNCHED_BY, so they leave the member's totals once #1384 reads launchedBy, and they write no capture |
src/utils/ai-client.ts |
9 importers | deleted, with its lint rule |
Acceptance criteria
ToolDescriptorhas a requiredlaunchfacet, a launch adapter id or'none'; a tool without it failstsc. The adapters live insrc/tool-launch/, one per family (claude, codex, opencode, codebuddy, workbuddy, openclaw).- Each adapter builds a subset of the profiles
call,stepandjudge, and every adapter buildscall.LAUNCH_TOOLSis derived from the tools whose adapter buildsstep, and the judge list (JUDGE_TOOLS) from those whose adapter buildsjudge. Today that is the claude, codex and opencode families forstep, plus codebuddy forjudge, exactly as now.callcoverage is at least today'sai-clienttool list, so no caller loses a tool. - Migrated callers use
call, except a caller whose prompt has the tool read its working directory, which usesstep. - The facet and the adapters replace the per-tool launch tables:
LAUNCH_TOOLSandlocalToolFamily(src/tool-launch/tools.ts:4,:12), and the judge'sTOOL_ENV,HOME_ENV,VERIFIEDandACCESS(src/scorers/judge.ts:21-38). Each becomes data on the adapter, or is derived from the descriptors. - All 9 importers above call the launcher. Every command keeps its flags, output and delivery:
ci extract-mrcomment and write modes,--outputartifacts,import --from-mrqueueing,recall promotelisting,--category,--dry-run, writes and the learning mark. recall promoteandteamai dashboardkeep today's fallback when no AI tool is installed: the original content and the keyword category, with the same message.ci extract-mrruns in a bare CI checkout with noteamai init, as today, through every AI call it reaches:import-mr, anddeep-enrichandenrich-with-aithroughimportLocalCheckout(src/ci/extract-mr.ts:189-190). The launcher needs noLocalConfigfor them.- An AI call keeps
TEAMAI_CONTRIBUTE_HINT_DISABLED=1in the launched tool's environment, so its hooks print no share hint into the output it returns. - Every process the launcher starts carries the launch variables, set explicitly in every profile, also where a profile filters the rest of the environment (the judge builds its own from
COMMON_ENVandTOOL_ENVtoday,src/scorers/judge.ts:96-98):TEAMAI_LAUNCHED_BYwith one value per caller (codebase,import,recall-promote, …), and the caller'sTEAMAI_MODE. Reuse feat(workflow): add session discovery and managed public leases #1304'sresolveRunModewhere a resolved mode must be exported; [feat] Run mode: tell attended runs from unattended ones #1384 adds the attribution and recording that read these variables. So the sessionscodebase,import,recall promoteand the other callers start stop counting as the member's own sessions ([feat] Run mode: tell attended runs from unattended ones #1384), and write no capture. Today anai-clientlaunch sets none (src/utils/ai-client.ts:131-132).scorerstays the judges' value, which hook dispatch skips at once (src/hook-dispatch-cli.ts:422); no other caller uses it. src/utils/ai-client.tsis deleted, and nothing imports it. The cases ofsrc/__tests__/ai-client.test.tsandsrc/__tests__/ai-client-openclaw.test.tsmove to the adapter tests. The comments insrc/utils/cli-path.tsandsrc/providers/cnb/cnb-cli.tsthat name it are updated.- If the migration spans several PRs, the first adds the lint rule above, and its override list holds exactly the files oxlint reports when the rule runs with no file overrides (static and dynamic
import()both count). The last PR deletes the file and the rule.npm run lintpasses after each.
Definition of done
docs/dev/notes where AI launches live (src/tool-launch/, thelaunchfacet, the three profiles) for maintainers. User guides change only where a command's output changes, and it should not.CHANGELOG.md, under## [Unreleased]›### Changed: sessions teamai starts forcodebase,import,recall promote,ci extract-mrand the other AI calls are marked as launched by teamai, so they no longer count as the member's sessions once usage reads that mark.- Help text is unchanged unless a command's tool choice becomes visible; if it does,
skill-data/core/references/commands.mdis regenerated withnpx vitest run commands-reference -u. - Real-CLI record:
teamai recall promote <id> --dry-runandteamai ci extract-mr --mode commentin a bare checkout, each showing the tool it launched and theTEAMAI_LAUNCHED_BYvalue in the launched tool's environment (for example from aSessionStarthook that logs it, or from its dashboard event'slaunchedByonce [feat] Run mode: tell attended runs from unattended ones #1384 lands), plus one command run with onlycodebuddyoropenclawon PATH.
Out of scope
- Default workflows for MR learnings or learning promotion.
ci extract-mrmust keep running in a bare CI checkout withoutteamai init, and promotion from a workflow command step hitsread_only_dirtyin self mode. A team that wants scorers or approval around MR extraction wraps it in its own workflow (below). session-to-task([feat] Default workflows a team can override, starting with session-close #1388).- Removing
votes-judge([Proposal] Judge recall adoption with a session scorer #1391). - New step or judge tools. A tool joins those lists only when its adapter builds that profile, and this issue adds no such profile.
- Publishing the tokens of AI calls outside workflows.
- A guard against new direct tool spawns outside
src/tool-launch/. The lint rule covers imports ofai-clientonly, and it goes with the file.
A team-authored workflow that wraps MR extraction, for the guide. It parses with parseWorkflow as written:
schemaVersion: v1alpha1
name: mr-learnings
description: Extract learnings from a merged PR after a person approves it.
inputs: { url: string }
steps:
- id: extract
approve: Extract learnings from {{ inputs.url }} and post them as a comment?
command: teamai ci extract-mr --url "$URL" --mode comment
env: { URL: "{{ inputs.url }}" }Notes
- Descriptor:
ToolDescriptor(src/tools.ts:191),TOOLS(:228). This follows [feat] One descriptor per tool, and one resolver for where teamai writes and where the tool reads #1221's descriptor and adapter pattern, where every facet is required; [feat] One descriptor per tool, and one resolver for where teamai writes and where the tool reads #1221's tool-list test then coverslaunch. - Today's launch code:
LAUNCH_TOOLSandlocalToolFamily(src/tool-launch/tools.ts:4,:12), step arguments (invocationinsrc/workflow/drivers/local/local-tool-adapter.ts), judge arguments and answer parsing (src/scorers/judge.ts:36,:195,:251), stdout reports (toolStdout,src/tool-launch/stdout.ts:132). ai-client: candidates and ordersrc/utils/ai-client.ts:7-9, argumentsbuildCliArgs(:74-83), which pass-p <prompt>and return plain text, the hint-disabled environment (:131-132).- Importers: six static imports, and three
await importcallers a plain grep forfrom '…ai-client'misses:src/maintenance/promote.ts:130,:226,src/maintenance/quality-update.ts:180,src/votes-judge.ts:191. oxlint 1.85.0 flags static imports,import()and./ai-client.jsfrom a sibling insrc/utils/with the config above. - Promotion:
inferCategoryruns once per candidate infindPromotionCandidates(src/maintenance/promote.ts:74,:224-251), whichbuildVizData(src/viz.ts:430, call at:463) also calls forteamai dashboard. Drafting:generatePromotedContent(:104-147). The skill directory keeps the learning's full file name (executePromotion,src/maintenance/promote.ts:161-163). Writes and the learning mark:src/index.ts:1651-1657. - MR extraction in CI:
src/ci/extract-mr.ts:248-253reusesimportFromMR, whose only AI call issrc/import-mr.ts:327. - A team workflow's command step that runs
teamai ci extract-mr --mode commentchanges no file, so it is notread_only_dirty.scorersis allowed only on a prompt, prompt_file or workflow step (parseBody,src/workflow/parse.ts), so a team that wants a judge on the extraction adds a prompt step for it. - Capture skips a session with
TEAMAI_LAUNCHED_BYset (src/session-flow/captures.ts:60), so a launcher call's session writes none. - The
launchfacet sits next to thesessionsfacet [feat] One descriptor per tool, and one resolver for where teamai writes and where the tool reads #1221 plans (plan item F). For a tool with a launch adapter, the first platform in itssessionslist is the one its launched sessions write, so the local driver can read a step's session from the descriptor too.
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request
Activity
SaulMoro commentedon Oct 25, 2026
Added a section at the end of the body: a lint rule (
no-restricted-importsin.oxlintrc.json) that fails on any new importer ofsrc/utils/ai-client.ts. The 9 files that import it today are an explicit exception list, and this issue removessrc/import-mr.tsandsrc/maintenance/promote.tsfrom it. Tests keep their exception, merged with the existingsrc/__tests__/**override.[-][feat] Default workflows for MR learnings, learning promotion and benchmark tasks[/-][+][feat] One launcher for every AI call, from the tool descriptor[/+]SaulMoro commentedon Oct 25, 2026
Retitled and rewrote the body. This is now the launcher issue: a
launchfacet onToolDescriptor(src/tools.ts:191) that replacesLAUNCH_TOOLS,localToolFamilyand the judge's per-tool tables, and all 9ai-clientimporters migrated beforesrc/utils/ai-client.tsis deleted.Every launched process carries
TEAMAI_LAUNCHED_BY, so the sessionscodebase,importandrecall promotestart stop counting as the member's.ai-clientsets none today (src/utils/ai-client.ts:131-132).The
mr-learningsandpromote-learningsdefault workflows are dropped:ci extract-mrmust run in a bare CI checkout, which covers theimport-mranddeep-enrichcalls it reaches.session-to-taskmoves to #1388.The lint guardrail stays while the migration spans PRs; oxlint 1.85.0 flags static, dynamic and sibling imports with that config. This issue no longer depends on #1388 or #1180.
SaulMoro commentedon Oct 25, 2026
call(a prompt in, text and tokens out, asai-clientdoes today),stepandjudge.LAUNCH_TOOLSand the judge list come only from the adapters that buildstepandjudge, so no tool becomes a new step or judge choice.launchis required,'none'when teamai never launches the tool, as [feat] One descriptor per tool, and one resolver for where teamai writes and where the tool reads #1221's other facets are. Every profile sets the launch variables andTEAMAI_MODEexplicitly.teamai viz: the promotion AI call is reached fromteamai dashboardthroughbuildVizData(src/viz.ts:463). The promote example keeps the learning's full file name, asexecutePromotiondoes.SaulMoro commentedon Oct 26, 2026
Updated the references to #1304, which is now merged: what it brought is on
main.feat(tool-launch): launch AI calls from the tool descriptor, starting…
feat(tool-launch): call Codex, OpenCode, CodeBuddy and OpenClaw from …
refactor: run teamai's own AI calls through callTool (Tencent#1389)
refactor(tool-launch): derive the step and judge tools from the adapt…
147 remaining items
docs: note where tool launches live, the tool_unsupported step failur…
fix(import): fail an import when the member names an AI tool teamai c…
test(workflow): let the skill driver read LAUNCH_ORDER (Tencent#1389)
test(e2e): fake the AI tools with the output teamai now reads (Tencen…
fix(tool-launch): keep AI calls in teamai's process group, so Ctrl-C …
fix(workflow): read a member's ai.tool from ~/.teamai/config.yaml in …
fix(tool-launch): pick the AI tool again after a failed pick (Tencent…
fix(scorers): fail a pi judge with the error pi reports (Tencent#1389)
refactor(workflow): read the member's ai.tool in the workflow command…
feat(tool-launch): read ai.tool per project, before the machine's, th…
fix(init): keep a member's ai.tool when init writes the config (Tence…
fix(config): load no user scope from a machine file that holds only ai (
feat(tool-launch): one launcher for every AI call, from the tool desc…