session-orchestrator 5.10.0 → 5.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +32 -0
- package/README.md +9 -8
- package/SECURITY.md +1 -1
- package/agents/code-implementer.md +7 -1
- package/docs/agent-authoring.md +2 -0
- package/docs/events-schema.md +2 -1
- package/docs/fleet-checkin-audit.md +172 -0
- package/docs/role-profile-contract.md +139 -0
- package/docs/scope-collision-guard.md +3 -0
- package/docs/session-config-reference.md +1 -1
- package/evals/instruction-ablation/run.mjs +11 -6
- package/hooks/_lib/hook-import-set.json +27 -2
- package/hooks/_lib/orchestrator-ignore.mjs +112 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +8 -1
- package/hooks/on-session-start.mjs +7 -0
- package/hooks/post-bash-write-verify.mjs +16 -3
- package/hooks/pre-task-scope-disjoint.mjs +12 -0
- package/hooks/subagent-telemetry.mjs +8 -3
- package/hooks/wave-scope-commit-guard.mjs +4 -1
- package/package.json +1 -1
- package/rules/opt-in-stack/security-web.md +1 -1
- package/scripts/emit-session.mjs +13 -0
- package/scripts/fleet-checkin-audit.mjs +138 -0
- package/scripts/lib/common.mjs +2 -4
- package/scripts/lib/context-overhead-format.mjs +57 -0
- package/scripts/lib/fleet-checkin-audit.mjs +244 -0
- package/scripts/lib/memory-proposals/sink.mjs +79 -17
- package/scripts/lib/memory-proposals/store.mjs +245 -9
- package/scripts/lib/project-hygiene.mjs +37 -0
- package/scripts/lib/role-profile/schema.mjs +219 -0
- package/scripts/lib/session-lock.mjs +57 -10
- package/scripts/lib/session-record-repair.mjs +63 -30
- package/scripts/lib/session-schema/constants.mjs +8 -0
- package/scripts/lib/session-schema/validator.mjs +33 -0
- package/scripts/lib/subagents-schema.mjs +9 -0
- package/scripts/lib/telemetry/pricing.mjs +11 -0
- package/scripts/measure-context-overhead.sh +4 -21
- package/scripts/release.mjs +359 -11
- package/scripts/run-quality-gate.mjs +47 -0
- package/scripts/validate-wave-scope.mjs +59 -6
- package/skills/claude-md-drift-check/checker.mjs +8 -6
- package/skills/npm-publish/SKILL.md +3 -3
- package/skills/release/SKILL.md +2 -2
- package/skills/repo-audit/SKILL.md +1 -1
- package/skills/session-end/metrics-collection.md +2 -2
- package/skills/session-end/session-metrics-write.md +1 -1
- package/skills/session-start/references/phase-1-2-session-lock.md +1 -1
- package/skills/wave-executor/circuit-breaker.md +1 -0
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
assertFileScopeSubset,
|
|
93
93
|
assertTestSiblingCoverage,
|
|
94
94
|
testSiblingExpansionApplies,
|
|
95
|
+
testSiblingsFor,
|
|
95
96
|
TEST_SIBLING_EXPANSION_ROLES,
|
|
96
97
|
findScopeCollisions,
|
|
97
98
|
unionFileScopes,
|
|
@@ -663,6 +664,44 @@ function knownRepoFiles() {
|
|
|
663
664
|
}
|
|
664
665
|
}
|
|
665
666
|
|
|
667
|
+
/**
|
|
668
|
+
* WARN — never fail — when two agents' RAW scopes are disjoint but test-sibling
|
|
669
|
+
* expansion hands both the same synthesized glob (#1026 point 2, owner decision
|
|
670
|
+
* option a). `scripts/lib/foo.mjs` and `hooks/foo.mjs` share no file, yet both
|
|
671
|
+
* expand to `tests/**\/foo*.test.mjs`, so the union grants each agent the
|
|
672
|
+
* other's tests. Exit code is unchanged: the glob is a GRANT that may match no
|
|
673
|
+
* file at all, so failing the wave on it would block plans that never collide.
|
|
674
|
+
*
|
|
675
|
+
* `role` gates expansion exactly as `--union` does; `undefined` (sidecar-only
|
|
676
|
+
* `--no-manifest`, or a manifest without a role) checks anyway, because there
|
|
677
|
+
* the expansion that will run later is unknown and silence would hide it.
|
|
678
|
+
* Compares synthesized siblings only, by exact string — a sibling glob one agent
|
|
679
|
+
* DECLARED by hand is not matched (named ceiling; revisit if a wave plan is seen
|
|
680
|
+
* declaring sibling globs explicitly). Overlapping globs of DIFFERENT basenames
|
|
681
|
+
* (`foo*` also covers `foo-bar.test.mjs`) are not matched either.
|
|
682
|
+
*
|
|
683
|
+
* @param {Array<{id?: string, files: string[]}>} agentScopes
|
|
684
|
+
* @param {string|undefined} role
|
|
685
|
+
*/
|
|
686
|
+
function warnSiblingGlobCollisions(agentScopes, role) {
|
|
687
|
+
if (role !== undefined && !testSiblingExpansionApplies({ role })) return;
|
|
688
|
+
const owners = new Map(); // sibling glob -> agent ids, first-seen order
|
|
689
|
+
agentScopes.forEach((rec, i) => {
|
|
690
|
+
const id = typeof rec.id === 'string' && rec.id.length > 0 ? rec.id : `<unnamed#${i}>`;
|
|
691
|
+
for (const glob of testSiblingsFor(rec.files)) {
|
|
692
|
+
const ids = owners.get(glob) ?? [];
|
|
693
|
+
if (!ids.includes(id)) ids.push(id);
|
|
694
|
+
owners.set(glob, ids);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
for (const [glob, ids] of owners) {
|
|
698
|
+
if (ids.length < 2) continue;
|
|
699
|
+
warn(
|
|
700
|
+
`sibling-glob: agents ${ids.map((id) => `"${id}"`).join(', ')} are disjoint as declared but test-sibling expansion grants each of them ${glob} — split the same-basename files across waves or name the tests explicitly (#1026)`,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
666
705
|
/**
|
|
667
706
|
* Assert that no file is claimed by two agents of the SAME wave (#1020).
|
|
668
707
|
* Exits 1 with one message per collision (plus one per duplicate agent id);
|
|
@@ -670,8 +709,8 @@ function knownRepoFiles() {
|
|
|
670
709
|
*
|
|
671
710
|
* @param {string} sidecarPath
|
|
672
711
|
*/
|
|
673
|
-
function assertDisjointOrDie(sidecarPath) {
|
|
674
|
-
if (!reportScopeCollisions(sidecarPath)) process.exit(1);
|
|
712
|
+
function assertDisjointOrDie(sidecarPath, role) {
|
|
713
|
+
if (!reportScopeCollisions(sidecarPath, role)) process.exit(1);
|
|
675
714
|
}
|
|
676
715
|
|
|
677
716
|
/**
|
|
@@ -683,12 +722,17 @@ function assertDisjointOrDie(sidecarPath) {
|
|
|
683
722
|
* @param {string} sidecarPath
|
|
684
723
|
* @returns {boolean} true when the wave is disjoint
|
|
685
724
|
*/
|
|
686
|
-
function reportScopeCollisions(sidecarPath) {
|
|
725
|
+
function reportScopeCollisions(sidecarPath, role) {
|
|
687
726
|
const agentScopes = readAgentScopesOrDie(sidecarPath, '--assert-disjoint');
|
|
688
727
|
const { ok, collisions, duplicateIds } = findScopeCollisions(agentScopes, {
|
|
689
728
|
knownFiles: knownRepoFiles(),
|
|
690
729
|
});
|
|
691
|
-
|
|
730
|
+
// Only a raw-disjoint wave gets the sibling hint: its text says the scopes
|
|
731
|
+
// are disjoint as declared, which is false once a real collision exists.
|
|
732
|
+
if (ok) {
|
|
733
|
+
warnSiblingGlobCollisions(agentScopes, role);
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
692
736
|
|
|
693
737
|
// Duplicate ids FIRST: they are a malformed plan, and a reader who fixes them
|
|
694
738
|
// may well change which collisions remain.
|
|
@@ -708,6 +752,15 @@ function reportScopeCollisions(sidecarPath) {
|
|
|
708
752
|
return false;
|
|
709
753
|
}
|
|
710
754
|
|
|
755
|
+
/**
|
|
756
|
+
* The manifest's `role` when it is a string, else `undefined` (unknown).
|
|
757
|
+
* @param {unknown} obj
|
|
758
|
+
* @returns {string|undefined}
|
|
759
|
+
*/
|
|
760
|
+
function roleOf(obj) {
|
|
761
|
+
return obj !== null && typeof obj === 'object' && typeof obj.role === 'string' ? obj.role : undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
711
764
|
/**
|
|
712
765
|
* QUERY MODE (#1020): print `expandTestSiblings(unionFileScopes(scopes), {role})`
|
|
713
766
|
* as JSON on stdout, using the MANIFEST'S own role. Replaces the manifest echo —
|
|
@@ -757,7 +810,7 @@ function validate(
|
|
|
757
810
|
}
|
|
758
811
|
// #1026.4 — the collision check reads only the sidecar, so a schema error
|
|
759
812
|
// must not hide its findings: report both, exit 1 either way.
|
|
760
|
-
if (assertDisjointPath) reportScopeCollisions(assertDisjointPath);
|
|
813
|
+
if (assertDisjointPath) reportScopeCollisions(assertDisjointPath, roleOf(obj));
|
|
761
814
|
process.exit(1);
|
|
762
815
|
}
|
|
763
816
|
|
|
@@ -771,7 +824,7 @@ function validate(
|
|
|
771
824
|
// violates BOTH the subset relation and disjointness must keep the older,
|
|
772
825
|
// byte-pinned subset message. Only ever ADDS a failure mode.
|
|
773
826
|
if (assertDisjointPath) {
|
|
774
|
-
assertDisjointOrDie(assertDisjointPath);
|
|
827
|
+
assertDisjointOrDie(assertDisjointPath, roleOf(obj));
|
|
775
828
|
}
|
|
776
829
|
|
|
777
830
|
// #1020 QUERY MODE — replaces the echo below; see the STDOUT CONTRACT note in
|
|
@@ -555,10 +555,11 @@ function buildSurfaceDescriptors(vaultDir, commandsDir) {
|
|
|
555
555
|
id: 'test-count',
|
|
556
556
|
noun: 'test files',
|
|
557
557
|
actual: countTestFiles(vaultDir),
|
|
558
|
-
// "N test files"
|
|
558
|
+
// "N test files" or "N+ test files" (an explicit lower bound).
|
|
559
|
+
// The README badge claims a runtime PASS count
|
|
559
560
|
// ("9303 tests"), which a static checker cannot derive, so we match only
|
|
560
|
-
// the explicit file-count phrasing.
|
|
561
|
-
claimRe: /\b(\d+)
|
|
561
|
+
// the explicit file-count phrasing.
|
|
562
|
+
claimRe: /\b(\d+)(\+)?\s+test\s+files?\b/gi,
|
|
562
563
|
skipMsg: 'test-count: no tests/ directory found',
|
|
563
564
|
},
|
|
564
565
|
];
|
|
@@ -1831,7 +1832,7 @@ function main() {
|
|
|
1831
1832
|
}
|
|
1832
1833
|
}
|
|
1833
1834
|
|
|
1834
|
-
// Surface
|
|
1835
|
+
// Surface counts are exact, except explicit "N+ test files" lower bounds.
|
|
1835
1836
|
// A surface only fires when the doc makes an explicit numeric claim that
|
|
1836
1837
|
// its regex matches; an unclaimed surface produces no errors (skip).
|
|
1837
1838
|
for (const surface of activeSurfaces) {
|
|
@@ -1840,10 +1841,11 @@ function main() {
|
|
|
1840
1841
|
let m;
|
|
1841
1842
|
while ((m = re.exec(line)) !== null) {
|
|
1842
1843
|
const claimed = parseInt(m[1], 10);
|
|
1843
|
-
|
|
1844
|
+
const lowerBound = surface.id === 'test-count' && m[2] === '+';
|
|
1845
|
+
if (lowerBound ? surface.actual < claimed : claimed !== surface.actual) {
|
|
1844
1846
|
const err = {
|
|
1845
1847
|
check: surface.id, file: rel, line: lineNum,
|
|
1846
|
-
message: `Narrative claims ${claimed} ${surface.noun} but actual on-disk count is ${surface.actual}`,
|
|
1848
|
+
message: `Narrative claims ${claimed}${lowerBound ? '+' : ''} ${surface.noun} but actual on-disk count is ${surface.actual}`,
|
|
1847
1849
|
extracted: m[0],
|
|
1848
1850
|
count: { surface: surface.id, actual: surface.actual, claimed },
|
|
1849
1851
|
};
|
|
@@ -39,7 +39,7 @@ The script gates mechanics. These three are yours, and it will not make them for
|
|
|
39
39
|
|
|
40
40
|
**2. What a leak means when one is found.** A hit from the leakage gate is not a pattern to silence. Decide which of two it is: a real leak (fix `package.json` `files`, re-pack, re-check) or genuine over-matching (fix `LEAKAGE_PATTERNS` in `scripts/release.mjs` **with a test**). There is no third option, and neither is "publish anyway and clean it up in the next version" — an npm publish is not revocable, and unpublishing burns the version number permanently. Operator handling detail: `docs/distribution/npm-publish-checklist.md` § 3.
|
|
41
41
|
|
|
42
|
-
**3. When to abort instead of repair.** Abort — do not patch forward — when the failure is upstream of the target-confirmed npm receipt: a red preflight row, a lagging `github` mirror, CI not green on the exact commit **on either platform** (`--check` carries both `ci-green-on-head` for GitLab and `ci-green-on-head-github` for the mirror, whose macOS matrix leg has no GitLab equivalent), a dead token, or a publish that did not issue the target receipt. These are cheap to fix and re-run from the top. Repair-in-place is only appropriate *after* that receipt, where the version is already immutable: registry
|
|
42
|
+
**3. When to abort instead of repair.** Abort — do not patch forward — when the failure is upstream of the target-confirmed npm receipt: a red preflight row, a lagging `github` mirror, CI not green on the exact commit **on either platform** (`--check` carries both `ci-green-on-head` for GitLab and `ci-green-on-head-github` for the mirror, whose macOS matrix leg has no GitLab equivalent), a dead token, or a publish that did not issue the target receipt. These are cheap to fix and re-run from the top. Repair-in-place is only appropriate *after* that receipt, where the version is already immutable: registry availability (`--reconcile`), a missing GitHub release, or a lagging site deploy can be reconciled because npm already has the correct artifact. When `--publish` reports **Post-publish reconciliation required**, **do not rerun `--publish`**; repair the listed state directly. `skills/release/SKILL.md` § Abort criteria is the operative list.
|
|
43
43
|
|
|
44
44
|
## Failure-mode table
|
|
45
45
|
|
|
@@ -48,13 +48,13 @@ The script gates mechanics. These three are yours, and it will not make them for
|
|
|
48
48
|
| `E403 ... Two-factor authentication or granular access token with bypass 2fa enabled is required` — no OTP prompt | Account has no 2FA enrolled AND token (if any) lacks Bypass-2FA | Create granular token with all four requirements above, or enroll 2FA |
|
|
49
49
|
| Same E403 despite a fresh token | Token created without the Bypass-2FA checkbox, or Read-only, or package-scoped on a first publish | Re-create: RW + All packages + Bypass-2FA |
|
|
50
50
|
| `npm whoami` silent or non-zero | Token expired, or `.env.local` missing | Re-create the token; do not proceed — the preflight fails this row on purpose |
|
|
51
|
-
| `E404` on `npm view
|
|
51
|
+
| `E404` on `npm view <pkg>@<target>` after a target-confirmed publish receipt (npm answered HTTP 202) | The upload is **accepted, not yet available**. npm scans every new publish before it becomes installable: "typically around five minutes", "up to 15 minutes or more" at peak or depending on content/size, no service guarantee; the outcome may be published, **held for manual review**, or **blocked** (GitHub changelog 2026-07-28, read 2026-10-08, npm CLI 11.19.0). 5.10.0 stayed 404 for more than 25 minutes. A 404 alone does **not** tell you which of these applies — the package token cannot see the scan state | Let the script finish its tag/push/GitHub/site tail, then run `node scripts/release.mjs --reconcile --target <target>` (read-only, resumable; exit 0 only once installable). While still `pending`, check the owner-visible side: npm says a blocked package's publisher "may receive a notification with the option to appeal" — look at the npm account notifications/e-mail of the publishing account and the package page on npmjs.com. Do **not** rerun `--publish` and do not declare it blocked from the 404 |
|
|
52
52
|
| `ENEEDAUTH` | No login/token at all | Token flow above, or `npm login` |
|
|
53
53
|
| OTP prompt appears but flow is non-interactive (`!`-prefix, script) | No TTY for the prompt | Use the token flow, or a real terminal |
|
|
54
54
|
|
|
55
55
|
## Post-publish — the human half
|
|
56
56
|
|
|
57
|
-
`--publish` attempts registry verification and polls the live site itself. A target-confirmed receipt plus a delayed registry result is a reconciliation outcome, not a failed publish or a retry instruction. What still needs a person:
|
|
57
|
+
`--publish` attempts registry verification and polls the live site itself. A target-confirmed receipt plus a delayed registry result is a reconciliation outcome, not a failed publish or a retry instruction: the receipt is persisted as `.orchestrator/runtime/release-<target>.json`, and `--reconcile` resumes from it. The proof lives in the gitignored runtime dir of the checkout or worktree that ran `--publish`: run `--reconcile` from that same checkout, because any other checkout reports `proof-missing`. What still needs a person:
|
|
58
58
|
|
|
59
59
|
1. **Rotate/delete the token** at https://www.npmjs.com/settings/<user>/tokens. A token that ever transited a conversation, a screenshot, or any log is burned — rotate immediately.
|
|
60
60
|
2. **pi.dev gallery**: indexing is asynchronous — check https://pi.dev/packages later; do not block on it.
|
package/skills/release/SKILL.md
CHANGED
|
@@ -48,7 +48,7 @@ The same reading shows the other half: the GitHub releases for 3.15/3.18/3.19/3.
|
|
|
48
48
|
6. **Preflight.** `node scripts/release.mjs --check --json` — every row green. Run this after the release commit is pushed and its CI succeeds: the preflight requires a clean working tree, exact HEAD parity on both remotes and green CI on that commit. It derives its target from `package.json`, so checking the pre-bump version instead produces registry- and tag-collision failures.
|
|
49
49
|
7. **Publish.** `node scripts/release.mjs --publish` — **give it ≥600 s of wall clock, or run it in the background.** The script sets no spawn timeout on purpose (a kill mid-`npm publish` or mid-push is the very failure the receipt boundary exists to avoid), and the tail is slow by construction: each of the two `git push` remotes re-runs the husky pre-push full gate, plus up to 120 s of live-site polling. A 3-minute default command timeout kills it mid-tail.
|
|
50
50
|
|
|
51
|
-
The target-confirmed npm receipt is the irreversible boundary. Before that receipt, any failure aborts normally. After it, never rerun `--publish`: registry
|
|
51
|
+
The target-confirmed npm receipt is the irreversible boundary. Before that receipt, any failure aborts normally. After it, never rerun `--publish`: the script writes the publish proof `.orchestrator/runtime/release-<target>.json` (checked archive sha256 + sha512 integrity, receipt time, commit, tag), and a registry availability timeout/query/wait failure is reconciliation while the script still tags, pushes `main` + tag to both remotes, handles the GitHub release, and polls the live site. If tag/push fails after the receipt, the dependent GitHub-release and site steps are skipped and the script returns structured reconciliation guidance instead. Add the GitHub release for the new tag (`gh release create`) as part of this step, not "later" — "later" is what produced the three-second backfill above.
|
|
52
52
|
|
|
53
53
|
Steps 2–7 are one continuous act. A release left parked between step 4 and step 7 is exactly the `3.18.0` state: every surface says released, the registry disagrees.
|
|
54
54
|
|
|
@@ -68,7 +68,7 @@ Steps 2–7 are one continuous act. A release left parked between step 4 and ste
|
|
|
68
68
|
|
|
69
69
|
## After
|
|
70
70
|
|
|
71
|
-
`--publish` reports either **Release complete** or **Post-publish reconciliation required**. The latter means npm accepted the target release but registry propagation, tag/push, GitHub-release handling, or the live-site check still needs repair; a tag/push failure explicitly skips its dependent GitHub-release and site steps. **Do not rerun `--publish`**: reconcile the listed state directly, because a second publish cannot replace the immutable version. Exit `1` means a preflight/check failure or post-publish reconciliation is required; exit `2` remains a system/usage failure before the receipt.
|
|
71
|
+
`--publish` reports either **Release complete** or **Post-publish reconciliation required**. The latter means npm accepted the target release but registry propagation, tag/push, GitHub-release handling, or the live-site check still needs repair; a tag/push failure explicitly skips its dependent GitHub-release and site steps. **Do not rerun `--publish`**: reconcile the listed state directly, because a second publish cannot replace the immutable version. For the registry state run `node scripts/release.mjs --reconcile --target X.Y.Z [--json]` — read-only (no publish, tag, push or repo pack), bounded and resumable: it reports `installable` (exit 0) only when the exact version is served as `latest` with the proof's integrity AND a consumer download hashes to it, or `superseded` (exit 0: same checks pass, but `latest` already points at a later release); otherwise `pending` (rerun later), `failed` (integrity mismatch) or `proof-missing` (exit 1 — success is never inferred from `latest`). See `skills/npm-publish/SKILL.md` § Failure-mode table for why an accepted upload can stay unavailable. Exit `1` means a preflight/check failure or post-publish reconciliation is required; exit `2` remains a system/usage failure before the receipt.
|
|
72
72
|
|
|
73
73
|
After a complete release, `--publish` prints the remaining manual items (token rotation, async gallery indexing). Rotate the npm token — write tokens are short-lived by policy, and a token that transited a log or a chat is burned.
|
|
74
74
|
|
|
@@ -118,7 +118,7 @@ Commands resolved from Session Config per Phase 1.
|
|
|
118
118
|
| Auth-at-boundary pattern (`requireAuth`) | `grep -r 'requireAuth' --include='*.ts' src/ 2>/dev/null \|\| echo "N/A (no src/)"` |
|
|
119
119
|
| Zod validation on inputs | `grep -r 'z\.object\|z\.string\|z\.parse\|safeParse' --include='*.ts' src/ 2>/dev/null \|\| echo "N/A"` |
|
|
120
120
|
| No hardcoded secrets (scan for API key patterns) | `grep -r 'sk-\|api_key\s*=\s*"' --include='*.ts' --include='*.mts' --exclude-dir=node_modules . 2>/dev/null` — warn if found |
|
|
121
|
-
| No PAT/token in settings-allowlist entries (`.claude/settings.json`, `.claude/settings.local.json` — on-disk, incl. untracked) | `grep -nEo 'glpat-[A-Za-z0-9_-]{20,}\|ghp_[A-Za-z0-9]{36,}\|github_pat_[A-Za-z0-9_]{22,}\|sk-ant-[A-Za-z0-9_-]{20,}\|AKIA[0-9A-Z]{16}' .claude/settings.json .claude/settings.local.json 2>/dev/null \| grep -vE 'AKIAIOSFODNN7EXAMPLE\|-PLACEHOLDER'` — **fail** if any match (hard — unlike the `sk-` heuristic above, these 5 prefixes are high-signal; see SEC-
|
|
121
|
+
| No PAT/token in settings-allowlist entries (`.claude/settings.json`, `.claude/settings.local.json` — on-disk, incl. untracked) | `grep -nEo 'glpat-[A-Za-z0-9_-]{20,}\|ghp_[A-Za-z0-9]{36,}\|github_pat_[A-Za-z0-9_]{22,}\|sk-ant-[A-Za-z0-9_-]{20,}\|AKIA[0-9A-Z]{16}' .claude/settings.json .claude/settings.local.json 2>/dev/null \| grep -vE 'AKIAIOSFODNN7EXAMPLE\|-PLACEHOLDER'` — **fail** if any match (hard — unlike the `sk-` heuristic above, these 5 prefixes are high-signal; see SEC-025 in `.claude/rules/security.md`). This check is required because `check-owner-leakage.mjs` scans `git ls-files` only — `settings.local.json` is gitignored/untracked by convention and structurally invisible to it. |
|
|
122
122
|
| `.env*` files not tracked | `git ls-files | grep '\.env'` — fail if any `.env` (not `.env.example`) tracked |
|
|
123
123
|
| `.env.example` documents all secrets | `ls .env.example 2>/dev/null` |
|
|
124
124
|
|
|
@@ -17,9 +17,9 @@ Finalize session metrics by reading the wave data accumulated during execution:
|
|
|
17
17
|
- `total_duration_seconds`: from `started_at` to now (ISO 8601 diff)
|
|
18
18
|
- `total_waves`: count of completed waves
|
|
19
19
|
- `total_agents`: sum of agents across all waves
|
|
20
|
-
- `total_files_changed`: unique files changed across entire session (from `git diff --stat`)
|
|
20
|
+
- `total_files_changed`: unique files changed across entire session (from `git diff --stat`). **Not measured** (empty `$SESSION_RANGE`, #1505): the value is unknown, not zero — write `total_files_changed: 0` (`validateSession` requires a non-negative number; `null` or omitting the key is rejected) AND `_backfill_incomplete_fields: ["total_files_changed"]` (append if the array exists), which marks that 0 as a placeholder, the same pair `scripts/lib/session-record-repair.mjs` writes. A bare 0 without the marker claims a measured "no changes".
|
|
21
21
|
- `agent_summary`: `{complete: N, partial: N, failed: N, spiral: N}`
|
|
22
|
-
- **Coordinator-direct fallback (#1321):** when Wave History is empty AND the session ran coordinator-direct (housekeeping — `scripts/lib/session-shape.mjs:364-376` resolves it to ONE coordinator-direct wave that `/go` never hands to wave-executor, so no wave metrics are captured — or the express path), do NOT write `waves: []` / `total_waves: 0`. Write exactly one wave entry `{wave: 1, role: "Housekeeping", agent_count: 0, coordinator_direct: true, files_changed: <N>, quality: "<pass|fail|skip — the close quality-gate result>"}` with `total_waves: 1`, `total_agents: 0`. On this path both that wave's `files_changed` and `total_files_changed` are `git diff --name-only "$SESSION_RANGE" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor; an empty `$SESSION_RANGE` is a skip, not zero changes — the
|
|
22
|
+
- **Coordinator-direct fallback (#1321):** when Wave History is empty AND the session ran coordinator-direct (housekeeping — `scripts/lib/session-shape.mjs:364-376` resolves it to ONE coordinator-direct wave that `/go` never hands to wave-executor, so no wave metrics are captured — or the express path), do NOT write `waves: []` / `total_waves: 0`. Write exactly one wave entry `{wave: 1, role: "Housekeeping", agent_count: 0, coordinator_direct: true, files_changed: <N>, quality: "<pass|fail|skip — the close quality-gate result>"}` with `total_waves: 1`, `total_agents: 0`. On this path both that wave's `files_changed` and `total_files_changed` are `git diff --name-only "$SESSION_RANGE" | wc -l` (accessor: `plan-verification.md` § SESSION_START_REF accessor; an empty `$SESSION_RANGE` is a skip, not zero changes — write the unmeasured pair from the `total_files_changed` bullet above and name the accessor's reason in the record's `notes`). The single definition of this shape is `isCoordinatorDirectHousekeeping(record)` in `scripts/lib/session-schema/filters.mjs` (every wave `role: "Housekeeping"` AND `coordinator_direct: true`). Consumers key on it (eval gate-health, evolve effective-sizing), so write `role` and `coordinator_direct` exactly as shown. `session_type` alone never marks it.
|
|
23
23
|
3. Read `.orchestrator/metrics/events.jsonl` **once** and build both event aggregates in a single pass. If the file does not exist, treat both aggregates as zero events (omit both fields per the rules below) — do NOT fail the session close.
|
|
24
24
|
|
|
25
25
|
Filter all lines where `session == <session_id>`, then partition by `event` value:
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
1a. **Token Rollup (#644, #1244 — mechanical since #1436)** — no coordinator action. `scripts/emit-session.mjs` (step 2) runs `rollupSessionTokens()` itself whenever the incoming record carries no `total_tokens`: it resolves the session's raw UUID from `.orchestrator/current-session.json` (adopted only when its `semantic_session_id` equals the record's `session_id` and no process-local session id contradicts it), joins the sibling `subagents.jsonl` on `parent_session_id`, and fills `total_tokens`, `total_token_input`, `total_token_output`, the three cache buckets, `total_cost_usd`, `subagents_with_tokens`, `matched_records`, `cost_records_priced`, `cost_records_total` and `_token_schema` for every key the record does not already carry — plus `raw_session_id`. No matching records, an unreadable ledger or a foreign marker omits the fields with a WARN on stderr, never a fabricated `0`. An explicit key on `$METRICS_ENTRY` still wins. The same UUID also backs `session_start_ref` when STATE.md has none (the `head_sha` of the session's own `orchestrator.session.started` event).
|
|
33
33
|
|
|
34
|
-
**Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`, or an agent known only from `schema_version` 1 records — #1487 Pkt 1) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible. A session spanning the 2026-09-09 schema boundary therefore reports no cost (its v1 agents are unpriced candidates since 2026-10-02; before, it was priced over its v2 records only — 4 distinct fleet sessions carried such a partial sum). A subagent whose turns span several models carries a per-model `models_usage` breakdown in `subagents.jsonl` (#1470) and is priced part by part. The emit-session warning names the reason via `match_status` (ledger absent, ledger with no readable records, or records present but none for the own raw UUID); none of these means zero cost.
|
|
34
|
+
**Semantics:** `null` totals mean "no token data was captured for this session" — this is NOT the same as zero cost. Do NOT coerce null to 0 when displaying or summing across sessions. The same holds for `total_cost_usd`: it is omitted whenever `cost_records_priced < cost_records_total` (#1475) — at least one cost-relevant subagent record could not be priced (a model the price table does not know, a token-bearing turn naming no model, a transcript that yielded no tokens, e.g. one over the hook's size cap, or an agent with a start record whose stop found no transcript of its own, mostly `workflow-subagent`, or an agent known only from `schema_version` 1 records — #1487 Pkt 1) — and also when `cost_records_total` is 0, because there was nothing to price. It is never "$0" for an unknown cost and never a partial sum over the v2 agents the ledger can see. Phantom stops (#939, no start record) are no agents and never count. The two counters are persisted beside it, so a missing cost says whether part of the session was unpriceable or nothing was priceable. Blind spots remain: an agent whose start record fell outside the hook's tail window looks like a phantom, an agent with neither start nor stop record is invisible. A session spanning the 2026-09-09 schema boundary therefore reports no cost (its v1 agents are unpriced candidates since 2026-10-02; before, it was priced over its v2 records only — 4 distinct fleet sessions carried such a partial sum). A subagent whose turns span several models carries a per-model `models_usage` breakdown in `subagents.jsonl` (#1470) and is priced part by part. A written `total_cost_usd` is the **API list-price equivalent** of the metered tokens, never a payment: subscription quota, purchased credits and the amount actually billed are invisible to the rollup, and no record states any of them. Since #1470 emit-session marks this with `cost_basis: "api-list-equivalent"` beside every `total_cost_usd` it writes, and writes `pricing_table_date` (the `PRICING_TABLE_DATE` of `scripts/lib/telemetry/pricing.mjs` that priced the session) beside the cost counters. A record without these fields predates #1470; its cost is a list-price estimate too, priced by an older table, not a verified bill. The emit-session warning names the reason via `match_status` (ledger absent, ledger with no readable records, or records present but none for the own raw UUID); none of these means zero cost.
|
|
35
35
|
|
|
36
36
|
**Provenance (#949):** the rollup sums ONLY records carrying `subagent_transcript_found: true` — the flag the producer sets when it read the subagent's own transcript. Pre-#949 records carry the PARENT transcript's running totals and are excluded, so a session made up entirely of them now reports `null` rather than a fabricated sum (73 historical sessions, 96,148,781 phantom tokens, measured 2026-08-11). Two consequences for readers: totals already written into `sessions.jsonl` before 2026-08-11 were produced by the unfiltered recipe and are a series break, not a trend; and `matched_records` counts start records and phantom stops alike, so it is NOT the denominator for a coverage ratio — use `subagents_with_tokens` against the session's real agent count.
|
|
37
37
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
Acquire a distributed session-lock to detect parallel sessions in the same repo before initializing STATE.md. This prevents two concurrent Claude/Codex sessions from stomping each other's wave state and metrics writes.
|
|
12
12
|
|
|
13
|
-
**Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all five outcomes (active / stale / corrupt / unreadable / fs-error) when the prose path needs to acquire.
|
|
13
|
+
**Mechanical wiring (Epic #583, 2026-05-27):** The SessionStart hook (`hooks/on-session-start.mjs` → `hooks/_lib/lock-bootstrap.mjs`) now writes `.orchestrator/session.lock` mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via `readLock({ repoRoot: process.cwd() })`. Re-call `acquire()` only if `readLock()` returns `null` (mechanical hook failed) OR the existing lock's raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all five outcomes (active / stale / corrupt / unreadable / fs-error) when the prose path needs to acquire. Every `ok: true` from `acquire()` / `forceAcquire()` also rewrites `.orchestrator/runtime/lock-owner-proof.json` for the new lock (#1541), so the `/close` release stays proof-gated; a failed proof write is reported as `result.ownerProof: { ok: false, reason }`, never thrown.
|
|
14
14
|
|
|
15
15
|
```javascript
|
|
16
16
|
import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
|
|
@@ -50,6 +50,7 @@ The coordinator determines each agent's status after the wave completes:
|
|
|
50
50
|
| **partial** | Agent made progress but couldn't finish | Turn limit hit, or agent reports `STATUS: partial` | Carry forward remaining work to next wave with context |
|
|
51
51
|
| **failed** | Agent couldn't make meaningful progress | Tool errors, invalid assumptions, agent reports `STATUS: failed` | Re-dispatch with corrected instructions and narrower scope |
|
|
52
52
|
| **spiral** | Agent got stuck in an edit loop | Same file edited 3+ times (detected post-wave) | Revert agent's changes, narrow scope, split task if needed |
|
|
53
|
+
| **limit** | Agent stopped by a session/usage limit or HTTP 429 mid-wave — not by its task | Output ends in a limit/429 error instead of a `STATUS:` line | No re-dispatch, no carryover issue: read `git diff` in the agent's worktree, finish the small remaining fix passes coordinator-direct, log a Deviation via `appendDeviationOnDisk()` (`scripts/lib/state-md.mjs`) |
|
|
53
54
|
|
|
54
55
|
3. **Recovery protocol**:
|
|
55
56
|
- FAILED agent → log in STATE.md, add fix task to next wave with corrected instructions, AND auto-create a carryover issue (see "Carryover Auto-Create" below) the first time this task is marked FAILED (check STATE.md Wave History for a prior `→ issue #NNN` on this task before filing).
|