session-orchestrator 3.24.0 → 4.0.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/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -268,6 +268,82 @@ export function extractTestCounts(output) {
|
|
|
268
268
|
return { passed, failed, total, files };
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* ANSI SGR / CSI escapes vitest emits when it believes it writes to a TTY.
|
|
273
|
+
* `runCheck` captures through a pipe, where vitest disables colour — but a
|
|
274
|
+
* runner invoked through a pty wrapper, or with `FORCE_COLOR`, still colours
|
|
275
|
+
* its output and would defeat every anchored match below.
|
|
276
|
+
*/
|
|
277
|
+
// eslint-disable-next-line no-control-regex
|
|
278
|
+
const ANSI_ESCAPE = /\u001b\[[0-9;]*[A-Za-z]/g;
|
|
279
|
+
|
|
280
|
+
/** Path shape of a test/spec file, as printed by vitest/jest. */
|
|
281
|
+
const TEST_FILE_PATH = String.raw`[^\s()]+\.(?:test|spec)\.[cm]?[jt]sx?`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* `FAIL <path>` — printed once per failing FILE in the "Failed Tests" and
|
|
285
|
+
* "Failed Suites" sections. Two real shapes, both covered by this anchor
|
|
286
|
+
* (measured against vitest 4.1.5, 2026-09-06):
|
|
287
|
+
*
|
|
288
|
+
* ` FAIL dead.test.mjs [ dead.test.mjs ]` ← suite died at import
|
|
289
|
+
* ` FAIL red.test.mjs > red > fails` ← a test case failed
|
|
290
|
+
*/
|
|
291
|
+
const FAIL_LINE = new RegExp(String.raw`^\s*FAIL\s+(${TEST_FILE_PATH})(?:\s|$)`);
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* `❯ <path> (N tests | M failed)` — the per-file line of the run summary.
|
|
295
|
+
*
|
|
296
|
+
* The `(… | … failed)` group is load-bearing, NOT decoration: vitest prefixes
|
|
297
|
+
* STACK FRAMES with the same `❯` and the frame carries the same file name
|
|
298
|
+
* (` ❯ red.test.mjs:3:33`). Requiring the parenthesised counts is what keeps a
|
|
299
|
+
* frame — and a PASSING file's summary line, ` ❯ green.test.mjs (2 tests)` —
|
|
300
|
+
* out of the result.
|
|
301
|
+
*/
|
|
302
|
+
const FILE_SUMMARY_FAIL_LINE = new RegExp(
|
|
303
|
+
String.raw`^\s*❯\s+(${TEST_FILE_PATH})\s+\(\d+\s+tests?[^)]*\|\s*\d+\s+failed`,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Name the test FILES a failing run blamed — the one thing the gate envelope
|
|
308
|
+
* never carried.
|
|
309
|
+
*
|
|
310
|
+
* Measured 2026-09-06: a husky pre-push run blocked `git push origin main` with
|
|
311
|
+
* `{"test":{"status":"fail","total":16417,"passed":16415,"failed":2,
|
|
312
|
+
* "files_total":662,"files_passed":661,"files_failed":1,…}}` — a COUNT and no
|
|
313
|
+
* name. `files_failed: 1` says a file died; reconstructing WHICH one cost a
|
|
314
|
+
* full manual re-materialisation of the tracked tree, even though the runner
|
|
315
|
+
* had printed the path in the very output this envelope was built from.
|
|
316
|
+
*
|
|
317
|
+
* Order is first-appearance and duplicates are dropped: vitest names the same
|
|
318
|
+
* file on its summary line AND once per failing case inside it, so a raw match
|
|
319
|
+
* list would repeat one path N times and read as N failing files.
|
|
320
|
+
*
|
|
321
|
+
* Absolute paths are relativised against `cwd` when it is a prefix — a gate
|
|
322
|
+
* report is read next to `git status`, so a repo-relative path is the useful
|
|
323
|
+
* form. A path outside `cwd` is left verbatim rather than turned into a `../..`
|
|
324
|
+
* chain that names nothing an operator can act on.
|
|
325
|
+
*
|
|
326
|
+
* @param {string} output - Captured test-runner stdout/stderr.
|
|
327
|
+
* @param {string} [cwd=process.cwd()] - Root to relativise absolute paths against.
|
|
328
|
+
* @returns {string[]} De-duplicated file paths, in first-appearance order.
|
|
329
|
+
*/
|
|
330
|
+
export function extractFailedTestFiles(output, cwd = process.cwd()) {
|
|
331
|
+
if (!output || typeof output !== 'string') return [];
|
|
332
|
+
|
|
333
|
+
const prefix = cwd.endsWith('/') ? cwd : `${cwd}/`;
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
|
|
336
|
+
for (const rawLine of output.split('\n')) {
|
|
337
|
+
const line = rawLine.replace(ANSI_ESCAPE, '');
|
|
338
|
+
const match = FAIL_LINE.exec(line) ?? FILE_SUMMARY_FAIL_LINE.exec(line);
|
|
339
|
+
if (!match) continue;
|
|
340
|
+
const file = match[1].startsWith(prefix) ? match[1].slice(prefix.length) : match[1];
|
|
341
|
+
seen.add(file);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return [...seen];
|
|
345
|
+
}
|
|
346
|
+
|
|
271
347
|
/**
|
|
272
348
|
* Admit a suite-count triple, or refuse to claim a measurement.
|
|
273
349
|
*
|
|
@@ -49,9 +49,26 @@ export const HW_SIGNALS = Object.freeze([
|
|
|
49
49
|
// Per-signal detectors — stateless, pure functions over arrays
|
|
50
50
|
// ---------------------------------------------------------------------------
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Terminal turn-event names accepted by the OOM detector.
|
|
54
|
+
*
|
|
55
|
+
* `orchestrator.turn.stopped` is the canonical name; `orchestrator.session.stopped`
|
|
56
|
+
* is the DEPRECATED legacy alias `hooks/on-stop.mjs` still emits beside it
|
|
57
|
+
* (carrying `deprecated: true`) for one deprecation generation. BOTH must be
|
|
58
|
+
* accepted here: every OOM record already on disk carries only the legacy name,
|
|
59
|
+
* so a hard switch would blind this detector to the entire history it exists to
|
|
60
|
+
* mine — and the `≥2 occurrences` aggregation threshold makes that silent
|
|
61
|
+
* (fewer hits, never an error).
|
|
62
|
+
*
|
|
63
|
+
* REMOVAL: drop `'orchestrator.session.stopped'` from this set on 2027-03-06,
|
|
64
|
+
* the date `hooks/on-stop.mjs` stops emitting the legacy name. See
|
|
65
|
+
* `skills/evolve/SKILL.md` § hardware-pattern, which documents the same window.
|
|
66
|
+
*/
|
|
67
|
+
const OOM_TERMINAL_EVENTS = new Set(['orchestrator.turn.stopped', 'orchestrator.session.stopped']);
|
|
68
|
+
|
|
52
69
|
function detectOomKill(events) {
|
|
53
70
|
const hits = events.filter((e) => {
|
|
54
|
-
if (e.event
|
|
71
|
+
if (!OOM_TERMINAL_EVENTS.has(e.event)) return false;
|
|
55
72
|
if (e.exit_code === 137) return true;
|
|
56
73
|
if (typeof e.error === 'string' && /out of memory|killed/i.test(e.error)) return true;
|
|
57
74
|
return false;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Checks: state-md-schema, sessions-jsonl-recent, learnings-prunable,
|
|
5
5
|
* vault-sync-validator, orphaned-session-lock
|
|
6
6
|
*
|
|
7
|
-
* Stdlib only
|
|
7
|
+
* Stdlib + intra-repo helpers only (node:fs, node:path, ./helpers.mjs,
|
|
8
|
+
* ../../session-schema/filters.mjs, ../../sessions-canonical.mjs,
|
|
9
|
+
* ../../config/section-extractor.mjs).
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { existsSync } from 'node:fs';
|
|
@@ -12,6 +14,7 @@ import { join, relative, sep } from 'node:path';
|
|
|
12
14
|
|
|
13
15
|
import { parseFrontmatter, safeRead, parseJsonl, pass, fail } from './helpers.mjs';
|
|
14
16
|
import { isRealSession } from '../../session-schema/filters.mjs';
|
|
17
|
+
import { canonicalizeSessions } from '../../sessions-canonical.mjs';
|
|
15
18
|
import { findSessionConfigBlock } from '../../config/section-extractor.mjs';
|
|
16
19
|
|
|
17
20
|
// Default session-lock TTL in hours — mirrors DEFAULT_TTL_HOURS in
|
|
@@ -95,17 +98,34 @@ export function runCategory4(root) {
|
|
|
95
98
|
{ latestCompletedAt: null, ageInDays: null },
|
|
96
99
|
'sessions.jsonl is empty'));
|
|
97
100
|
} else {
|
|
98
|
-
//
|
|
99
|
-
// (#
|
|
100
|
-
//
|
|
101
|
-
//
|
|
101
|
+
// Most-recent REAL session by completed_at, over the CANONICAL record
|
|
102
|
+
// set (#1209b) — canonicalizeSessions() already collapses duplicate
|
|
103
|
+
// `session_id` lines to the newest occurrence, drops the systemic
|
|
104
|
+
// double-stub twin, and removes any record a surviving one
|
|
105
|
+
// `supersedes`. A raw backward-scan-to-last-real-LINE (the pre-#1209b
|
|
106
|
+
// approach) cannot see this: a duplicate id whose LAST occurrence was
|
|
107
|
+
// retroactively corrected to `status: 'abandoned'` still let an
|
|
108
|
+
// EARLIER, now-superseded raw line for the same identity count as
|
|
109
|
+
// "recent". Max, not backward-scan, because canonical order is
|
|
110
|
+
// first-appearance order per id, not completed_at order.
|
|
111
|
+
//
|
|
112
|
+
// `keepUnidentified: true` — unlike the bare `readCanonicalSessions()`
|
|
113
|
+
// wrapper, this check has never required a `session_id` (it only ever
|
|
114
|
+
// read `completed_at`/`status`), so silently dropping id-less records
|
|
115
|
+
// would be a behaviour change this migration does not intend.
|
|
116
|
+
const parsedLines = [];
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
try { parsedLines.push(JSON.parse(line)); } catch { /* skip malformed */ }
|
|
119
|
+
}
|
|
120
|
+
const records = canonicalizeSessions(parsedLines, { keepUnidentified: true });
|
|
102
121
|
let lastObj = null;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
let lastMs = -Infinity;
|
|
123
|
+
for (const rec of records) {
|
|
124
|
+
if (!isRealSession(rec)) continue;
|
|
125
|
+
const ms = Date.parse(rec?.completed_at);
|
|
126
|
+
if (Number.isFinite(ms) && ms > lastMs) {
|
|
127
|
+
lastMs = ms;
|
|
128
|
+
lastObj = rec;
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
const completedAt = lastObj ? lastObj.completed_at : null;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* ram_total_gb: 18,
|
|
18
18
|
* hostname_hash: '<sha256 hex>',
|
|
19
19
|
* is_ssh: false,
|
|
20
|
-
* platform: 'claude' | 'codex' | 'cursor' |
|
|
20
|
+
* platform: 'claude' | 'codex' | 'cursor' | 'pi',
|
|
21
21
|
* first_seen: '2026-04-19T11:00:00Z',
|
|
22
22
|
* }
|
|
23
23
|
*
|
|
@@ -40,16 +40,25 @@ import {
|
|
|
40
40
|
unlinkSync,
|
|
41
41
|
} from 'node:fs';
|
|
42
42
|
import { digestSha256WithSalt } from './crypto-digest-utils.mjs';
|
|
43
|
+
import { resolvePrivateConfigDir } from './config/private-config-dir.mjs';
|
|
43
44
|
import path from 'node:path';
|
|
44
45
|
import os from 'node:os';
|
|
45
46
|
// NOTE (#1072 gate fix): platform.mjs is deliberately NOT imported statically.
|
|
46
|
-
// Its module level
|
|
47
|
-
// resolvePluginRoot(), which walk the filesystem
|
|
48
|
-
// package.json up the tree). session-lock.mjs
|
|
49
|
-
// put those import-time reads into every
|
|
50
|
-
// including fs-mocked test graphs
|
|
51
|
-
// red: the walk consumed the
|
|
52
|
-
//
|
|
47
|
+
// Its module level used to compute SO_PLATFORM/SO_PLUGIN_ROOT via
|
|
48
|
+
// detectPlatform()/resolvePluginRoot(), which walk the filesystem
|
|
49
|
+
// (existsSync + readFileSync of package.json up the tree). session-lock.mjs
|
|
50
|
+
// now imports this module, which put those import-time reads into every
|
|
51
|
+
// graph that imports session-lock — including fs-mocked test graphs
|
|
52
|
+
// (tests/lib/vault-mirror/process.test.mjs went red: the walk consumed the
|
|
53
|
+
// mocks' sequenced return values). The only two consumers in
|
|
54
|
+
// collectFingerprint (os name + platform) are async — load lazily.
|
|
55
|
+
//
|
|
56
|
+
// (#1153 P5) platform.mjs now exports memoized lazy getters
|
|
57
|
+
// (getPlatform(), getPluginRoot(), ...); the old SO_PLATFORM/SO_PROJECT_DIR/…
|
|
58
|
+
// names are deprecated `export let` live bindings that stay `undefined`
|
|
59
|
+
// until the matching getter has run elsewhere in the process. Destructure
|
|
60
|
+
// and CALL getPlatform() below — never read the deprecated SO_PLATFORM
|
|
61
|
+
// binding directly, or `platform` silently records null/undefined.
|
|
53
62
|
|
|
54
63
|
const FINGERPRINT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
55
64
|
const PLACEHOLDER_SALT = 'env-aware-v1-default-salt-replaced-by-owner-yaml';
|
|
@@ -345,8 +354,38 @@ export function lockHostCandidate(lock) {
|
|
|
345
354
|
// Paths
|
|
346
355
|
// ---------------------------------------------------------------------------
|
|
347
356
|
|
|
357
|
+
/**
|
|
358
|
+
* The host-private config directory — every per-host artefact below it
|
|
359
|
+
* (`host-private.json`, `owner.yaml`, the alias ledger) moves with it.
|
|
360
|
+
*
|
|
361
|
+
* Precedence (`SO_CONFIG_HOME` = the dir itself > `XDG_CONFIG_HOME` = its
|
|
362
|
+
* parent > homedir, each trimmed) now lives in ONE place: `resolvePrivateConfigDir`
|
|
363
|
+
* in `config/private-config-dir.mjs` (#1223) — a ZERO-IMPORT leaf (`node:os` +
|
|
364
|
+
* `node:path` only).
|
|
365
|
+
*
|
|
366
|
+
* The leaf is deliberately NOT `owner-yaml.mjs`, although that module re-exports
|
|
367
|
+
* the same function: owner-yaml.mjs statically imports `js-yaml`, and THIS module
|
|
368
|
+
* sits on `on-stop.mjs`'s import subgraph (via `session-lock.mjs`). Importing the
|
|
369
|
+
* resolver from owner-yaml.mjs therefore put a bare specifier on THAT subgraph and
|
|
370
|
+
* broke the GH#63 contract that on-stop degrades with one actionable line when
|
|
371
|
+
* node_modules is absent (measured: `ERR_MODULE_NOT_FOUND: Cannot find package
|
|
372
|
+
* 'js-yaml' imported from …/scripts/lib/owner-yaml.mjs`, 2 red tests in
|
|
373
|
+
* tests/hooks/on-stop.test.mjs). Never import owner-yaml.mjs from here.
|
|
374
|
+
*
|
|
375
|
+
* This says nothing about the WHOLE hook import graph, which is NOT bare-free.
|
|
376
|
+
* Measured 2026-09-05 over `hooks/_lib/hook-import-set.json` (150 entries, head
|
|
377
|
+
* 4b45130): two bare specifiers remain, both pre-existing — `owner-yaml.mjs →
|
|
378
|
+
* js-yaml` (reachable from on-session-start, on-session-end, post-edit-validate,
|
|
379
|
+
* skill-invocation-telemetry) and `scripts/lib/worktree/listing.mjs → zx`
|
|
380
|
+
* (on-session-start). Only `on-stop.mjs` degrades without node_modules today;
|
|
381
|
+
* the other four throw ERR_MODULE_NOT_FOUND. The guard in
|
|
382
|
+
* `tests/hooks/on-stop.test.mjs` allowlists exactly those two, so a THIRD one
|
|
383
|
+
* turns red.
|
|
384
|
+
*
|
|
385
|
+
* @returns {string} absolute path to the host-private config directory
|
|
386
|
+
*/
|
|
348
387
|
function _privateDir() {
|
|
349
|
-
return
|
|
388
|
+
return resolvePrivateConfigDir();
|
|
350
389
|
}
|
|
351
390
|
|
|
352
391
|
function _privateFile() {
|
|
@@ -393,7 +432,7 @@ export async function resolveSalt() {
|
|
|
393
432
|
* @param {string} [opts.salt] — override salt (tests)
|
|
394
433
|
*/
|
|
395
434
|
export async function collectFingerprint(opts = {}) {
|
|
396
|
-
const { SO_OS,
|
|
435
|
+
const { SO_OS, getPlatform } = await import('./platform.mjs');
|
|
397
436
|
const osName = SO_OS;
|
|
398
437
|
const arch = process.arch;
|
|
399
438
|
const cpus = os.cpus() || [];
|
|
@@ -417,7 +456,7 @@ export async function collectFingerprint(opts = {}) {
|
|
|
417
456
|
ram_total_gb: ramTotalGb,
|
|
418
457
|
hostname_hash: hostnameHash,
|
|
419
458
|
is_ssh: isSSH(),
|
|
420
|
-
platform:
|
|
459
|
+
platform: getPlatform(),
|
|
421
460
|
first_seen: new Date().toISOString(),
|
|
422
461
|
};
|
|
423
462
|
}
|
|
@@ -76,9 +76,9 @@
|
|
|
76
76
|
* self-relative growth ratchet calibrated just above our own baseline.
|
|
77
77
|
*/
|
|
78
78
|
|
|
79
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
79
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
80
80
|
import { basename, join } from 'node:path';
|
|
81
|
-
import { loadApplicableRules } from './rule-loader.mjs';
|
|
81
|
+
import { loadApplicableRules, parseGlobsFrontmatter } from './rule-loader.mjs';
|
|
82
82
|
|
|
83
83
|
/** Default directive ceiling (operator-chosen growth ratchet just above the ~457 baseline). */
|
|
84
84
|
export const DEFAULT_CEILING = 480;
|
|
@@ -140,6 +140,76 @@ export const DEFAULT_CEILING = 480;
|
|
|
140
140
|
*/
|
|
141
141
|
export const DEFAULT_BYTE_CEILING = 121000;
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Default byte ceiling for the PATH-SCOPED rule surface — the third axis, and
|
|
145
|
+
* the one that closes this guard's largest blind spot.
|
|
146
|
+
*
|
|
147
|
+
* Both ceilings above measure ONLY always-on rules (`loadApplicableRules`
|
|
148
|
+
* with `scopePaths: []` returns nothing else), so every `globs:`-scoped rule
|
|
149
|
+
* file was invisible to this guard by construction. That blind spot was
|
|
150
|
+
* MEASURED, not hypothesised: on 2026-09-06 @ `e4674109` the reconciliation
|
|
151
|
+
* engine had accumulated **43 generated rule files / 112,443 B** in
|
|
152
|
+
* `.claude/rules/` — 46.2 % of it pure frontmatter+provenance overhead — while
|
|
153
|
+
* this guard reported a comfortable 470/480 directives over 15 files and could
|
|
154
|
+
* not have fired on that growth at any ceiling, because it never counted it.
|
|
155
|
+
* A wave agent paid those bytes twice (rule-injection block + native delivery,
|
|
156
|
+
* see `docs/instruction-delivery.md` §5).
|
|
157
|
+
*
|
|
158
|
+
* Derivation (HR-105 — a threshold whose firing rate nothing records is not a
|
|
159
|
+
* rule): measured 2026-09-06 after the 43→8 consolidation AND the
|
|
160
|
+
* harness-parity repair that followed it, the path-scoped surface is
|
|
161
|
+
* **96,757 B over 11 files** (8 consolidated + the 3 hand-written scoped rules
|
|
162
|
+
* `testing.md` / `cli-design.md` / `bash-harness-pitfalls.md`), counted the
|
|
163
|
+
* same way as `totalBytes` — frontmatter stripped, via
|
|
164
|
+
* {@link countContentBytes}. RE-MEASURED 2026-09-06 later the same day
|
|
165
|
+
* (`computeInstructionBudget({repoRoot}).bySurface.generated` @ `bc49301b` →
|
|
166
|
+
* `{ bytes: 109593, files: 11 }`): the live figure is **109,593 B over the same
|
|
167
|
+
* 11 files**, ×1.13 under the unchanged 124,000 ceiling — restored prose, not
|
|
168
|
+
* reconciliation growth (the consolidation pass had dropped 10 learnings'
|
|
169
|
+
* bodies, which were written back into the same 8 files).
|
|
170
|
+
*
|
|
171
|
+
* CORRECTED 2026-09-06, one wave after the line above was first written. Two
|
|
172
|
+
* things in that first derivation no longer hold:
|
|
173
|
+
*
|
|
174
|
+
* - The base was **95,277 B**, measured while the 8 consolidated files still
|
|
175
|
+
* carried `globs:` with no `paths:`. Adding `paths:` + `learning-key:` did
|
|
176
|
+
* NOT move this number by a single byte — {@link countContentBytes} strips
|
|
177
|
+
* frontmatter — so the +1,480 B is entirely the 8 × 185 B provenance note
|
|
178
|
+
* the repair added to the BODIES. Written down because the number moved and
|
|
179
|
+
* the cause was hand-editing, NOT the reconciliation growth this axis
|
|
180
|
+
* exists to catch.
|
|
181
|
+
* - "The ~30 % headroom is deliberately the SAME relative slack the byte
|
|
182
|
+
* ceiling above carries" was simply false. {@link DEFAULT_BYTE_CEILING}
|
|
183
|
+
* carries +5 % (121,000 / 115,730 = 1.046), not +30 %. The axes are NOT
|
|
184
|
+
* calibrated alike, and saying they were hid a 6× difference in tolerance.
|
|
185
|
+
*
|
|
186
|
+
* The ceiling STAYS at 124,000 rather than re-deriving upward to
|
|
187
|
+
* 96,757 × 1.30 ≈ 126,000. The guard has never fired at this base, and raising
|
|
188
|
+
* an unfired ceiling to preserve a round multiplier is the threshold-patch
|
|
189
|
+
* `.claude/rules/development.md` § Guard & Threshold Design forbids. 124,000 is
|
|
190
|
+
* now ×1.28 over the live corpus — tighter than before, not looser.
|
|
191
|
+
* Re-derive it (never merely raise it) with:
|
|
192
|
+
*
|
|
193
|
+
* `node -e "…parseGlobsFrontmatter over .claude/rules/*.md…"` — or simply
|
|
194
|
+
* read `computeInstructionBudget().bySurface.generated` and re-apply ×1.30.
|
|
195
|
+
*
|
|
196
|
+
* What makes this ceiling reachable rather than decorative (HR-105 again — a
|
|
197
|
+
* threshold nobody can falsify is not a rule): the SAME measurement replayed
|
|
198
|
+
* against `git show HEAD:.claude/rules/*` at `e4674109`, i.e. the tree one
|
|
199
|
+
* commit before the consolidation, returns **137,410 B over 46 path-scoped
|
|
200
|
+
* files** — over this ceiling, so the guard WOULD have fired there and is
|
|
201
|
+
* silent at 96,757 B / 11 files now. Firing rate on the two states that exist:
|
|
202
|
+
* 1 of 2. That is the intended condition — reconciliation growth, not the
|
|
203
|
+
* hand-written corpus, which contributes 3 of the 11 files.
|
|
204
|
+
*
|
|
205
|
+
* (Note 137,410 is not the 112,443 B the audit quotes for the 43 generated
|
|
206
|
+
* files: that figure is raw bytes INCLUDING frontmatter and EXCLUDING the 3
|
|
207
|
+
* hand-written scoped rules; this axis strips frontmatter and counts all 46.
|
|
208
|
+
* Two different populations — see `.claude/rules/measurement-discipline.md`
|
|
209
|
+
* § "the unnamed population".)
|
|
210
|
+
*/
|
|
211
|
+
export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
|
|
212
|
+
|
|
143
213
|
/**
|
|
144
214
|
* Read the `instruction-budget:` nested block from the `## Session Config`
|
|
145
215
|
* section of CLAUDE.md (or AGENTS.md) at `repoRoot`. Synchronous + never throws.
|
|
@@ -412,6 +482,65 @@ function sumBytes(entries) {
|
|
|
412
482
|
return bytes;
|
|
413
483
|
}
|
|
414
484
|
|
|
485
|
+
/**
|
|
486
|
+
* Measure the PATH-SCOPED rule surface: every `.claude/rules/*.md` whose
|
|
487
|
+
* frontmatter carries `globs:` (or its `paths:` alias — issue #795), i.e.
|
|
488
|
+
* exactly the complement of the always-on set the three tier surfaces above
|
|
489
|
+
* measure.
|
|
490
|
+
*
|
|
491
|
+
* This cannot reuse `loadApplicableRules`: that loader takes a `scopePaths`
|
|
492
|
+
* list and returns the rules APPLICABLE to it, so with `scopePaths: []` it
|
|
493
|
+
* yields always-on rules only, and with a non-empty list it yields a
|
|
494
|
+
* scope-dependent subset. Neither answers "how big is the path-scoped corpus",
|
|
495
|
+
* which is a property of the DIRECTORY, not of any one wave's file scope. The
|
|
496
|
+
* frontmatter reading is still delegated (`parseGlobsFrontmatter`), so the
|
|
497
|
+
* always-on/path-scoped split stays decided in exactly one place.
|
|
498
|
+
*
|
|
499
|
+
* Named `generated` at the call site because reconciliation output is what
|
|
500
|
+
* grows here, but it deliberately measures every path-scoped file, including
|
|
501
|
+
* the hand-written ones — a ceiling that skipped them would report a number
|
|
502
|
+
* the operator cannot reproduce from `ls .claude/rules/` (HR-106: the banner
|
|
503
|
+
* reports what the rule judges).
|
|
504
|
+
*
|
|
505
|
+
* Bytes are counted with {@link countContentBytes} — frontmatter stripped —
|
|
506
|
+
* so this axis is directly comparable to `totalBytes` and to the tier
|
|
507
|
+
* surfaces. Never throws: an unreadable dir or file yields `{bytes:0,files:0}`
|
|
508
|
+
* / is skipped, matching this module's never-throw posture.
|
|
509
|
+
*
|
|
510
|
+
* @param {string} rulesDir
|
|
511
|
+
* @returns {{ bytes: number, files: number }}
|
|
512
|
+
*/
|
|
513
|
+
function measurePathScopedSurface(rulesDir) {
|
|
514
|
+
let names;
|
|
515
|
+
try {
|
|
516
|
+
names = readdirSync(rulesDir);
|
|
517
|
+
} catch {
|
|
518
|
+
return { bytes: 0, files: 0 };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let bytes = 0;
|
|
522
|
+
let files = 0;
|
|
523
|
+
for (const name of names) {
|
|
524
|
+
if (!name.endsWith('.md')) continue;
|
|
525
|
+
let content;
|
|
526
|
+
try {
|
|
527
|
+
content = readFileSync(join(rulesDir, name), 'utf8');
|
|
528
|
+
} catch {
|
|
529
|
+
continue; // unreadable file — skip, never throw
|
|
530
|
+
}
|
|
531
|
+
let globs;
|
|
532
|
+
try {
|
|
533
|
+
({ globs } = parseGlobsFrontmatter(content));
|
|
534
|
+
} catch {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (globs === null) continue; // always-on — already counted by the tier surfaces
|
|
538
|
+
files += 1;
|
|
539
|
+
bytes += countContentBytes(content);
|
|
540
|
+
}
|
|
541
|
+
return { bytes, files };
|
|
542
|
+
}
|
|
543
|
+
|
|
415
544
|
/**
|
|
416
545
|
* Pure computation — always returns the full shape (never null, never throws).
|
|
417
546
|
*
|
|
@@ -517,6 +646,10 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
517
646
|
const ceiling = typeof opts.ceiling === 'number' ? opts.ceiling : DEFAULT_CEILING;
|
|
518
647
|
const byteCeiling =
|
|
519
648
|
typeof opts.byteCeiling === 'number' ? opts.byteCeiling : DEFAULT_BYTE_CEILING;
|
|
649
|
+
const generatedByteCeiling =
|
|
650
|
+
typeof opts.generatedByteCeiling === 'number'
|
|
651
|
+
? opts.generatedByteCeiling
|
|
652
|
+
: DEFAULT_GENERATED_BYTE_CEILING;
|
|
520
653
|
// #893 fix: 'coordinator' used to fall through to the `null` (untiered)
|
|
521
654
|
// branch below — silently measuring the WRONG rule set for a coordinator
|
|
522
655
|
// context (it never excluded `tier: wave-only`). Now explicitly recognised
|
|
@@ -531,11 +664,13 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
531
664
|
perFile: [],
|
|
532
665
|
ceiling,
|
|
533
666
|
byteCeiling,
|
|
667
|
+
generatedByteCeiling,
|
|
534
668
|
overDirectiveBudget: false,
|
|
535
669
|
overByteBudget: false,
|
|
670
|
+
overGeneratedBudget: false,
|
|
536
671
|
overBudget: false,
|
|
537
672
|
severity: 'ok',
|
|
538
|
-
bySurface: { coordinator: 0, wave: 0, always: 0 },
|
|
673
|
+
bySurface: { coordinator: 0, wave: 0, always: 0, generated: { bytes: 0, files: 0 } },
|
|
539
674
|
};
|
|
540
675
|
|
|
541
676
|
let allEntries;
|
|
@@ -574,6 +709,13 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
574
709
|
coordinator: sumBytes(alwaysOnCoordinator),
|
|
575
710
|
wave: sumBytes(alwaysOnWave),
|
|
576
711
|
always: sumBytes(alwaysOnAll.filter((e) => e.tier === 'always')),
|
|
712
|
+
// The fourth surface is deliberately a different SHAPE from its three
|
|
713
|
+
// siblings ({bytes, files} vs. a bare byte number): a path-scoped corpus
|
|
714
|
+
// grows by FILE COUNT as much as by size — 43 files averaging 2.6 kB is
|
|
715
|
+
// the shape this axis exists to catch — and a bare number would hide that.
|
|
716
|
+
// It is also the only surface disjoint from `totalBytes`, which counts
|
|
717
|
+
// always-on rules exclusively.
|
|
718
|
+
generated: measurePathScopedSurface(rulesDir),
|
|
577
719
|
};
|
|
578
720
|
|
|
579
721
|
// Surface-selected entry set for the PRIMARY totals. `context: null`
|
|
@@ -605,7 +747,9 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
605
747
|
// long-standing boundary semantics rather than inventing a second rule.
|
|
606
748
|
const overDirectiveBudget = totalDirectives > ceiling;
|
|
607
749
|
const overByteBudget = totalBytes > byteCeiling;
|
|
608
|
-
|
|
750
|
+
// Third axis, same strict `>` boundary semantics as the two above.
|
|
751
|
+
const overGeneratedBudget = bySurface.generated.bytes > generatedByteCeiling;
|
|
752
|
+
const overBudget = overDirectiveBudget || overByteBudget || overGeneratedBudget;
|
|
609
753
|
|
|
610
754
|
return {
|
|
611
755
|
totalDirectives,
|
|
@@ -613,8 +757,10 @@ export function computeInstructionBudget(opts = {}) {
|
|
|
613
757
|
perFile,
|
|
614
758
|
ceiling,
|
|
615
759
|
byteCeiling,
|
|
760
|
+
generatedByteCeiling,
|
|
616
761
|
overDirectiveBudget,
|
|
617
762
|
overByteBudget,
|
|
763
|
+
overGeneratedBudget,
|
|
618
764
|
overBudget,
|
|
619
765
|
severity: overBudget ? 'warn' : 'ok',
|
|
620
766
|
bySurface,
|
|
@@ -655,6 +801,7 @@ export function checkInstructionBudget(opts = {}) {
|
|
|
655
801
|
enabled: true,
|
|
656
802
|
ceiling: DEFAULT_CEILING,
|
|
657
803
|
'byte-ceiling': DEFAULT_BYTE_CEILING,
|
|
804
|
+
'generated-byte-ceiling': DEFAULT_GENERATED_BYTE_CEILING,
|
|
658
805
|
mode: 'warn',
|
|
659
806
|
};
|
|
660
807
|
}
|
|
@@ -671,9 +818,19 @@ export function checkInstructionBudget(opts = {}) {
|
|
|
671
818
|
? cfg['byte-ceiling']
|
|
672
819
|
: DEFAULT_BYTE_CEILING;
|
|
673
820
|
|
|
821
|
+
// Same precedence as the two axes above: explicit opt > Session Config >
|
|
822
|
+
// module default. Config-key is optional, so a repo that never heard of this
|
|
823
|
+
// axis still gets the measured default rather than `undefined`.
|
|
824
|
+
const generatedByteCeiling =
|
|
825
|
+
typeof opts.generatedByteCeiling === 'number'
|
|
826
|
+
? opts.generatedByteCeiling
|
|
827
|
+
: typeof cfg['generated-byte-ceiling'] === 'number'
|
|
828
|
+
? cfg['generated-byte-ceiling']
|
|
829
|
+
: DEFAULT_GENERATED_BYTE_CEILING;
|
|
830
|
+
|
|
674
831
|
let budget;
|
|
675
832
|
try {
|
|
676
|
-
budget = computeInstructionBudget({ ...opts, ceiling, byteCeiling });
|
|
833
|
+
budget = computeInstructionBudget({ ...opts, ceiling, byteCeiling, generatedByteCeiling });
|
|
677
834
|
} catch {
|
|
678
835
|
return null; // never throw out of the banner wrapper
|
|
679
836
|
}
|
|
@@ -689,6 +846,15 @@ export function checkInstructionBudget(opts = {}) {
|
|
|
689
846
|
if (budget.overByteBudget) {
|
|
690
847
|
axes.push(`bytes ${budget.totalBytes} > ${budget.byteCeiling}`);
|
|
691
848
|
}
|
|
849
|
+
if (budget.overGeneratedBudget) {
|
|
850
|
+
// Reported with its FILE COUNT, because the actionable lever on this axis
|
|
851
|
+
// is consolidating files, not trimming prose inside them (HR-106: the
|
|
852
|
+
// banner reports the number the rule judged).
|
|
853
|
+
axes.push(
|
|
854
|
+
`path-scoped ${budget.bySurface.generated.bytes} B over ` +
|
|
855
|
+
`${budget.bySurface.generated.files} files > ${budget.generatedByteCeiling} B`,
|
|
856
|
+
);
|
|
857
|
+
}
|
|
692
858
|
|
|
693
859
|
// `perFile` arrives sorted DESC by directive count. When ONLY the byte axis
|
|
694
860
|
// broke, that ordering points at the wrong files — re-sort by bytes so the
|