Skip to content

feat(score): score a finished agent session with a team scorer - #1298

Merged
jeff-r2026 merged 8 commits into
mainfrom
saulmoro/1178-score-session
Oct 24, 2026
Merged

jeff-r2026 merged 8 commits into
mainfrom
saulmoro/1178-score-session

Conversation

@SaulMoro

@SaulMoro SaulMoro commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #1178. Stacked on #1297.

teamai score can now judge a finished Claude Code, Codex, or OpenCode session with a team scorer. The judge runs on the member's machine, the reasoning stays there, and --push publishes only the label.

What a team member sees

Inside a session, teamai score scores that session. Outside one, --session names it:

$ teamai score --scorer quality                  # inside a Claude Code session
ℹ Scoring claude-code session bee63c0d… in /work/app
ℹ First prompt: The code word is zebra-quartz-1178. Reply with the single word pong and nothing else.
ℹ quality: followed (score 1, passed). Reasoning: ~/.teamai/scores/reasoning/1215c60a….md

$ teamai score --session e8c70848… --source codex --scorer quality
ℹ Scoring codex session e8c70848… in /work/app
ℹ First prompt: Say hello
ℹ quality: strayed (score 0, did not pass). Reasoning: ~/.teamai/scores/reasoning/6470194d….md
ℹ To share what went wrong, ask your agent to read ~/.teamai/scores/reasoning/6470194d….md and share it with the team (teamai contribute).

$ teamai score --push
ℹ Every score label of this machine is on teamai-reports.

Scoring never guesses the session, and it has no dry run:

$ teamai score --scorer quality                  # outside any session
{"error":{"code":"session_required","message":"Pass --session <id>; scoring never guesses the session."}}
$ teamai --dry-run score --session bee63c0d… --source claude-code --scorer quality --push
teamai score has no --dry-run preview, nothing was run

The label is the workflow label from #1297, with source: "session":

{"scorer":"quality","rubric":{"sourceHash":"f47a4a4a…","teamRevision":"6ae64ecd…"},
 "label":"followed","score":1,"passed":true,"source":"session",
 "subject":{"platform":"claude-code","sessionId":"baa8107c…"},
 "used":{"skills":["deploy-notes","review-notes"]},
 "agent":{"v":1,"tool":{"name":"claude","version":"2.1.308"},"model":{"reported":"…"},
          "team":{"revision":"2ab46400…","localChanges":false,"selection":{"primaryRole":"backend",…}},
          "project":{"commit":"f666c037…","dirty":false},"driver":"session","fingerprint":"…"},
 "judge":{"kind":"model","tool":"claude","model":{"requested":"haiku","reported":"claude-haiku-5-5","alias":"fast"},"isolation":"verified","access":"read","usage":{…}},
 "scoredAt":"2026-10-23T02:45:21.940Z"}
session-start hook ──► <dataHome>/sessions/captures/<platform>/<sessionId>/<captureId>.json   (what it ran as)
teamai score --session ──► session + subagents, redacted ──► local judge, confined profile
                       ──► labels.jsonl, reasoning/<labelVersion>.md, pending/
teamai score --push    ──► scores/<member>/<YYYY-MM>.jsonl on teamai-reports   (label only)
  • agent comes from the captures the session-start hook writes, before that hook's pull starts. A resume writes another capture. A session started by a workflow driver or a judge writes none. The label keeps agent only when every capture agrees. When the session's own pull delivered a new revision, it drops team and fingerprint. Scoring reads the capture's scope, so teamai score --session from another directory keeps agent. Captures of sessions whose file is gone are swept.
  • team.selection is the role, projects, and tags the last pull delivered for, kept in that checkout's pull record. Changing a role without pulling does not change it.
  • used.skills lists the team skills the session or its subagents loaded through the skill tool, a read of their SKILL.md, or a /name or $name mention the member typed. Each one is checked against what the capture in effect when it loaded recorded as delivered, and a skill a resume's pull delivers counts from that resume on, so a member's own skill with a team skill's name is not listed, and a later pull that removes a skill does not change a rescore. Text Codex injects from AGENTS.md does not count. A Codex command's paths are read from its workdir.
  • Workflow labels carry used.skills too, with every driver. An external driver reports used: {skills} with the pass, names only, and drops what is not a skill name. The local driver works it out from the pass's transcript since its launch and what the pull delivered, so a pass that resumes a session does not take an earlier pass's loads. A sub-workflow step's label carries the skills of its child passes.
  • The judge reads the redacted session with its subagents' transcripts. A transcript over 200,000 characters keeps its start and end, with one note of how much was cut. Sampling does not apply.
  • The judge's model is the scorer's, with feat(scorers): judge workflow steps with team scorers #1297's alias rules. Since the judge reads at most 200,000 characters, a session scorer naming a cheaper alias such as model: fast caps what a long session costs. When a member's ~ entry or models switch turns the alias off, score prints one ℹ line saying so, in the words doctor uses.
  • With usage reporting off, --push prints Usage reporting is off, so labels stay on this machine. and the labels stay pending. On a team repo without a teamai-reports branch it answers unsupported_repo. teamai doctor fails Score labels are published while labels wait.
  • A failed label's hint passes --session-id <scored id>, so contribute marks the scored session. Codex session titles are the member's own first line, also when it names a file.
  • Docs: a "Score a session" section in docs/guide/scorers.md and its Chinese page. skill-data/share lists a failed label's reasoning file as something to share. skill-data/core/references/commands.md is regenerated.

