session-orchestrator 5.1.0 → 5.2.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/autopilot/SKILL.md +1 -0
- package/.agents/skills/bootstrap/SKILL.md +2 -0
- package/.agents/skills/brainstorm/SKILL.md +3 -0
- package/.agents/skills/close/SKILL.md +17 -0
- package/.agents/skills/debug/SKILL.md +2 -0
- package/.agents/skills/discovery/SKILL.md +2 -1
- package/.agents/skills/dispatcher/SKILL.md +2 -0
- package/.agents/skills/eli5/SKILL.md +2 -0
- package/.agents/skills/eval/SKILL.md +1 -0
- package/.agents/skills/evolve/SKILL.md +2 -1
- package/.agents/skills/go/SKILL.md +18 -0
- package/.agents/skills/grill/SKILL.md +2 -0
- package/.agents/skills/harness-audit/SKILL.md +16 -0
- package/.agents/skills/memory-cleanup/SKILL.md +1 -0
- package/.agents/skills/persona-panel/SKILL.md +1 -0
- package/.agents/skills/plan/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +17 -0
- package/.agents/skills/reconcile/SKILL.md +1 -0
- package/.agents/skills/release/SKILL.md +18 -0
- package/.agents/skills/repo-audit/SKILL.md +1 -0
- package/.agents/skills/spinout/SKILL.md +1 -0
- package/.agents/skills/sunset-review/SKILL.md +2 -0
- package/.agents/skills/test/SKILL.md +17 -0
- package/.agents/skills/ux-grill/SKILL.md +2 -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/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +2 -0
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +2 -0
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/convergence-monitoring.md +13 -0
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/npm-publish.md +13 -0
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +61 -0
- package/README.md +11 -9
- package/commands/session.md +10 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +11 -6
- package/docs/events-schema.md +4 -1
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +83 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +6 -4
- package/hooks/_lib/hook-import-set.json +46 -6
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-session-end.mjs +14 -2
- package/hooks/on-stop.mjs +43 -1
- package/hooks/post-bash-write-verify.mjs +3 -0
- package/hooks/pre-auq-clarity.mjs +3 -0
- package/hooks/pre-bash-issue-budget.mjs +103 -17
- package/hooks/pre-task-scope-disjoint.mjs +152 -3
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/convergence-monitoring.md +11 -0
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/npm-publish.md +11 -0
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +1 -1
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/backfill-abandoned-sessions.mjs +57 -3
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/emit-session.mjs +2 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +173 -9
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +133 -5
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/convergence-monitor.mjs +2 -2
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/instruction-budget-guard.mjs +186 -46
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +69 -3
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +642 -0
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +392 -7
- package/scripts/lib/session-close-backfill.mjs +58 -6
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +2 -1
- package/scripts/lib/user-invocable-skills.mjs +185 -0
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +2 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +366 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-skill-links.mjs +27 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +2 -2
- package/scripts/lib/validate/check-test-git-config-target.mjs +2 -2
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +2 -2
- package/scripts/lib/validate/check-unwired-features.mjs +91 -7
- package/scripts/lib/validate/check-validator-registration.mjs +2 -2
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate-vendored-rules.mjs +35 -9
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release.mjs +80 -35
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +216 -0
- package/scripts/validate-plugin.mjs +9 -0
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +2 -3
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +58 -4
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +14 -0
- package/skills/evolve/SKILL.md +8 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +6 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +10 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-plan/SKILL.md +1 -1
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +4 -3
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +19 -1
- package/skills/wave-executor/SKILL.md +7 -4
- package/skills/wave-executor/references/wave-executor-state-init.md +13 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +3 -1
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -743,6 +743,31 @@ export { splitSegments as splitChainSegments };
|
|
|
743
743
|
|
|
744
744
|
const ENV_ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
745
745
|
|
|
746
|
+
/**
|
|
747
|
+
* Verbs that name a WRITE TARGET as a bare operand — no redirect operator, no
|
|
748
|
+
* wrapper flag (#1366). A sibling of WRAPPER_UNWRAP's `fileArgFlags`: that table
|
|
749
|
+
* answers "does this WRAPPER flag's operand name a file the wrapper writes?",
|
|
750
|
+
* this one "does this VERB's operand name a file the verb writes?".
|
|
751
|
+
*
|
|
752
|
+
* Spec fields:
|
|
753
|
+
* - prefix: the operand's literal prefix (`of=`), matched on a token AFTER
|
|
754
|
+
* the resolved verb.
|
|
755
|
+
* - mode: the default redirect mode the write is equivalent to.
|
|
756
|
+
* - appendRe: operands that turn the write into an APPEND. `dd` truncates by
|
|
757
|
+
* default (`conv=notrunc` / `oflag=append` do not), so reporting
|
|
758
|
+
* `truncate` unconditionally would over-report a rule class that
|
|
759
|
+
* deliberately permits `>>`.
|
|
760
|
+
*
|
|
761
|
+
* Deliberately narrow — one entry, measured. `tee`/`sponge` and in-process
|
|
762
|
+
* writers (`node -e fs.writeFileSync`) are NOT here: they are not redirect-shaped
|
|
763
|
+
* and belong to a different rule class (see the `redirect-harness-memory`
|
|
764
|
+
* rationale in .orchestrator/policy/blocked-commands.json). REVISIT TRIGGER: a
|
|
765
|
+
* second `<verb> <prefix>file` writer showing up in a transcript census.
|
|
766
|
+
*/
|
|
767
|
+
const VERB_FILE_OPERANDS = new Map([
|
|
768
|
+
['dd', { prefix: 'of=', mode: 'truncate', appendRe: /^(?:conv=.*\bnotrunc\b|oflag=.*\bappend\b)/ }],
|
|
769
|
+
]);
|
|
770
|
+
|
|
746
771
|
/**
|
|
747
772
|
* Transparent process-wrapper table for verb resolution (#982), keyed by
|
|
748
773
|
* basename. Each spec describes how to skip a wrapper's own options so the
|
|
@@ -946,15 +971,37 @@ function resolveCore(segment, unknownFlagsTakeValue) {
|
|
|
946
971
|
if (spec.splitString
|
|
947
972
|
&& (text === '-S' || text === '--split-string'
|
|
948
973
|
|| text.startsWith('--split-string=') || text.startsWith('-S'))) {
|
|
974
|
+
// env(1) APPENDS the remaining argv to the split string: `env -S bash
|
|
975
|
+
// -c '…'` runs `bash -c '…'`, not `bash` (#1366). Collecting only the
|
|
976
|
+
// operand made the trailing `-c '…'` execute unseen — the segment then
|
|
977
|
+
// resolved to `verb: 'echo x > CLAUDE.md'` (the quoted payload read as
|
|
978
|
+
// the verb) and neither the match surface nor the redirect surface ever
|
|
979
|
+
// saw it. So the payload is the operand PLUS every remaining token, and
|
|
980
|
+
// the wrapper CONSUMES the segment: nothing is left to resolve as a
|
|
981
|
+
// verb, which is why this now yields `verb: null, index: -1`.
|
|
982
|
+
let operand = null;
|
|
949
983
|
if (text.startsWith('--split-string=')) {
|
|
950
|
-
|
|
984
|
+
operand = text.slice('--split-string='.length);
|
|
985
|
+
i++;
|
|
951
986
|
} else if (text !== '-S' && text !== '--split-string') {
|
|
952
|
-
|
|
953
|
-
} else if (i + 1 < segment.length) {
|
|
954
|
-
payloads.push(segment[i + 1].text);
|
|
987
|
+
operand = text.slice(2); // attached form: -S'string'
|
|
955
988
|
i++;
|
|
989
|
+
} else if (i + 1 < segment.length) {
|
|
990
|
+
operand = segment[i + 1].text;
|
|
991
|
+
i += 2;
|
|
992
|
+
} else {
|
|
993
|
+
i++; // `env -S` with no operand at all — nothing to collect
|
|
994
|
+
}
|
|
995
|
+
if (operand !== null) {
|
|
996
|
+
// Raw token texts, joined by one space. Quoting is NOT re-applied:
|
|
997
|
+
// the tokenizer already stripped it, and re-inventing it would be a
|
|
998
|
+
// second quoting grammar. The direction of that loss is safe — an
|
|
999
|
+
// over-wide payload can only make MORE text visible to the matcher
|
|
1000
|
+
// (fail-closed), never hide a redirect operator.
|
|
1001
|
+
const rest = segment.slice(i).map((t) => t.text);
|
|
1002
|
+
payloads.push(rest.length > 0 ? [operand, ...rest].join(' ') : operand);
|
|
1003
|
+
i = segment.length;
|
|
956
1004
|
}
|
|
957
|
-
i++;
|
|
958
1005
|
continue;
|
|
959
1006
|
}
|
|
960
1007
|
if (spec.shellFlags && spec.shellFlags.has(text)) { sawShellFlag = true; i++; continue; }
|
|
@@ -1189,15 +1236,65 @@ function dedupedSegmentPayloads(segment, resolved) {
|
|
|
1189
1236
|
if (resolved.verb && DASH_C_SHELLS.has(resolved.verb)) {
|
|
1190
1237
|
for (const p of dashCPayloads(segment, resolved.index)) payloadSet.add(p);
|
|
1191
1238
|
}
|
|
1239
|
+
addArgJoinedPayload(segment, resolved.verb, resolved.index, payloadSet);
|
|
1192
1240
|
if (resolved.alt) {
|
|
1193
1241
|
for (const p of resolved.alt.payloads) payloadSet.add(p);
|
|
1194
1242
|
if (resolved.alt.verb && DASH_C_SHELLS.has(resolved.alt.verb)) {
|
|
1195
1243
|
for (const p of dashCPayloads(segment, resolved.alt.index)) payloadSet.add(p);
|
|
1196
1244
|
}
|
|
1245
|
+
addArgJoinedPayload(segment, resolved.alt.verb, resolved.alt.index, payloadSet);
|
|
1197
1246
|
}
|
|
1198
1247
|
return [...payloadSet];
|
|
1199
1248
|
}
|
|
1200
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* Interpreters that JOIN their remaining arguments with a space and execute the
|
|
1252
|
+
* result — no `-c` flag, no wrapper option to key on (#1366).
|
|
1253
|
+
*
|
|
1254
|
+
* `eval` is the whole set today. It was already in SHELL_EXEC_INTERPRETERS, but
|
|
1255
|
+
* that set only powers the QUOTED-TOKEN match in matchSegments — it never fed
|
|
1256
|
+
* the payload traversal, so `eval 'echo x > CLAUDE.md'` carried a redirect no
|
|
1257
|
+
* redirect rule ever saw (`resolveSegmentVerb` reports `payloads: []` for it).
|
|
1258
|
+
*
|
|
1259
|
+
* NAMED CEILING (BV-004): only a LITERAL payload is reachable. `eval "$CMD"` and
|
|
1260
|
+
* `eval $(…)` do not exist as command text at hook time and stay in the existing
|
|
1261
|
+
* `unresolved` class (#641). REVISIT TRIGGER: a second arg-joining interpreter
|
|
1262
|
+
* showing up in a transcript census — extend the set, never special-case `eval`.
|
|
1263
|
+
*/
|
|
1264
|
+
const ARG_JOINING_INTERPRETERS = new Set(['eval']);
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Add the joined post-verb argument text as a recursion payload when `verb` is
|
|
1268
|
+
* an arg-joining interpreter. No-op otherwise — every other caller shape keeps
|
|
1269
|
+
* the pre-#1366 payload set byte-identical.
|
|
1270
|
+
*
|
|
1271
|
+
* Quoting is not re-applied for the same reason as the `env -S` tail (see
|
|
1272
|
+
* resolveCore): the tokenizer stripped it, and a wider payload can only make
|
|
1273
|
+
* more text visible to the matcher, never hide an operator.
|
|
1274
|
+
*
|
|
1275
|
+
* @param {Array<{ text: string, quoted: boolean }>} segment
|
|
1276
|
+
* @param {string|null} verb
|
|
1277
|
+
* @param {number} index — the verb's token index (`-1` when unresolved)
|
|
1278
|
+
* @param {Set<string>} payloadSet — mutated in place
|
|
1279
|
+
*/
|
|
1280
|
+
function addArgJoinedPayload(segment, verb, index, payloadSet) {
|
|
1281
|
+
if (!verb || index < 0 || !ARG_JOINING_INTERPRETERS.has(verb)) return;
|
|
1282
|
+
const words = [];
|
|
1283
|
+
for (let i = index + 1; i < segment.length; i++) {
|
|
1284
|
+
if (segment[i].redirect) {
|
|
1285
|
+
// A redirect belongs to the SEGMENT (`eval 'x' > f` redirects eval's own
|
|
1286
|
+
// stdout), never to the joined argv — the segment scan already reported
|
|
1287
|
+
// it, and re-reporting it through the payload would duplicate the entry.
|
|
1288
|
+
const end = redirectSpanEnd(segment, i);
|
|
1289
|
+
if (end > i) i = end;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
words.push(segment[i].text);
|
|
1293
|
+
}
|
|
1294
|
+
const joined = words.join(' ').trim();
|
|
1295
|
+
if (joined.length > 0) payloadSet.add(joined);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1201
1298
|
/**
|
|
1202
1299
|
* Match a blocked-pattern regex against tokenized chain segments — the shared
|
|
1203
1300
|
* core of commandMatchesBlocked, recursion-capable for `-c` payloads (#982).
|
|
@@ -1347,6 +1444,37 @@ function collectRedirectTargets(segments, out, depth, budget) {
|
|
|
1347
1444
|
out.push({ target: wa.value, mode: 'truncate', fd: null });
|
|
1348
1445
|
}
|
|
1349
1446
|
|
|
1447
|
+
// A VERB can name its write target as a bare operand, with no redirect
|
|
1448
|
+
// operator and no wrapper flag: `dd of=CLAUDE.md` truncates CLAUDE.md while
|
|
1449
|
+
// `> CLAUDE.md` is denied (measured ALLOW pre-#1366 against the real
|
|
1450
|
+
// policy). Keyed on the RESOLVED verb, so `sudo dd of=X` is reached through
|
|
1451
|
+
// the existing wrapper unwrap; both readings contribute, as above.
|
|
1452
|
+
const seenVerbOperands = new Set();
|
|
1453
|
+
for (const reading of resolved.alt ? [resolved, resolved.alt] : [resolved]) {
|
|
1454
|
+
const spec = reading.verb ? VERB_FILE_OPERANDS.get(reading.verb) : null;
|
|
1455
|
+
if (!spec || reading.index < 0) continue;
|
|
1456
|
+
let operand = null;
|
|
1457
|
+
let append = false;
|
|
1458
|
+
for (let k = reading.index + 1; k < segment.length; k++) {
|
|
1459
|
+
const text = segment[k].text;
|
|
1460
|
+
if (segment[k].redirect) continue;
|
|
1461
|
+
if (text.startsWith(spec.prefix)) operand = text.slice(spec.prefix.length);
|
|
1462
|
+
else if (spec.appendRe?.test(text)) append = true;
|
|
1463
|
+
}
|
|
1464
|
+
if (operand === null || operand.length === 0) continue;
|
|
1465
|
+
const mode = append ? 'append' : spec.mode;
|
|
1466
|
+
const key = `${operand}:${mode}`;
|
|
1467
|
+
if (seenVerbOperands.has(key)) continue;
|
|
1468
|
+
seenVerbOperands.add(key);
|
|
1469
|
+
if (/[$`]/.test(operand)) {
|
|
1470
|
+
// Same fail-visible rule as a redirect operand (#983) — never guess at a
|
|
1471
|
+
// variable or a command substitution, never silently drop it either.
|
|
1472
|
+
out.push({ target: null, mode, fd: null, unresolved: true });
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
out.push({ target: operand, mode, fd: null });
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1350
1478
|
// Both readings of an ambiguous unknown flag contribute payloads (#1000),
|
|
1351
1479
|
// UNIONED and de-duped (HIGH-2). Walking parse A only let
|
|
1352
1480
|
// `env -Q x bash -c 'echo pwned > CLAUDE.md'` — which resolves to the
|
|
@@ -5,6 +5,25 @@ import { preprocessBlockLines } from './block-preprocess.mjs';
|
|
|
5
5
|
* drift-check.mjs — Parser for the top-level `drift-check:` YAML block.
|
|
6
6
|
*
|
|
7
7
|
* Ported from config-yaml-parser.sh (v2).
|
|
8
|
+
*
|
|
9
|
+
* SCOPE NOTE — this file parses FLAGS ONLY; it implements no check. The
|
|
10
|
+
* `check-command-count` flag below toggles the `command-count` surface in
|
|
11
|
+
* `skills/claude-md-drift-check/checker.mjs`, which is where the counting lives.
|
|
12
|
+
*
|
|
13
|
+
* `command-count` STAYS on `ls commands/*.md | wc -l` on purpose. The checker is
|
|
14
|
+
* GENERIC over any consumer repo, where a slash command still IS a
|
|
15
|
+
* `commands/*.md` file. The 2026-09-16 command→skill fold (#1370) is a fact
|
|
16
|
+
* about THIS plugin — 24 command files folded into their same-named
|
|
17
|
+
* `skills/<name>/SKILL.md` behind an explicit `user-invocable: true` — so
|
|
18
|
+
* teaching the shared checker that union would mis-measure every repo that never
|
|
19
|
+
* folded. `scripts/site-numbers.mjs` `countCommands()` carries the union for this
|
|
20
|
+
* plugin's own tile, and names the divergence in its docblock.
|
|
21
|
+
*
|
|
22
|
+
* CONSEQUENCE for THIS repo, left deliberately visible rather than papered over:
|
|
23
|
+
* a prose claim of the shape "N commands" / "N slash commands" in a
|
|
24
|
+
* drift-check-scanned doc is compared against the `commands/` listing (2 after
|
|
25
|
+
* the fold), so such a claim must either be dropped or reworded off the
|
|
26
|
+
* `\b(\d+)\s+(?:\/)?commands?\b` pattern the checker matches.
|
|
8
27
|
*/
|
|
9
28
|
|
|
10
29
|
/**
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
|
|
46
46
|
import { existsSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
47
47
|
import { join } from 'node:path';
|
|
48
|
-
import {
|
|
48
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
49
49
|
|
|
50
50
|
const DEFAULT_INTERVAL_S = 2;
|
|
51
51
|
const EVENTS_FILE_REL = '.orchestrator/metrics/events.jsonl';
|
|
@@ -502,7 +502,7 @@ function main() {
|
|
|
502
502
|
|
|
503
503
|
// Run the tail loop only when executed as a script. Importing the module (for
|
|
504
504
|
// unit tests over `classify`) must not parse vitest's argv and exit 1.
|
|
505
|
-
if (
|
|
505
|
+
if (isMainModule(import.meta.url)) {
|
|
506
506
|
main();
|
|
507
507
|
}
|
|
508
508
|
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
-
import { pathToFileURL } from 'node:url';
|
|
22
21
|
|
|
23
22
|
import { matcherMatches, readHookDecision } from './pi-hook-bridge.mjs';
|
|
24
23
|
import { resolvePluginRoot } from './plugin-root.mjs';
|
|
24
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
25
25
|
|
|
26
26
|
export const CURSOR_TO_CANONICAL_EVENT = Object.freeze({
|
|
27
27
|
sessionStart: 'SessionStart',
|
|
@@ -434,7 +434,7 @@ async function main() {
|
|
|
434
434
|
process.stdout.write(`${JSON.stringify(toCursorHookOutput(eventName, result))}\n`);
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
const isMain =
|
|
437
|
+
const isMain =isMainModule(import.meta.url);
|
|
438
438
|
if (isMain) {
|
|
439
439
|
main().catch((err) => {
|
|
440
440
|
process.stderr.write(`cursor-hook-bridge: ${err instanceof Error ? err.message : err}\n`);
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
59
59
|
import { fileURLToPath } from 'node:url';
|
|
60
60
|
import { join, relative, resolve } from 'node:path';
|
|
61
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
61
62
|
|
|
62
63
|
/** Block-scalar indicator on the `description:` line: '>' (folded) or '|' (literal), optional chomping '+'/'-'. */
|
|
63
64
|
const BLOCK_SCALAR_RE = /^([>|])([+-]?)\s*$/;
|
|
@@ -526,10 +527,6 @@ function main() {
|
|
|
526
527
|
}
|
|
527
528
|
|
|
528
529
|
const __filename = fileURLToPath(import.meta.url);
|
|
529
|
-
const isMain =
|
|
530
|
-
typeof process !== 'undefined' &&
|
|
531
|
-
process.argv[1] !== null &&
|
|
532
|
-
process.argv[1] !== undefined &&
|
|
533
|
-
resolve(process.argv[1]) === resolve(__filename);
|
|
530
|
+
const isMain =isMainModule(import.meta.url);
|
|
534
531
|
|
|
535
532
|
if (isMain) main();
|
|
@@ -32,6 +32,7 @@ import { warn } from '../common.mjs';
|
|
|
32
32
|
import { acquire } from '../session-lock.mjs';
|
|
33
33
|
import { enumerateCandidates, freeCandidates } from './enumerate.mjs';
|
|
34
34
|
import { rankCandidates } from './rank.mjs';
|
|
35
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
35
36
|
|
|
36
37
|
const VERSION = '1.0.0';
|
|
37
38
|
|
|
@@ -247,7 +248,7 @@ export async function main(argv) {
|
|
|
247
248
|
}
|
|
248
249
|
|
|
249
250
|
// Run main only when executed directly, never on import.
|
|
250
|
-
if (import.meta.url
|
|
251
|
+
if (isMainModule(import.meta.url)) {
|
|
251
252
|
main(process.argv.slice(2))
|
|
252
253
|
.then((code) => process.exit(code))
|
|
253
254
|
.catch((err) => {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { runEcosystemWizard } from './ecosystem-wizard/wizard-prompt.mjs';
|
|
10
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
10
11
|
|
|
11
12
|
export { detectCiProvider } from './ecosystem-wizard/ci-detector.mjs';
|
|
12
13
|
export {
|
|
@@ -31,7 +32,7 @@ export { runEcosystemWizard } from './ecosystem-wizard/wizard-prompt.mjs';
|
|
|
31
32
|
// CLI entry
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
|
|
34
|
-
if (
|
|
35
|
+
if (isMainModule(import.meta.url)) {
|
|
35
36
|
const args = process.argv.slice(2);
|
|
36
37
|
const repoRootIdx = args.indexOf('--repo-root');
|
|
37
38
|
const dryRun = args.includes('--dry-run');
|
|
@@ -30,6 +30,8 @@ import { promises as fs } from 'node:fs';
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
import { execSync } from 'node:child_process';
|
|
32
32
|
|
|
33
|
+
import { isMainModule } from './is-main-module.mjs';
|
|
34
|
+
|
|
33
35
|
// ---------------------------------------------------------------------------
|
|
34
36
|
// Configuration defaults (mirrors the .sh defaults)
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -296,14 +298,7 @@ async function _cliMain() {
|
|
|
296
298
|
process.exit(3);
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
const isMain = process.argv[1] && (
|
|
301
|
-
process.argv[1] === new URL(import.meta.url).pathname ||
|
|
302
|
-
// Handle invocation without file:// prefix
|
|
303
|
-
process.argv[1].endsWith('fetch-baseline.mjs')
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (isMain) {
|
|
301
|
+
if (isMainModule(import.meta.url)) {
|
|
307
302
|
_cliMain().catch((err) => {
|
|
308
303
|
process.stderr.write(`FATAL: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
309
304
|
process.exit(3);
|
|
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
|
|
|
24
24
|
|
|
25
25
|
import { detectVcsForRepo, discoverVaultRepos } from '../gitlab-portfolio/vcs-detect.mjs';
|
|
26
26
|
import { resolveRepoSpec } from '../vcs-repo-spec.mjs';
|
|
27
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
27
28
|
|
|
28
29
|
const realExecFile = promisify(execFileCb);
|
|
29
30
|
|
|
@@ -455,7 +456,7 @@ export async function main(argv, deps = {}) {
|
|
|
455
456
|
|
|
456
457
|
// ── CLI guard — prevent process.exit during test-time imports ─────────────────
|
|
457
458
|
|
|
458
|
-
if (import.meta.url
|
|
459
|
+
if (isMainModule(import.meta.url)) {
|
|
459
460
|
main(process.argv.slice(2))
|
|
460
461
|
.then((result) => {
|
|
461
462
|
process.exit(result.exitCode);
|
|
@@ -36,6 +36,7 @@ import { expandTilde } from '../common.mjs';
|
|
|
36
36
|
import { discoverVaultRepos } from './vcs-detect.mjs';
|
|
37
37
|
import { fetchIssuesMultiRepo, summarizeRepo } from './aggregator.mjs';
|
|
38
38
|
import { renderPortfolio, writePortfolio } from './markdown-writer.mjs';
|
|
39
|
+
import { isMainModule } from '../is-main-module.mjs';
|
|
39
40
|
|
|
40
41
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
42
|
|
|
@@ -406,7 +407,7 @@ EXIT CODES
|
|
|
406
407
|
|
|
407
408
|
// ── CLI guard — prevent process.exit during test-time imports ─────────────────
|
|
408
409
|
|
|
409
|
-
if (import.meta.url
|
|
410
|
+
if (isMainModule(import.meta.url)) {
|
|
410
411
|
main(process.argv.slice(2)).then((result) => {
|
|
411
412
|
process.exit(result.exitCode);
|
|
412
413
|
}).catch((err) => {
|
|
@@ -255,17 +255,17 @@ export const DEFAULT_BYTE_CEILING = 121000;
|
|
|
255
255
|
* → rulesDir extracted from `c73c094f`: { bytes: 76114, files: 8 }
|
|
256
256
|
* → rulesDir extracted from `e4674109`: { bytes: 89763, files: 43 }
|
|
257
257
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
* /reconcile wave of the size
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
258
|
+
* HISTORY as of 2026-09-16 — every number in this paragraph described the
|
|
259
|
+
* 124,000 ceiling, which no longer exists on this axis. It read, correctly for
|
|
260
|
+
* 2026-09-11: firing rate 0 of 3, not 0 of 2; the generated corpus then sat
|
|
261
|
+
* **2,427 B** below the peak known at that time (87,336 of 89,763 B), not
|
|
262
|
+
* 13,649 B, so one /reconcile wave of the size that session absorbed would have
|
|
263
|
+
* restored its worst recorded state with the ceiling still silent. Superseded
|
|
264
|
+
* on BOTH terms by the RE-CALIBRATED block below (ceiling 95,000, true peak
|
|
265
|
+
* 99,774 B, live distance to the peak 29,833 B measured 2026-09-16). Kept
|
|
266
|
+
* because it records the rule that produced the eventual fix: raising OR
|
|
267
|
+
* lowering a threshold inside a population fix is exactly the conflation this
|
|
268
|
+
* comment exists to end.
|
|
269
269
|
*
|
|
270
270
|
* FIRING-RATE AUDIT — widened from 3 states to 89 (#1308, 2026-09-11T17:26Z,
|
|
271
271
|
* clean tree @ `4a49adc6`). HR-105 forbids a rule nothing records; "0 of 3"
|
|
@@ -273,35 +273,63 @@ export const DEFAULT_BYTE_CEILING = 121000;
|
|
|
273
273
|
* guard was replayed over EVERY commit that touched `.claude/rules/`:
|
|
274
274
|
* `git log --format=%h -- .claude/rules/` → 89 commits, each extracted with
|
|
275
275
|
* `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
|
|
276
|
-
* `computeInstructionBudget({rulesDir})
|
|
276
|
+
* `computeInstructionBudget({rulesDir})`. Firing rates only — the headroom
|
|
277
|
+
* annotations this table used to carry are deliberately gone, because an
|
|
278
|
+
* absolute headroom rots between two edits of the corpus; read the live one
|
|
279
|
+
* from the axis itself (command in the RE-MEASURED block below):
|
|
277
280
|
*
|
|
278
281
|
* directive axis (480) → fires 4/89 = 4.5 % rare, healthy
|
|
279
|
-
* byte axis (121,000) → fires 0/89 = 0.0 %
|
|
280
|
-
* generated axis (124,000) → fires 0/
|
|
281
|
-
* pathScoped axis (124,000) → fires
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
282
|
+
* byte axis (121,000) → fires 0/89 = 0.0 %
|
|
283
|
+
* generated axis (124,000) → fires 0/91 = 0.0 % → superseded, see below
|
|
284
|
+
* pathScoped axis (124,000) → fires 5/91 = 5.5 % → calibrated
|
|
285
|
+
*
|
|
286
|
+
* RE-CALIBRATED 2026-09-16 (#1308). The replay was re-run over the FULL
|
|
287
|
+
* recorded history of this directory, not the 89-state subset:
|
|
288
|
+
*
|
|
289
|
+
* `git log --format=%h -- ./.claude/rules/` → 91 states; each materialized
|
|
290
|
+
* with `git archive <sha> .claude/rules | tar -x -C <tmp>` and fed to
|
|
291
|
+
* `computeInstructionBudget({ rulesDir })`.
|
|
292
|
+
*
|
|
293
|
+
* True peak of the generated axis: **99,774 B @ `2ae08fe0`** — which CORRECTS
|
|
294
|
+
* the "peak ever 89,763 B" this docblock carried until now (that number came
|
|
295
|
+
* from a single replayed commit, not from the population). Median state:
|
|
296
|
+
* 4,802 B. Firing table over the same 91 states:
|
|
297
|
+
*
|
|
298
|
+
* 124,000 → 0/91 = 0.0 % (the standing, unfalsifiable value)
|
|
299
|
+
* 102,268 (peak+2.5%)→ 0/91 = 0.0 % (a ceiling above the peak never fires)
|
|
300
|
+
* 95,000 → 1/91 = 1.1 % ← chosen
|
|
301
|
+
* 88,000 → 3/91 = 3.3 %
|
|
302
|
+
* 80,000 → 5/91 = 5.5 %
|
|
303
|
+
*
|
|
304
|
+
* Why 95,000: it FIRES on recorded history (HR-105 — a rule you cannot falsify
|
|
305
|
+
* is not a rule), at 1.1 % it sits in HR-101's rare band (a class above ~10 %
|
|
306
|
+
* is a broken instrument), and it stays silent on the corpus it lands on, so it
|
|
307
|
+
* does not go red before the #1367 consolidation has been committed. "Peak +
|
|
308
|
+
* 2.5 %" is REJECTED here for the reason the old text recommended it: above the
|
|
309
|
+
* peak, the axis is decorative by construction.
|
|
310
|
+
*
|
|
311
|
+
* RE-MEASURED 2026-09-16 @ `ca214376`, after the #1367 consolidation landed in
|
|
312
|
+
* the working tree (the state this ceiling was calibrated for is the state it
|
|
313
|
+
* now judges):
|
|
314
|
+
*
|
|
315
|
+
* node --input-type=module -e "const m = await import('./scripts/lib/instruction-budget-guard.mjs'); \
|
|
316
|
+
* console.log(JSON.stringify(m.computeInstructionBudget({ repoRoot: process.cwd() }).bySurface))"
|
|
317
|
+
* → generated { bytes: 69941, files: 7 } ×1.36 under 95,000
|
|
318
|
+
* pathScoped { bytes: 109932, files: 10 }
|
|
319
|
+
*
|
|
320
|
+
* Read the headroom off that command, never off a sentence in this block: the
|
|
321
|
+
* two numbers a verdict needs are the AXIS and its CEILING, and the distance
|
|
322
|
+
* between them changes with every rule edit. For the record at that SHA the
|
|
323
|
+
* generated axis sat 25,059 B under its ceiling and 29,833 B under the 99,774 B
|
|
324
|
+
* peak — i.e. one /reconcile wave the size of the largest recorded one would
|
|
325
|
+
* still not reach the peak, but a repeat of the growth that produced it would
|
|
326
|
+
* cross 95,000 first. That is the intended, rare signal: a cue to consolidate.
|
|
327
|
+
*
|
|
328
|
+
* BV-004 revisit trigger: re-run the 91-state replay when the generated corpus
|
|
329
|
+
* passes 90,000 B, or on this ceiling's FIRST fire — at which point it is
|
|
330
|
+
* calibrated by a real event rather than by a replay.
|
|
303
331
|
*/
|
|
304
|
-
export const DEFAULT_GENERATED_BYTE_CEILING =
|
|
332
|
+
export const DEFAULT_GENERATED_BYTE_CEILING = 95000;
|
|
305
333
|
|
|
306
334
|
/**
|
|
307
335
|
* Default byte ceiling for the PATH-SCOPED rule surface — the fourth axis,
|
|
@@ -353,11 +381,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
|
|
|
353
381
|
* working tree (2026-09-11): 134,969 B / 11 files → FIRES
|
|
354
382
|
*
|
|
355
383
|
* Firing rate 2 of 3, falsifiable in both directions — the condition
|
|
356
|
-
* {@link DEFAULT_GENERATED_BYTE_CEILING}
|
|
357
|
-
* Confirmed on a wider base (#1308, 2026-09-11T17:26Z @
|
|
358
|
-
* over all 89 commits that touched `.claude/rules/`, this
|
|
359
|
-
* (4.5 %)
|
|
360
|
-
*
|
|
384
|
+
* {@link DEFAULT_GENERATED_BYTE_CEILING} did not meet while it stood at
|
|
385
|
+
* 124,000 (0 of 3). Confirmed on a wider base (#1308, 2026-09-11T17:26Z @
|
|
386
|
+
* `4a49adc6`): replayed over all 89 commits that touched `.claude/rules/`, this
|
|
387
|
+
* axis fires 4/89 (4.5 %) — inside HR-101's rare band, and falsifiable.
|
|
388
|
+
* SUPERSEDED as a CONTRAST 2026-09-16: the sibling ceiling was re-calibrated to
|
|
389
|
+
* 95,000 in the same #1308 replay and now fires 1/91 (1.1 %) on recorded
|
|
390
|
+
* history, so "the property the sibling ceiling lacks" is no longer true of it —
|
|
391
|
+
* both axes are falsifiable, and this one is simply the looser of the two.
|
|
361
392
|
* This is also why it is not re-derived upward off the live number: a ceiling
|
|
362
393
|
* placed above 134,969 would be silent on all three states, i.e. the same
|
|
363
394
|
* unfalsifiable shape, obtained by the threshold-patch move
|
|
@@ -385,9 +416,14 @@ export const DEFAULT_GENERATED_BYTE_CEILING = 124000;
|
|
|
385
416
|
* the threshold-patch move `development.md` § Guard & Threshold Design
|
|
386
417
|
* forbids.
|
|
387
418
|
*
|
|
388
|
-
* Headroom
|
|
389
|
-
*
|
|
390
|
-
*
|
|
419
|
+
* Headroom moves with every rule edit, so it is measured, never quoted from
|
|
420
|
+
* here: `computeInstructionBudget({ repoRoot }).bySurface.pathScoped` against
|
|
421
|
+
* this constant. At 2026-09-16 @ `ca214376` that was 109,932 B / 10 files —
|
|
422
|
+
* 14,068 B under the ceiling, after the #1367 consolidation; on 2026-09-12 it
|
|
423
|
+
* was 1,237 B, which is how narrow this axis can get. The next `/reconcile`
|
|
424
|
+
* rule (~2.5 KB written standalone) trips it from a margin like that one
|
|
425
|
+
* unless it is absorbed into a thematic file in the SAME write step — an
|
|
426
|
+
* obligation written in the /reconcile-overshoot
|
|
391
427
|
* learning of `.claude/rules/measurement-discipline.md` and checked by the
|
|
392
428
|
* reconcile writer's budget pre-flight (`scripts/lib/reconcile/writer.mjs`).
|
|
393
429
|
* A breach sets `overBudget` AND turns `tests/rules/receiving-review.test.mjs`
|
|
@@ -749,7 +785,7 @@ function sumBytes(entries) {
|
|
|
749
785
|
* @param {Record<string, unknown>} meta
|
|
750
786
|
* @returns {boolean}
|
|
751
787
|
*/
|
|
752
|
-
function isMachineGeneratedRule(meta) {
|
|
788
|
+
export function isMachineGeneratedRule(meta) {
|
|
753
789
|
if (!meta || typeof meta !== 'object') return false;
|
|
754
790
|
return (
|
|
755
791
|
meta['auto-generated'] === true ||
|
|
@@ -758,6 +794,110 @@ function isMachineGeneratedRule(meta) {
|
|
|
758
794
|
);
|
|
759
795
|
}
|
|
760
796
|
|
|
797
|
+
/** One day in milliseconds — local to this module, as in every other lib here. */
|
|
798
|
+
const MS_PER_DAY = 86_400_000;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Whole days from `now` until this rule's `expires-at`, or `null` when the rule
|
|
802
|
+
* declares no expiry or an unparseable one.
|
|
803
|
+
*
|
|
804
|
+
* Negative ⇒ already expired. The comparison basis is deliberately the SAME one
|
|
805
|
+
* `rule-loader.mjs` `applyGates` uses — `Date.parse(expires-at) < now`, i.e. a
|
|
806
|
+
* bare `YYYY-MM-DD` is UTC midnight — so "expired" here means exactly "the
|
|
807
|
+
* loader already filters it out of every wave's rule set", never a second
|
|
808
|
+
* definition of the same word. `Math.floor` therefore returns `-1` for a rule
|
|
809
|
+
* that expired at midnight today: past, but by less than a day.
|
|
810
|
+
*
|
|
811
|
+
* An unparseable date returns `null` (fail-OPEN, matching the loader, which
|
|
812
|
+
* warns and ignores the expiry rather than excluding the file) — the malformed
|
|
813
|
+
* value is a separate finding, not an expiry verdict.
|
|
814
|
+
*
|
|
815
|
+
* @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
|
|
816
|
+
* @returns {number|null}
|
|
817
|
+
*/
|
|
818
|
+
export function daysUntilGeneratedRuleExpiry(opts = {}) {
|
|
819
|
+
const meta = opts.meta;
|
|
820
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
821
|
+
if (!Object.prototype.hasOwnProperty.call(meta, 'expires-at')) return null;
|
|
822
|
+
const expiresAt = Date.parse(String(meta['expires-at']));
|
|
823
|
+
if (!Number.isFinite(expiresAt)) return null;
|
|
824
|
+
|
|
825
|
+
const raw = opts.now;
|
|
826
|
+
let now;
|
|
827
|
+
if (raw instanceof Date) now = raw.getTime();
|
|
828
|
+
else if (typeof raw === 'number' && Number.isFinite(raw)) now = raw;
|
|
829
|
+
else if (typeof raw === 'string' && Number.isFinite(Date.parse(raw))) now = Date.parse(raw);
|
|
830
|
+
else now = Date.now();
|
|
831
|
+
|
|
832
|
+
return Math.floor((expiresAt - now) / MS_PER_DAY);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Is this generated rule's `expires-at` already in the past?
|
|
837
|
+
*
|
|
838
|
+
* The defect it names (#1372): `rule-loader.mjs` filters an expired rule out at
|
|
839
|
+
* READ time, but nothing DELETES it — so the file keeps being counted by every
|
|
840
|
+
* byte ceiling above and keeps being loaded verbatim by Claude Code, which has
|
|
841
|
+
* no notion of `expires-at`.
|
|
842
|
+
*
|
|
843
|
+
* @param {{ meta?: Record<string, unknown>, now?: Date|number|string }} [opts]
|
|
844
|
+
* @returns {boolean}
|
|
845
|
+
*/
|
|
846
|
+
export function isExpiredGeneratedRule(opts = {}) {
|
|
847
|
+
const days = daysUntilGeneratedRuleExpiry(opts);
|
|
848
|
+
return days !== null && days < 0;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Enumerate the machine-generated rule files in `rulesDir`.
|
|
853
|
+
*
|
|
854
|
+
* Same population, same filter and same frontmatter parser as
|
|
855
|
+
* {@link measureRuleCorpora}'s `generated` surface — that is the point of
|
|
856
|
+
* exporting it: a consumer that needs the FILES rather than the BYTES (the
|
|
857
|
+
* expiry test, the session-start maintenance probe) must not mirror the
|
|
858
|
+
* predicate, because a mirror agrees only until one side is edited.
|
|
859
|
+
*
|
|
860
|
+
* Three-state, like every reader in the banner family: `ok: false` means the
|
|
861
|
+
* directory exists but could not be enumerated, which a caller must record as
|
|
862
|
+
* "undeterminable" and never as "no generated rules". A MISSING directory is
|
|
863
|
+
* `ok: true` with an empty list — a repo without `.claude/rules/` genuinely has
|
|
864
|
+
* none.
|
|
865
|
+
*
|
|
866
|
+
* @param {{ rulesDir?: string, repoRoot?: string }} [opts]
|
|
867
|
+
* @returns {{ ok: boolean, rules: Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }> }}
|
|
868
|
+
*/
|
|
869
|
+
export function listMachineGeneratedRules(opts = {}) {
|
|
870
|
+
const dir =
|
|
871
|
+
typeof opts.rulesDir === 'string' && opts.rulesDir.length > 0
|
|
872
|
+
? opts.rulesDir
|
|
873
|
+
: join(opts.repoRoot ?? process.cwd(), '.claude', 'rules');
|
|
874
|
+
|
|
875
|
+
/** @type {Array<{ file: string, meta: Record<string, unknown>, expiresAt: string|null }>} */
|
|
876
|
+
const rules = [];
|
|
877
|
+
let names;
|
|
878
|
+
try {
|
|
879
|
+
names = readdirSync(dir);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
// ENOENT is an answer ("no generated rules"); anything else is not knowing.
|
|
882
|
+
return { ok: err?.code === 'ENOENT', rules };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
for (const file of names) {
|
|
886
|
+
if (!file.endsWith('.md')) continue;
|
|
887
|
+
let meta;
|
|
888
|
+
try {
|
|
889
|
+
({ meta } = parseGlobsFrontmatter(readFileSync(join(dir, file), 'utf8')));
|
|
890
|
+
} catch {
|
|
891
|
+
continue; // unreadable or unparseable — skip, never throw
|
|
892
|
+
}
|
|
893
|
+
if (!isMachineGeneratedRule(meta)) continue;
|
|
894
|
+
const raw = meta?.['expires-at'];
|
|
895
|
+
rules.push({ file, meta, expiresAt: typeof raw === 'string' ? raw : null });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return { ok: true, rules };
|
|
899
|
+
}
|
|
900
|
+
|
|
761
901
|
/**
|
|
762
902
|
* Single-pass scan of `.claude/rules/*.md` producing the TWO corpus measures
|
|
763
903
|
* this guard's third axis needs, which #1297 proved are NOT the same set:
|