session-orchestrator 5.0.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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +2 -2
- 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 +2 -2
- package/.orchestrator/policy/blocked-commands.json +10 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +74 -235
- package/commands/session.md +10 -0
- package/docs/USER-GUIDE.md +24 -0
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +1 -1
- package/docs/components.md +12 -5
- package/docs/events-schema.md +5 -1
- package/docs/install.md +128 -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/docs/session-config-template.md +38 -0
- package/docs/telemetry.md +15 -0
- 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/enforce-scope.mjs +42 -1
- 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 +3 -2
- 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 +209 -9
- 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 +63 -2
- 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 +266 -11
- 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 +124 -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/skills/playwright-driver/SKILL.md +7 -10
- 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
package/hooks/on-stop.mjs
CHANGED
|
@@ -551,6 +551,39 @@ async function handleSubagentStop(input) {
|
|
|
551
551
|
} catch { /* probe failed — omit rather than assert `false` */ }
|
|
552
552
|
|
|
553
553
|
try {
|
|
554
|
+
// `sidecar_missing` — the DENOMINATOR for every meta-derived field below
|
|
555
|
+
// (#1289 Befund 2). `agent_type_meta` and `tool_use_id` come from the same
|
|
556
|
+
// read, so they co-occur perfectly (measured 2026-09-16 over
|
|
557
|
+
// `.orchestrator/metrics/events.jsonl`: 15.291 `orchestrator.agent.stopped`
|
|
558
|
+
// records, 542 with `agent_type_meta`, 542 with `tool_use_id`,
|
|
559
|
+
// both/only-meta/only-tool = 542/0/0). That made the low presence rate look
|
|
560
|
+
// like a producer defect in the KEY, when 1000/1000 sidecars on this host
|
|
561
|
+
// carry `agentType`. This flag makes "no sidecar found" visible in the
|
|
562
|
+
// record instead of indistinguishable from "sidecar found, field absent".
|
|
563
|
+
//
|
|
564
|
+
// WHAT THIS KEY IS NOT: a sidecar-LOOKUP failure rate. Re-measured
|
|
565
|
+
// 2026-09-16 over 15.457 `orchestrator.agent.stopped` records (5086
|
|
566
|
+
// `transcript_found:false` vs 553 `true`), the false mass is the #939/#949
|
|
567
|
+
// PHANTOM-STOP class — no subagent ever existed, so there is nothing for
|
|
568
|
+
// the lookup to find:
|
|
569
|
+
// - 0 of 5077 distinct false `agent_id`s have a sidecar ANYWHERE on this
|
|
570
|
+
// host (`find ~/.claude/projects -path '*/subagents/agent-*.jsonl'` →
|
|
571
|
+
// 8626 ids), and 0 have a `SubagentStart` record (2176 starts in
|
|
572
|
+
// `.orchestrator/metrics/subagents.jsonl`);
|
|
573
|
+
// - 0 records carrying a valid `agent_id` got a null resolver result;
|
|
574
|
+
// - the two classes are perfectly bimodal on the type field:
|
|
575
|
+
// `false` + no `agent` 5086, `true` + typed 553, off-diagonal 0.
|
|
576
|
+
// `agent` (the harness `agent_type`) is therefore the ready-made
|
|
577
|
+
// discriminator — no further key is needed. Do NOT "repair" the derivation
|
|
578
|
+
// in hooks/_lib/subagent-paths.mjs to chase this rate: it already resolves
|
|
579
|
+
// 553/553 of the real stops, and a widened one would only start resolving
|
|
580
|
+
// onto a FOREIGN agent's sidecar. This misreading has now cost three
|
|
581
|
+
// investigations (#939, the 2026-08-11 ledger re-run, #1289 Befund 2).
|
|
582
|
+
//
|
|
583
|
+
// Present ONLY when true, like every other optional key in this payload;
|
|
584
|
+
// its own probe is inside this try, so a failed `existsSync` omits it and
|
|
585
|
+
// nothing else (a telemetry fault must never change a decision).
|
|
586
|
+
if (!existsSync(metaPath)) payload.sidecar_missing = true;
|
|
554
587
|
// One small read, one parse, two fields. `description` is operator prose
|
|
555
588
|
// and is deliberately NOT carried: this payload also travels over the
|
|
556
589
|
// optional Clank webhook unredacted.
|
|
@@ -565,7 +598,16 @@ async function handleSubagentStop(input) {
|
|
|
565
598
|
payload.tool_use_id = meta.toolUseId.trim();
|
|
566
599
|
}
|
|
567
600
|
// A SECOND witness for the type — never merged into `agent`, so the
|
|
568
|
-
// empty-`agent_type` rate stays measurable.
|
|
601
|
+
// empty-`agent_type` rate stays measurable. NO FALLBACK MERGE, deliberately:
|
|
602
|
+
// `payload.agent` already carries the harness's `input.agent_type`, and
|
|
603
|
+
// filling one from the other would erase exactly the signal this pair
|
|
604
|
+
// exists to produce.
|
|
605
|
+
//
|
|
606
|
+
// REPORTING RULE: the presence rate of this key is only meaningful against
|
|
607
|
+
// the events whose SIDECAR WAS FOUND — "present on N of M events without
|
|
608
|
+
// `sidecar_missing`", never N of all `orchestrator.agent.stopped` records.
|
|
609
|
+
// Measured against all events the rate reads as a producer defect in this
|
|
610
|
+
// block; against its real denominator it is a lookup-failure rate.
|
|
569
611
|
if (typeof meta?.agentType === 'string' && AGENT_TYPE_META_RE.test(meta.agentType.trim())) {
|
|
570
612
|
payload.agent_type_meta = meta.agentType.trim();
|
|
571
613
|
}
|
|
@@ -1120,6 +1120,9 @@ async function main() {
|
|
|
1120
1120
|
// string compare then reads false and silently no-ops the whole scope-detector
|
|
1121
1121
|
// under a symlinked `$CLAUDE_PLUGIN_ROOT`. realpath'ing both sides also survives
|
|
1122
1122
|
// `--preserve-symlinks` (where import.meta.url stays symlinked instead).
|
|
1123
|
+
// BV-004 ceiling: kept inline — hooks avoid importing scripts/lib on the hot
|
|
1124
|
+
// path; the canonical predicate is scripts/lib/is-main-module.mjs. Revisit if the
|
|
1125
|
+
// hook-import-set (hooks/_lib/hook-import-set.json) ever admits scripts/lib here.
|
|
1123
1126
|
function invokedAsScript() {
|
|
1124
1127
|
const entry = process.argv[1];
|
|
1125
1128
|
if (!entry) return false;
|
|
@@ -778,6 +778,9 @@ async function main() {
|
|
|
778
778
|
// under a symlinked plugin install) while `import.meta.url` is realpath-resolved
|
|
779
779
|
// by node's default loader, so BOTH sides are realpath'd.
|
|
780
780
|
// ---------------------------------------------------------------------------
|
|
781
|
+
// BV-004 ceiling: kept inline — hooks avoid importing scripts/lib on the hot
|
|
782
|
+
// path; the canonical predicate is scripts/lib/is-main-module.mjs. Revisit if the
|
|
783
|
+
// hook-import-set (hooks/_lib/hook-import-set.json) ever admits scripts/lib here.
|
|
781
784
|
function invokedAsScript() {
|
|
782
785
|
const entry = process.argv[1];
|
|
783
786
|
if (!entry) return false;
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
import { readStdin, emitAllow, emitDeny, emitWarn } from '../scripts/lib/io.mjs';
|
|
54
54
|
import { resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
55
55
|
import { readJson } from '../scripts/lib/common.mjs';
|
|
56
|
-
import { findIssueCreateStatements,
|
|
56
|
+
import { findIssueCreateStatements, findLoopedIssueCreates } from './_lib/vcs-create-matcher.mjs';
|
|
57
57
|
import {
|
|
58
58
|
loadIssueBudgetConfig,
|
|
59
59
|
resolveIssueBudgetSessionId,
|
|
@@ -187,15 +187,39 @@ function resolveToolCallId(input) {
|
|
|
187
187
|
* entry #N … nothing is lost", which would be false here — an uncountable bulk
|
|
188
188
|
* request is not parked, it is handed back whole.
|
|
189
189
|
*
|
|
190
|
+
* ## Why the lane is a parameter (#1379)
|
|
191
|
+
*
|
|
192
|
+
* Until 2026-09-17 this text said "sits inside a shell loop body (`do … done`)"
|
|
193
|
+
* on BOTH lanes, so the `xargs` deny (`echo b | xargs -I% glab issue create
|
|
194
|
+
* --title junk%` — no loop anywhere) sent the operator looking for a loop the
|
|
195
|
+
* command does not have. Only the lane SENTENCE varies; every other line is
|
|
196
|
+
* byte-identical across lanes, and tests pin them.
|
|
197
|
+
*
|
|
190
198
|
* @param {{ "max-per-session": number }} config
|
|
199
|
+
* @param {{ lane?: "loop"|"xargs"|"mixed" }} [opts]
|
|
191
200
|
* @returns {string}
|
|
192
201
|
*/
|
|
193
|
-
function formatLoopDenyReason(config) {
|
|
202
|
+
function formatLoopDenyReason(config, { lane = 'loop' } = {}) {
|
|
203
|
+
const LANE_SENTENCES = {
|
|
204
|
+
loop: [
|
|
205
|
+
'The `issue create` call sits inside a shell loop body (`do … done`), so the cap cannot',
|
|
206
|
+
'charge it honestly: the word list is expanded by the shell AFTER this hook runs, so',
|
|
207
|
+
'`for i in $(seq 1 50)` would file 50 issues against a count of 1.',
|
|
208
|
+
],
|
|
209
|
+
xargs: [
|
|
210
|
+
'The `issue create` call is driven by `xargs`, so the cap cannot charge it honestly: the',
|
|
211
|
+
'word list arrives on stdin AFTER this hook runs, so `seq 1 50 | xargs` would file 50',
|
|
212
|
+
'issues against a count of 1.',
|
|
213
|
+
],
|
|
214
|
+
mixed: [
|
|
215
|
+
'The `issue create` calls are driven by a shell loop body (`do … done`) AND by `xargs`,',
|
|
216
|
+
'so the cap cannot charge them honestly: both word lists are produced AFTER this hook',
|
|
217
|
+
'runs, so `seq 1 50 | xargs` would file 50 issues against a count of 1.',
|
|
218
|
+
],
|
|
219
|
+
};
|
|
194
220
|
return [
|
|
195
221
|
'issue-budget: this command creates an UNKNOWN number of issues — refusing to guess.',
|
|
196
|
-
|
|
197
|
-
'charge it honestly: the word list is expanded by the shell AFTER this hook runs, so',
|
|
198
|
-
'`for i in $(seq 1 50)` would file 50 issues against a count of 1.',
|
|
222
|
+
...(LANE_SENTENCES[lane] ?? LANE_SENTENCES.loop),
|
|
199
223
|
'',
|
|
200
224
|
'Nothing was parked as overflow, because nothing is lost: re-issue the create calls as',
|
|
201
225
|
'SEPARATE commands and each one is counted normally against the cap',
|
|
@@ -302,18 +326,59 @@ async function main() {
|
|
|
302
326
|
const config = loadIssueBudgetConfig(projectDir);
|
|
303
327
|
if (config.mode === 'off') return emitAllow();
|
|
304
328
|
|
|
305
|
-
// G3b — bulk creation whose multiplicity is not computable (#1145).
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
329
|
+
// G3b — bulk creation whose multiplicity is not computable (#1145). Two
|
|
330
|
+
// sources, one policy: a LOOP BODY (#1145) and an `xargs`-driven create
|
|
331
|
+
// (#1289), where the word list arrives on stdin. Both file N issues for one
|
|
332
|
+
// statement, so both are denied rather than charged 1 — the fix had to land
|
|
333
|
+
// here and not only in `isLoopedIssueCreate`, because G3 above short-circuits
|
|
334
|
+
// on `statements.length === 0` and an xargs create used to produce zero
|
|
335
|
+
// statements, so no loop-side fix could ever run.
|
|
336
|
+
//
|
|
337
|
+
// THE INVARIANT (rewritten 2026-09-16): the exemption is classified on the
|
|
338
|
+
// statement that CAUSED the bulk classification — never on `statements[0]`,
|
|
339
|
+
// and never on the whole command. A `[Carryover]` create standing NEXT TO an
|
|
340
|
+
// uncountable one is an unrelated neighbour and must not lift the deny; a
|
|
341
|
+
// `[Carryover]` create that IS the bulk statement keeps its documented
|
|
342
|
+
// unconditional pass (session-end's "those are never deferred" promise, which
|
|
343
|
+
// holds inside a loop too). Measured 2026-09-16 against the previous
|
|
344
|
+
// `statements[0]` binding, both lanes ALLOW where they must DENY:
|
|
345
|
+
// glab issue create --title "[Carryover] real"; echo X | xargs -I% glab issue create --title %
|
|
346
|
+
// glab issue create --title "[Carryover] real"; for i in 1 2 3; do glab issue create --title junk$i; done
|
|
347
|
+
// Same class as the bypass-scoping regression on `matchesBypass` — a matcher
|
|
348
|
+
// widened per statement while its exemption stayed whole-command
|
|
349
|
+
// (`.claude/rules/guard-design.md` § "Widening a matcher without narrowing
|
|
350
|
+
// its bypass", #1106).
|
|
351
|
+
//
|
|
352
|
+
// Fail-CLOSED when a command carries SEVERAL bulk statements and only some are
|
|
353
|
+
// exempt: `some()` over the non-exempt ones denies, because the command as a
|
|
354
|
+
// whole still files an uncountable number of untemplated issues.
|
|
355
|
+
//
|
|
356
|
+
// EVERY loop, not the first (#1379): the loop lane used to contribute at most
|
|
357
|
+
// ONE entry here, so an exempt FIRST loop was the only loop classified and
|
|
358
|
+
// every later loop went unjudged. Reproduced 2026-09-17 @ `9e8146b4`:
|
|
359
|
+
// for i in 1 2 3; do glab issue create --label carryover --title x$i; done;
|
|
360
|
+
// for j in 1 2 3; do glab issue create --title junk$j; done → ALLOW (count=1)
|
|
361
|
+
const bulkEntries = [
|
|
362
|
+
...findLoopedIssueCreates(command).map((tokens) => ({
|
|
363
|
+
lane: 'loop',
|
|
364
|
+
text: tokens.map((t) => t.text).join(' '),
|
|
365
|
+
})),
|
|
366
|
+
...statements.filter((s) => s.bulk).map((s) => ({ lane: 'xargs', text: s.text })),
|
|
367
|
+
];
|
|
368
|
+
const uncountableEntries = bulkEntries.filter((e) => !classifyExemption(e.text).exempt);
|
|
369
|
+
const uncountableBulk = uncountableEntries.length > 0;
|
|
370
|
+
// ONE lane resolution for BOTH reports (#1379 follow-up). The deny already
|
|
371
|
+
// named the lane it fired on; the `warn` undercount notice at the bottom of
|
|
372
|
+
// main() said "inside a loop body" unconditionally, so the xargs lane — where
|
|
373
|
+
// no loop exists anywhere in the command — sent the operator looking for one.
|
|
374
|
+
// Resolved here rather than twice, so the two reports can never disagree.
|
|
375
|
+
const uncountableLanes = new Set(uncountableEntries.map((e) => e.lane));
|
|
376
|
+
const uncountableLane =
|
|
377
|
+
uncountableLanes.size > 1 ? 'mixed' : ([...uncountableLanes][0] ?? 'loop');
|
|
313
378
|
if (uncountableBulk && config.mode === 'strict') {
|
|
314
379
|
// Nothing is charged and nothing is parked — the command is handed back
|
|
315
380
|
// whole, which is what makes unrolling it the correct next action.
|
|
316
|
-
return emitDeny(formatLoopDenyReason(config));
|
|
381
|
+
return emitDeny(formatLoopDenyReason(config, { lane: uncountableLane }));
|
|
317
382
|
}
|
|
318
383
|
|
|
319
384
|
const sessionId = await resolveSessionId(input, projectDir);
|
|
@@ -401,11 +466,32 @@ async function main() {
|
|
|
401
466
|
// to name the undercount out loud — a silent 1-for-N is the exact failure the
|
|
402
467
|
// deny above exists to prevent, and `warn` must not reintroduce it quietly.
|
|
403
468
|
// emitWarn, not stderr: under exit 0 stderr reaches only the debug log (#916).
|
|
404
|
-
|
|
469
|
+
//
|
|
470
|
+
// GATED ON THE BULK STATEMENTS, NEVER ON THE LAST VERDICT (2026-09-17). The
|
|
471
|
+
// condition used to read `verdict.decision === 'allow'`, i.e. the verdict of
|
|
472
|
+
// the LAST statement of the chain — so one exempt statement written AFTER an
|
|
473
|
+
// uncountable one silenced the notice the two paragraphs above declare
|
|
474
|
+
// mandatory. Reproduced through this hook binary in `mode: warn`:
|
|
475
|
+
// for i in 1 2; do glab issue create --title j$i; done; \
|
|
476
|
+
// glab issue create --title "[Carryover] z"
|
|
477
|
+
// → stdout EMPTY, ledger count=1 exempt=1 (the trailing exempt statement is
|
|
478
|
+
// an unrelated neighbour, exactly as in G3b's own invariant); without it,
|
|
479
|
+
// the identical loop reported the UNDERCOUNT.
|
|
480
|
+
// Reaching this point already means no statement blocked and none warned
|
|
481
|
+
// (both branches above return), so the uncountable bulk statement — non-exempt
|
|
482
|
+
// by construction of `uncountableEntries` — was charged as 1. The two negated
|
|
483
|
+
// conditions are kept explicit so a future reordering of those branches cannot
|
|
484
|
+
// turn this back into a report about the wrong statement.
|
|
485
|
+
if (uncountableBulk && !blocked && !verdicts.some((v) => v.decision === 'warn')) {
|
|
486
|
+
const LANE_PHRASES = {
|
|
487
|
+
loop: 'inside a loop body',
|
|
488
|
+
xargs: 'driven by `xargs`',
|
|
489
|
+
mixed: 'inside a loop body and by `xargs`',
|
|
490
|
+
};
|
|
405
491
|
return emitWarn(
|
|
406
|
-
`pre-bash-issue-budget: bulk create
|
|
492
|
+
`pre-bash-issue-budget: bulk create ${LANE_PHRASES[uncountableLane]} charged as 1 ` +
|
|
407
493
|
`(${verdict.count}/${verdict.max}) — the real number of issues this files is not ` +
|
|
408
|
-
`knowable before the
|
|
494
|
+
`knowable before the word list is expanded, so the count is an UNDERCOUNT. ` +
|
|
409
495
|
`Set \`issue-budget.mode: strict\` to deny this shape instead.`,
|
|
410
496
|
);
|
|
411
497
|
}
|
|
@@ -230,6 +230,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
230
230
|
/** @type {typeof import('../scripts/lib/io.mjs').emitWarn} */ let emitWarn;
|
|
231
231
|
/** @type {typeof import('../scripts/lib/io.mjs').writeJsonAtomicSync} */ let writeJsonAtomicSync;
|
|
232
232
|
/** @type {typeof import('../scripts/lib/file-lock.mjs').withFileLock} */ let withFileLock;
|
|
233
|
+
/** @type {typeof import('../scripts/lib/scope-echo.mjs').scopeDigest} */ let scopeDigest;
|
|
233
234
|
let findScopeCollisions;
|
|
234
235
|
|
|
235
236
|
const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
|
|
@@ -287,6 +288,25 @@ const SHAPE_NONE = 'none';
|
|
|
287
288
|
/** The per-dispatch observability record (#1092) — see § Observability. */
|
|
288
289
|
const SCOPE_EVENT = 'orchestrator.wave_dispatch.scope_checked';
|
|
289
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The receive-side instruction line `renderScopeEchoInstruction()` renders and
|
|
293
|
+
* the coordinator appends immediately after the fenced block
|
|
294
|
+
* (`wave-loop-dispatch.md` § Pre-Dispatch: File-Scope Injection).
|
|
295
|
+
*
|
|
296
|
+
* Matching it HERE, in the same prompt, is what makes the agent-A-block-with-
|
|
297
|
+
* agent-B-line mix-up catchable without a single filesystem read: the block and
|
|
298
|
+
* the line must come from the same `$AGENT_FILESCOPE_JSON`, so their digests
|
|
299
|
+
* must agree (`digest_consistent`). The marker half stays case-sensitive for the
|
|
300
|
+
* same reason `scope-echo.mjs` keeps it so — a lowercase lookalike is not the
|
|
301
|
+
* line; the HEX half is read case-insensitively and normalised to lowercase.
|
|
302
|
+
* Non-global on purpose: a `g` regex carries `lastIndex` between calls.
|
|
303
|
+
*/
|
|
304
|
+
const ECHO_INSTRUCTION_RE =
|
|
305
|
+
/End your final report with the line:[ \t]*`{0,3}SCOPE-DIGEST:[ \t]*`{0,3}([0-9a-fA-F]{8})(?![0-9a-fA-F])/;
|
|
306
|
+
|
|
307
|
+
/** Shape of a well-formed digest, used to reject anything a broken digest fn returns. */
|
|
308
|
+
const DIGEST_RE = /^[0-9a-f]{8}$/;
|
|
309
|
+
|
|
290
310
|
/**
|
|
291
311
|
* Clamp for the one free-form string the event carries (`agent_id`, built from
|
|
292
312
|
* the coordinator's own `description`). A dispatch description is a label, but
|
|
@@ -358,6 +378,14 @@ async function bootstrap() {
|
|
|
358
378
|
io: { specifier: lib('io.mjs') },
|
|
359
379
|
scopeGate: { specifier: lib('scope-gate.mjs') },
|
|
360
380
|
fileLock: { specifier: lib('file-lock.mjs') },
|
|
381
|
+
// ONE normalization for the digest, shared with the receive side (#1092):
|
|
382
|
+
// `scope-echo.mjs` is pure (stdlib + `crypto-digest-utils.mjs`) and its
|
|
383
|
+
// `scopeDigest` is what `--verify` joins the two halves on. A second
|
|
384
|
+
// implementation here would let the halves disagree while both looked
|
|
385
|
+
// right — the class `guard-design.md` § "zero-import predicate module"
|
|
386
|
+
// names. Late-bound like every other repo module so a load failure
|
|
387
|
+
// banners instead of disarming the guard silently (#993).
|
|
388
|
+
scopeEcho: { specifier: lib('scope-echo.mjs') },
|
|
361
389
|
},
|
|
362
390
|
{
|
|
363
391
|
hookName: HOOK_NAME,
|
|
@@ -370,6 +398,7 @@ async function bootstrap() {
|
|
|
370
398
|
({ readStdin, emitAllow, emitDeny, emitWarn, writeJsonAtomicSync } = modules.io);
|
|
371
399
|
({ findScopeCollisions } = modules.scopeGate);
|
|
372
400
|
({ withFileLock } = modules.fileLock);
|
|
401
|
+
({ scopeDigest } = modules.scopeEcho);
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
// ---------------------------------------------------------------------------
|
|
@@ -407,8 +436,51 @@ const SCOPE_TERMS = 'DATEI[- ]SCOPE|FILE[- ]SCOPE|FILE SCOPE|DEIN SCOPE|SCOPE \\
|
|
|
407
436
|
* hit in a region as one is the recorded failure of `parseEpicRef` (#1112).
|
|
408
437
|
*
|
|
409
438
|
* The real miss class is a different SHAPE, handled by {@link INLINE_SCOPE_DECL}.
|
|
439
|
+
*
|
|
440
|
+
* ## The marker vocabulary is CASE-SENSITIVE, and that is a measurement (#1092)
|
|
441
|
+
*
|
|
442
|
+
* Until 2026-09-16 both shapes carried the `i` flag, so the term `FILE SCOPE`
|
|
443
|
+
* also matched ordinary lowercase prose. The Learnings-Index header this repo
|
|
444
|
+
* injects into every agent prompt —
|
|
445
|
+
* `## Learnings Index (selected for your file scope) — …` — therefore matched
|
|
446
|
+
* the marker at column ~38 of a line-leading window, and the FIRST fenced block
|
|
447
|
+
* anywhere after it (a verification command, a report template) decided the
|
|
448
|
+
* class: no path survived, so the dispatch was recorded `unparseable`, i.e.
|
|
449
|
+
* matrix row 6 — "a declaration is present but the parser gave up".
|
|
450
|
+
*
|
|
451
|
+
* MEASURED in this session's own wave 1 (`.orchestrator/wave-dispatch-scopes.json`,
|
|
452
|
+
* waveKey `5de6560c-…|w1|Discovery`): `unparseable: 5, extracted: 0` for FIVE
|
|
453
|
+
* Discovery dispatches that carried no `FILE-SCOPE` block at all. Host-wide the
|
|
454
|
+
* same confusion accounts for the 52 historical `unparseable` records.
|
|
455
|
+
*
|
|
456
|
+
* Every DOCUMENTED marker is upper-case (`wave-loop-dispatch.md` § Pre-Dispatch:
|
|
457
|
+
* File-Scope Injection writes `FILE-SCOPE — exactly these:`), while every
|
|
458
|
+
* measured false positive is prose in sentence case — so dropping `i` separates
|
|
459
|
+
* them without narrowing the search window, which the § above showed recovers
|
|
460
|
+
* nothing.
|
|
461
|
+
*
|
|
462
|
+
* ## What dropping `i` costs, stated honestly (corrected 2026-09-16)
|
|
463
|
+
*
|
|
464
|
+
* The first cut of this note claimed the change "moves a CLASSIFICATION and
|
|
465
|
+
* never a decision", on the grounds that `marker-absent` and `unparseable` both
|
|
466
|
+
* resolve to ALLOW. That is only true for a declaration the parser would have
|
|
467
|
+
* given up on anyway. A MIXED-CASE declaration that WOULD have yielded paths
|
|
468
|
+
* (`File-Scope:` followed by a fenced path list) now matches nothing, so
|
|
469
|
+
* `extractScopeFromPrompt` returns `[]`, `findScopeCollisions` never runs, and
|
|
470
|
+
* the dispatch is ALLOWED unconditionally — a lost DENY capability for that
|
|
471
|
+
* spelling, i.e. a decision change, not a reclassification.
|
|
472
|
+
*
|
|
473
|
+
* It is safe today because every LIVE injector writes the upper-case canonical
|
|
474
|
+
* marker: `skills/wave-executor/references/wave-loop-dispatch.md`
|
|
475
|
+
* § Pre-Dispatch: File-Scope Injection documents `FILE-SCOPE — exactly these:`,
|
|
476
|
+
* and the repo-wide census (2026-09-16) finds the mixed-case spellings only in
|
|
477
|
+
* prose, never in an injected block. That is a property of the injectors, not of
|
|
478
|
+
* this regex, so it is PINNED rather than assumed:
|
|
479
|
+
* `tests/skills/wave-loop-scope-marker.test.mjs` asserts the documented marker
|
|
480
|
+
* line is upper-case and matches {@link SCOPE_MARKER}, so a future template edit
|
|
481
|
+
* to `File-Scope:` goes red instead of silently disarming the guard.
|
|
410
482
|
*/
|
|
411
|
-
const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, '
|
|
483
|
+
export const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'm');
|
|
412
484
|
|
|
413
485
|
/**
|
|
414
486
|
* SHAPE 2 (lower precedence) — the measured miss class: a declaration written
|
|
@@ -436,7 +508,7 @@ const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
|
|
|
436
508
|
* one false extraction (`[".filter"]`) is a prose fragment, which is why the
|
|
437
509
|
* fenced shape keeps precedence and this one runs only when that found nothing.
|
|
438
510
|
*/
|
|
439
|
-
const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, '
|
|
511
|
+
const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'g');
|
|
440
512
|
const INLINE_DECL_OPERATOR = /^[^\n(:—–]{0,2}(\([^)\n]{0,80}\))?\s*(?:[—–][^\n:]{0,30})?\s*(:|—|–)/;
|
|
441
513
|
|
|
442
514
|
/** Separators a coordinator uses between paths in an inline declaration. */
|
|
@@ -980,6 +1052,69 @@ export function bumpSignalCounter(ledger, waveKey, status) {
|
|
|
980
1052
|
return next;
|
|
981
1053
|
}
|
|
982
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* The digest half of the per-dispatch record (#1092) — the field set that makes
|
|
1057
|
+
* the SEND side joinable to the RECEIVE side.
|
|
1058
|
+
*
|
|
1059
|
+
* Four facts, all derived from the PROMPT alone, none of them a path:
|
|
1060
|
+
*
|
|
1061
|
+
* `scope_digest` — `scopeDigest()` over the paths extracted FROM THE
|
|
1062
|
+
* PROMPT. This is the join key `scope-echo --verify`
|
|
1063
|
+
* uses; `agent_id` cannot be one, because the two
|
|
1064
|
+
* halves spell it differently (measured 2026-09-16:
|
|
1065
|
+
* 609 send-side vs 51 receive-side records, agent-id
|
|
1066
|
+
* overlap ZERO — send writes
|
|
1067
|
+
* `"i-3 #1353 … (session-orchestrator:code-implementer)"`,
|
|
1068
|
+
* receive writes `"i-3"`).
|
|
1069
|
+
* `echo_instruction_present`— the coordinator appended the echo line at all.
|
|
1070
|
+
* `instructed_digest` — the digest THAT LINE names.
|
|
1071
|
+
* `digest_consistent` — the two agree. `false` is agent A's fenced block
|
|
1072
|
+
* beside agent B's echo line, caught at dispatch
|
|
1073
|
+
* time with no filesystem read.
|
|
1074
|
+
*
|
|
1075
|
+
* ABSENT IS NOT ZERO, in both directions (`docs/events-schema.md`):
|
|
1076
|
+
* `scope_digest` is OMITTED for an empty scope — never the digest of the empty
|
|
1077
|
+
* string, which is a real 8-hex value and would join marker-absent Discovery
|
|
1078
|
+
* dispatches to each other. `instructed_digest` is omitted when no line was
|
|
1079
|
+
* found, and `digest_consistent` unless BOTH are present.
|
|
1080
|
+
*
|
|
1081
|
+
* TOTAL by construction: every branch is wrapped, because this runs on the
|
|
1082
|
+
* decision path of a deny-capable hook. A throwing digest function must cost the
|
|
1083
|
+
* FIELD, never the verdict — same discipline as the awaited-and-caught emit.
|
|
1084
|
+
*
|
|
1085
|
+
* @param {string[]} files paths extracted from the prompt
|
|
1086
|
+
* @param {unknown} prompt the dispatch prompt
|
|
1087
|
+
* @param {((paths: string[]) => string)} [digestFn] injectable for tests; defaults
|
|
1088
|
+
* to the late-bound `scopeDigest` (undefined when `bootstrap()` has not run,
|
|
1089
|
+
* which this function treats exactly like a throwing one — the field is omitted)
|
|
1090
|
+
* @returns {{scope_digest?: string, echo_instruction_present: boolean,
|
|
1091
|
+
* instructed_digest?: string, digest_consistent?: boolean}}
|
|
1092
|
+
*/
|
|
1093
|
+
export function scopeDigestFields(files, prompt, digestFn) {
|
|
1094
|
+
/** @type {Record<string, unknown>} */
|
|
1095
|
+
const out = {};
|
|
1096
|
+
try {
|
|
1097
|
+
const fn = typeof digestFn === 'function' ? digestFn : scopeDigest;
|
|
1098
|
+
if (typeof fn === 'function' && Array.isArray(files) && files.length > 0) {
|
|
1099
|
+
const digest = fn(files);
|
|
1100
|
+
if (typeof digest === 'string' && DIGEST_RE.test(digest)) out.scope_digest = digest;
|
|
1101
|
+
}
|
|
1102
|
+
} catch { /* a broken digest costs the field, never the decision */ }
|
|
1103
|
+
|
|
1104
|
+
let instructed = null;
|
|
1105
|
+
try {
|
|
1106
|
+
const match = typeof prompt === 'string' ? ECHO_INSTRUCTION_RE.exec(prompt) : null;
|
|
1107
|
+
if (match !== null) instructed = match[1].toLowerCase();
|
|
1108
|
+
} catch { instructed = null; }
|
|
1109
|
+
|
|
1110
|
+
out.echo_instruction_present = instructed !== null;
|
|
1111
|
+
if (instructed !== null) out.instructed_digest = instructed;
|
|
1112
|
+
if (typeof out.scope_digest === 'string' && instructed !== null) {
|
|
1113
|
+
out.digest_consistent = out.scope_digest === instructed;
|
|
1114
|
+
}
|
|
1115
|
+
return /** @type {any} */ (out);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
983
1118
|
/**
|
|
984
1119
|
* @typedef {{action: 'allow'|'deny'|'warn', reason?: string, suggestion?: string,
|
|
985
1120
|
* ledger?: object|null, note?: string, telemetry?: object}} Verdict
|
|
@@ -997,9 +1132,11 @@ export function bumpSignalCounter(ledger, waveKey, status) {
|
|
|
997
1132
|
* @param {Function} params.collide `findScopeCollisions` (injected for testability)
|
|
998
1133
|
* @param {(entry: object) => boolean} [params.isFinished] liveness probe (§ Liveness)
|
|
999
1134
|
* @param {string} [params.nowIso] dispatch timestamp recorded on the entry
|
|
1135
|
+
* @param {(paths: string[]) => string} [params.digestFn] scope-digest function
|
|
1136
|
+
* (injected for testability; defaults to the late-bound `scopeDigest`)
|
|
1000
1137
|
* @returns {Verdict}
|
|
1001
1138
|
*/
|
|
1002
|
-
export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso }) {
|
|
1139
|
+
export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, collide, isFinished, nowIso, digestFn }) {
|
|
1003
1140
|
const toolName = input?.tool_name;
|
|
1004
1141
|
// Row 4: not our tool.
|
|
1005
1142
|
if (toolName !== DISPATCH_TOOL) return { action: 'allow' };
|
|
@@ -1023,13 +1160,19 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
1023
1160
|
// agent id, never a path, never a byte of the prompt (issue #1092 acceptance
|
|
1024
1161
|
// criterion 3). `wave` is omitted rather than zeroed when unknown.
|
|
1025
1162
|
const wave = waveNumberOf(waveKey);
|
|
1163
|
+
const digestFields = scopeDigestFields(files, toolInput.prompt, digestFn);
|
|
1026
1164
|
const telemetryFor = (ledgerResult, collisionCount = 0) => ({
|
|
1027
1165
|
...(wave === null ? {} : { wave }),
|
|
1028
1166
|
agent_id: id.slice(0, MAX_AGENT_ID_CHARS),
|
|
1029
1167
|
declared_path_count: files.length,
|
|
1030
1168
|
injected: files.length > 0,
|
|
1169
|
+
// `marker_found` is NOT a second spelling of `injected`: a fenced block whose
|
|
1170
|
+
// lines are prose is `marker_found: true, injected: false` (matrix row 6),
|
|
1171
|
+
// which is the row-5-vs-row-6 split expressed as a boolean a query can group by.
|
|
1172
|
+
marker_found: signal.status !== SIGNAL_MARKER_ABSENT,
|
|
1031
1173
|
shape: signal.shape,
|
|
1032
1174
|
signal: signal.status,
|
|
1175
|
+
...digestFields,
|
|
1033
1176
|
ledger_result: ledgerResult,
|
|
1034
1177
|
collision_count: collisionCount,
|
|
1035
1178
|
});
|
|
@@ -1309,6 +1452,12 @@ async function main() {
|
|
|
1309
1452
|
// symlinked plugin install) while `import.meta.url` is realpath-resolved by
|
|
1310
1453
|
// node's default loader, so BOTH sides are realpath'd — the same comparison
|
|
1311
1454
|
// `hooks/post-bash-write-verify.mjs` documents (#938 MED-2).
|
|
1455
|
+
//
|
|
1456
|
+
// NAMED CEILING (BV-004): kept inline rather than imported — hooks avoid
|
|
1457
|
+
// importing `scripts/lib` on the hot path, where every added module is paid on
|
|
1458
|
+
// every dispatch. The canonical predicate is `scripts/lib/is-main-module.mjs`;
|
|
1459
|
+
// revisit if this hook ever imports that tree for another reason, at which
|
|
1460
|
+
// point the copy costs more than the import saves.
|
|
1312
1461
|
// ---------------------------------------------------------------------------
|
|
1313
1462
|
function invokedAsScript() {
|
|
1314
1463
|
const entry = process.argv[1];
|
|
@@ -37,6 +37,7 @@ import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
|
37
37
|
import { shouldDailyFlush } from '../scripts/lib/telemetry/sync.mjs';
|
|
38
38
|
import { resolveConsent, readTelemetryState } from '../scripts/lib/telemetry/consent.mjs';
|
|
39
39
|
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
40
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
40
41
|
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Constants
|
|
@@ -209,7 +210,7 @@ async function main() {
|
|
|
209
210
|
// Self-execution guard — run only when invoked directly (not when imported).
|
|
210
211
|
// ---------------------------------------------------------------------------
|
|
211
212
|
|
|
212
|
-
const isMain =
|
|
213
|
+
const isMain =isMainModule(import.meta.url);
|
|
213
214
|
if (isMain) {
|
|
214
215
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
215
216
|
if (!shouldRunHook('skill-invocation-telemetry')) process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "5.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "5.2.0",
|
|
4
|
+
"description": "A repeatable Plan, Go, Close workflow for AI coding sessions: /session reads your repo and agrees the scope, /go runs the work in waves with a quality gate between each, /close verifies and commits. Runs on Claude Code, Codex CLI, Cursor and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://session-orchestrator.com",
|
|
7
7
|
"keywords": [
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"test": "node scripts/check-package-manager.mjs && vitest --run",
|
|
58
58
|
"test:coverage": "node scripts/check-package-manager.mjs && vitest --run --coverage",
|
|
59
59
|
"test:watch": "node scripts/check-package-manager.mjs && vitest",
|
|
60
|
+
"test:pack": "node scripts/check-package-manager.mjs && SO_PACK_TEST=1 vitest --run tests/scripts/pack-install-lifecycle.test.mjs",
|
|
60
61
|
"quality-gate": "node scripts/run-quality-gate.mjs --variant full-gate",
|
|
61
62
|
"lint": "eslint .",
|
|
62
63
|
"lint:fix": "eslint . --fix",
|
package/pi/prompts/autopilot.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: "Use this skill when running an autonomous session-orchestration loop. Chains session-start → session-plan → wave-executor → session-end for N iterations with all 10 kill-switches (SPIRAL, FAILED wave, carryover > 50%, max-hours, max-sessions, resource-overload, token-budget, stall-timeout, sub-threshold confidence, user-abort). Reads Mode-Selector output (Phase B) to decide auto-execute vs. fallback. Writes one autopilot.jsonl record per loop run. Phase C scaffold (issue #277); implementation lives in scripts/lib/autopilot.mjs (Phase C-1 follow-up)."
|
|
3
3
|
argument-hint: "[--headless] [--verbose] [--max-sessions=N] [--max-hours=H] [--confidence-threshold=0.X] [--dry-run]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /autopilot
|
|
7
7
|
|
|
8
|
-
Use the Session Orchestrator
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/autopilot/SKILL.md`.
|
|
9
9
|
|
|
10
10
|
Arguments: $@
|
|
11
11
|
|
|
12
|
-
Read that
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/pi/prompts/bootstrap.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: "Use this skill when scaffolding the minimum repository structure required by session-orchestrator. Invoked automatically by the Bootstrap Gate when CLAUDE.md, Session Config, or bootstrap.lock is missing. Also available as /bootstrap for manual invocation. Three intensity tiers: fast (demos/spikes), standard (MVPs), deep (production/team)."
|
|
3
3
|
argument-hint: "[--upgrade <tier>]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /bootstrap
|
|
7
7
|
|
|
8
|
-
Use the Session Orchestrator
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/bootstrap/SKILL.md`.
|
|
9
9
|
|
|
10
10
|
Arguments: $@
|
|
11
11
|
|
|
12
|
-
Read that
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/pi/prompts/brainstorm.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: "Use when you have a feature idea but the scope or UX is still ambiguous — runs a lightweight Socratic design dialogue (3-5 AUQ rounds) and writes a spec markdown file. Use BEFORE /plan feature when product intent needs validation; skip to /plan feature when scope is already clear. HARD-GATE prevents any code work until the design is user-approved."
|
|
3
3
|
argument-hint: "[topic-or-feature-slug]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /brainstorm
|
|
7
7
|
|
|
8
|
-
Use the Session Orchestrator
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/brainstorm/SKILL.md`.
|
|
9
9
|
|
|
10
10
|
Arguments: $@
|
|
11
11
|
|
|
12
|
-
Read that
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/pi/prompts/close.md
CHANGED
|
@@ -4,8 +4,8 @@ description: End session with verification, commits, and documentation
|
|
|
4
4
|
|
|
5
5
|
# /close
|
|
6
6
|
|
|
7
|
-
Use the Session Orchestrator
|
|
7
|
+
Use the Session Orchestrator skill definition at `skills/close/SKILL.md`.
|
|
8
8
|
|
|
9
9
|
Arguments: $@
|
|
10
10
|
|
|
11
|
-
Read that
|
|
11
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Monitor iterative improvement loops for convergence. Three signals — shrinking diff, pass-rate plateau, velocity — drive a Stop/Continue/Investigate decision at each inter-wave checkpoint. Distinct from /evolve (retrospective) and session-reviewer (wave output review): convergence-monitoring answers \"are we making progress?\" not \"was the last wave correct?\". Primary consumer: /autoresearch loops and wave-executor inter-wave checkpoints."
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /convergence-monitoring
|
|
6
|
+
|
|
7
|
+
Use the Session Orchestrator skill definition at `skills/convergence-monitoring/SKILL.md`.
|
|
8
|
+
|
|
9
|
+
Arguments: $@
|
|
10
|
+
|
|
11
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/pi/prompts/debug.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: "Use when encountering any bug, test failure, build break, or unexpected behavior — runs a 4-phase systematic debugging process before proposing any fix. Iron Law: NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST. Produces a `.orchestrator/debug/` artifact the fixer agent must reference."
|
|
3
3
|
argument-hint: "[bug-description-or-issue-ref]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /debug
|
|
7
7
|
|
|
8
|
-
Use the Session Orchestrator
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/debug/SKILL.md`.
|
|
9
9
|
|
|
10
10
|
Arguments: $@
|
|
11
11
|
|
|
12
|
-
Read that
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/pi/prompts/discovery.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: "Use this skill when running systematic quality discovery and issue detection. Runs modular probes adapted to the project's tech stack, presents findings interactively for user triage, and creates VCS issues for confirmed problems. Invoked standalone via /discovery or embedded in session-end."
|
|
3
3
|
argument-hint: "[all|code|infra|ui|arch|session|audit|vault|feature] [--since <git-ref>] [--full]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /discovery
|
|
7
7
|
|
|
8
|
-
Use the Session Orchestrator
|
|
8
|
+
Use the Session Orchestrator skill definition at `skills/discovery/SKILL.md`.
|
|
9
9
|
|
|
10
10
|
Arguments: $@
|
|
11
11
|
|
|
12
|
-
Read that
|
|
12
|
+
Read that skill file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|