Out of scope

Scoring part of a session, background scoring (#1179), scoring a subagent as its own subject, showing labels (#1182), and a spending budget. Assets delivered to the user scope (HOME) are not in agent.

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 (3c415e32), on #1297 at fe7e22b0 536 files passed, 1 skipped. 15402 tests passed, 21 skipped.
Rebase onto #1297 at fe7e22b0 after #1280 merged No conflicts. Each commit's patch is unchanged except for context lines.
npm run test:e2e -- score-external-driver score-session workflow-records-reports-branch-1083 workflow-publish-resume hooks-team-e2e hooks-project-isolation-issue373 scope-isolation-issue85 at the head (3c415e32) 7 files, 53 tests passed
Real CLI at d148b2bb (same code as the head for these paths) and at 12cd3f2c, after npm run build, for this round's two fixes Both pass at the head and fail at 12cd3f2c (record below)
Real CLI at 12cd3f2c, on #1280 at ac82dde5, after npm run build 30 checks on used.skills for sessions and workflow passes, all passed. Three runs shared with #1297, 191 checks, all passed.
Real CLI at 58b18c5f 47 checks passed, 0 failed
Real CLI at 77e9c5d2, the judge model session label judge.model {requested, reported, alias}; one ℹ line when a member's ~ entry turns the alias off
Real CLI at cbe8f764, the session path end to end 46 checks passed, 0 failed

The two review fixes, real CLI at d148b2bb and at 12cd3f2c, after npm run build.

A skill a resume's pull delivers. Project install with --agent claude. A real Claude Code 2.1.308 claude -p session, where the member types /notes-r19b before the team has that skill. The team then publishes it. The session-start hook runs again with source: resume, and its pull delivers the skill. The judge is real Claude Code.

# d148b2bb
capture (start)   ['deploy-notes', 'notes-r19a', 'release-notes']
capture (resume)  ['deploy-notes', 'notes-r19a', 'notes-r19b', 'release-notes']
$ teamai score --session 59ad545d… --scorer quality
ℹ quality: followed (score 1, passed). Reasoning: …
label used: {"skills": []}

# 12cd3f2c, the same steps with notes-r19c
capture (start)   ['deploy-notes', 'notes-r19a', 'notes-r19b', 'notes-r19c', 'release-notes']
capture (resume)  ['deploy-notes', 'notes-r19a', 'notes-r19b', 'notes-r19c', 'release-notes']
label used: {"skills": ["notes-r19c"]}

A Codex command's workdir. User install. The run uses a Codex rollout copied from a real one, plus one code-mode call tools.exec_command({cmd:"cat .codex/skills/wayfinder/SKILL.md",workdir:"<home>"}), made from a session whose cwd is the app.

$ teamai score --session 01a16770… --scorer quality
label used: {"skills": ["poteto-mode", "wayfinder"]}    # d148b2bb
label used: {"skills": ["poteto-mode"]}                 # 12cd3f2c reads the path from the app

The run at 12cd3f2c used teamai init --scope project --agent claude against a bare git team repo with two team skills, plus one skill of the member's own. Sessions are real Claude Code 2.1.308 claude -p runs, and every judge is real Claude Code.

$ teamai score --session 738259ed… --scorer quality
ℹ Scoring claude-code session 738259ed… in /work/app
ℹ First prompt: The code word is zebra-quartz-final-1178. Do these three steps in order, then reply with the single word done. …
ℹ quality: followed (score 1, passed). Reasoning: …/scores/reasoning/c6eab82e….md
PASS used.skills {"skills": ["deploy-notes", "review-notes"]} names exactly the two team skills, not my-notes or later-notes

$ teamai score --session 0000feedbeef --source codex --scorer quality
{"error":{"code":"session_ambiguous","message":"--session 0000feedbeef names more than one codex session: 4ced82e3-…-0000feedbeef, 6d95ae84-…-0000feedbeef. Pass the whole id of one."}}
Section What passed
Capture The session-start hook wrote one capture holding the revision from before its own pull, with team.selection. The pull delivered a new revision and marked the capture teamChanged.
Scoring No --source needed. The printed first prompt is the member's own line. used.skills names review-notes (skill tool) and deploy-notes (loaded only by a subagent), not the member's own skill. Because the team changed, agent keeps no team and no fingerprint. The judge ran verified with read access. A phrase only the judge wrote is in the local reasoning file.
Selection A second session with no team change gives a label with agent.team.selection and a fingerprint.
--push Both labels reached teamai-reports with no reasoning. The judge's phrase, a code word from the transcript, and the first prompt are nowhere on the branch.
Ambiguous id Refused before any judge ran. No label written.
Evidence limit A 408,587-character session gave a 199,814-character transcript that keeps its start and end around one note of what was cut.

Earlier real-CLI runs on this branch, with the same code as the head for these paths, covered a typed /review-notes check whether attempts < 3 as the first prompt, a read of a team SKILL.md, Codex rollouts copied from real ones where $review-notes appears only in the injected AGENTS.md block, a native Codex session TeamAI had no dashboard events for, scoring from inside a session through CLAUDE_CODE_SESSION_ID, the no-session and dry-run refusals, resume captures, a label with no agent when two captures differ in model, no capture from a --driver local step, and two members on different roles with different team.selection.

At 58b18c5f, real Claude Code sessions in a project install:

$ teamai score --session 4772182c --scorer quality        # the session loaded the team's review-notes
used.skills {"skills": ["review-notes"]}
# another member removes review-notes from the team; the member adds their own review-notes; pull
$ teamai score --session 4772182c --scorer quality
used.skills {"skills": ["review-notes"]}                  # the team credit stays, once
$ teamai score --session c2969913 --scorer quality        # this session loaded the member's own review-notes
used.skills {"skills": []}
$ cd ~/elsewhere && teamai score --session 4772182c --scorer quality
# the label is kept in the project scope and carries agent
$ teamai score --push                                     # http team repo
{"error":{"code":"unsupported_repo","message":"Publishing score labels needs a team repo with a teamai-reports branch; …"}}

Not run here: a live Codex or OpenCode session (the Codex checks use copied rollouts), CodeBuddy, and the gitlab and github providers. That coverage is left to CI. While this PR is stacked, CI runs only the review and the source scan here; Lint & Test and the E2E jobs run once it targets main.

Known limits

used.skills comes from what a session's commands say, not from what they did. Two cases follow from that, and both are kept on purpose:

  • A SKILL.md read after && counts even when the command before it fails, as in false && cat .claude/skills/review/SKILL.md; true. Parsing the command line cannot tell whether the left side succeeded, and in real sessions it does (test -f .claude/skills/review/SKILL.md && cat .claude/skills/review/SKILL.md), so dropping reads after && would lose real loads. A read after ||, which runs only when the left side fails, does not count.
  • In a Codex code-mode script, every tools.exec_command read counts, including one in a branch that never runs, such as if (false) { … }. The rollout records the script, not which of its calls ran.

Each trades a rare false positive for not missing real loads. Telling them apart needs the command's output as evidence of the read, which is a change to how reads are attributed and is not part of this PR.

Known follow-ups

  • A capture whose session has no transcript path is never swept, since nothing tells whether that session still exists.

Commits

Slice Subject
1 feat(score): judge a session with --session
2 feat(score): publish session labels with --push
3 feat(score): capture what a session starts as
4 feat(score): keep the selection a pull delivered for
5 feat(score): name the team skills a session used
6 feat(workflow): drivers report the team skills a pass used
7 feat(workflow): the local driver names the team skills a pass used
8 docs(score): score a finished session

@SaulMoro
SaulMoro added this pull request to stack #1300 October 23, 2026 05:04
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] A local skill with a matching name is credited as a team skill — src/session-flow/scoring.ts:132
    skillsLoadedBy checks installation, but team.has(skill) checks only the name. If the team’s review skill is excluded and the member loads their own installed review, the label still publishes used.skills: ["review"]. Verify delivery provenance rather than name membership.

  • [P2 non-blocking] Historical skill usage depends on the current checkout — src/session-flow/scoring.ts:122
    If a session successfully loaded review, then a subsequent pull removes that skill before scoring, the current team inventory no longer contains it and the label omits its recorded usage. The live installation gate also drops skills removed from disk. Use session-time delivery information for historical attribution.

Testing: The description documents sufficient testing: a post-build real-CLI run at cbe8f764, with concrete scoring and publishing results. The explicitly deferred provider/agent coverage is not blocking. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from cbe8f76 to 77e9c5d Compare October 23, 2026 06:08
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] A member’s same-name skill is still credited to the team — src/session-flow/scoring.ts:135
    Attribution checks installation and team-name membership, not delivery provenance. If the team’s review skill is excluded but the member loads their own installed review, the published label still lists used.skills: ["review"]. Verify that the loaded skill came from the team.

  • [P2 non-blocking] Historical skill attribution still depends on current files — src/session-flow/scoring.ts:125
    If a session loaded a team skill and a later pull removes it before scoring, the current inventory omits its recorded usage. The live installation check also drops skills subsequently removed from disk. Use session-time delivery evidence for historical attribution.

Both earlier findings remain unresolved; neither is raised as a new issue.

Testing: The description documents sufficient post-build real-CLI verification at cbe8f764. Its older commit reference is not blocking, and explicitly deferred provider/agent coverage is acceptable. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 77e9c5d to 58b18c5 Compare October 23, 2026 07:48
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Skills delivered by the session’s own pull are omitted — src/session-flow/captures.ts:152
    Captures record teamSkills before the startup pull, but markTeamChanged only sets a flag. If that pull delivers a new skill and the session subsequently loads it, used.skills omits it because no capture contains its delivery evidence. Record post-pull skill delivery too, while retaining the pre-pull history.

Earlier findings: Both are resolved: attribution now uses captured delivery provenance, and historical usage no longer depends on current files.

Testing: The description provides sufficient post-build real-CLI verification. Older commit references and explicitly deferred provider/agent coverage are not blocking. I ran no PR code, builds, installations, or tests.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] An incompatible driver closes work another driver could judge — src/workflow/scoring.ts:426
    With fast mapped only for Codex, a Claude-only score --next now marks the work failed instead of skipping it. A later Codex driver cannot take that work without an explicit reopen. Keep driver-specific model incompatibility nonterminal.

  • [P2 non-blocking] Workflow evidence omits changes from archived child passes — src/workflow/scoring-evidence.ts:51
    Live passes precede archived passes. When child pass 1 changed c0 → c1 and its live retry changed c1 → c2, selecting the first array entry starts the workflow diff at c1, omitting pass 1’s changes. Restore chronological range selection; the deleted regression test covers this scenario.

  • [P2 non-blocking] Resume captures can credit a member’s replacement skill to the team — src/session-flow/scoring.ts:123
    If a session initially had the team’s review, then resumes after a pull removes it and the member installs their own review, merging every capture’s delivery inventory still credits subsequent loads of the replacement. Match usage to the delivery evidence applicable when it occurred, rather than the session-wide union.

Earlier findings: The original provenance and historical-file checks are resolved, as is the omission of skills delivered by the startup pull. The resume scenario above is distinct.

Testing: The description documents sufficient post-build real-CLI verification. Older commit references and explicitly deferred provider/agent coverage are not blocking. I ran no PR code, builds, installations, or tests.

Scope limitation: The shallow checkout has no merge base for the requested three-dot diff. This review uses the direct comparison of the two supplied commits.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 1f395bb to 280753a Compare October 23, 2026 09:26
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] A same-revision startup pull can omit restored team skills — src/session-flow/captures.ts:167
    If a delivered skill is missing from disk at session start, the capture excludes it. When the startup pull restores it without changing revision, selection, or children, this early return skips recording the restored delivery. Subsequent loads are omitted from used.skills. Refresh delivery evidence even when those fields stay unchanged.

  • [P2 non-blocking] Skipped shell reads count as skill loads — src/utils/shell-command.ts:466
    namedReads collects readers regardless of shell control flow. A successful command such as true || cat /work/app/.claude/skills/review/SKILL.md can therefore credit review although cat never runs. Only credit reads that executed.

Earlier findings: The resume/replacement-skill attribution issue remains unresolved: src/session-flow/scoring.ts:106 still unions every capture’s inventory across all usage. It is not a new finding. The original provenance, historical-file, new-revision startup-pull, incompatible-driver, and archived-pass-range findings are resolved in the requested diff.

Testing: The description documents sufficient post-build real-CLI verification. Older commit references and explicitly deferred provider/agent coverage are not blocking. I read only the requested diff; I ran no PR code, builds, installations, or tests.

@github-actions

Copy link
Copy Markdown

Earlier findings still unresolved

  • [P2 non-blocking] Same-revision startup pulls omit restored skills — src/session-flow/captures.ts:167: When a skill is missing at startup and the pull restores it without changing revision, selection, or children, the early return skips recording its delivery. Subsequent loads are omitted from used.skills.
  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:466: true || cat /work/app/.claude/skills/review/SKILL.md credits review even though cat never executes. Attribution needs execution evidence, not merely a reader named in the command.
  • [P2 non-blocking] Resume captures credit replacement skills — src/session-flow/scoring.ts:106: The session-wide inventory union credits a member’s replacement review loaded after resume because an earlier capture recorded the team’s review. Match loads to delivery evidence applicable at that time.

Resolved: The original provenance and historical-file issues, new-revision startup-pull omission, incompatible-driver closure, and archived-pass-range regression remain resolved. No additional confident findings.

Testing: The description documents sufficient post-build real-CLI verification. Older commit references are not blocking; explicitly deferred provider/agent coverage is acceptable.

I read the requested three-dot diff and trusted base review criteria. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 005f08c to 49cdb78 Compare October 23, 2026 11:56
@github-actions

Copy link
Copy Markdown

Earlier Findings Still Unresolved

  • [P2 non-blocking] Same-revision startup pulls omit restored skills — src/session-flow/captures.ts:167: If a skill is missing at startup and the pull restores it without changing revision, selection, or children, the early return skips recording its delivery. Later loads are omitted from used.skills. Refresh delivery evidence even when those fields remain unchanged.
  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:466: true || cat /work/app/.claude/skills/review/SKILL.md credits review although cat never executes. Attribute reads using execution evidence, not merely commands present in the string.
  • [P2 non-blocking] Resume captures credit replacement skills — src/session-flow/scoring.ts:106: Unioning every capture’s inventory credits a member’s replacement review loaded after resume because an earlier capture recorded the team’s review. Match loads to the delivery evidence applicable when they occurred.

Resolved: The original provenance and historical-file issues, new-revision startup-pull omission, incompatible-driver closure, and archived-pass-range regression remain resolved. No additional confident findings.

Testing: The description documents sufficient post-build real-CLI verification. Older commit references are not blocking, and explicitly deferred provider/agent coverage is acceptable.

I read the requested three-dot diff and trusted base review criteria. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 49cdb78 to 9339c56 Compare October 23, 2026 12:39
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Skipped shell reads still count as loads — src/utils/shell-command.ts:466: The new guard handles ||, but false && cat /work/app/.claude/skills/review/SKILL.md; true still credits review. The command succeeds although cat never executes. Account for skipped && branches too.

  • [P2 non-blocking] Resume pulls retroactively change earlier skill attribution — src/session-flow/captures.ts:172: Post-pull delivery is merged into every capture, including earlier starts. If a session first loads the member’s own review, then resumes after that copy is removed and the startup pull delivers the team’s review, the earlier load is incorrectly credited to the team. Preserve earlier inventories and record post-pull delivery with its own effective time.

Earlier findings: Same-revision restoration and timestamp-based resume/replacement attribution are resolved. The skipped-read finding remains partially unresolved, as described above. The original provenance, historical-file, new-revision startup-pull, incompatible-driver, and archived-pass-range findings remain resolved.

Testing: The description documents sufficient post-build real-CLI verification. Older commit references are not blocking, and explicitly deferred provider/agent coverage is acceptable.

I read the specified three-dot diff and trusted base review criteria. I ran no PR code, builds, installations, or tests.

@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Fixed in 9339c56, each folded into the slice that introduced it:

Finding Now
Same-revision startup pull restores a skill The session's own pull always adds what the checkout then holds as delivered to the session's captures. teamChanged is still set only when revision, selection or children changed.
true || cat …/SKILL.md A read after || is not credited, in scoring and in the live hook. Reads after && and ; still are.
Resume credits a replacement Each load is matched to the capture in effect when it happened, so a member's replacement loaded after a resume is not credited; a load before it still is.

Checked with a real Claude Code session for the first and third, and the live post-tool-use hook for the second.

@SaulMoro

Copy link
Copy Markdown
Collaborator Author

On the three findings still listed at 5b4e493:

Finding Now
Resumed workflow passes inherit earlier usage Fixed in 9ed0093. The local driver credits only loads from this pass's launch on. In a session this pass resumes, a skill an earlier pass loaded belongs to that earlier pass.
Skipped && shell reads Kept. A static parse cannot tell whether the left side of && succeeds. In normal use it does, as in cd dir && cat …/SKILL.md, and dropping reads after && would lose those real loads. || stays excluded, since its right side runs only when the left fails. A literal false && is not something an agent writes to read a skill.
Unexecuted code-mode calls Kept, for the same reason. The rollout records the script, not which calls in it ran. A read under if (false) is not a pattern agents write.

The last two are decided and will not change in this PR.

@github-actions

Copy link
Copy Markdown

I couldn’t review the current diff: the shallow checkout contains the base commit but lacks 9ed0093e8e66029e348fd5f786cc10a8beeeb036, so the requested three-dot comparison fails. Network access also failed.

  • Findings: None asserted. I cannot determine whether earlier findings remain unresolved or are fixed without the current diff.
  • Testing description: Sufficient under the trusted base rules: it records post-build real-CLI verification with concrete results. Older commit references and explicitly deferred provider/agent coverage are not blocking.

Please provide the requested diff or make the head commit and merge-base history available locally. I ran no PR code, builds, installations, or tests.

@github-actions

Copy link
Copy Markdown

I couldn’t review the current diff: the shallow checkout lacks 9ed0093e8e66029e348fd5f786cc10a8beeeb036, so the requested three-dot comparison fails. Retrieving the exact diff also failed because github.com could not be resolved.

  • Findings: None asserted. Without the current diff, I cannot determine whether earlier findings remain unresolved or are fixed.
  • Testing description: Sufficient under the trusted base rules. It documents post-build real-CLI verification with concrete results; older commit references and explicitly deferred provider/agent coverage are not blocking.

Please provide the requested diff or make the head commit and merge-base history available locally. I read the trusted base review criteria and ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch 2 times, most recently from 6842f11 to 57953c6 Compare October 23, 2026 16:36
@github-actions

Copy link
Copy Markdown

Earlier Findings Still Unresolved

  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:470: false && cat /work/app/.claude/skills/review/SKILL.md; true still credits review although cat never executes. The guard handles skipped || branches but not && branches.
  • [P2 non-blocking] Unexecuted code-mode calls count as loads — src/skill-use.ts:161: A successful script containing if (false) { await tools.exec_command({cmd:"cat /work/app/.agents/skills/review/SKILL.md"}); } still credits review. The regex extracts calls from source rather than execution evidence.

Resolved: Resumed workflow passes now filter usage by launch time, fixing inherited usage from earlier passes. All other previously resolved findings remain resolved. No additional confident findings.

Testing: The description documents sufficient post-build real-CLI verification with concrete results. Older commit references are not blocking, and explicitly deferred provider/agent coverage is acceptable.

I reviewed the specified three-dot diff against the trusted base criteria. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro removed this pull request from stack #1300 October 24, 2026 04:01
@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 57953c6 to 882de19 Compare October 24, 2026 04:01
@SaulMoro
SaulMoro added this pull request to stack #1355 October 24, 2026 04:01
@github-actions

Copy link
Copy Markdown

Code Erosion Report

Informational — never blocks the merge. scb-check==0.2.0 SlopCodeBench metrics; method & caveats in docs/dev/ci-code-erosion.md.

Metric Value Human band Agent band Reading
Verbosity* 0.058 0.15 0.33 human
Erosion 0.707 0.31 0.68 agent
Cognitive erosion 0.875 – – –

Scanned 431 files / 97050 SLOC · high-CC functions 761/8522

* On TypeScript, verbosity is partial and the bands are Python-calibrated.

Rule hits (TS verbosity layer)

Standalone ast-grep, separate from the number above.

Rule Hits
unnecessary-else-after-return 61
empty-catch-block 5

66 distinct lines flagged across src/.

@github-actions

Copy link
Copy Markdown

Earlier Findings Still Unresolved

  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:470: false && cat /work/app/.claude/skills/review/SKILL.md; true still credits review although cat never executes. The guard excludes skipped || branches, but not && branches.
  • [P2 non-blocking] Unexecuted code-mode calls count as loads — src/skill-use.ts:161: A successful script containing if (false) { await tools.exec_command({cmd:"cat /work/app/.agents/skills/review/SKILL.md"}); } still credits review. The regex extracts calls from source rather than execution evidence.

Resolved: Resumed workflow passes now filter usage by launch time. All other previously resolved findings remain resolved. No additional confident findings.

Testing: The description documents sufficient post-build real-CLI verification with concrete results. Older commit references are not blocking; explicitly deferred provider/agent coverage is acceptable.

I reviewed the requested three-dot diff against the trusted checked-out criteria. I ran no PR code, builds, installations, or tests.

@SaulMoro
SaulMoro force-pushed the saulmoro/1178-score-session branch from 882de19 to 3c415e3 Compare October 24, 2026 04:18
@github-actions

Copy link
Copy Markdown

Earlier Findings Still Unresolved

  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:470: false && cat /work/app/.claude/skills/review/SKILL.md; true still credits review although cat never executes. The guard excludes skipped || branches but not && branches.
  • [P2 non-blocking] Unexecuted code-mode calls count as loads — src/skill-use.ts:161: A successful script containing if (false) { await tools.exec_command({cmd:"cat /work/app/.agents/skills/review/SKILL.md"}); } still credits review. The regex extracts calls from source rather than execution evidence.

Resolved: Resumed workflow passes filter usage by launch time. Other previously resolved findings remain resolved, including historical capture attribution, same-revision restoration, and code-mode workdir. No additional confident findings.

Testing: The description documents sufficient post-build real-CLI verification with concrete results. Older commit references are not blocking; explicitly deferred provider/agent coverage is acceptable.

I reviewed the requested three-dot diff against the trusted base criteria. I ran no PR code, builds, installations, or tests.

Base automatically changed from saulmoro/1085-workflow-scorers to main October 24, 2026 04:37
teamai score --session <id> [--source <platform>] --scorer <name> judges a
finished agent session with the local judge and keeps one label per scorer
with source "session" and subject { platform, sessionId }. Without
--session, the agent session variable names the session when it names
exactly one; otherwise the command fails and never guesses. An id that
ends more than one session's id is refused with the ids it matches.

The label, its scorers and its reasoning belong to the scope the session
ran in, found from the session's directory as its hooks found it, so
scoring from another directory changes nothing.

The evidence is the redacted session with its subagents' transcripts,
fenced as untrusted data and cut once at 200,000 characters; the
transcript file the judge can read holds the same cut text. Redaction
also masks the values of the secrets the team declares in
env/secrets.yaml, in the scorer prompts too. The reasoning file starts
with the member's own redacted first prompt, not text the tool
injected. Codex session titles use the same typed text, so a first line
naming a file stays the title.

A label that did not pass prints the share hint workflow labels print,
with the scored session:

  To share what went wrong, ask your agent to read <reasoning file> and
  share it with the team (teamai contribute --session-id <id>).
teamai score --push publishes every pending label to
scores/<member>/<YYYY-MM>.jsonl on teamai-reports, alone or after
--session, through the label outbox: merged by key against origin, the
newest scoredAt kept. With usage reporting off, or when the push fails,
the labels stay pending for the next --push. An http team repo has no
teamai-reports branch, so --push answers the unsupported_repo error and
the labels stay here. teamai --dry-run score stays refused before
anything runs.

teamai doctor checks "Score labels are published" next to the
learnings check, with the fix teamai score --push.
The session-start hook writes one capture per start, before the
background pull runs:

  <dataHome>/sessions/captures/claude-code/<session>/<captureId>.json
  { captureId, platform, sessionId, capturedAt, sessionFile,
    agent: { tool, model, team: { revision, localChanges }, project, driver: "session", fingerprint } }

The hook reads the checkout's git state once, capped at two seconds,
and hands it to both the capture and the dashboard's session_start
event. The tool version is added by the background pass, so the hook
never waits on --version. A resume writes another capture; a
compaction, or a session teamai launched (TEAMAI_LAUNCHED_BY), writes
none. When the session's own pull delivers a new revision, its captures
get teamChanged: true. Both rewrites take a per-session lock, so two
hook processes of one session keep each other's change. A capture that
fails is logged at debug level. The background pass removes the
captures of a session whose file has been gone for a day.

A session label takes its agent from the captures:

  every capture the same (tool, model, effort, team)  -> agent
  same, one marked teamChanged                        -> agent without team and fingerprint
  captures differ, or none                            -> no agent

model.reported is set only when the session's messages name one model.
--source can be left out when the captures name one platform for the id.
A pull writes the member's selection into the checkout's record when it
delivers:

  lastPullByWorkspace[<checkout>].selection =
    { scope, primaryRole, additionalRoles, projects, subscribedTags,
      excludedSkills, inheritUserScope }

The agent observation copies it into team.selection, with the record's
workspace children, never from the current config; the fingerprint
covers it. A record an older CLI wrote without it gives no team and no
fingerprint. Session captures and workflow passes share the builder.

  backend member  -> team.selection.primaryRole "backend",  fingerprint A
  frontend member -> team.selection.primaryRole "frontend", fingerprint B
  teamai roles set, no pull -> the next capture keeps the last pull's selection

A session's pull that delivers the same revision for another selection,
or for other workspace children, marks its captures teamChanged, as a new
revision does.
A session label carries the team skills the session or its subagents
loaded:

  used: { skills: ["deploy-notes", "review-notes"] }

They are found in the session's messages by the rules the skill-use
hooks apply to their events: a call to the tool's skill tool, a read of
a SKILL.md under a skill root, or a /name or Codex $name mention the
member typed, not one in text the tool injected, such as AGENTS.md.
A read counts on one line of a longer shell command too, and in a
Codex code-mode script, for each command it passes as a string:

  sed -n '1,260p' ~/.agents/skills/review/SKILL.md\nrg -n gate notes.md
  tools.exec_command({cmd:"cat ~/.agents/skills/review/SKILL.md"})

A skill is credited only when the session's captures recorded that
teamai delivered it, unchanged, when the session started or by the
session's own pull, and a read counts only when it read that delivered
copy. Today's files and team repo play no part:

  team skill loaded, removed by a later pull     -> still credited
  team skill the session's own pull delivered    -> credited
  member's own skill with a team skill's name    -> not credited
  no capture for the session                     -> nothing credited
A driver's report names the team skills the pass loaded, and the
workflow label of that pass carries them, as a session label does:

  teamai workflow report <run> --json '{..., "used": {"skills": ["review-notes"]}}'
  label: { source: "workflow", subject: {...}, used: { skills: ["review-notes"] } }

Names only. A value not shaped like a skill name, such as a path, is
dropped, not rejected; a `used` that is not an object with a list of
strings is refused as invalid_report. A report without `used` means
nothing was observed, so its label has none; `"skills": []` means no
team skill was loaded. A `workflow:` step's label lists every skill its
child passes reported, and has none when none of them reported `used`.

The pass keeps `used` in the run file and in the published snapshot.
The run record does not carry it.
The local driver reports `used` for a prompt pass, so its workflow
label names the team skills the pass or its subagents loaded, by the
rules a session label applies:

  pass loads review-notes, delivered unchanged     -> ["review-notes"]
  member's own skill with a team skill's name      -> not credited
  a pull removes the skill while the pass runs     -> still credited
  no pull record for the checkout                  -> no `used`

At launch the driver reads the checkout's pull record once, for the
agent observation and for the team skills the checkout holds as
delivered. After the pass it reads the session the tool reported,
subagents included. Those files stay on this machine; the report
carries names only.
The scorer guide, in both languages, shows scoring a session from
inside it and by id, what the command prints before and after the
judge, --push and the usage-reporting opt-out, and where captures,
labels and reasoning live. The share workflow lists a failed label's
reasoning file as something it can turn into a learning.

The scorer guide also says which team skills a workflow label names,
how a driver reports them with `used` in `teamai workflow report`, and
that a session's own pull at start counts. The workflow guide lists
`used` with the other report fields.
@jeff-r2026
jeff-r2026 force-pushed the saulmoro/1178-score-session branch from 3c415e3 to 0f21aba Compare October 24, 2026 04:37
@github-actions

Copy link
Copy Markdown

Earlier Findings Still Unresolved

  • [P2 non-blocking] Skipped shell reads count as loads — src/utils/shell-command.ts:470: false && cat /work/app/.claude/skills/review/SKILL.md; true credits review although cat never executes. The guard excludes || branches, not skipped && branches.
  • [P2 non-blocking] Unexecuted code-mode calls count as loads — src/skill-use.ts:161: A successful script containing if (false) { await tools.exec_command({cmd:"cat /work/app/.agents/skills/review/SKILL.md"}); } credits review without reading it. The regex extracts calls from source rather than execution evidence.

The description now acknowledges both limitations; neither is fixed. No additional confident findings.

Resolved: Resumed workflow passes filter usage by launch time. Other previously resolved findings remain resolved, including historical capture attribution, same-revision restoration, and code-mode workdir.

Testing: The description documents sufficient post-build real-CLI verification with concrete results. Older commit references and explicitly deferred provider/agent coverage are not blocking.

I reviewed the exact requested three-dot diff against the trusted checked-out rules. I ran no PR code, builds, installations, or tests.

@jeff-r2026 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed (agent review: no blocking findings; P2 follow-ups: doctor's 'Score labels are published' check never clears on http team repos because LabelStore.unpublished() ignores usesBranchWorktree, and SessionCaptures.atStart has no time cap on the session-start path). Verified merged onto main: tsc, lint, build pass; full unit suite 15376 passed with only environment failures that reproduce on plain main; hooks-team / scope-isolation / scorer E2E 17/17. The rebased head 0f21aba has the identical tree to the verified merge. CI 18/18.

@jeff-r2026
jeff-r2026 merged commit fb17c48 into main Oct 24, 2026
18 checks passed
@SaulMoro

Copy link
Copy Markdown
Collaborator Author

Follow-up: both [P2] findings, the skipped read after && and the code-mode call under if (false), are fixed in #1358. A read that may not have run now counts only when the call's output shows the skill's name: line.

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.

[feat] Score a finished session on demand

2 participants