session-orchestrator 5.3.0 → 5.5.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/.agents/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.agents/skills/test-audit/SKILL.md +21 -0
- package/.agents/skills/test-audit/agents/openai.yaml +5 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.codex-plugin/skills/test-audit/SKILL.md +21 -0
- package/.codex-plugin/skills/test-audit/agents/openai.yaml +5 -0
- package/.cursor/commands/test-audit.md +14 -0
- package/.cursor/rules/060-evolve.mdc +28 -13
- package/.cursor/skills/claude-md-drift-check/SKILL.md +1 -1
- package/.cursor/skills/test-audit/SKILL.md +12 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +9 -31
- package/CHANGELOG.md +162 -0
- package/README.md +14 -10
- package/docs/agent-authoring.md +2 -2
- package/docs/baseline.md +10 -1
- package/docs/ci-setup.md +154 -4
- package/docs/components.md +11 -11
- package/docs/events-schema.md +25 -19
- package/docs/install.md +14 -2
- package/docs/migration-v4.md +24 -17
- package/docs/persona-panel.md +5 -4
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +27 -12
- package/docs/scope-collision-guard.md +40 -16
- package/docs/session-config-reference.md +61 -12
- package/docs/session-config-template.md +11 -2
- package/docs/vault-docs-architecture.md +8 -4
- package/hooks/_lib/hook-import-set.json +42 -29
- package/hooks/_lib/lock-bootstrap.mjs +118 -26
- package/hooks/config-protection.mjs +1 -1
- package/hooks/enforce-commands.mjs +28 -13
- package/hooks/enforce-scope.mjs +201 -23
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +5 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/on-session-start.mjs +57 -2
- package/hooks/on-stop.mjs +70 -168
- package/hooks/post-bash-write-verify.mjs +98 -17
- package/hooks/post-edit-validate.mjs +15 -5
- package/hooks/post-tool-batch-wave-signal.mjs +32 -152
- package/hooks/pre-bash-memory-propose-audit.mjs +71 -10
- package/hooks/pre-bash-sessions-ledger-guard.mjs +28 -2
- package/hooks/pre-bash-staging-fence.mjs +29 -8
- package/hooks/pre-edit-sessions-ledger-guard.mjs +162 -0
- package/hooks/pre-task-scope-disjoint.mjs +1143 -279
- package/hooks/subagent-telemetry.mjs +96 -19
- package/hooks/wave-scope-commit-guard.mjs +168 -72
- package/package.json +2 -6
- package/pi/prompts/test-audit.md +12 -0
- package/rules/README.md +6 -1
- package/rules/_index.md +3 -3
- package/rules/always-on/bash-harness-pitfalls.md +7 -1
- package/rules/opt-in-domain/prompt-caching.md +5 -1
- package/rules/opt-in-stack/backend-data.md +9 -1
- package/rules/opt-in-stack/backend.md +7 -1
- package/rules/opt-in-stack/frontend.md +6 -1
- package/rules/opt-in-stack/security-web.md +7 -1
- package/rules/opt-in-stack/swift.md +5 -1
- package/scripts/apply-session-learnings.mjs +513 -0
- package/scripts/archive-closed-prds.mjs +40 -12
- package/scripts/autopilot.mjs +57 -54
- package/scripts/backfill-abandoned-sessions.mjs +81 -6
- package/scripts/backfill-learnings-from-vault.mjs +49 -22
- package/scripts/baseline-archetypes.mjs +1 -1
- package/scripts/check-package-manager.mjs +62 -5
- package/scripts/codex-install.mjs +3 -0
- package/scripts/cursor-install.mjs +47 -6
- package/scripts/emit-session.mjs +298 -2
- package/scripts/export-hw-learnings.mjs +40 -10
- package/scripts/fleet-checkin.mjs +170 -0
- package/scripts/lib/agent-frontmatter.mjs +3 -3
- package/scripts/lib/auto-dream.mjs +7 -6
- package/scripts/lib/autopilot/flags.mjs +6 -1
- package/scripts/lib/autopilot/loop.mjs +32 -13
- package/scripts/lib/autopilot/stall-sampler.mjs +13 -9
- package/scripts/lib/baseline-archetypes.mjs +41 -22
- package/scripts/lib/blocked-commands-policy.mjs +40 -0
- package/scripts/lib/common.mjs +41 -0
- package/scripts/lib/config/host-paths.mjs +79 -0
- package/scripts/lib/config/persona-gate-wave.mjs +3 -3
- package/scripts/lib/config/reaper.mjs +7 -6
- package/scripts/lib/config/vault-integration.mjs +151 -16
- package/scripts/lib/config-schema.mjs +13 -0
- package/scripts/lib/config.mjs +75 -10
- package/scripts/lib/eval/engine.mjs +13 -0
- package/scripts/lib/events-retention-banner.mjs +393 -0
- package/scripts/lib/events-rotation.mjs +21 -6
- package/scripts/lib/events.mjs +72 -5
- package/scripts/lib/fleet-protocol.mjs +288 -0
- package/scripts/lib/gates/gate-baseline.mjs +13 -3
- package/scripts/lib/gates/gate-full.mjs +7 -2
- package/scripts/lib/gates/gate-helpers.mjs +93 -2
- package/scripts/lib/gates/gate-incremental.mjs +23 -22
- package/scripts/lib/gates/gate-per-file.mjs +16 -7
- package/scripts/lib/hardening.mjs +1 -0
- package/scripts/lib/harness-audit/categories/category4.mjs +8 -6
- package/scripts/lib/instruction-budget-guard.mjs +77 -11
- package/scripts/lib/learnings/expiry-sweep.mjs +420 -22
- package/scripts/lib/learnings/io.mjs +274 -37
- package/scripts/lib/learnings/schema.mjs +163 -15
- package/scripts/lib/learnings/sizing-subject.mjs +2 -2
- package/scripts/lib/lock-reaper.mjs +38 -14
- package/scripts/lib/maintenance-due-banner.mjs +23 -8
- package/scripts/lib/memory-proposals/schema.mjs +11 -5
- package/scripts/lib/named-baseline-resolver.mjs +53 -28
- package/scripts/lib/named-vault-resolver.mjs +147 -5
- package/scripts/lib/orphan-reaper/defaults.mjs +41 -0
- package/scripts/lib/orphan-reaper/no-follow-append.mjs +87 -0
- package/scripts/lib/orphan-reaper/ps-snapshot.mjs +181 -0
- package/scripts/lib/orphan-reaper/reaper-audit.mjs +323 -0
- package/scripts/lib/orphan-reaper/reaper-decide.mjs +395 -0
- package/scripts/lib/orphan-reaper/scan-throttle.mjs +144 -0
- package/scripts/lib/orphan-reaper/scan.mjs +510 -0
- package/scripts/lib/orphan-reaper/trigger.mjs +173 -0
- package/scripts/lib/orphan-reaper.mjs +47 -1315
- package/scripts/lib/owner-yaml.mjs +104 -6
- package/scripts/lib/persona-panel/catalog-loader.mjs +1 -1
- package/scripts/lib/platform.mjs +206 -2
- package/scripts/lib/plugin-update-banner.mjs +3 -1
- package/scripts/lib/process-group.mjs +134 -22
- package/scripts/lib/project-hygiene.mjs +21 -1
- package/scripts/lib/quality-gate.mjs +45 -5
- package/scripts/lib/resource-probe.mjs +1 -1
- package/scripts/lib/rule-loader.mjs +100 -62
- package/scripts/lib/rules-sync.mjs +27 -12
- package/scripts/lib/scope-baseline.mjs +119 -29
- package/scripts/lib/scope-echo.mjs +7 -0
- package/scripts/lib/scope-gate.mjs +194 -27
- package/scripts/lib/session-close-backfill.mjs +129 -4
- package/scripts/lib/session-end/tail-runner.mjs +10 -5
- package/scripts/lib/session-lock-shape.mjs +6 -3
- package/scripts/lib/session-lock.mjs +320 -67
- package/scripts/lib/session-record-repair.mjs +183 -26
- package/scripts/lib/session-registry.mjs +67 -17
- package/scripts/lib/session-schema/constants.mjs +30 -4
- package/scripts/lib/session-schema/validator.mjs +28 -1
- package/scripts/lib/session-start-probes.mjs +242 -68
- package/scripts/lib/session-token-rollup.mjs +252 -54
- package/scripts/lib/sessions-canonical.mjs +100 -9
- package/scripts/lib/sessions-staleness-banner.mjs +3 -1
- package/scripts/lib/sh-quote.mjs +29 -0
- package/scripts/lib/subagents-schema.mjs +30 -3
- package/scripts/lib/tail-window.mjs +27 -3
- package/scripts/lib/telemetry/pricing.mjs +121 -71
- package/scripts/lib/telemetry/sync.mjs +17 -0
- package/scripts/lib/tests-src-ratio.mjs +215 -23
- package/scripts/lib/tmux-layout/layouts.mjs +1 -11
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +12 -0
- package/scripts/lib/tmux-layout/tmux-shell.mjs +2 -11
- package/scripts/lib/ux-grill/reconcile.mjs +1 -1
- package/scripts/lib/validate/check-agents.mjs +1 -1
- package/scripts/lib/validate/check-hooks-symmetry.mjs +5 -0
- package/scripts/lib/validate/check-learnings-shell-writes.mjs +214 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +388 -27
- package/scripts/lib/validate/check-unicode-safety.mjs +1 -0
- package/scripts/lib/validate/check-unwired-features.mjs +1 -1
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +1 -1
- package/scripts/lib/validate-vendored-rules.mjs +206 -81
- package/scripts/lib/vault-mirror/auto-commit.mjs +5 -3
- package/scripts/lib/vault-mirror/namespace.mjs +47 -26
- package/scripts/lib/vault-mirror/process.mjs +350 -62
- package/scripts/lib/vault-mirror/pseudonym-map.mjs +14 -5
- package/scripts/lib/vault-mirror/render-learnings.mjs +66 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +28 -4
- package/scripts/lib/vault-mirror/telemetry.mjs +25 -11
- package/scripts/lib/vault-status/board-writer.mjs +53 -12
- package/scripts/lib/vault-status/narrative-mirror.mjs +40 -9
- package/scripts/lib/vcs-repo-spec.mjs +8 -0
- package/scripts/lib/wave-executor/dispatch-common.mjs +10 -18
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +1 -1
- package/scripts/lib/wave-executor/remote-dispatch.mjs +1 -1
- package/scripts/lib/wave-resource-gate.mjs +34 -8
- package/scripts/materialize-wave-scope.mjs +7 -4
- package/scripts/memory-propose.mjs +18 -5
- package/scripts/release-session-lock.mjs +2 -1
- package/scripts/release.mjs +11 -3
- package/scripts/repair-invalid-sessions.mjs +37 -4
- package/scripts/run-migrate-v2-cross-repo.mjs +171 -31
- package/scripts/run-quality-gate.mjs +331 -307
- package/scripts/self-update.mjs +229 -0
- package/scripts/site-numbers.mjs +163 -5
- package/scripts/sweep-expired-learnings.mjs +603 -20
- package/scripts/sweep-expired-rules.mjs +2 -1
- package/scripts/validate-plugin.mjs +7 -0
- package/scripts/validate-wave-scope.mjs +21 -2
- package/scripts/vault-backfill.mjs +50 -37
- package/scripts/vault-integration-watcher.mjs +2 -1
- package/scripts/vault-mirror.mjs +238 -75
- package/scripts/wave-scope-binding.mjs +2 -1
- package/skills/_shared/config-reading.md +3 -3
- package/skills/_shared/fleet-protocol.md +134 -0
- package/skills/_shared/state-ownership.md +41 -12
- package/skills/autopilot/SKILL.md +25 -11
- package/skills/bootstrap/SKILL.md +6 -4
- package/skills/bootstrap/_shared-template.md +97 -2
- package/skills/bootstrap/fast-template.md +4 -1
- package/skills/bootstrap/private-contract.md +2 -1
- package/skills/bootstrap/public-fallback.md +6 -3
- package/skills/bootstrap/references/bootstrap-refresh-lock-flow.md +5 -3
- package/skills/bootstrap/references/bootstrap-retroactive-flow.md +2 -1
- package/skills/bootstrap/standard-template.md +2 -0
- package/skills/claude-md-drift-check/SKILL.md +10 -10
- package/skills/claude-md-drift-check/checker.mjs +26 -18
- package/skills/discovery/probes/ssot-code-diff.mjs +12 -6
- package/skills/discovery/probes/vault-narrative-staleness.mjs +6 -2
- package/skills/discovery/probes/vault-staleness.mjs +9 -3
- package/skills/docs-orchestrator/SKILL.md +12 -5
- package/skills/docs-orchestrator/audience-mapping.md +5 -3
- package/skills/evolve/SKILL.md +13 -4
- package/skills/evolve/references/evolve-analyze-mode.md +42 -18
- package/skills/memory-cleanup/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +2 -2
- package/skills/persona-panel/persona-format.md +2 -2
- package/skills/persona-panel/presets/designer-lens.md +1 -1
- package/skills/persona-panel/presets/engineer-lens.md +1 -1
- package/skills/persona-panel/presets/pm-lens.md +1 -1
- package/skills/plan/mode-retro.md +19 -4
- package/skills/quality-gates/SKILL.md +6 -4
- package/skills/remote-offload/SKILL.md +4 -2
- package/skills/session-end/learning-patterns.md +23 -13
- package/skills/session-end/metrics-collection.md +6 -7
- package/skills/session-end/phase-3-2-docs-verification.md +7 -6
- package/skills/session-end/plan-verification.md +25 -9
- package/skills/session-end/references/phase-3-documentation-updates.md +13 -3
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +2 -0
- package/skills/session-end/references/session-summary-template.md +1 -1
- package/skills/session-end/session-metrics-write.md +65 -68
- package/skills/session-plan/SKILL.md +0 -3
- package/skills/session-start/SKILL.md +8 -2
- package/skills/session-start/references/operations-contract.md +6 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +32 -7
- package/skills/session-start/references/phase-1-7-vault-status-board.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +10 -3
- package/skills/test-audit/SKILL.md +152 -0
- package/skills/test-audit/references/campaign.md +111 -0
- package/skills/test-audit/references/mutate.sh +50 -0
- package/skills/vault-mirror/SKILL.md +21 -11
- package/skills/wave-executor/SKILL.md +2 -2
- package/skills/wave-executor/circuit-breaker.md +1 -1
- package/skills/wave-executor/references/wave-executor-state-init.md +3 -0
- package/skills/wave-executor/references/wave-loop-dispatch.md +11 -1
- package/skills/wave-executor/references/wave-loop-review.md +4 -6
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +13 -5
- package/templates/personas/README.md +5 -5
- package/templates/personas/accounting-compliance.v1.md +1 -1
- package/templates/personas/accounting-tax-advisor.v1.md +1 -1
- package/templates/personas/buyer-p1-cto.v1.md +1 -1
- package/templates/personas/buyer-p2-kanzlei.v1.md +1 -1
- package/templates/personas/buyer-p3-build.v1.md +1 -1
- package/templates/personas/buyer-p4-tech-veto.v1.md +1 -1
- package/templates/personas/buyer-p5-solo.v1.md +1 -1
- package/templates/personas/buyer-p6-ld.v1.md +1 -1
- package/templates/personas/klima-ai-expert.v1.md +1 -1
- package/templates/personas/klima-physicist.v1.md +1 -1
|
@@ -133,7 +133,16 @@ function jsonlPath() {
|
|
|
133
133
|
* Defense-in-depth byte ceiling for transcript reads (#624). The transcript path
|
|
134
134
|
* is harness-trusted, so this is not an exploitable vector — but the repo follows
|
|
135
135
|
* a bounded-read discipline: a pathologically large transcript is skipped (yields
|
|
136
|
-
* the
|
|
136
|
+
* the null-usage result) rather than read whole into memory.
|
|
137
|
+
*
|
|
138
|
+
* CEILING: a skipped transcript yields a stop record with found=true but null
|
|
139
|
+
* tokens and model; the session rollup counts it as an unpriced cost candidate
|
|
140
|
+
* (#1474) instead of reading it. Frequency per issue #1474 (2026-09-29): 2 of
|
|
141
|
+
* 10,482 subagent transcripts exceeded this size. REVISIT when oversized
|
|
142
|
+
* transcripts exceed ~0.1% of found stop records — count them from transcript
|
|
143
|
+
* sizes, because the ledger writes the same token-less record for an oversized,
|
|
144
|
+
* a usage-free and an unreadable transcript — then read large transcripts
|
|
145
|
+
* streamed instead of skipping them.
|
|
137
146
|
*/
|
|
138
147
|
const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024; // ~50 MB
|
|
139
148
|
|
|
@@ -423,13 +432,23 @@ function readStdinJson() {
|
|
|
423
432
|
* Four buckets, not two (#1244 / schema_version 2): `input_tokens`,
|
|
424
433
|
* `cache_read_input_tokens`, `cache_creation_input_tokens` and `output_tokens`
|
|
425
434
|
* are accumulated SEPARATELY per deduped turn, because they are billed at three
|
|
426
|
-
* different rates. `model` is
|
|
427
|
-
*
|
|
428
|
-
*
|
|
435
|
+
* different rates. `model` is what makes a cost estimate possible at all
|
|
436
|
+
* downstream, so it names only a model that actually carried tokens (#1470): a
|
|
437
|
+
* kept turn is token-bearing when its four clamped buckets sum to > 0. Exactly
|
|
438
|
+
* one distinct `message.model` across the token-bearing turns → that model;
|
|
439
|
+
* zero-token turns of another model (a trailing `<synthetic>` turn) are ignored.
|
|
440
|
+
* Two or more, or a token-bearing turn that names no model → null, an honest
|
|
441
|
+
* unknown, with the token sums unchanged. No token-bearing turn at all → the
|
|
442
|
+
* last kept block that carries one (null when absent). Two or more models with
|
|
443
|
+
* every token-bearing turn named → `modelsUsage` carries the four buckets per
|
|
444
|
+
* model, so the session rollup can price each part; otherwise it is null.
|
|
429
445
|
*
|
|
430
446
|
* @param {string|undefined|null} transcriptPath — absolute path from stdin
|
|
431
447
|
* @returns {{ tokenInputUncached: number|null, tokenCacheRead: number|null,
|
|
432
|
-
* tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null
|
|
448
|
+
* tokenCacheCreation: number|null, tokenOutput: number|null, model: string|null,
|
|
449
|
+
* modelsUsage: Array<{ model: string, token_input_uncached: number,
|
|
450
|
+
* token_cache_read: number, token_cache_creation: number,
|
|
451
|
+
* token_output: number }>|null }}
|
|
433
452
|
*/
|
|
434
453
|
function extractTranscriptUsage(transcriptPath) {
|
|
435
454
|
const nullResult = {
|
|
@@ -438,6 +457,7 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
438
457
|
tokenCacheCreation: null,
|
|
439
458
|
tokenOutput: null,
|
|
440
459
|
model: null,
|
|
460
|
+
modelsUsage: null,
|
|
441
461
|
};
|
|
442
462
|
try {
|
|
443
463
|
if (typeof transcriptPath !== 'string' || !transcriptPath.trim()) return nullResult;
|
|
@@ -523,22 +543,73 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
523
543
|
let tokenCacheRead = 0;
|
|
524
544
|
let tokenCacheCreation = 0;
|
|
525
545
|
let tokenOutput = 0;
|
|
546
|
+
/** Last non-null model over ALL kept turns — the zero-token fallback. */
|
|
526
547
|
let model = null;
|
|
548
|
+
/** Per-model bucket sums of the token-bearing kept turns (#1470). */
|
|
549
|
+
const billedModels = new Map();
|
|
550
|
+
/** A token-bearing turn that names no model: its price is unknowable. */
|
|
551
|
+
let unattributed = false;
|
|
552
|
+
// Per-turn clamp (#624): a turn's value counts ONLY when it is a non-negative
|
|
553
|
+
// integer. A poisoned value (negative, NaN, float like 10.5) contributes 0 so
|
|
554
|
+
// the good turns survive. An absent side contributes 0, not null.
|
|
555
|
+
const clamp = (value) => (Number.isInteger(value) && value >= 0 ? value : 0);
|
|
527
556
|
for (const { usage, model: turnModel } of kept) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
-
|
|
557
|
+
const inTok = clamp(usage.input_tokens);
|
|
558
|
+
const cacheRead = clamp(usage.cache_read_input_tokens);
|
|
559
|
+
const cacheCreation = clamp(usage.cache_creation_input_tokens);
|
|
560
|
+
const outTok = clamp(usage.output_tokens);
|
|
561
|
+
tokenInputUncached += inTok;
|
|
562
|
+
tokenCacheRead += cacheRead;
|
|
563
|
+
tokenCacheCreation += cacheCreation;
|
|
564
|
+
tokenOutput += outTok;
|
|
565
|
+
const billed = inTok + cacheRead + cacheCreation + outTok > 0;
|
|
566
|
+
if (turnModel === null) {
|
|
567
|
+
if (billed) unattributed = true;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
model = turnModel;
|
|
571
|
+
if (!billed) continue;
|
|
572
|
+
let part = billedModels.get(turnModel);
|
|
573
|
+
if (part === undefined) {
|
|
574
|
+
part = {
|
|
575
|
+
model: turnModel,
|
|
576
|
+
token_input_uncached: 0,
|
|
577
|
+
token_cache_read: 0,
|
|
578
|
+
token_cache_creation: 0,
|
|
579
|
+
token_output: 0,
|
|
580
|
+
};
|
|
581
|
+
billedModels.set(turnModel, part);
|
|
582
|
+
}
|
|
583
|
+
part.token_input_uncached += inTok;
|
|
584
|
+
part.token_cache_read += cacheRead;
|
|
585
|
+
part.token_cache_creation += cacheCreation;
|
|
586
|
+
part.token_output += outTok;
|
|
540
587
|
}
|
|
541
588
|
|
|
589
|
+
// Model attribution (#1470): only a token-bearing turn is an observation of
|
|
590
|
+
// its model; one distinct model → that model, two or more → null, none →
|
|
591
|
+
// the fallback above. A token-bearing turn that names no model also gives
|
|
592
|
+
// null: its tokens must not be priced at whichever other model was named.
|
|
593
|
+
// A mixed-model record carries its per-model sums as `modelsUsage`, which
|
|
594
|
+
// the session rollup prices part by part (#1470). Measured 2026-09-29 over
|
|
595
|
+
// 10,745 subagent transcripts under `~/.claude/projects`: 17 (0.16%)
|
|
596
|
+
// contained more than one model on token-bearing turns (0 of 917 in this
|
|
597
|
+
// repo), while 273 contained more than one model id only through zero-token
|
|
598
|
+
// `<synthetic>` turns, which this rule ignores. A model-less token-bearing
|
|
599
|
+
// turn gets no breakdown: its tokens belong to no part, so the record stays
|
|
600
|
+
// unpriced. The ledger now tells a mixed-model null (`models_usage`
|
|
601
|
+
// present) from a missing model (absent). The fallback also names the LAST
|
|
602
|
+
// model of a transcript whose turns all carry 0 tokens — measured 2026-09-29,
|
|
603
|
+
// 31 of 10,479 records, all `<synthetic>`; the session rollup prices a record
|
|
604
|
+
// whose four buckets are all 0 at $0 regardless of its model (#1474), so that
|
|
605
|
+
// name is harmless.
|
|
606
|
+
let modelsUsage = null;
|
|
607
|
+
if (unattributed) model = null;
|
|
608
|
+
else if (billedModels.size > 1) {
|
|
609
|
+
model = null;
|
|
610
|
+
modelsUsage = [...billedModels.values()];
|
|
611
|
+
} else if (billedModels.size === 1) [model] = billedModels.keys();
|
|
612
|
+
|
|
542
613
|
// The aggregates are guaranteed non-negative integers by per-turn clamping
|
|
543
614
|
// above (Σ of non-negative integers), so emit them directly.
|
|
544
615
|
return {
|
|
@@ -547,6 +618,7 @@ function extractTranscriptUsage(transcriptPath) {
|
|
|
547
618
|
tokenCacheCreation,
|
|
548
619
|
tokenOutput,
|
|
549
620
|
model,
|
|
621
|
+
modelsUsage,
|
|
550
622
|
};
|
|
551
623
|
} catch {
|
|
552
624
|
return nullResult;
|
|
@@ -752,7 +824,7 @@ async function main() {
|
|
|
752
824
|
// NO fallback to input.transcript_path: that path is the parent session
|
|
753
825
|
// transcript, and reading it is the #949 defect (every stop inherited the
|
|
754
826
|
// parent's running totals). A phantom stop gets null — the honest value.
|
|
755
|
-
const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model } =
|
|
827
|
+
const { tokenInputUncached, tokenCacheRead, tokenCacheCreation, tokenOutput, model, modelsUsage } =
|
|
756
828
|
subagentTranscriptFound
|
|
757
829
|
? extractTranscriptUsage(subagentTranscriptPath)
|
|
758
830
|
: {
|
|
@@ -761,6 +833,7 @@ async function main() {
|
|
|
761
833
|
tokenCacheCreation: null,
|
|
762
834
|
tokenOutput: null,
|
|
763
835
|
model: null,
|
|
836
|
+
modelsUsage: null,
|
|
764
837
|
};
|
|
765
838
|
|
|
766
839
|
// schema_version 2 (#1244): `token_input` is now BILLABLE PROMPT VOLUME —
|
|
@@ -776,11 +849,15 @@ async function main() {
|
|
|
776
849
|
}
|
|
777
850
|
if (tokenOutput !== null) record.token_output = tokenOutput;
|
|
778
851
|
|
|
779
|
-
// Model id (#1244) — null when the transcript exposes none
|
|
852
|
+
// Model id (#1244) — null when the transcript exposes none, or when its
|
|
853
|
+
// token-bearing turns are not attributable to one model (#1470). Cost is NOT
|
|
780
854
|
// computed here: pricing lives in scripts/lib/telemetry/pricing.mjs and is
|
|
781
855
|
// applied by the session rollup, so this hot-path hook keeps its import
|
|
782
856
|
// graph unchanged.
|
|
783
857
|
record.model = model;
|
|
858
|
+
// Per-model breakdown (#1470) — written only for a mixed-model record, so
|
|
859
|
+
// the rollup can price it part by part; absent on every other record.
|
|
860
|
+
if (modelsUsage !== null) record.models_usage = modelsUsage;
|
|
784
861
|
|
|
785
862
|
// Cost is best-effort / forward-compat (#624): the native transcript does
|
|
786
863
|
// NOT expose total_cost_usd today, so this is null in practice. No rate
|
|
@@ -22,26 +22,48 @@
|
|
|
22
22
|
// agent's fence — i.e. another agent's intent to stage that path.
|
|
23
23
|
//
|
|
24
24
|
// Behavior summary
|
|
25
|
-
// - No
|
|
26
|
-
//
|
|
27
|
-
// -
|
|
25
|
+
// - No wave-scope.json → exit 0 (no active wave; sub-mode B short-circuits).
|
|
26
|
+
// Sub-mode C still runs if a fence directory exists.
|
|
27
|
+
// - Sub-mode B, in order (#1493.1): a manifest bound to ANOTHER session is
|
|
28
|
+
// not this commit's scope (stand down, one stderr note); `enforcement:
|
|
29
|
+
// "off"` skips.
|
|
30
|
+
// - Non-empty allowedPaths: a staged path outside it BLOCKS (exit 1) under
|
|
31
|
+
// every enforcement level, `warn` included. Deliberately not tied to
|
|
32
|
+
// `enforcement`: in a warn-mode wave enforce-scope lets out-of-scope writes
|
|
33
|
+
// through, so this guard is the only hard PSA-004 stop left for a
|
|
34
|
+
// lint-staged sweep — softening it would weaken detection for the very
|
|
35
|
+
// class it exists to catch.
|
|
36
|
+
// - EMPTY allowedPaths is no longer a silent pass: a Discovery (read-only)
|
|
37
|
+
// manifest means no agent is committing, i.e. a coordinator commit between
|
|
38
|
+
// waves → allow with a note; a manifest older than the newest session
|
|
39
|
+
// clock may be a leftover or a parallel session's live one → report,
|
|
40
|
+
// never block; any other role means the `--union` step never completed →
|
|
41
|
+
// `strict` (the default when the field is absent, as in enforce-scope)
|
|
42
|
+
// blocks, `warn` reports and lets the commit through. Only this new
|
|
43
|
+
// verdict follows `enforcement`: it judges the coordinator's own
|
|
44
|
+
// bookkeeping, not a staged foreign file.
|
|
28
45
|
// - Cross-agent fence overlap → exit 1 with "staging-fence: cross-agent
|
|
29
46
|
// overlap" stderr.
|
|
30
47
|
// - --no-verify bypass: this hook is invoked as a git pre-commit hook;
|
|
31
48
|
// `git commit --no-verify` skips it entirely (operator opts out by name).
|
|
32
49
|
|
|
33
50
|
import { execSync } from 'node:child_process';
|
|
34
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
35
|
-
import { join } from 'node:path';
|
|
51
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
52
|
+
import { join, relative } from 'node:path';
|
|
36
53
|
|
|
37
54
|
// Re-use existing helpers from scripts/lib/hardening.mjs.
|
|
38
55
|
// IMPORTANT: import is resolved relative to THIS file's location (the hook
|
|
39
56
|
// ships with the session-orchestrator package), NOT relative to the git
|
|
40
57
|
// repo it is protecting. The two diverge when the hook is invoked from a
|
|
41
58
|
// consumer repo or a test tmp-dir.
|
|
42
|
-
import { pathMatchesPattern
|
|
59
|
+
import { pathMatchesPattern } from '../scripts/lib/hardening.mjs';
|
|
43
60
|
import { withStagingFenceLock } from '../scripts/lib/session-lock.mjs';
|
|
44
61
|
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
62
|
+
import { classifyEmptyScope, findOwnScopeFile, sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
|
|
63
|
+
import {
|
|
64
|
+
classifyManifestSession,
|
|
65
|
+
readProcessLocalSessionIds,
|
|
66
|
+
} from '../scripts/lib/session-identity/own-session.mjs';
|
|
45
67
|
|
|
46
68
|
// Resolve the "current" agent id from SO_WAVE_AGENT_ID (when set by the
|
|
47
69
|
// caller) or fall back to a PID-derived marker. The fence files themselves
|
|
@@ -56,19 +78,6 @@ const ownAgentId = process.env.SO_WAVE_AGENT_ID ?? null;
|
|
|
56
78
|
*/
|
|
57
79
|
const ALL_PATHS_MARKER = '*';
|
|
58
80
|
|
|
59
|
-
/**
|
|
60
|
-
* Build a regex that finds a staged path inside a `git add` command string.
|
|
61
|
-
* Word-boundary on both sides so `src/foo.ts` does not match `src/foo.ts.bak`.
|
|
62
|
-
* The path is escaped so glob metacharacters (`*`, `?`) and shell metas
|
|
63
|
-
* cannot be reinterpreted.
|
|
64
|
-
*
|
|
65
|
-
* LEGACY PATH ONLY since #1404 — see the fallback branch in findOverlaps.
|
|
66
|
-
*/
|
|
67
|
-
function pathRegex(p) {
|
|
68
|
-
const escaped = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
-
return new RegExp(`(?:^|[\\s'"])${escaped}(?:$|[\\s'"])`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
81
|
/**
|
|
73
82
|
* Normalise a path token so reader and writer compare the same spelling.
|
|
74
83
|
*
|
|
@@ -107,19 +116,13 @@ function pathsOverlap(fencePath, ourPath) {
|
|
|
107
116
|
* Walk a single sibling fence file and return the staged paths a sibling
|
|
108
117
|
* agent also recorded an intent to stage.
|
|
109
118
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* for an entry that has `command` and no `paths`.
|
|
118
|
-
* NAMED CEILING (BV-004): this fallback exists only to span the sessions
|
|
119
|
-
* running across the upgrade. REVISIT TRIGGER — remove it one minor
|
|
120
|
-
* release after 5.3.0 (i.e. in 5.4.0), by which point no process started
|
|
121
|
-
* before the upgrade can still be writing fence files. The command text it
|
|
122
|
-
* reads is never PRINTED (issue #1404) — only the fact of the match is.
|
|
119
|
+
* One entry shape is read: the #1404 `{ paths, command_hash, timestamp }`,
|
|
120
|
+
* whose path LIST is compared against our staged set. An entry without a
|
|
121
|
+
* `paths` array is skipped — that includes the pre-#1404 `{ command }` shape,
|
|
122
|
+
* whose raw-command regex fallback was removed in 5.4.x as its own revisit
|
|
123
|
+
* trigger demanded (#1487 point 16): the only fence writer,
|
|
124
|
+
* hooks/pre-bash-staging-fence.mjs, has written `paths` + `command_hash` and
|
|
125
|
+
* never `command` since 5.3.0, and no other hook or bridge writes fence files.
|
|
123
126
|
*/
|
|
124
127
|
function findOverlaps(fenceJsonPath, ourStaged) {
|
|
125
128
|
let body;
|
|
@@ -135,36 +138,126 @@ function findOverlaps(fenceJsonPath, ourStaged) {
|
|
|
135
138
|
const siblingAgent = body.agent_id ?? '<unknown>';
|
|
136
139
|
const matches = [];
|
|
137
140
|
for (const entry of body.staged_paths) {
|
|
138
|
-
if (Array.isArray(entry?.paths))
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
141
|
+
if (!Array.isArray(entry?.paths)) continue;
|
|
142
|
+
const hash = typeof entry.command_hash === 'string' ? entry.command_hash : '<no-hash>';
|
|
143
|
+
for (const raw of entry.paths) {
|
|
144
|
+
if (typeof raw !== 'string') continue;
|
|
145
|
+
const fencePath = normalizeStagedPath(raw);
|
|
146
|
+
for (const ours of ourStaged) {
|
|
147
|
+
if (pathsOverlap(fencePath, normalizeStagedPath(ours))) {
|
|
148
|
+
matches.push({ ourPath: ours, siblingAgent, fencePath, hash });
|
|
147
149
|
}
|
|
148
150
|
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const cmd = entry?.command;
|
|
153
|
-
if (typeof cmd !== 'string') continue;
|
|
154
|
-
for (const ours of ourStaged) {
|
|
155
|
-
if (pathRegex(ours).test(cmd)) {
|
|
156
|
-
matches.push({
|
|
157
|
-
ourPath: ours,
|
|
158
|
-
siblingAgent,
|
|
159
|
-
fencePath: '<legacy entry, pre-#1404>',
|
|
160
|
-
hash: '<legacy entry, pre-#1404>',
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
151
|
}
|
|
164
152
|
}
|
|
165
153
|
return matches;
|
|
166
154
|
}
|
|
167
155
|
|
|
156
|
+
/** mtime of a file in ms, or `null` when it cannot be stat'ed. */
|
|
157
|
+
function mtimeMsOf(file) {
|
|
158
|
+
try {
|
|
159
|
+
return statSync(file).mtimeMs;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Sub-mode B's decision, pure: which lines to print and whether to block.
|
|
167
|
+
*
|
|
168
|
+
* Until #1493.1 an empty `allowedPaths` skipped the check outright, the
|
|
169
|
+
* manifest's `role` and `enforcement: "off"` were never read, and a PEER
|
|
170
|
+
* session's manifest governed this session's commits. Each rung below closes
|
|
171
|
+
* one of those; the order is load-bearing — ownership first (a foreign
|
|
172
|
+
* manifest's enforcement level is not ours to honour), then `off`, then the
|
|
173
|
+
* verdict. `warn` softens ONLY the empty-union verdict; a non-empty violation
|
|
174
|
+
* blocks at every level (header § Behavior summary).
|
|
175
|
+
*
|
|
176
|
+
* @param {object} p
|
|
177
|
+
* @param {Record<string, unknown>} p.scope parsed manifest
|
|
178
|
+
* @param {string[]} p.stagedFiles
|
|
179
|
+
* @param {string} p.scopeRel manifest path for messages, repo-relative
|
|
180
|
+
* @param {Set<string>} p.ownIds this process's session ids
|
|
181
|
+
* @param {number|null} p.scopeMtimeMs
|
|
182
|
+
* @param {number|null} p.sessionAge ms since the newest session clock
|
|
183
|
+
* @returns {{action: 'pass'|'report'|'block', lines: string[]}}
|
|
184
|
+
*/
|
|
185
|
+
function scopeCommitVerdict({ scope, stagedFiles, scopeRel, ownIds, scopeMtimeMs, sessionAge }) {
|
|
186
|
+
if (stagedFiles.length === 0) return { action: 'pass', lines: [] };
|
|
187
|
+
|
|
188
|
+
// Same classifier and same disposition as enforce-scope Gate 3b: only a
|
|
189
|
+
// PROVABLY foreign manifest stands down; unknown ownership enforces.
|
|
190
|
+
if (classifyManifestSession(scope, ownIds).verdict === 'foreign') {
|
|
191
|
+
return {
|
|
192
|
+
action: 'pass',
|
|
193
|
+
lines: [`wave-scope-commit-guard: ${scopeRel} belongs to another session — its allowedPaths do not apply to this commit.`],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const enforcement = scope.enforcement ?? 'strict';
|
|
198
|
+
if (enforcement === 'off') return { action: 'pass', lines: [] };
|
|
199
|
+
|
|
200
|
+
const allowedPaths = Array.isArray(scope.allowedPaths) ? scope.allowedPaths : [];
|
|
201
|
+
if (allowedPaths.length > 0) {
|
|
202
|
+
const violations = stagedFiles.filter(
|
|
203
|
+
(f) => !allowedPaths.some((pattern) => pathMatchesPattern(f, pattern)),
|
|
204
|
+
);
|
|
205
|
+
if (violations.length === 0) return { action: 'pass', lines: [] };
|
|
206
|
+
// Blocks under `warn` too — see the header: in a warn-mode wave this is
|
|
207
|
+
// the only hard stop a lint-staged sweep meets before the commit exists.
|
|
208
|
+
return {
|
|
209
|
+
action: 'block',
|
|
210
|
+
lines: [
|
|
211
|
+
'✗ wave-scope-commit-guard: staged paths outside wave-scope.allowedPaths:',
|
|
212
|
+
...violations.map((v) => ` - ${v}`),
|
|
213
|
+
'',
|
|
214
|
+
'These files were likely added by lint-staged eslint --fix / prettier --write.',
|
|
215
|
+
'To proceed:',
|
|
216
|
+
' 1) git restore --staged <path> # for each foreign path',
|
|
217
|
+
' 2) git commit # retry',
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// From here on the verdict judges the coordinator's own bookkeeping (an empty
|
|
223
|
+
// union), not a staged foreign file — only this part follows `enforcement`.
|
|
224
|
+
const failAction = enforcement === 'strict' ? 'block' : 'report';
|
|
225
|
+
|
|
226
|
+
const role = typeof scope.role === 'string' ? scope.role.trim() : '';
|
|
227
|
+
const reason = classifyEmptyScope({
|
|
228
|
+
role,
|
|
229
|
+
parseOk: true,
|
|
230
|
+
scopeMtimeMs,
|
|
231
|
+
sessionStartMs: typeof sessionAge === 'number' ? Date.now() - sessionAge : null,
|
|
232
|
+
});
|
|
233
|
+
if (reason === 'read-only-role') {
|
|
234
|
+
// No wave agent commits (PSA-007), so a commit under a read-only manifest
|
|
235
|
+
// is the coordinator's own, between waves.
|
|
236
|
+
return {
|
|
237
|
+
action: 'pass',
|
|
238
|
+
lines: [`wave-scope-commit-guard: ${scopeRel} is a ${role} (read-only) manifest — treated as a coordinator commit between waves; allowedPaths not checked.`],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (reason === 'stale-manifest') {
|
|
242
|
+
// Older than the newest session clock of this working copy: a leftover OR a
|
|
243
|
+
// parallel session's live manifest, which the clocks cannot tell apart
|
|
244
|
+
// (scope-gate.mjs § "…and the reader may not be the OWNER") — so it is
|
|
245
|
+
// never grounds to block this commit.
|
|
246
|
+
return {
|
|
247
|
+
action: 'report',
|
|
248
|
+
lines: [`⚠ wave-scope-commit-guard: ${scopeRel} grants no paths and predates the newest session start in this working copy — a leftover or a parallel session's live manifest; allowedPaths not checked for this commit.`],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
action: failAction,
|
|
253
|
+
lines: [
|
|
254
|
+
`${failAction === 'block' ? '✗' : '⚠'} wave-scope-commit-guard: ${scopeRel} grants no paths${role ? ` for role \`${role}\`` : ''} — the coordinator's \`--union\` step did not complete, so none of the ${stagedFiles.length} staged path(s) can be checked against this wave's scope.`,
|
|
255
|
+
'Re-run the union step (skills/wave-executor/references/wave-loop-scope-manifest.md § 3.3), then retry the commit; do not hand-edit allowedPaths.',
|
|
256
|
+
...(failAction === 'block' ? [] : ['(enforcement: warn — reported, the commit proceeds)']),
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
168
261
|
/**
|
|
169
262
|
* Both sub-modes, in the order the pre-commit hook needs them. Every statement
|
|
170
263
|
* here used to sit at module top level, which meant a bare `import()` of this
|
|
@@ -181,7 +274,16 @@ async function main() {
|
|
|
181
274
|
// skills/wave-executor/wave-loop.md), so this guard never fired. findScopeFile
|
|
182
275
|
// returns null when no scope file exists at any precedence dir, preserving
|
|
183
276
|
// the "no active wave → exit 0" semantics below.
|
|
184
|
-
|
|
277
|
+
//
|
|
278
|
+
// #1504 point 6: the first manifest that is not provably a peer's, so a
|
|
279
|
+
// peer's higher-precedence manifest no longer hides this session's own one;
|
|
280
|
+
// when every candidate is foreign the first comes back and
|
|
281
|
+
// `scopeCommitVerdict` passes it as before. The git process inherits the
|
|
282
|
+
// committing session's env; there is no hook payload here, so the env tier is
|
|
283
|
+
// the only process-local identity.
|
|
284
|
+
const ownIds = new Set(readProcessLocalSessionIds());
|
|
285
|
+
const located = findOwnScopeFile(repoRoot, ownIds, classifyManifestSession);
|
|
286
|
+
const scopePath = located.path ?? located.foreignPath;
|
|
185
287
|
const fenceDir = join(repoRoot, '.orchestrator', 'staging-fence');
|
|
186
288
|
|
|
187
289
|
// -------------------------------------------------------------------------
|
|
@@ -200,22 +302,16 @@ async function main() {
|
|
|
200
302
|
process.exit(1);
|
|
201
303
|
}
|
|
202
304
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
process.stderr.write('To proceed:\n');
|
|
214
|
-
process.stderr.write(' 1) git restore --staged <path> # for each foreign path\n');
|
|
215
|
-
process.stderr.write(' 2) git commit # retry\n');
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
305
|
+
const verdict = scopeCommitVerdict({
|
|
306
|
+
scope: scope !== null && typeof scope === 'object' ? scope : {},
|
|
307
|
+
stagedFiles,
|
|
308
|
+
scopeRel: relative(repoRoot, scopePath) || scopePath,
|
|
309
|
+
ownIds,
|
|
310
|
+
scopeMtimeMs: mtimeMsOf(scopePath),
|
|
311
|
+
sessionAge: sessionAgeMs(repoRoot),
|
|
312
|
+
});
|
|
313
|
+
for (const line of verdict.lines) process.stderr.write(`${line}\n`);
|
|
314
|
+
if (verdict.action === 'block') process.exit(1);
|
|
219
315
|
}
|
|
220
316
|
|
|
221
317
|
// -------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://session-orchestrator.com",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"!**/.DS_Store"
|
|
55
55
|
],
|
|
56
56
|
"scripts": {
|
|
57
|
+
"update:local": "node scripts/self-update.mjs",
|
|
57
58
|
"test": "node scripts/check-package-manager.mjs && vitest --run",
|
|
58
59
|
"test:coverage": "node scripts/check-package-manager.mjs && vitest --run --coverage",
|
|
59
60
|
"test:watch": "node scripts/check-package-manager.mjs && vitest",
|
|
@@ -92,11 +93,6 @@
|
|
|
92
93
|
"prettier": "^3.0.0",
|
|
93
94
|
"vitest": "^4.1.11"
|
|
94
95
|
},
|
|
95
|
-
"overrides": {
|
|
96
|
-
"vite": "^8.0.16",
|
|
97
|
-
"fast-uri": "^3.1.5",
|
|
98
|
-
"brace-expansion": "^5.0.9"
|
|
99
|
-
},
|
|
100
96
|
"license": "MIT",
|
|
101
97
|
"repository": {
|
|
102
98
|
"type": "git",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Use when writing, reviewing, or pruning tests. Three modes, one value bar: a four-question gate before any new test is written, a focused audit of one scope, and a campaign over a whole repo or subsystem that removes the least useful tests under a coverage guard. Every delete, merge, or repair carries written evidence and a caught mutation. Triggered by \"audit the tests\", \"prune the test suite\", \"remove low-value tests\", \"test diet\", \"is this test worth adding\", /test-audit."
|
|
3
|
+
argument-hint: "[gate|audit|campaign] [scope] [--goal \\\"<goal>\\\"]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /test-audit
|
|
7
|
+
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/test-audit/SKILL.md`.
|
|
9
|
+
|
|
10
|
+
Arguments: $@
|
|
11
|
+
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/rules/README.md
CHANGED
|
@@ -11,7 +11,12 @@ It is **not** the always-on rule set of *this* repository — those live in
|
|
|
11
11
|
|
|
12
12
|
Every rule file here carries a
|
|
13
13
|
`<!-- source: session-orchestrator plugin (canonical: rules/<path>) -->` header.
|
|
14
|
-
|
|
14
|
+
In a rule with frontmatter it stands on the first line directly after the
|
|
15
|
+
closing `---` — the frontmatter itself must open on line 1, because Claude Code
|
|
16
|
+
reads a rule's frontmatter only there. A rule without frontmatter carries the
|
|
17
|
+
header on line 1. A path-scoped rule declares its file patterns twice, as
|
|
18
|
+
`globs:` and `paths:` with identical lists: `paths:` is the only key Claude Code
|
|
19
|
+
reads, `globs:` is what `rule-loader.mjs` reads first (#1449). On sync, plugin-sourced files are overwritten in the consumer while its
|
|
15
20
|
locally-authored rules are preserved (copy-on-write). The canonical manifest of
|
|
16
21
|
what vendors where is [`_index.md`](./_index.md).
|
|
17
22
|
|
package/rules/_index.md
CHANGED
|
@@ -40,7 +40,7 @@ baseline project type (`nextjs-saas`, `express-service`, `docker-service`,
|
|
|
40
40
|
- `always-on/build-value.md` — BV-001..004: the seven-rung build ladder, the four protections never simplified away
|
|
41
41
|
- `always-on/cross-session-messaging.md` — CSM-001..005: messaging is transport, not shared state; no permission laundering
|
|
42
42
|
- `always-on/loop-and-monitor.md` — LM-001..008: routing between `/goal`, Workflows, Channels, Monitor, `/loop`, and Routines
|
|
43
|
-
- `always-on/bash-harness-pitfalls.md` — six false-green shell-harness failure classes (path-scoped via
|
|
43
|
+
- `always-on/bash-harness-pitfalls.md` — six false-green shell-harness failure classes (path-scoped via `globs:` + `paths:` frontmatter carrying identical lists — `paths:` is what Claude Code reads, `globs:` what `rule-loader.mjs` reads first; #1449)
|
|
44
44
|
|
|
45
45
|
## opt-in-stack (vendored on match)
|
|
46
46
|
|
|
@@ -52,8 +52,8 @@ baseline project type (`nextjs-saas`, `express-service`, `docker-service`,
|
|
|
52
52
|
|
|
53
53
|
## opt-in-domain (vendored on match)
|
|
54
54
|
|
|
55
|
-
- `opt-in-domain/prompt-caching.md` — Anthropic prompt caching (breakpoint placement, TTL selection, pre-warming, verification) [archetypes: nextjs-saas, express-service, docker-service, monorepo-oss
|
|
55
|
+
- `opt-in-domain/prompt-caching.md` — Anthropic prompt caching (breakpoint placement, TTL selection, pre-warming, verification) [archetypes: nextjs-saas, express-service, docker-service, monorepo-oss]
|
|
56
56
|
|
|
57
57
|
## Sync mechanism
|
|
58
58
|
|
|
59
|
-
Consumer repos receive these files via `/bootstrap --sync-rules`. Re-running the command syncs every manifest category that has entries: universal entries always vendor, and archetype-tagged entries vendor only when the consumer repo's resolved archetype matches. Plugin-sourced files are overwritten while local rules are preserved (copy-on-write via the `<!-- source: session-orchestrator plugin ... -->` header on plugin files). Category entries must not share a basename, because all synced files flatten into `.claude/rules/`.
|
|
59
|
+
Consumer repos receive these files via `/bootstrap --sync-rules`. Re-running the command syncs every manifest category that has entries: universal entries always vendor, and archetype-tagged entries vendor only when the consumer repo's resolved archetype matches. Plugin-sourced files are overwritten while local rules are preserved (copy-on-write via the `<!-- source: session-orchestrator plugin ... -->` header on plugin files). That header stands on the first line directly after the closing `---` of a rule's frontmatter, because Claude Code reads frontmatter only when it opens on line 1; only a rule without frontmatter carries it on line 1 (#1449). A consumer copy written in the older format (header on line 1, frontmatter after it) still counts as plugin-owned and is rewritten into the current format on the next sync. Category entries must not share a basename, because all synced files flatten into `.claude/rules/`.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<!-- source: session-orchestrator plugin (canonical: rules/always-on/bash-harness-pitfalls.md) -->
|
|
2
1
|
---
|
|
3
2
|
globs:
|
|
4
3
|
- "**/*.sh"
|
|
@@ -6,8 +5,15 @@ globs:
|
|
|
6
5
|
- scripts/**
|
|
7
6
|
- tests/**
|
|
8
7
|
- .husky/**
|
|
8
|
+
paths:
|
|
9
|
+
- "**/*.sh"
|
|
10
|
+
- "**/*.bash"
|
|
11
|
+
- scripts/**
|
|
12
|
+
- tests/**
|
|
13
|
+
- .husky/**
|
|
9
14
|
tier: wave-only
|
|
10
15
|
---
|
|
16
|
+
<!-- source: session-orchestrator plugin (canonical: rules/always-on/bash-harness-pitfalls.md) -->
|
|
11
17
|
# Bash / Shell-Harness Pitfalls (Path-scoped)
|
|
12
18
|
|
|
13
19
|
Six recurring bash-harness failure classes, each surfaced by a live run rather
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-domain/prompt-caching.md) -->
|
|
2
1
|
---
|
|
3
2
|
globs:
|
|
4
3
|
- "**/lib/ai/**"
|
|
5
4
|
- "**/lib/llm/**"
|
|
6
5
|
- "**/providers/**"
|
|
6
|
+
paths:
|
|
7
|
+
- "**/lib/ai/**"
|
|
8
|
+
- "**/lib/llm/**"
|
|
9
|
+
- "**/providers/**"
|
|
7
10
|
tier: wave-only
|
|
8
11
|
---
|
|
12
|
+
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-domain/prompt-caching.md) -->
|
|
9
13
|
# Prompt Caching Rules (Path-scoped — repos using `@anthropic-ai/sdk` or `@ai-sdk/anthropic`)
|
|
10
14
|
|
|
11
15
|
> Path-scoped — applies anywhere a project imports `@anthropic-ai/sdk` or uses the `@ai-sdk/anthropic` provider. Out of scope: `session-orchestrator` itself (no SDK use; `backend.md` § "AI Provider Abstraction" already forbids direct SDK imports in business logic, and the orchestrator runs inside Claude Code's harness which manages caching at the platform layer).
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend-data.md) -->
|
|
2
1
|
---
|
|
3
2
|
globs:
|
|
4
3
|
- src/lib/db/**
|
|
@@ -8,8 +7,17 @@ globs:
|
|
|
8
7
|
- migrations/**
|
|
9
8
|
- src/lib/redis/**
|
|
10
9
|
- src/lib/queue/**
|
|
10
|
+
paths:
|
|
11
|
+
- src/lib/db/**
|
|
12
|
+
- src/lib/cache/**
|
|
13
|
+
- src/services/db/**
|
|
14
|
+
- supabase/**
|
|
15
|
+
- migrations/**
|
|
16
|
+
- src/lib/redis/**
|
|
17
|
+
- src/lib/queue/**
|
|
11
18
|
tier: wave-only
|
|
12
19
|
---
|
|
20
|
+
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend-data.md) -->
|
|
13
21
|
# Backend Data Rules (Path-scoped)
|
|
14
22
|
|
|
15
23
|
## Database (Supabase)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend.md) -->
|
|
2
1
|
---
|
|
3
2
|
globs:
|
|
4
3
|
- src/app/**
|
|
@@ -6,8 +5,15 @@ globs:
|
|
|
6
5
|
- src/lib/api/**
|
|
7
6
|
- src/routes/**
|
|
8
7
|
- src/app/api/**
|
|
8
|
+
paths:
|
|
9
|
+
- src/app/**
|
|
10
|
+
- src/services/**
|
|
11
|
+
- src/lib/api/**
|
|
12
|
+
- src/routes/**
|
|
13
|
+
- src/app/api/**
|
|
9
14
|
tier: wave-only
|
|
10
15
|
---
|
|
16
|
+
<!-- source: session-orchestrator plugin (canonical: rules/opt-in-stack/backend.md) -->
|
|
11
17
|
# Backend Rules (Path-scoped)
|
|
12
18
|
|
|
13
19
|
## Server Actions (Next.js)
|