Skip to content

[feat] One launcher for every AI call, from the tool descriptor #1389

Closed

Description

@SaulMoro

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

  • ToolDescriptor has a required launch facet, a launch adapter id or 'none'; a tool without it fails tsc. The adapters live in src/tool-launch/, one per family (claude, codex, opencode, codebuddy, workbuddy, openclaw).
  • Each adapter builds a subset of the profiles call, step and judge, and every adapter builds call. LAUNCH_TOOLS is derived from the tools whose adapter builds step, and the judge list (JUDGE_TOOLS) from those whose adapter builds judge. Today that is the claude, codex and opencode families for step, plus codebuddy for judge, exactly as now. call coverage is at least today's ai-client tool list, so no caller loses a tool.
  • Migrated callers use call, except a caller whose prompt has the tool read its working directory, which uses step.
  • The facet and the adapters replace the per-tool launch tables: LAUNCH_TOOLS and localToolFamily (src/tool-launch/tools.ts:4, :12), and the judge's TOOL_ENV, HOME_ENV, VERIFIED and ACCESS (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-mr comment and write modes, --output artifacts, import --from-mr queueing, recall promote listing, --category, --dry-run, writes and the learning mark.
  • recall promote and teamai dashboard keep today's fallback when no AI tool is installed: the original content and the keyword category, with the same message.
  • ci extract-mr runs in a bare CI checkout with no teamai init, as today, through every AI call it reaches: import-mr, and deep-enrich and enrich-with-ai through importLocalCheckout (src/ci/extract-mr.ts:189-190). The launcher needs no LocalConfig for them.
  • An AI call keeps TEAMAI_CONTRIBUTE_HINT_DISABLED=1 in 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_ENV and TOOL_ENV today, src/scorers/judge.ts:96-98): TEAMAI_LAUNCHED_BY with one value per caller (codebase, import, recall-promote, …), and the caller's TEAMAI_MODE. Reuse feat(workflow): add session discovery and managed public leases #1304's resolveRunMode where 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 sessions codebase, import, recall promote and 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 an ai-client launch sets none (src/utils/ai-client.ts:131-132). scorer stays the judges' value, which hook dispatch skips at once (src/hook-dispatch-cli.ts:422); no other caller uses it.
  • src/utils/ai-client.ts is deleted, and nothing imports it. The cases of src/__tests__/ai-client.test.ts and src/__tests__/ai-client-openclaw.test.ts move to the adapter tests. The comments in src/utils/cli-path.ts and src/providers/cnb/cnb-cli.ts that 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 lint passes after each.

Definition of done

  • docs/dev/ notes where AI launches live (src/tool-launch/, the launch facet, 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 for codebase, import, recall promote, ci extract-mr and 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.md is regenerated with npx vitest run commands-reference -u.
  • Real-CLI record: teamai recall promote <id> --dry-run and teamai ci extract-mr --mode comment in a bare checkout, each showing the tool it launched and the TEAMAI_LAUNCHED_BY value in the launched tool's environment (for example from a SessionStart hook that logs it, or from its dashboard event's launchedBy once [feat] Run mode: tell attended runs from unattended ones #1384 lands), plus one command run with only codebuddy or openclaw on PATH.

Out of scope

  • Default workflows for MR learnings or learning promotion. ci extract-mr must keep running in a bare CI checkout without teamai init, and promotion from a workflow command step hits read_only_dirty in 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 of ai-client only, 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 covers launch.
  • Today's launch code: LAUNCH_TOOLS and localToolFamily (src/tool-launch/tools.ts:4, :12), step arguments (invocation in src/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 order src/utils/ai-client.ts:7-9, arguments buildCliArgs (:74-83), which pass -p <prompt> and return plain text, the hint-disabled environment (:131-132).
  • Importers: six static imports, and three await import callers a plain grep for from '…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.js from a sibling in src/utils/ with the config above.
  • Promotion: inferCategory runs once per candidate in findPromotionCandidates (src/maintenance/promote.ts:74, :224-251), which buildVizData (src/viz.ts:430, call at :463) also calls for teamai 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-253 reuses importFromMR, whose only AI call is src/import-mr.ts:327.
  • A team workflow's command step that runs teamai ci extract-mr --mode comment changes no file, so it is not read_only_dirty. scorers is 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_BY set (src/session-flow/captures.ts:60), so a launcher call's session writes none.
  • The launch facet sits next to the sessions facet [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 its sessions list is the one its launched sessions write, so the local driver can read a step's session from the descriptor too.

Activity

  1. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    Added a section at the end of the body: a lint rule (no-restricted-imports in .oxlintrc.json) that fails on any new importer of src/utils/ai-client.ts. The 9 files that import it today are an explicit exception list, and this issue removes src/import-mr.ts and src/maintenance/promote.ts from it. Tests keep their exception, merged with the existing src/__tests__/** override.

  2. changed the title [-][feat] Default workflows for MR learnings, learning promotion and benchmark tasks[/-] [+][feat] One launcher for every AI call, from the tool descriptor[/+] on Oct 25, 2026
  3. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor

    Retitled and rewrote the body. This is now the launcher issue: a launch facet on ToolDescriptor (src/tools.ts:191) that replaces LAUNCH_TOOLS, localToolFamily and the judge's per-tool tables, and all 9 ai-client importers migrated before src/utils/ai-client.ts is deleted.
    Every launched process carries TEAMAI_LAUNCHED_BY, so the sessions codebase, import and recall promote start stop counting as the member's. ai-client sets none today (src/utils/ai-client.ts:131-132).
    The mr-learnings and promote-learnings default workflows are dropped: ci extract-mr must run in a bare CI checkout, which covers the import-mr and deep-enrich calls it reaches. session-to-task moves 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.

  4. SaulMoro commented on Oct 25, 2026

    @SaulMoro
    CollaboratorAuthor
    • Adapters now build three profiles: call (a prompt in, text and tokens out, as ai-client does today), step and judge. LAUNCH_TOOLS and the judge list come only from the adapters that build step and judge, so no tool becomes a new step or judge choice.
    • launch is 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 and TEAMAI_MODE explicitly.
    • There is no teamai viz: the promotion AI call is reached from teamai dashboard through buildVizData (src/viz.ts:463). The promote example keeps the learning's full file name, as executePromotion does.
  5. SaulMoro commented on Oct 26, 2026

    @SaulMoro
    CollaboratorAuthor

    Updated the references to #1304, which is now merged: what it brought is on main.

  6. 147 remaining items

  7. added a commit that references this issue on Oct 27, 2026
    edebc6c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions