Skip to content

feat(scorers): judge workflow steps with team scorers - #1297

Open
SaulMoro wants to merge 12 commits into
saulmoro/1084-publish-resumefrom
saulmoro/1085-workflow-scorers
Open

SaulMoro wants to merge 12 commits into
saulmoro/1084-publish-resumefrom
saulmoro/1085-workflow-scorers

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1085. Stacked on #1280.

A team keeps a rubric in scorers/<name>.md, and a workflow step lists it. After the run settles, any driver can take the judge work with teamai score --next, or a member can judge it locally. The label goes to its own store on teamai-reports. The judge's reasoning stays on the machine that judged.

What a team member sees

A scorer in the team repo:

<!-- scorers/review-quality.md -->
---
labels:
  - { value: thorough, score: 1, description: Finds the real defects and nothing else. }
  - { value: shallow, score: 0, description: Misses a real defect. }
passingScore: 1
samplingRate: 25
---
Judge the review against the diff. Answer with one label.

A step that lists it:

# workflows/review.yaml
steps:
  - id: review
    prompt: Review the change.
    scorers: [review-quality]     # or { names: [review-quality, tests], batch: true }

The run settles first. Judge work is taken separately:

$ teamai workflow start review
{"data":{"run":"r0688b17…","name":"review","status":"running","revision":1,"record":"published"}}
… workflow next / workflow report until done …
{"data":{"done":{"run":"r0688b17…","outcome":"succeeded","record":"published"},"cancelled":[]}}

$ teamai score --next --json '{"driver":{"kind":"external-json","machine":"m1","workspace":"/work/app","can":{"judge":{"isolation":"verified","tools":["claude"]}}},"running":[]}'
{"data":{"lease":{"ref":"s451c25…/aaf022a62","scorer":"review-quality",
  "rubric":{"sourceHash":"8a3a0bfd…","teamRevision":"a1df6014…"},
  "subject":{"runId":"r0688b17…","step":"review","pass":1},
  "judge":{"tool":"claude","by":"tool_default","isolation":"verified","access":"full"},
  "evidence":"~/.teamai/scores/evidence/aaf022a62","prompt":"~/.teamai/scores/evidence/aaf022a62/prompt.md",
  "answer":{"schema":{… "label":{"enum":["thorough","shallow"]} …}},
  "submit":"teamai score --lease s451c25…/aaf022a62 --submit --stdin"}}}

$ teamai score --lease s451c25…/aaf022a62 --submit --json '{"driver":{…},"outcome":"success","answer":{"label":"thorough","reasoning":"run() retries three times."}}'
{"data":{"label":{"scorer":"review-quality","subject":{"runId":"r0688b17…","step":"review","pass":1},
  "label":"thorough","score":1,"passed":true,"labelId":"24c01357…",
  "reasoning":"~/.teamai/scores/reasoning/c2181bea….md"},"published":true}}

teamai score --lease <ref> without --submit runs the judge on this machine in a confined profile instead. teamai score --run <id> --step review --scorer review-quality [--queue] judges a pass on demand, whatever sampling says.

workflow next  ──► step work only, never judge work ──► run settles, end record published
score --next   ──► judge lease ──► evidence dir ──► judge (local --lease, or the driver's own) ──► --submit
                                                                                                    │
           <dataHome>/scores/labels.jsonl + reasoning/   ◄──────────────────────────────────────────┤
           scores/<member>/<YYYY-MM>.jsonl on teamai-reports (labels only)   ◄──────────────────────┘

Where things live:

Path What
<dataHome>/scores/labels.jsonl Every label on this machine.
<dataHome>/scores/reasoning/<labelVersion>.md The judge's reasoning, one file per verdict. Never published.
<dataHome>/scores/pending/ Labels waiting for a push.
scores/<member>/<YYYY-MM>.jsonl on teamai-reports Published labels. The newest scoredAt wins per scorer, rubric, subject and judge kind.

Labels never enter the run or its record. Workflow Runs shows {scorer, label, passed} per pass and a scoring total kept apart from the run's own tokens.

What else is in this change

  • The evidence directory holds prompt.md, diff.patch, repo/ (tracked files at the judged commit), and transcripts/ with subagents. Every file is redacted, including values of the team's declared secrets in env/secrets.yaml. Rubric, request and raw judge files that no attempt reads any more are swept with the evidence.

  • A label the scorer does not list is refused, and the lease stays open. workflow report refuses a judge lease.

  • Isolation: the team's scoring.isolation, a scorer's isolation, and the member's scoring.isolation in the local config can each ask for verified. The strictest wins. teamai doctor prints the level, what sets it, and each judge tool on this machine with whether it runs verified. The local judge finds each tool's login where the rest of TeamAI finds that tool's home.

  • Local judge profiles: Claude Code, Codex and OpenCode reach verified with read access on the versions measured. OpenCode reaches it only outside a git or hg checkout. Any other version, and CodeBuddy, is best-effort.

  • Batching: the scorers of one pass that resolve to the same tool and model share one lease and one judge call.

  • teamai pull caches valid scorers and warns about the rest, and teamai status lists them. teamai review --branch fails on a malformed scorer, an unknown name in scorers:, or a credential in a scorer's text, as it does for a rule.

  • A failed label prints the same hint a failed session label prints: ask your agent to read the reasoning file and share it with the team.

  • A scorer's model is a team model alias from models/aliases.yaml, resolved per judge tool like a workflow step's, or a model name. The label records the judge the way a pass records its agent:

    "judge":{"kind":"model","tool":"claude","model":{"requested":"haiku","reported":"claude-haiku-5-5","alias":"fast"},"isolation":"verified","access":"read"}

    teamai doctor notes when no installed judge tool maps a scorer's alias, or when a member's ~ entry or models switch makes the tool default judge. teamai review --branch fails on a malformed models/aliases.yaml and on an alias that no tool maps in an agent, a workflow step or a scorer, judged from the team files only, for each role and project the branch's manifests declare. A defaults.model behind an agent with no model is checked, and an agent's tool_extras.<tool>.model satisfies that tool.

  • Docs: docs/guide/scorers.md and its Chinese page, with pointers from the workflow pages. skill-data/core/references/commands.md is regenerated.

An older teamai does not know the scorers key. Update every member and runner before a team adds it to a step.

Out of scope

How it was verified

Check Result
npm run build, npx tsc --noEmit, npm run lint, and the tests each commit touches Pass at every commit
npx vitest run at the head (6a167a32), on #1280 at e70a3339 530 files passed, 1 skipped. 15219 tests passed, 21 skipped.
Real CLI at the head and at 4d9175f0, review --branch on a branch whose ops scorer asks for fast, mapped only in models/ops/aliases.yaml Head: no model finding. 4d9175f0: fails it for no role or project, role dev, project svc-a, which do not receive it
Restack onto e70a3339 (main with #1265) #1265 removed ALL_SUPPORTED_TOOLS. The judge's tool check and review --branch's alias check now use isAgentToolId and AGENT_TOOLS from tools.ts, as main's own callers do (same set: ToolName is AgentToolId). #1265's two new doctor tests mock isSilent like the other doctor tests here. Not run with the real CLI.
npm run test:e2e -- score-external-driver workflow-publish-resume at #1298's head, which carries this one 1 and 5 passed
Real CLI at 78a79737, an earlier head, after npm run build 191 checks over three runs shared with #1298, all passed: scoring.yaml per namespace, doctor naming the file that set the isolation, review --branch with inherited model aliases and namespaced workflows, declared secrets in a scorer prompt, on-demand failures
Real CLI at 6fb5549b sections 1-7 of a 47-check run, all passed: credential in a scorer, model aliases in agents, steps and defaults per role, doctor judge tools, status scorers, the failed-label hint
Real CLI at 92784ceb, model aliases 16 checks passed, 0 failed
Real CLI at 58893b8a, the scoring path end to end 33 checks passed, 0 failed

At the head, a scorer with model: fast (mapped to haiku for Claude) was judged by real Claude. The stored and published label carries judge.model {requested: haiku, reported: claude-haiku-5-5, alias: fast}. An external driver that submits without a model still gets {requested, alias}. A scorer alias no tool maps makes doctor and score --run print the same text, and fails review --branch along with a step alias:

$ teamai score --run <id> --step review --scorer deep
{"error":{"code":"judge_model_unavailable","message":"Scorer \"deep\" asks for model alias \"strong\", which has no model for claude, codex, opencode in models/aliases.yaml. …"}}

The 58893b8a run used isolated HOMEs, bare git team repos, and real tools:

Section What passed
Claude Code 2.1.308, --driver local A prompt step ran and the run settled and published its record before any scoring. score --next then score --lease judged it with real Claude: a listed label, verified isolation, read access, published without reasoning. A phrase only the judge wrote is in the local reasoning file and nowhere in the run dir or on teamai-reports. Scoring left the run file unchanged.
External JSON driver It took a lease with can.judge and settled it with --submit --stdin. workflow report <judge ref> exits 1 with judge_lease.
Batch Two scorers, one lease, one Claude call, two labels. The call's tokens are counted once, locally and in what is reported.
On demand At samplingRate: 0, score --next is idle and score --run … --step … --scorer … still judges and publishes.
OpenCode v2.1.2 Judged one lease with verified and read.
review --branch Fails on a malformed scorer.
Namespaces A team without roles or projects caches scorers/ops/review-quality.md. When activation switches between identical root and namespace files at one revision, the cache, the run's pin, and isolation_unmet all name the file in use.
$ teamai score --lease s3f0…/a68e2719f --submit --json '{… "judge":{"isolation":"best-effort"}}'
{"error":{"code":"isolation_unmet","message":"This answer comes from a best-effort judge, and scorers/quality.md isolation asks for verified. The lease stays open: judge it at verified isolation, then submit again."}}

Codex was checked on an earlier commit of this branch, whose judge profile is unchanged since. Codex CLI 0.163.0 judged four leases whose transcripts and rubrics told it to write files, read outside the evidence, use the network, and spawn a subagent. Every label was verified with read, and no planted secret appeared in any output. Codex attempted none of those actions, so I ran each probe through codex sandbox with the profile teamai generates. Writes, reads outside the evidence, and network access were all denied.

Not run here: CodeBuddy (not installed) and the gitlab and github providers. That coverage is left to CI. While this PR's base is #1280's branch, CI runs only the review and the source scan here; Lint & Test and the E2E jobs run once #1280 merges and this PR targets main.

Known follow-ups

  • CodeBuddy stays best-effort until a signed-in run proves its profile.

Commits

Slice Subject
0 refactor: move launch, hashing and revision helpers out of workflow
1 feat(scorers): validate and cache scorers/.md
2 feat(workflow): list scorers on a step
3 feat(scorers): take judge work with teamai score --next
4 feat(scorers): one label store, published to teamai-reports
5 feat(scorers): prepare evidence for judge leases
6 feat(scorers): judge a lease locally
7 feat(scorers): require a judge isolation from team, scorer and machine
8 feat(scorers): batch the scorers of one pass into one judge lease
9 feat(scorers): score a pass on demand
9b feat(review): check model aliases in agents, workflows and scorers
10 docs(scorers): one guide from scorer file to labels

Slice 0 moves code only. Each later slice works on its own.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Preserve ownership through nested archived passes — src/workflow/scoring.ts:69: childrenOf only follows a direct within link. If a nested workflow loops and archives its stamped prompt passes, those passes point to the nested workflow, not the outer scored workflow. After a handover, the outer workflow has no directly matching stamped descendant; the unstamped fallback fails because run.publication exists. Its scorer is therefore never offered on the owning install. Follow the ancestry transitively, as evidencePlan does.

  • [P2 non-blocking] Preserve Codex’s configured model without a custom provider — src/scorers/judge.ts:100: A member configuration containing model="…" but no custom model_providers entry takes this early return, discarding the model. Since the judge then uses a temporary CODEX_HOME, the original configuration is unavailable and a scorer without an explicit model silently runs Codex’s built-in default instead of the member’s chosen model. Preserve the model override independently of custom-provider settings.

The PR description documents sufficient testing, including a post-build real-CLI record at the head. No testing-record finding. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 58893b8 to 92784ce Compare October 23, 2026 06:08
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Restore team-declared secret checks — src/workflow/handoff.ts:204: Removing collectTeamSecrets allows declared secrets whose names do not match SECRET_NAME_PATTERN to reach teamai-reports. For example, declare WAREHOUSE_DSN in env/secrets.yaml, set it to mysql-prod-7Hq2xVb9Lk, and include that value in a step’s outputs: publication now accepts it instead of returning secret_detected.

  • [P2 non-blocking] Keep unrelated handoff policy changes out of this feature — src/workflow/store.ts:152, src/workflow/snapshot.ts:280: Switching ownership display from the TeamAI username to the OS username and removing absolute-path publication rejection are unrelated behavior changes. The Code Review Rules require surgical changes that trace directly to the PR’s stated goal; preserve these policies or split their changes into a separate PR.

Both earlier findings are resolved: ownership follows transitive archived ancestry, and Codex preserves its configured model without a custom provider.

The description documents sufficient post-build real-CLI testing. The older recorded commit is a note, not blocking.

Review limitation: the shallow checkout cannot compute the requested merge base, so I inspected the endpoint diff. I did not run, build, or install PR code.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Keep work open for compatible drivers — src/workflow/scoring.ts:425: If fast maps only to Codex, a Claude-only driver polling first permanently fails the entry with judge_model_unavailable. A subsequent Codex driver receives no lease because entryState treats that failure as terminal. Skip work incompatible with the requesting driver rather than closing it globally.

  • [P2 non-blocking] Include archived passes in the aggregate diff — src/workflow/scoring-evidence.ts:52: Given an archived child pass covering C0→C1 and its live successor covering C1→C2, runPasses places the live pass first. The workflow’s aggregate diff therefore becomes C1→C2, omitting the first pass’s changes. Choose the workflow’s starting commit, not the first range in storage order.

  • [P2 non-blocking] Record the local judge’s measured isolation — src/workflow/scoring.ts:632: A best-effort lease judged locally with a supported verified profile still produces a best-effort label: this expression ignores the local result’s verified level. Preserve the measured isolation for local judging rather than the driver’s original advertisement.

All four earlier findings are resolved in the current diff.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed the requested three-dot diff only. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 6fb5549 to 5f4825d Compare October 23, 2026 08:27
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Redact the rubric before writing the judge prompt — src/workflow/scoring-evidence.ts:175: rubricPrompt(rubrics) bypasses mask. A valid scorer containing a declared WAREHOUSE_DSN value therefore writes that secret verbatim into prompt.md and sends it to the judge, even though other evidence redacts it. Apply the same secret redaction to the rubric text.

  • [P1 blocking] Preserve the first settling install’s workflow ownership — src/workflow/scoring.ts:76: passesWithin retains runPasses ordering—live passes before archived ones. If install A settles a child that a nested loop archives, then install B settles its live successor after handover, .find selects B. The workflow scorer is consequently assigned to B rather than A, contrary to the first-stamped-child ownership policy. Select the earliest stamped settlement instead of the first stored pass.

All previously reported findings are resolved; these are separate remaining issues.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 5f4825d to 3737a5c Compare October 23, 2026 08:50
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Redact the rubric before writing the judge prompt — src/workflow/scoring-evidence.ts:175: rubricPrompt(rubrics) still bypasses mask. A scorer containing a declared WAREHOUSE_DSN value writes that secret verbatim into prompt.md and sends it to the judge. Apply the same secret redaction to the rubric text.

  • [P1 blocking] Preserve the first settling install’s workflow ownership — src/workflow/scoring.ts:76: passesWithin preserves live-before-archived ordering. If install A settles a child that a nested loop archives, then install B settles its live successor after handover, .find selects B, assigning the workflow scorer to the wrong install. Select the earliest stamped settlement instead.

These two latest findings remain unresolved. All findings from the earlier three passes are resolved.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed the requested three-dot diff only. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 3737a5c to 8e85930 Compare October 23, 2026 09:20
@github-actions

Copy link
Copy Markdown

No new findings I’m confident warrant reporting.

All earlier findings are resolved in the current diff, including rubric redaction and ownership selection across archived passes.

The PR description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff. I did not run, build, or install PR code.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Check inherited defaults in referenced workflows — src/review-branch.ts:707: Model validation reads only each file’s own defaults.model. If parent.yaml sets defaults.model: fast and only calls child.yaml, whose prompt step has no model or defaults, neither file checks the inherited alias. With no mapping for fast, review --branch passes even though the child fails at runtime with model_unavailable. Propagate effective defaults through workflow references when validating models.

All earlier findings are resolved in the current diff.

The PR description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed the requested three-dot diff only. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from acbb6fd to df34d57 Compare October 23, 2026 11:56
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Check inherited defaults in referenced workflows — src/review-branch.ts:723: Validation still reads only each file’s own defaults.model. If parent.yaml sets defaults.model: fast and calls child.yaml, whose prompt step has no model or defaults, neither file checks the inherited alias. Without a mapping for fast, review --branch passes although the child fails at runtime with model_unavailable. Propagate effective defaults through workflow references.

This latest earlier finding remains unresolved. All other previously reported findings are resolved; no additional findings I’m confident warrant reporting.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff. I did not run, build, or install PR code.

@github-actions

Copy link
Copy Markdown

No new findings I’m confident warrant reporting.

All earlier findings are resolved in the current diff, including inherited defaults.model validation through referenced workflows.

The PR description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff against the checked-out base’s Code Review Rules. I did not run, build, or install PR code.

@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Fixed in 00a4fd5 (folded into feat(review): check model aliases in agents, workflows and scorers): inherited defaults.model in referenced workflows.

review --branch now checks each prompt step with the default it inherits through workflow references, using the same workflow loading as the runtime. It also checks namespaced workflows (workflows/<ns>/x.yaml), which it skipped before.

parent.yaml  defaults.model: fast  → runs child.yaml
child.yaml   prompt step, no model, no defaults
review --branch, fast unmapped      → fail (was: pass; runtime: model_unavailable)
review --branch, fast mapped        → pass
workflows/ops/x.yaml, strong unmapped → fail (was: not checked)

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 00a4fd5 to 78a7973 Compare October 23, 2026 13:32
@github-actions

Copy link
Copy Markdown

No new findings I’m confident warrant reporting.

All earlier findings are resolved in the current diff, including inherited defaults.model validation through referenced workflows.

The PR description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff against the trusted base’s Code Review Rules. I did not run, build, or install PR code.

@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 78a7973 to 147c3fc Compare October 23, 2026 15:16
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Validate scorer models only for members receiving that scorer — src/review-branch.ts:788: Every scorer is checked against every model context, including contexts where its namespace is inactive. For example, an ops role receiving both scorers: [ops] and models: [ops], with scorers/ops/quality.md requesting fast mapped only in models/ops/aliases.yaml, incorrectly fails review --branch for “no role or project.” Restrict validation to contexts that receive the scorer.

All earlier findings are resolved in the current diff, including inherited defaults.model validation.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff against the trusted base’s Code Review Rules. I did not run, build, or install PR code.

Scorers need these without importing src/workflow/. Move only, no
behavior change:

- canonicalJson: src/workflow/snapshot.ts -> src/utils/canonical-json.ts
- NoModel and its classification: src/workflow/step-model.ts ->
  src/models/aliases.ts (noModelReason); step messages stay put
- stopProcess: src/workflow/process-stop.ts -> src/tool-launch/
- tool names and families, stdout parsers and TokenUsage:
  src/workflow/local-tool-adapter.ts and types.ts -> src/tool-launch/
- teamRevision helpers: src/workflow/cache.ts -> src/utils/team-revision.ts
- DriverSchema: src/workflow/step-state.ts -> src/driver-identity.ts
A scorer is a Markdown file with Warp-style frontmatter:

    ---
    labels:
      - { value: good, score: 1, description: Does what was asked. }
      - { value: bad, score: 0, description: Misses the request. }
    passingScore: 1
    samplingRate: 25   # 0-100, default 25
    model: fast        # optional
    isolation: verified  # optional, verified | best-effort
    ---
    Judge whether the change does what the step asked.

Workflow sync now syncs scorers first, so pull (both paths), push and
review read the same tree. Valid scorers go to their own cache under
<dataHome>/scorers/cache/. scorers/README.md is ignored; any other file
warns at pull and is a fail finding in teamai review --branch. Review
also runs its leak check on every file in scorers/, as it does for
rules: a credential or an internal address fails the branch.

Adds docs/guide/scorers.md and its zh-CN twin, checked by the docs
example test. The failure-code guard now scans src/workflow/ and
src/scorers/ recursively.
    defaults:
      scorers: { batch: true }
    steps:
      - id: implement
        prompt: Implement the change.
        scorers: [review-quality]
      - id: review
        prompt: Review the change.
        scorers: { names: [review-quality], batch: false }

scorers: goes on prompt, prompt_file and workflow steps. Batching
resolves step > defaults.scorers.batch > the team's default > off.

A name with no valid scorers/<name>.md is a fail finding in teamai
review --branch. At pull it is a warning, the workflow stays cached and
the step runs without it. teamai doctor lists invalid scorer files and
unknown names, since hook pulls run silently.

Nothing is judged yet.
    $ teamai score --next --json '{"driver":{"kind":"t3","machine":"m","workspace":"/w",
        "can":{"judge":{"isolation":"verified","tools":["claude"]}}},"running":[]}'
    {"data":{"lease":{"ref":"s5d1e07c2a9b4/a91c0d7e","scorer":"review-quality",
      "subject":{"runId":"r3f9…","step":"review","pass":1},"evidence":"…","prompt":"…/prompt.md",
      "answer":{"schema":{…}},"expiresAt":"…","submit":"teamai score --lease s5d1e07c2a9b4/a91c0d7e --submit --stdin"}}}
    $ teamai score --lease s5d1e07c2a9b4/a91c0d7e --submit --stdin < answer.json
    {"data":{"label":{"scorer":"review-quality","label":"thorough","score":1,"passed":true,…}}}

Open judge work is computed, never queued: succeeded passes of steps that
list a scorer, the scorers the run pinned at start, deterministic sampling,
and the attempts under <dataHome>/scores/attempts/. The engine only stamps
installId on the passes it settles and within on archived passes; scoring
never writes a run file, so the run settles without waiting.
A workflow pass belongs to the install that settled its first child
pass: a step's passes settle in pass order, and a pass whose commits
start where another's end settled after it.

Leases last 30 minutes and renew while the driver lists them in running;
the fourth lapse fails the entry lease_expired. Entries expire 14 days after
the run last changed. A label outside the scorer's list is refused and the
lease stays open. workflow report refuses a judge ref. A project whose data
still lives in <project>/.teamai is offered nothing, and doctor says why.
Evidence is a placeholder until it is built (gaps: evidence_not_built).
    <dataHome>/scores/labels.jsonl             every label on this machine
    <dataHome>/scores/reasoning/<hash>.md      the judge's reasoning per label, never published
    <dataHome>/scores/pending/<labelId>.json   outbox
    scores/<member>/<YYYY-MM>.jsonl            on teamai-reports

One ScoreLabel type and parser for workflow and session labels. The newest
scoredAt wins per (scorer, rubric.sourceHash, subject, judge.kind), merged
against origin so an older label from another clone never replaces a newer
one. A pending label is removed only while it still holds what was sent.
Submit publishes at once; pull retries what is left. With usage reporting
off, labels stay pending.

Workflow Runs shows {scorer, label, passed} per pass, a hand label over a
model label, and a scoring total kept apart from the run's tokens: every
attempt on this machine, labelled ones for a published run. The same run at
samplingRate 100 and 0 has the same stepCounts, iterations, failure and
work tokens.
```sh
teamai score --lease "$REF" --json '{"driver":{"kind":"t3","machine":"m1","workspace":"/work"}}'
```

Runs the tool the lease names in a confined profile and settles the
lease through the same submit path as any driver.

- Claude: restricted read tools, evidence as an appended system prompt
  file, prompt on stdin, model-switch env passed with --settings.
  Verified for 2.1.308.
- Codex: temporary home with auth and a permission profile that lets
  its shell read only the evidence, no network, no writes, core env;
  [agents] enabled = false, --ephemeral, hooks/plugins/apps/multi-agent
  disabled. Verified for codex-cli 0.163.0.
- OpenCode: --standalone with isolated config denying bash, edit, web,
  task and external directories, no project config, and its database
  and XDG dirs in a temp dir; the judge session and its children are
  deleted after. Verified for v2.1.2 when the temp dir is outside any
  git or hg checkout, best-effort otherwise.
- CodeBuddy: its Claude-like flags (read tools, no MCP, no saved
  session, no setting sources), prompt on stdin. Best-effort; a run
  that prints "Authentication required" fails judge_tool_unavailable
  whatever its exit code.
- Env comes from an allow-list with TEAMAI_LAUNCHED_BY=scorer; the hook
  dispatcher exits at once for scorer launches.
- The lease records the chosen tool and model (scorer alias, or tool
  default for a local default or switch); an unmapped or unresolved
  alias closes the entry with judge_model_unavailable.
- Each tool's login is read from its own home (toolHome), so
  claude-internal, tcodex and a home moved by tool-homes.json judge
  with their own settings and auth. The judge tools are the launch
  tools plus CodeBuddy.
- LocalWorkflowTool is now LaunchTool.
```yaml
# scorers/scoring.yaml
isolation: verified
```

```text
$ teamai doctor
  Scoring isolation: verified, set by scorers/scoring.yaml isolation.
```

The team's scorers/scoring.yaml, a scorer's isolation and
scoring.isolation in the scope's config.yaml may each ask for verified;
the strictest wins and the default is best-effort. A value teamai does
not know counts as verified and warns. score --next leases work only to
a driver whose can.judge.isolation meets it, and the local judge
refuses a tool whose profile is below it.

scorers/<ns>/scoring.yaml applies where <ns> is an active scorers
namespace, as its scorers do, and replaces the root file's key; two
active namespaces give the strictest. A scoring.yaml that does not
parse stops scoring in that scope: score --next hands out nothing and a
submit fails with scoring_settings_invalid. teamai review --branch
checks every scoring.yaml in every namespace, and neither pull nor
review takes it for a scorer.
```yaml
- id: review
  prompt: Review the change.
  scorers: { names: [review-quality, test-coverage], batch: true }
```

```json
{"driver":{"kind":"t3","machine":"m1","workspace":"/work"},"outcome":"success",
 "answer":{"review-quality":{"label":"good","reasoning":"…"},"test-coverage":{"label":"thin","reasoning":"…"}}}
```

A step's scorers.batch, its workflow's defaults.scorers.batch and the
team's batch in scorers/scoring.yaml now act. Two active namespaces
whose scoring.yaml set batch differently stop scoring in that scope, as
one model alias in two active namespaces stops aliases. Scorers on the
same pass that resolve to the same tool and model share one lease, one
evidence dir and one prompt listing each rubric; the answer schema has
one label per scorer and each label records judge.batched: true. A
batch never mixes tools or models. Submit checks every label first: one
bad label refuses the whole answer and the lease stays open. Renewal,
hand-back, release and the local judge treat the batch as one lease.

Evidence redaction also masks the values of the secrets the team
declares in env/secrets.yaml, in the scorer prompts too. A label that
did not pass prints where its reasoning is and how to share it:

  To share what went wrong, ask your agent to read <reasoning file> and
  share it with the team (teamai contribute).
```console
$ teamai score --run r3f9 --step review --scorer review-quality            # judge here, print the label
$ teamai score --run r3f9 --step review --pass 2 --scorer review-quality --queue   # any driver's --next takes it
$ teamai score --run r3f9 --step review --scorer quality --scorer tests --batch --queue
```

Sampling does not apply. `--pass` defaults to the step's latest succeeded
pass. A scorer the run pinned judges with that rubric; any other scorer
teamai pull cached judges with its current one.

The command records `<dataHome>/scores/attempts/<entryId>/request.json`
with the pinned rubric. Labels and settled attempts older than the request
no longer close the entry, so a failed or expired entry opens again, and
the request starts a new 14-day window.

Without `--queue`, the local judge runs here with the first judge tool on
PATH, preferring the tool that ran the pass. An unknown run, step, pass or
scorer fails with the values that exist.

Taking a lease also sweeps what no attempt reads any more: rubric files
no attempt names, the request of an entry that closed, and the raw
output of an attempt a newer one replaced. Labels are never swept.

teamai doctor lists the judge tools on PATH and the isolation each
reaches here, and teamai status lists the cached scorers:

  Judge tools here: claude 2.1.308 (Claude Code) is verified, codex codex-cli 0.163.0 is best-effort.
  scorers: 2 (review-quality, test-coverage)
@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 147c3fc to 4d9175f Compare October 23, 2026 16:02
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Validate scorer models only for members receiving that scorer — src/review-branch.ts:789: Every scorer is still checked against every model context. With separate dev and ops roles, an ops scorer requesting fast, mapped only in models/ops/aliases.yaml, incorrectly fails review for role dev, which receives neither resource. Restrict validation to contexts receiving the scorer.

This latest earlier finding remains unresolved. All other previously reported findings are resolved; no additional findings I’m confident warrant reporting.

The description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking.

I reviewed only the requested three-dot diff against the trusted base’s Code Review Rules. I did not run, build, or install PR code.

review --branch now reads the branch's models/ and fails:

- an aliases file pull cannot load, in the words doctor uses;
- an agent, step, defaults.model or scorer whose model alias the branch's
  team files map for none of the tools that would use it.

Member overrides, model switches and installed tools are the reviewing
machine's, so they play no part. A model name is never a finding.

Each role and project the branch declares, and members with neither,
get their own agents and aliases files, so each is checked on its own
and a finding names the ones it is about. An agent's model resolves as
pull resolves it, its tool_extras model first, and a step whose agent
sets no model is checked for defaults.model, as a run uses it.
docs/guide/scorers.md (and zh-CN) reads top to bottom: a scorer file, a
step with `scorers:`, a run and its judge, any driver with `can.judge`,
on demand with `--run`, batching, isolation per tool, and where labels
live. The workflow pages keep one example and point there.
docs/dev/workflow-engine.md names every way a judge lease settles.
@SaulMoro
SaulMoro force-pushed the saulmoro/1085-workflow-scorers branch from 4d9175f to 6a167a3 Compare October 23, 2026 16:30
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Fixed in 6a167a3. review --branch now checks a scorer's model alias only for the roles and projects whose pull delivers that scorer, as it already did for workflows and agents. A finding names only those.

Checked with the real CLI. A branch adds roles dev and ops and a project. It also adds scorers/ops/quality.md asking for fast, which only models/ops/aliases.yaml maps.

$ teamai review --branch teamai/push/alice/1 --json     # 6a167a32
ok: true, model findings: 0
$ teamai review --branch teamai/push/alice/1 --json     # 4d9175f0
fail scorers/ops/quality.md - Scorer "quality" asks for model alias "fast", which has no model for any tool in models/aliases.yaml (no role or project, role dev, project svc-a). …

@github-actions

Copy link
Copy Markdown

No new findings I’m confident warrant reporting.

All earlier findings are resolved in the current diff, including scorer-model validation being limited to contexts that receive the scorer.

The PR description documents sufficient post-build real-CLI testing. Older recorded commit IDs are a note, not blocking; no testing-record finding.

I reviewed only the requested three-dot diff against the trusted base’s Code Review Rules. I did not run, build, or install PR code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants