session-orchestrator 3.22.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot-multi.md +14 -0
- package/.cursor/commands/autopilot.md +14 -0
- package/.cursor/commands/bootstrap.md +14 -0
- package/.cursor/commands/brainstorm.md +14 -0
- package/.cursor/commands/close.md +13 -0
- package/.cursor/commands/contract-version-bump.md +14 -0
- package/.cursor/commands/debug.md +14 -0
- package/.cursor/commands/discovery.md +14 -0
- package/.cursor/commands/dispatcher.md +14 -0
- package/.cursor/commands/eli5.md +14 -0
- package/.cursor/commands/eval.md +14 -0
- package/.cursor/commands/evolve.md +14 -0
- package/.cursor/commands/go.md +14 -0
- package/.cursor/commands/grill.md +14 -0
- package/.cursor/commands/harness-audit.md +13 -0
- package/.cursor/commands/journey-audit.md +14 -0
- package/.cursor/commands/memory-cleanup.md +14 -0
- package/.cursor/commands/persona-panel.md +14 -0
- package/.cursor/commands/plan.md +14 -0
- package/.cursor/commands/portfolio.md +14 -0
- package/.cursor/commands/reconcile.md +14 -0
- package/.cursor/commands/release.md +14 -0
- package/.cursor/commands/repo-audit.md +13 -0
- package/.cursor/commands/session.md +14 -0
- package/.cursor/commands/spinout.md +14 -0
- package/.cursor/commands/sunset-review.md +14 -0
- package/.cursor/commands/templates-ack.md +14 -0
- package/.cursor/commands/test.md +14 -0
- package/.cursor/hooks.json +60 -0
- package/.cursor/rules/000-session-orchestrator.mdc +8 -0
- package/.cursor/rules/010-session-workflow.mdc +9 -1
- package/.cursor/rules/020-quality-gates.mdc +1 -1
- package/.cursor/rules/030-wave-execution.mdc +1 -1
- package/.cursor/rules/050-plan.mdc +2 -2
- package/.cursor/rules/070-gitlab-ops.mdc +73 -57
- package/.cursor/rules/080-ecosystem-health.mdc +7 -7
- package/.cursor/skills/architecture/SKILL.md +13 -0
- package/.cursor/skills/autopilot/SKILL.md +12 -0
- package/.cursor/skills/bootstrap/SKILL.md +12 -0
- package/.cursor/skills/brainstorm/SKILL.md +13 -0
- package/.cursor/skills/claude-md-drift-check/SKILL.md +13 -0
- package/.cursor/skills/contract-version-bump/SKILL.md +12 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +12 -0
- package/.cursor/skills/daily/SKILL.md +12 -0
- package/.cursor/skills/debug/SKILL.md +13 -0
- package/.cursor/skills/discovery/SKILL.md +13 -0
- package/.cursor/skills/dispatcher/SKILL.md +13 -0
- package/.cursor/skills/docs-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/domain-model/SKILL.md +13 -0
- package/.cursor/skills/ecosystem-health/SKILL.md +13 -0
- package/.cursor/skills/eli5/SKILL.md +13 -0
- package/.cursor/skills/eval/SKILL.md +12 -0
- package/.cursor/skills/evolve/SKILL.md +13 -0
- package/.cursor/skills/frontmatter-guard/SKILL.md +13 -0
- package/.cursor/skills/gitlab-ops/SKILL.md +13 -0
- package/.cursor/skills/gitlab-portfolio/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +13 -0
- package/.cursor/skills/hook-development/SKILL.md +13 -0
- package/.cursor/skills/journey-audit/SKILL.md +13 -0
- package/.cursor/skills/mcp-builder/SKILL.md +13 -0
- package/.cursor/skills/memory-cleanup/SKILL.md +12 -0
- package/.cursor/skills/mode-selector/SKILL.md +13 -0
- package/.cursor/skills/npm-publish/SKILL.md +12 -0
- package/.cursor/skills/peekaboo-driver/SKILL.md +13 -0
- package/.cursor/skills/persona-panel/SKILL.md +12 -0
- package/.cursor/skills/plan/SKILL.md +13 -0
- package/.cursor/skills/playwright-driver/SKILL.md +13 -0
- package/.cursor/skills/quality-gates/SKILL.md +13 -0
- package/.cursor/skills/reconcile/SKILL.md +12 -0
- package/.cursor/skills/repo-audit/SKILL.md +13 -0
- package/.cursor/skills/session-end/SKILL.md +13 -0
- package/.cursor/skills/session-plan/SKILL.md +13 -0
- package/.cursor/skills/session-start/SKILL.md +13 -0
- package/.cursor/skills/skill-creator/SKILL.md +13 -0
- package/.cursor/skills/spinout/SKILL.md +12 -0
- package/.cursor/skills/sunset-review/SKILL.md +13 -0
- package/.cursor/skills/test-runner/SKILL.md +13 -0
- package/.cursor/skills/tmux-layout/SKILL.md +13 -0
- package/.cursor/skills/ubiquitous-language/SKILL.md +13 -0
- package/.cursor/skills/using-orchestrator/SKILL.md +13 -0
- package/.cursor/skills/vault-mirror/SKILL.md +13 -0
- package/.cursor/skills/vault-sync/SKILL.md +13 -0
- package/.cursor/skills/wave-executor/SKILL.md +13 -0
- package/.cursor/skills/write-executable-plan/SKILL.md +13 -0
- package/.mcp.json +4 -1
- package/CHANGELOG.md +168 -0
- package/README.md +18 -15
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -1
- package/agents/docs-writer.md +3 -1
- package/agents/eval-judge.md +1 -1
- package/agents/session-reviewer.md +7 -1
- package/agents/test-writer.md +2 -1
- package/agents/ui-developer.md +2 -1
- package/commands/bootstrap.md +2 -2
- package/commands/close.md +3 -1
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/docs/USER-GUIDE.md +2 -2
- package/docs/ci-setup.md +14 -0
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +6 -6
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +76 -4
- package/docs/github-mirror-protection.md +197 -0
- package/docs/pi-setup.md +2 -0
- package/docs/rule-authoring.md +3 -1
- package/docs/scope-collision-guard.md +49 -2
- package/docs/session-config-reference.md +26 -4
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/enforce-scope.mjs +64 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +201 -20
- package/hooks/hooks-pi.json +1 -1
- package/hooks/hooks.json +2 -2
- package/hooks/on-session-end.mjs +211 -10
- package/hooks/on-session-start.mjs +214 -11
- package/hooks/on-stop.mjs +48 -9
- package/hooks/post-subagent-discovery-validator.mjs +34 -3
- package/hooks/post-tool-batch-wave-signal.mjs +11 -2
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-sessions-ledger-guard.mjs +159 -0
- package/hooks/pre-bash-staging-fence.mjs +4 -0
- package/hooks/pre-task-scope-disjoint.mjs +368 -35
- package/hooks/skill-invocation-telemetry.mjs +21 -10
- package/monitors/monitors.json +6 -0
- package/package.json +1 -1
- package/pi/prompts/journey-audit.md +12 -0
- package/rules/_index.md +9 -1
- package/rules/always-on/ask-via-tool.md +62 -0
- package/rules/always-on/bash-harness-pitfalls.md +168 -0
- package/rules/always-on/build-value.md +47 -0
- package/rules/always-on/cross-session-messaging.md +59 -0
- package/rules/always-on/loop-and-monitor.md +221 -0
- package/rules/always-on/parallel-sessions.md +142 -12
- package/rules/always-on/receiving-review.md +108 -0
- package/rules/always-on/test-value.md +40 -0
- package/rules/always-on/verification-before-completion.md +77 -0
- package/scripts/archive-closed-prds.mjs +258 -18
- package/scripts/autopilot.mjs +5 -0
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/export-hw-learnings.mjs +143 -2
- package/scripts/express-path.mjs +299 -0
- package/scripts/generate-cursor-adapter.mjs +253 -0
- package/scripts/github-protection-audit.mjs +358 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +240 -16
- package/scripts/lib/build-live-signals.mjs +24 -5
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +57 -6
- package/scripts/lib/convergence-monitor.mjs +13 -2
- package/scripts/lib/cursor-hook-bridge.mjs +443 -0
- package/scripts/lib/dispatcher/cli.mjs +2 -2
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +81 -8
- package/scripts/lib/gates/gate-helpers.mjs +76 -15
- package/scripts/lib/git-config-drift.mjs +134 -5
- package/scripts/lib/host-identity.mjs +247 -2
- package/scripts/lib/instruction-budget-guard.mjs +31 -1
- package/scripts/lib/issue-budget.mjs +229 -30
- package/scripts/lib/learnings/io.mjs +55 -10
- package/scripts/lib/learnings/schema.mjs +95 -28
- package/scripts/lib/lock-reaper.mjs +7 -1
- package/scripts/lib/locks/staging-fence-lock.mjs +5 -1
- package/scripts/lib/locks/state-md-lock.mjs +8 -1
- package/scripts/lib/memory-banner.mjs +5 -2
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +43 -3
- package/scripts/lib/quality-gate.mjs +271 -13
- package/scripts/lib/reconcile/emitter.mjs +87 -19
- package/scripts/lib/reconcile/engine.mjs +281 -13
- package/scripts/lib/reconcile/idempotency.mjs +102 -1
- package/scripts/lib/reconcile/renderer.mjs +148 -3
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/rule-loader.mjs +37 -2
- package/scripts/lib/rules-sync.mjs +51 -8
- package/scripts/lib/scope-gate.mjs +90 -0
- package/scripts/lib/session-close-backfill.mjs +369 -28
- package/scripts/lib/session-discovery.mjs +13 -3
- package/scripts/lib/session-end/phase-skip.mjs +37 -4
- package/scripts/lib/session-end/worktree-cleanup.mjs +154 -7
- package/scripts/lib/session-id.mjs +30 -14
- package/scripts/lib/session-identity/own-session.mjs +159 -0
- package/scripts/lib/session-lock.mjs +85 -30
- package/scripts/lib/session-schema/normalizer.mjs +70 -3
- package/scripts/lib/session-schema/validator.mjs +40 -0
- package/scripts/lib/session-start-probes.mjs +608 -0
- package/scripts/lib/session-transition.mjs +277 -0
- package/scripts/lib/sessions-staleness-banner.mjs +124 -57
- package/scripts/lib/spiral-carryover.mjs +90 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +350 -52
- package/scripts/lib/state-md/yaml-parser.mjs +145 -16
- package/scripts/lib/state-md.mjs +12 -2
- package/scripts/lib/telemetry/sync.mjs +46 -8
- package/scripts/lib/validate/check-agents.mjs +66 -0
- package/scripts/lib/validate/check-cursor-adapter.mjs +102 -0
- package/scripts/lib/validate/check-dead-bridge.mjs +24 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +16 -32
- package/scripts/lib/validate/check-hooks-symmetry.mjs +29 -63
- package/scripts/lib/validate/check-playwright-mcp-canary.mjs +13 -22
- package/scripts/lib/validate/check-plugin-monitors.mjs +10 -4
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-unwired-features.mjs +340 -32
- package/scripts/lib/validate/repo-files.mjs +275 -0
- package/scripts/lib/validate-vendored-rules.mjs +229 -7
- package/scripts/lib/vault-mirror/process.mjs +99 -43
- package/scripts/lib/vault-mirror/telemetry.mjs +210 -0
- package/scripts/lib/vault-staleness-banner.mjs +76 -6
- package/scripts/lib/vault-status/board-writer.mjs +211 -10
- package/scripts/lib/vault-status/narrative-mirror.mjs +188 -8
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +209 -12
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +1 -1
- package/skills/_shared/parallel-aware-preamble.md +4 -2
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +29 -2
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +3 -3
- package/skills/bootstrap/_shared-template.md +18 -10
- package/skills/bootstrap/deep-template.md +10 -6
- package/skills/bootstrap/fast-template.md +15 -8
- package/skills/bootstrap/standard-template.md +10 -6
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +41 -1
- package/skills/session-end/SKILL.md +17 -4
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +11 -3
- package/skills/session-end/phase-3-7a-recommendations.md +16 -2
- package/skills/session-plan/SKILL.md +6 -1
- package/skills/session-plan/wave-template.md +1 -0
- package/skills/session-start/SKILL.md +30 -16
- package/skills/session-start/phase-7-5-mode-selector.md +15 -3
- package/skills/session-start/phase-8-5-express-path.md +77 -12
- package/skills/vault-sync/validator.mjs +31 -0
- package/skills/wave-executor/SKILL.md +4 -2
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +102 -19
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
* | 2 | repo module failed to load | ALLOW + GUARD INACTIVE on stderr | #992/#993: a broken module must never brick the session — but never SILENTLY, or a crash is indistinguishable from `emitAllow` |
|
|
108
108
|
* | 3 | stdin empty / not JSON | ALLOW | not a real hook call; denying here blocks every dispatch on a harness quirk |
|
|
109
109
|
* | 4 | `tool_name` is not the dispatch tool | ALLOW | not our tool |
|
|
110
|
-
* | 5 | prompt carries no scope marker | ALLOW
|
|
111
|
-
* | 6 | scope block present but unparseable | ALLOW
|
|
110
|
+
* | 5 | prompt carries no scope marker | ALLOW + COUNT | 105 of 147 real prompts (71.4 %) have none. Non-extractable ≠ violation; denying these would deny 7 dispatches in 10. #1092: the allow now carries a counter-only record, so it is no longer byte-identical to "the guard never ran" |
|
|
111
|
+
* | 6 | scope block present but unparseable | ALLOW + COUNT | same reason as 5 — the parser is the fragile part, so its failures must resolve to the harmless side. Counted under a DISTINCT class from row 5 |
|
|
112
112
|
* | 7 | ledger unreadable / corrupt | WARN + ALLOW + SELF-HEAL | loss of state is not evidence of a violation; loud so it gets noticed. The verdict now CARRIES a fresh ledger, so the corruption is repaired on the spot — without it the guard stayed OFF for the whole remaining wave, visible only in one `systemMessage` |
|
|
113
113
|
* | 8 | `git ls-files` failed | ALLOW (degraded) | glob-vs-glob expansion degrades, concrete collisions are still found. A git outage is not a scope violation |
|
|
114
114
|
* | 9 | `findScopeCollisions` → not evaluable | WARN + ALLOW | the library says "not evaluable". Denying on a verdict with no witness is an assertion without evidence |
|
|
@@ -119,6 +119,13 @@
|
|
|
119
119
|
* |13 | liveness probe throws / no evidence at all | treat as IN FLIGHT | keeps row 11 biting; the blind case is bounded by `IN_FLIGHT_TTL_MS`, never unbounded |
|
|
120
120
|
* |14 | ledger lock not acquirable in `LEDGER_LOCK_TIMEOUT_MS` | run UNLOCKED (degraded) | the lock removes the read-modify-write race (below); failing to take it must not deny, so the cycle degrades to the pre-lock behaviour |
|
|
121
121
|
*
|
|
122
|
+
* Every row that reaches a verdict from `decide()` — 5–11 and 13–14 — also
|
|
123
|
+
* leaves an event record (§ Observability), so "which row fired" is answerable
|
|
124
|
+
* after the fact and not only in the moment. Rows 1–4 emit nothing (no decision
|
|
125
|
+
* was made), and neither do the two crash rows 2 and 12: a hook that fell over
|
|
126
|
+
* cannot describe itself, which is precisely why the GUARD INACTIVE banner on
|
|
127
|
+
* stderr is the signal there.
|
|
128
|
+
*
|
|
122
129
|
* Rows 7 and 9 use `emitWarn`, which calls `process.exit(0)` and NEVER RETURNS.
|
|
123
130
|
* That is why `decide()` below is a PURE function returning a verdict object and
|
|
124
131
|
* this module emits exactly ONCE, at the end. Warning from inside the checking
|
|
@@ -137,6 +144,26 @@
|
|
|
137
144
|
* PSA-005 STATE.md lock), with a dead-PID stale override and a short timeout;
|
|
138
145
|
* on timeout it degrades to the unlocked cycle (row 14) rather than denying.
|
|
139
146
|
*
|
|
147
|
+
* ## Observability — the ledger half of #1092
|
|
148
|
+
*
|
|
149
|
+
* Every dispatch DECISION also appends one `orchestrator.wave_dispatch.scope_checked`
|
|
150
|
+
* record to `<projectDir>/.orchestrator/metrics/events.jsonl`. The reason it
|
|
151
|
+
* exists is matrix rows 5/6: the no-signal ALLOW used to be byte-identical to
|
|
152
|
+
* "the guard never ran", and the in-ledger counter added first is a WAVE tally —
|
|
153
|
+
* it cannot say WHICH dispatch carried a scope. Payload: `wave` (omitted, never
|
|
154
|
+
* `0`, when unknown), `agent_id`, `declared_path_count`, `injected`, `shape`,
|
|
155
|
+
* `signal`, `ledger_result`, `collision_count`, `hook`, plus `session_id` /
|
|
156
|
+
* `semantic_session_id` when a session lock is readable.
|
|
157
|
+
*
|
|
158
|
+
* WHAT IT PROVES: this hook SAW (or did not see) a `FILE-SCOPE` declaration in
|
|
159
|
+
* the prompt the coordinator handed to the dispatch tool, and what the guard
|
|
160
|
+
* then decided. WHAT IT DOES NOT PROVE: that the block reached the agent's
|
|
161
|
+
* context, or that the agent read it. That is a RECEIVE-side question and the
|
|
162
|
+
* platform exposes no prompt-assembly boundary to answer it — the open half of
|
|
163
|
+
* #1092, with its revisit trigger in `docs/scope-collision-guard.md` § 4.2.
|
|
164
|
+
* NOTHING derived from the prompt BODY is in the payload: counts and enums
|
|
165
|
+
* only, no paths (#1092 acceptance criterion 3).
|
|
166
|
+
*
|
|
140
167
|
* ## stdout discipline
|
|
141
168
|
*
|
|
142
169
|
* Under the exit-0 protocol (#906, ADR-0011) allow and deny share exit code 0 —
|
|
@@ -240,6 +267,34 @@ const MAX_PATH_CHARS = 120;
|
|
|
240
267
|
/** Ledger bound: a wave dispatching more than this is pathological; drop oldest. */
|
|
241
268
|
const MAX_LEDGER_AGENTS = 64;
|
|
242
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Scope-signal classes (#1092). Counter-only — see {@link bumpSignalCounter}.
|
|
272
|
+
*/
|
|
273
|
+
const SIGNAL_MARKER_ABSENT = 'marker-absent';
|
|
274
|
+
const SIGNAL_UNPARSEABLE = 'unparseable';
|
|
275
|
+
const SIGNAL_EXTRACTED = 'extracted';
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Which declaration SHAPE produced the extracted paths (#1092). `none` means no
|
|
279
|
+
* shape yielded a path — the `marker-absent` AND the `unparseable` case alike,
|
|
280
|
+
* which is why the event carries `signal` beside `shape`: `shape` says which
|
|
281
|
+
* parser won, `signal` says whether a declaration was there at all.
|
|
282
|
+
*/
|
|
283
|
+
const SHAPE_FENCED = 'fenced';
|
|
284
|
+
const SHAPE_INLINE = 'inline';
|
|
285
|
+
const SHAPE_NONE = 'none';
|
|
286
|
+
|
|
287
|
+
/** The per-dispatch observability record (#1092) — see § Observability. */
|
|
288
|
+
const SCOPE_EVENT = 'orchestrator.wave_dispatch.scope_checked';
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Clamp for the one free-form string the event carries (`agent_id`, built from
|
|
292
|
+
* the coordinator's own `description`). A dispatch description is a label, but
|
|
293
|
+
* nothing enforces that, so the ledger line is bounded like every other payload
|
|
294
|
+
* in this file (§ stdout discipline).
|
|
295
|
+
*/
|
|
296
|
+
const MAX_AGENT_ID_CHARS = 120;
|
|
297
|
+
|
|
243
298
|
/**
|
|
244
299
|
* Blind-fallback liveness bound — see § Liveness for the measurement, the named
|
|
245
300
|
* ceiling and the revisit trigger. Only reached when the transcript carries NO
|
|
@@ -321,12 +376,74 @@ async function bootstrap() {
|
|
|
321
376
|
// Scope extraction — the fragile part, so every failure resolves to ALLOW
|
|
322
377
|
// ---------------------------------------------------------------------------
|
|
323
378
|
|
|
379
|
+
/** The marker vocabulary, shared by both declaration shapes below. */
|
|
380
|
+
const SCOPE_TERMS = 'DATEI[- ]SCOPE|FILE[- ]SCOPE|FILE SCOPE|DEIN SCOPE|SCOPE \\(|FILES? IN SCOPE';
|
|
381
|
+
|
|
324
382
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
383
|
+
* SHAPE 1 (highest precedence) — a marker near the START of a line, followed by
|
|
384
|
+
* a fenced block. This is the form `skills/wave-executor/wave-loop.md` § Scope
|
|
385
|
+
* Manifest specifies. Measured coverage: 42 of 147 archived prompts (28.6 %)
|
|
386
|
+
* carry one of these; the other 71.4 % are matrix row 5 — allowed, not denied.
|
|
387
|
+
*
|
|
388
|
+
* ## The 80-char window is NOT widened, and that is a measurement (#1092)
|
|
389
|
+
*
|
|
390
|
+
* The obvious repair for "the marker sits at column 210…506 of its line" is to
|
|
391
|
+
* widen the window. Measured 2026-08-26 over 4452 real first-record subagent
|
|
392
|
+
* prompts under `~/.claude/projects/ * / * /subagents/agent-*.jsonl` (709 of them
|
|
393
|
+
* this repo's own), comparing this regex against `^.{0,600}`:
|
|
394
|
+
*
|
|
395
|
+
* window 80 → 701 marker hits, 267 prompts yield ≥1 extracted path
|
|
396
|
+
* window 600 → 810 marker hits, 267 prompts yield ≥1 extracted path
|
|
397
|
+
* of the 102 prompts the wider window newly matches, 12 have any fenced block
|
|
398
|
+
* after the marker at all, and 0 yield a single path. In THIS repo: +34 newly
|
|
399
|
+
* matched, 0 with a fence, 0 paths.
|
|
400
|
+
*
|
|
401
|
+
* So widening recovers NOTHING and costs something real: it reclassifies 102
|
|
402
|
+
* prompts from "no marker" to "marker present but unparseable", which is
|
|
403
|
+
* precisely the distinction the signal counter below exists to record. Worse,
|
|
404
|
+
* most of what it newly matches is a CITATION, not a declaration — "quote the
|
|
405
|
+
* exact command, the file scope, the result", "outside your file scope",
|
|
406
|
+
* "File-Scope-Disjunktheit". A citation is not a declaration; reading the first
|
|
407
|
+
* hit in a region as one is the recorded failure of `parseEpicRef` (#1112).
|
|
408
|
+
*
|
|
409
|
+
* The real miss class is a different SHAPE, handled by {@link INLINE_SCOPE_DECL}.
|
|
328
410
|
*/
|
|
329
|
-
const SCOPE_MARKER =
|
|
411
|
+
const SCOPE_MARKER = new RegExp(`^.{0,80}(${SCOPE_TERMS})`, 'im');
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* SHAPE 2 (lower precedence) — the measured miss class: a declaration written
|
|
415
|
+
* INLINE, mid-sentence, with its paths comma-separated on the same line rather
|
|
416
|
+
* than in a fenced block:
|
|
417
|
+
*
|
|
418
|
+
* "…Max 25 turns. Edit ONLY your FILE-SCOPE: scripts/a.mjs, scripts/b.mjs"
|
|
419
|
+
*
|
|
420
|
+
* What separates this from a citation is not WHERE it sits but that it
|
|
421
|
+
* INTRODUCES something — the marker is followed by an optional short qualifier
|
|
422
|
+
* and/or parenthetical and then a declaration operator (`:` or an em/en dash).
|
|
423
|
+
* A bare hyphen is deliberately NOT an operator: it would admit
|
|
424
|
+
* "File-Scope-Planung". Anchored declaration shapes with precedence, rather than
|
|
425
|
+
* a narrower search space, is the fix #1112's learning prescribes.
|
|
426
|
+
*
|
|
427
|
+
* The tail window before the operator is 2 characters, and that is measured
|
|
428
|
+
* too: at 24 it admitted \`File-Scope-Disjunktheit: a.mjs, b.mjs\` — a compound
|
|
429
|
+
* NOUN reading as a declaration. A dash-introduced qualifier
|
|
430
|
+
* (\`FILE-SCOPE — exactly these:\`) is allowed explicitly instead of by window
|
|
431
|
+
* width, so widening the window is never the way to admit one.
|
|
432
|
+
*
|
|
433
|
+
* Measured 2026-08-26 on this repo's 709 subagent prompts: 136 yield paths
|
|
434
|
+
* today; this shape recovers 9 more, all of them genuine wave file scopes.
|
|
435
|
+
* Host-wide the same operator test admits 41 additional marker hits and its
|
|
436
|
+
* one false extraction (`[".filter"]`) is a prose fragment, which is why the
|
|
437
|
+
* fenced shape keeps precedence and this one runs only when that found nothing.
|
|
438
|
+
*/
|
|
439
|
+
const INLINE_SCOPE_DECL = new RegExp(`(${SCOPE_TERMS})`, 'ig');
|
|
440
|
+
const INLINE_DECL_OPERATOR = /^[^\n(:—–]{0,2}(\([^)\n]{0,80}\))?\s*(?:[—–][^\n:]{0,30})?\s*(:|—|–)/;
|
|
441
|
+
|
|
442
|
+
/** Separators a coordinator uses between paths in an inline declaration. */
|
|
443
|
+
const INLINE_SCOPE_SEPARATOR = /[,;·]| und | and | sowie /;
|
|
444
|
+
|
|
445
|
+
/** Bound the inline scan; a prompt naming the vocabulary this often is prose. */
|
|
446
|
+
const MAX_INLINE_MARKER_SCANS = 8;
|
|
330
447
|
|
|
331
448
|
/**
|
|
332
449
|
* A plausible repo-relative path. Deliberately strict — a false ACCEPT here
|
|
@@ -427,30 +544,19 @@ export function promoteDirEntries(files, known) {
|
|
|
427
544
|
}
|
|
428
545
|
|
|
429
546
|
/**
|
|
430
|
-
*
|
|
547
|
+
* Accept the segments that survive `looksLikeRepoPath`, normalised and deduped
|
|
548
|
+
* with order preserved. Shared by both declaration shapes.
|
|
431
549
|
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* is confidently extractable — which the caller treats as ALLOW (matrix rows
|
|
435
|
-
* 5 and 6), never as an empty scope that could collide.
|
|
436
|
-
*
|
|
437
|
-
* @param {string} prompt
|
|
438
|
-
* @returns {string[]} repo-relative paths/globs, normalised, deduped, order preserved
|
|
550
|
+
* @param {string[]} segments
|
|
551
|
+
* @returns {string[]}
|
|
439
552
|
*/
|
|
440
|
-
|
|
441
|
-
if (typeof prompt !== 'string' || prompt.length === 0) return [];
|
|
442
|
-
const markerMatch = SCOPE_MARKER.exec(prompt);
|
|
443
|
-
if (markerMatch === null) return [];
|
|
444
|
-
|
|
445
|
-
const after = prompt.slice(markerMatch.index + markerMatch[0].length);
|
|
446
|
-
// First fenced block after the marker. Non-greedy body; tolerates a language tag.
|
|
447
|
-
const fence = /```[^\n]*\n([\s\S]*?)```/.exec(after);
|
|
448
|
-
if (fence === null) return [];
|
|
449
|
-
|
|
553
|
+
function collectScopePaths(segments) {
|
|
450
554
|
const out = [];
|
|
451
555
|
const seen = new Set();
|
|
452
|
-
for (const
|
|
453
|
-
|
|
556
|
+
for (const raw of segments) {
|
|
557
|
+
// A trailing sentence period is punctuation, never part of a path
|
|
558
|
+
// (measured: "docs/events-schema.md." at the end of an inline declaration).
|
|
559
|
+
const cleaned = normalizeScopeEntry(cleanScopeLine(raw).replace(/\.$/, ''));
|
|
454
560
|
if (!looksLikeRepoPath(cleaned)) continue;
|
|
455
561
|
if (seen.has(cleaned)) continue;
|
|
456
562
|
seen.add(cleaned);
|
|
@@ -459,6 +565,94 @@ export function extractScopeFromPrompt(prompt) {
|
|
|
459
565
|
return out;
|
|
460
566
|
}
|
|
461
567
|
|
|
568
|
+
/**
|
|
569
|
+
* SHAPE 2 extraction — see {@link INLINE_SCOPE_DECL}. Reports whether a
|
|
570
|
+
* DECLARATION (not a citation) was seen at all, so the caller can tell matrix
|
|
571
|
+
* row 5 from row 6 even when no path survives.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} prompt
|
|
574
|
+
* @returns {{seen: boolean, files: string[]}}
|
|
575
|
+
*/
|
|
576
|
+
function extractInlineScopeDeclaration(prompt) {
|
|
577
|
+
INLINE_SCOPE_DECL.lastIndex = 0;
|
|
578
|
+
let seen = false;
|
|
579
|
+
let match;
|
|
580
|
+
let scans = 0;
|
|
581
|
+
while ((match = INLINE_SCOPE_DECL.exec(prompt)) !== null && scans < MAX_INLINE_MARKER_SCANS) {
|
|
582
|
+
scans++;
|
|
583
|
+
const after = prompt.slice(match.index + match[0].length);
|
|
584
|
+
const operator = INLINE_DECL_OPERATOR.exec(after);
|
|
585
|
+
if (operator === null) continue; // a citation, not a declaration
|
|
586
|
+
seen = true;
|
|
587
|
+
const line = after.slice(operator[0].length).split('\n')[0];
|
|
588
|
+
const files = collectScopePaths(line.split(INLINE_SCOPE_SEPARATOR));
|
|
589
|
+
if (files.length > 0) return { seen: true, files };
|
|
590
|
+
}
|
|
591
|
+
return { seen, files: [] };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Classify the scope signal a dispatch prompt carries, and extract it.
|
|
596
|
+
*
|
|
597
|
+
* Precedence, deliberately: SHAPE 1 (line-leading marker + fenced block, the
|
|
598
|
+
* documented form) first; SHAPE 2 (inline comma-separated declaration) only
|
|
599
|
+
* when SHAPE 1 produced nothing. The status is what makes a no-signal ALLOW
|
|
600
|
+
* distinguishable after the fact:
|
|
601
|
+
*
|
|
602
|
+
* `marker-absent` — no declaration of any recognised shape (matrix row 5)
|
|
603
|
+
* `unparseable` — a declaration is present but no path survived (row 6)
|
|
604
|
+
* `extracted` — `files` is non-empty
|
|
605
|
+
*
|
|
606
|
+
* `shape` names the parser that WON — `fenced` (SHAPE 1), `inline` (SHAPE 2), or
|
|
607
|
+
* `none` when neither produced a path. It is deliberately NOT a second spelling
|
|
608
|
+
* of `status`: a prompt carrying a fenced block whose lines are prose is
|
|
609
|
+
* `{status: 'unparseable', shape: 'none'}`, and collapsing the two would lose
|
|
610
|
+
* exactly the row-5/row-6 distinction the counter exists for.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} prompt
|
|
613
|
+
* @returns {{status: 'marker-absent'|'unparseable'|'extracted',
|
|
614
|
+
* shape: 'fenced'|'inline'|'none', files: string[]}}
|
|
615
|
+
*/
|
|
616
|
+
export function extractScopeSignal(prompt) {
|
|
617
|
+
if (typeof prompt !== 'string' || prompt.length === 0) {
|
|
618
|
+
return { status: SIGNAL_MARKER_ABSENT, shape: SHAPE_NONE, files: [] };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const markerMatch = SCOPE_MARKER.exec(prompt);
|
|
622
|
+
if (markerMatch !== null) {
|
|
623
|
+
const after = prompt.slice(markerMatch.index + markerMatch[0].length);
|
|
624
|
+
// First fenced block after the marker. Non-greedy body; tolerates a language tag.
|
|
625
|
+
const fence = /```[^\n]*\n([\s\S]*?)```/.exec(after);
|
|
626
|
+
if (fence !== null) {
|
|
627
|
+
const files = collectScopePaths(fence[1].split('\n'));
|
|
628
|
+
if (files.length > 0) return { status: SIGNAL_EXTRACTED, shape: SHAPE_FENCED, files };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const inline = extractInlineScopeDeclaration(prompt);
|
|
633
|
+
if (inline.files.length > 0) return { status: SIGNAL_EXTRACTED, shape: SHAPE_INLINE, files: inline.files };
|
|
634
|
+
return {
|
|
635
|
+
status: markerMatch !== null || inline.seen ? SIGNAL_UNPARSEABLE : SIGNAL_MARKER_ABSENT,
|
|
636
|
+
shape: SHAPE_NONE,
|
|
637
|
+
files: [],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Extract the declared file scope from a dispatch prompt. Returns `[]` when
|
|
643
|
+
* nothing is confidently extractable — which the caller treats as ALLOW (matrix
|
|
644
|
+
* rows 5 and 6), never as an empty scope that could collide.
|
|
645
|
+
*
|
|
646
|
+
* Thin wrapper over {@link extractScopeSignal}: callers that only need the paths
|
|
647
|
+
* (and the tests that pin them) keep the original signature.
|
|
648
|
+
*
|
|
649
|
+
* @param {string} prompt
|
|
650
|
+
* @returns {string[]} repo-relative paths/globs, normalised, deduped, order preserved
|
|
651
|
+
*/
|
|
652
|
+
export function extractScopeFromPrompt(prompt) {
|
|
653
|
+
return extractScopeSignal(prompt).files;
|
|
654
|
+
}
|
|
655
|
+
|
|
462
656
|
/**
|
|
463
657
|
* The dispatch's human description — the field the liveness probe matches
|
|
464
658
|
* against the transcript's `tool_use` blocks (present in 147/147 measured
|
|
@@ -662,6 +856,26 @@ export function waveKeyOf(projectDir, sessionId, readFn) {
|
|
|
662
856
|
return `${sessionId}|w?|?`;
|
|
663
857
|
}
|
|
664
858
|
|
|
859
|
+
/**
|
|
860
|
+
* The wave NUMBER out of a `waveKeyOf()` key, for the telemetry record (#1092).
|
|
861
|
+
*
|
|
862
|
+
* Returns `null` — never `0` — when the key carries the `w?` fallback or a
|
|
863
|
+
* non-positive value, so the caller can OMIT the field. `.claude/rules/host-
|
|
864
|
+
* resources.md` § HR-105 in one line: an invented `wave: 0` would read as a real
|
|
865
|
+
* wave in every later query, exactly like the `wave_number` contract the
|
|
866
|
+
* quality-gate event already follows (`docs/events-schema.md`).
|
|
867
|
+
*
|
|
868
|
+
* @param {string} waveKey
|
|
869
|
+
* @returns {number|null}
|
|
870
|
+
*/
|
|
871
|
+
function waveNumberOf(waveKey) {
|
|
872
|
+
if (typeof waveKey !== 'string') return null;
|
|
873
|
+
const seg = waveKey.split('|')[1];
|
|
874
|
+
if (typeof seg !== 'string' || !seg.startsWith('w')) return null;
|
|
875
|
+
const n = Number(seg.slice(1));
|
|
876
|
+
return Number.isSafeInteger(n) && n > 0 ? n : null;
|
|
877
|
+
}
|
|
878
|
+
|
|
665
879
|
/**
|
|
666
880
|
* Tracked files, for glob expansion inside `findScopeCollisions`. The library is
|
|
667
881
|
* pure and must not spawn — supplying this is precisely the hook's job.
|
|
@@ -716,9 +930,59 @@ function clipPath(p) {
|
|
|
716
930
|
// since #1020's lock landed, would also skip the lock's release `finally`.
|
|
717
931
|
// ---------------------------------------------------------------------------
|
|
718
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Record that ONE dispatch carried a given scope-signal class (#1092).
|
|
935
|
+
*
|
|
936
|
+
* ## Why this exists
|
|
937
|
+
*
|
|
938
|
+
* The no-signal path returned `{action: 'allow'}` with no `ledger` field, and
|
|
939
|
+
* the caller only writes `if (verdict.ledger)` — so nothing was written
|
|
940
|
+
* anywhere. Correct operation ("this agent legitimately has no scope") and total
|
|
941
|
+
* absence ("the coordinator injected nothing, or the marker never matched")
|
|
942
|
+
* produced BYTE-IDENTICAL evidence. Measured 2026-08-26 over 709 of this repo's
|
|
943
|
+
* subagent prompts: 136 yield paths, 47 carry a line-leading marker with no
|
|
944
|
+
* fenced block, 59 carry marker + fence but no surviving path. Without a counter
|
|
945
|
+
* none of those three classes is distinguishable from "the guard never ran".
|
|
946
|
+
*
|
|
947
|
+
* ## What it is NOT
|
|
948
|
+
*
|
|
949
|
+
* This is a SEND-SIDE counter. It observes what the COORDINATOR PUT IN THE
|
|
950
|
+
* PROMPT at dispatch time — never what the agent received, parsed, or obeyed. A
|
|
951
|
+
* non-zero `extracted` proves a scope was written into the prompt; it proves
|
|
952
|
+
* nothing about delivery or about the agent honouring it. Whether the injected
|
|
953
|
+
* block actually reached the agent's context is a RECEIVE-side question that
|
|
954
|
+
* only the subagent transcript can answer, and no number here may be read as
|
|
955
|
+
* that proof.
|
|
956
|
+
*
|
|
957
|
+
* Counter ONLY: no prompt text, no paths, no agent ids. The ledger is a shared
|
|
958
|
+
* working-copy artefact, and a scope-signal tally must not become a second,
|
|
959
|
+
* unreviewed copy of prompt content.
|
|
960
|
+
*
|
|
961
|
+
* Scoped to the wave, like `agents`: a new `waveKey` starts a fresh tally
|
|
962
|
+
* rather than accumulating across waves.
|
|
963
|
+
*
|
|
964
|
+
* @param {object|null} ledger previously recorded wave state
|
|
965
|
+
* @param {string} waveKey
|
|
966
|
+
* @param {'marker-absent'|'unparseable'|'extracted'} status
|
|
967
|
+
* @returns {{'marker-absent': number, unparseable: number, extracted: number}}
|
|
968
|
+
*/
|
|
969
|
+
export function bumpSignalCounter(ledger, waveKey, status) {
|
|
970
|
+
const carried = (ledger !== null && ledger?.waveKey === waveKey && ledger.scopeSignals !== null
|
|
971
|
+
&& typeof ledger.scopeSignals === 'object' && !Array.isArray(ledger.scopeSignals))
|
|
972
|
+
? ledger.scopeSignals
|
|
973
|
+
: {};
|
|
974
|
+
const next = {};
|
|
975
|
+
for (const key of [SIGNAL_MARKER_ABSENT, SIGNAL_UNPARSEABLE, SIGNAL_EXTRACTED]) {
|
|
976
|
+
const prior = carried[key];
|
|
977
|
+
next[key] = Number.isSafeInteger(prior) && prior >= 0 ? prior : 0;
|
|
978
|
+
}
|
|
979
|
+
next[status] += 1;
|
|
980
|
+
return next;
|
|
981
|
+
}
|
|
982
|
+
|
|
719
983
|
/**
|
|
720
984
|
* @typedef {{action: 'allow'|'deny'|'warn', reason?: string, suggestion?: string,
|
|
721
|
-
* ledger?: object|null, note?: string}} Verdict
|
|
985
|
+
* ledger?: object|null, note?: string, telemetry?: object}} Verdict
|
|
722
986
|
*/
|
|
723
987
|
|
|
724
988
|
/**
|
|
@@ -743,14 +1007,54 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
743
1007
|
const toolInput = input?.tool_input;
|
|
744
1008
|
if (toolInput === null || typeof toolInput !== 'object') return { action: 'allow' };
|
|
745
1009
|
|
|
746
|
-
const
|
|
1010
|
+
const at = typeof nowIso === 'string' ? nowIso : new Date().toISOString();
|
|
1011
|
+
|
|
1012
|
+
const signal = extractScopeSignal(toolInput.prompt);
|
|
1013
|
+
const files = signal.files;
|
|
1014
|
+
const id = agentIdOf(toolInput);
|
|
1015
|
+
const priorAgents = (ledger !== null && ledger?.waveKey === waveKey && Array.isArray(ledger.agents))
|
|
1016
|
+
? ledger.agents
|
|
1017
|
+
: [];
|
|
1018
|
+
|
|
1019
|
+
// #1092 — the observability record this dispatch will leave in the ledger.
|
|
1020
|
+
// BUILT here, EMITTED by the caller: `decide()` is a pure function (§ stdout
|
|
1021
|
+
// discipline), and an `await` inside it would put an I/O failure on the
|
|
1022
|
+
// decision path. Counter-shaped by construction — a count, three enums and the
|
|
1023
|
+
// agent id, never a path, never a byte of the prompt (issue #1092 acceptance
|
|
1024
|
+
// criterion 3). `wave` is omitted rather than zeroed when unknown.
|
|
1025
|
+
const wave = waveNumberOf(waveKey);
|
|
1026
|
+
const telemetryFor = (ledgerResult, collisionCount = 0) => ({
|
|
1027
|
+
...(wave === null ? {} : { wave }),
|
|
1028
|
+
agent_id: id.slice(0, MAX_AGENT_ID_CHARS),
|
|
1029
|
+
declared_path_count: files.length,
|
|
1030
|
+
injected: files.length > 0,
|
|
1031
|
+
shape: signal.shape,
|
|
1032
|
+
signal: signal.status,
|
|
1033
|
+
ledger_result: ledgerResult,
|
|
1034
|
+
collision_count: collisionCount,
|
|
1035
|
+
});
|
|
1036
|
+
|
|
747
1037
|
// Rows 5 + 6: nothing confidently extractable → allow. Non-extractable is not
|
|
748
1038
|
// a violation, and denying here would deny ~7 dispatches in 10.
|
|
749
|
-
|
|
1039
|
+
//
|
|
1040
|
+
// #1092: the ALLOW now CARRIES a counter so it leaves a trace. Before this,
|
|
1041
|
+
// "no scope in the prompt" and "the guard never ran" were indistinguishable
|
|
1042
|
+
// after the fact. `agents` is carried through UNCHANGED — this dispatch
|
|
1043
|
+
// declared no scope, so it adds no scope claim to the wave.
|
|
1044
|
+
if (files.length === 0) {
|
|
1045
|
+
return {
|
|
1046
|
+
action: 'allow',
|
|
1047
|
+
telemetry: telemetryFor('no-scope'),
|
|
1048
|
+
ledger: {
|
|
1049
|
+
waveKey,
|
|
1050
|
+
updated: at,
|
|
1051
|
+
agents: priorAgents,
|
|
1052
|
+
scopeSignals: bumpSignalCounter(ledger, waveKey, signal.status),
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
750
1056
|
|
|
751
|
-
const id = agentIdOf(toolInput);
|
|
752
1057
|
const desc = agentDescOf(toolInput);
|
|
753
|
-
const at = typeof nowIso === 'string' ? nowIso : new Date().toISOString();
|
|
754
1058
|
const self = { id, desc, files, at };
|
|
755
1059
|
|
|
756
1060
|
// Row 7: ledger existed but was unparseable. Terminal warn — decided here and
|
|
@@ -760,7 +1064,8 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
760
1064
|
if (ledgerCorrupt) {
|
|
761
1065
|
return {
|
|
762
1066
|
action: 'warn',
|
|
763
|
-
|
|
1067
|
+
telemetry: telemetryFor('warn-ledger-corrupt'),
|
|
1068
|
+
ledger: { waveKey, updated: at, agents: [self], scopeSignals: bumpSignalCounter(null, waveKey, signal.status) },
|
|
764
1069
|
note:
|
|
765
1070
|
`${HOOK_NAME}: wave dispatch ledger was unreadable — scope-disjointness NOT checked for ` +
|
|
766
1071
|
`"${id}"; the ledger has been reset, so the next dispatch is checked again.`,
|
|
@@ -792,6 +1097,7 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
792
1097
|
waveKey,
|
|
793
1098
|
updated: at,
|
|
794
1099
|
agents: [...others, self].slice(-MAX_LEDGER_AGENTS),
|
|
1100
|
+
scopeSignals: bumpSignalCounter(ledger, waveKey, signal.status),
|
|
795
1101
|
};
|
|
796
1102
|
|
|
797
1103
|
const collisions = Array.isArray(verdictLib?.collisions) ? verdictLib.collisions : [];
|
|
@@ -806,6 +1112,7 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
806
1112
|
if (verdictLib?.ok !== true && collisions.length === 0 && duplicateIds.length === 0) {
|
|
807
1113
|
return {
|
|
808
1114
|
action: 'warn',
|
|
1115
|
+
telemetry: telemetryFor('warn-not-evaluable'),
|
|
809
1116
|
ledger: nextLedger,
|
|
810
1117
|
note:
|
|
811
1118
|
`${HOOK_NAME}: scope collision check not evaluable for "${id}" — dispatch allowed, ` +
|
|
@@ -818,7 +1125,7 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
818
1125
|
// this guard, and re-denying it would block an innocent third agent.
|
|
819
1126
|
const mine = collisions.filter((c) => c?.a === id || c?.b === id);
|
|
820
1127
|
|
|
821
|
-
if (mine.length === 0) return { action: 'allow', ledger: nextLedger };
|
|
1128
|
+
if (mine.length === 0) return { action: 'allow', telemetry: telemetryFor('allow'), ledger: nextLedger };
|
|
822
1129
|
|
|
823
1130
|
// § Liveness — the review's HIGH finding. A collision with an agent that has
|
|
824
1131
|
// ALREADY FINISHED is a sequential repair pass, not a race. The probe is called
|
|
@@ -844,7 +1151,13 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
844
1151
|
const kept = others.filter((a) => !finishedIds.has(a.id));
|
|
845
1152
|
return {
|
|
846
1153
|
action: 'allow',
|
|
847
|
-
|
|
1154
|
+
telemetry: telemetryFor('allow-finished', mine.length),
|
|
1155
|
+
ledger: {
|
|
1156
|
+
waveKey,
|
|
1157
|
+
updated: at,
|
|
1158
|
+
agents: [...kept, self].slice(-MAX_LEDGER_AGENTS),
|
|
1159
|
+
scopeSignals: nextLedger.scopeSignals,
|
|
1160
|
+
},
|
|
848
1161
|
};
|
|
849
1162
|
}
|
|
850
1163
|
|
|
@@ -876,7 +1189,7 @@ export function decide({ input, ledger, ledgerCorrupt, waveKey, knownFiles, coll
|
|
|
876
1189
|
|
|
877
1190
|
// Deliberately NOT persisting the ledger on deny: the dispatch did not happen,
|
|
878
1191
|
// so recording it would make the retry-after-fix look like a duplicate.
|
|
879
|
-
return { action: 'deny', reason, suggestion };
|
|
1192
|
+
return { action: 'deny', telemetry: telemetryFor('deny', mine.length), reason, suggestion };
|
|
880
1193
|
}
|
|
881
1194
|
|
|
882
1195
|
// ---------------------------------------------------------------------------
|
|
@@ -964,6 +1277,26 @@ async function main() {
|
|
|
964
1277
|
verdict = cycle();
|
|
965
1278
|
}
|
|
966
1279
|
|
|
1280
|
+
// #1092 — one ledger line per dispatch DECISION, awaited BEFORE the terminal
|
|
1281
|
+
// emit: `emitAllow`/`emitDeny`/`emitWarn` all call `process.exit()`, which
|
|
1282
|
+
// discards a pending append (the same ordering `hooks/enforce-scope.mjs`
|
|
1283
|
+
// documents at its own event site). Best-effort in BOTH directions — the
|
|
1284
|
+
// import is dynamic so a checkout without `scripts/lib/events.mjs` degrades to
|
|
1285
|
+
// silence instead of crashing (§ Import safety), and the catch guarantees a
|
|
1286
|
+
// failed write cannot change the verdict or the exit code.
|
|
1287
|
+
if (verdict.telemetry) {
|
|
1288
|
+
try {
|
|
1289
|
+
const { emitEvent, sessionAttribution } = await import(
|
|
1290
|
+
pathToFileURL(path.join(PLUGIN_ROOT, 'scripts', 'lib', 'events.mjs')).href
|
|
1291
|
+
);
|
|
1292
|
+
await emitEvent(
|
|
1293
|
+
SCOPE_EVENT,
|
|
1294
|
+
{ hook: HOOK_NAME, ...verdict.telemetry, ...sessionAttribution(projectDir) },
|
|
1295
|
+
{ repoRoot: projectDir }
|
|
1296
|
+
);
|
|
1297
|
+
} catch { /* observability is best-effort — it never blocks the decision */ }
|
|
1298
|
+
}
|
|
1299
|
+
|
|
967
1300
|
if (verdict.action === 'deny') return emitDeny(verdict.reason, verdict.suggestion);
|
|
968
1301
|
if (verdict.action === 'warn') return emitWarn(verdict.note);
|
|
969
1302
|
return emitAllow();
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* 2. Read JSON payload from stdin: { tool_name, tool_input: { skill }, session_id }.
|
|
12
12
|
* 3. Belt-and-suspenders guard: if tool_name !== "Skill", exit 0 immediately.
|
|
13
13
|
* 4. Build a 'selected' record and call appendSkillInvocation().
|
|
14
|
-
* 5. Daily-fallback telemetry flush check (Epic #841, #844) — non-blocking:
|
|
15
|
-
* when
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* 5. Daily-fallback telemetry flush check (Epic #841, #844; #1138) — non-blocking:
|
|
15
|
+
* when more than 24h have passed since the last successful flush AND either
|
|
16
|
+
* the offline queue is non-empty or a session has completed since, AND
|
|
17
|
+
* consent resolves to send, spawn a detached child that runs
|
|
18
|
+
* `telemetry _flush`. Cheap by construction (env kill-switch pre-check →
|
|
19
|
+
* state stat → queue/ledger probe → consent) and never loads the roster on
|
|
20
|
+
* the hot hook path.
|
|
19
21
|
* 6. Output: nothing on stdout. Diagnostic errors to stderr only.
|
|
20
22
|
*
|
|
21
23
|
* Exit codes: 0 always (informational, never blocking).
|
|
@@ -40,7 +42,9 @@ import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
|
40
42
|
// Constants
|
|
41
43
|
// ---------------------------------------------------------------------------
|
|
42
44
|
|
|
43
|
-
const
|
|
45
|
+
const METRICS_DIR = path.join(SO_PROJECT_DIR, '.orchestrator', 'metrics');
|
|
46
|
+
|
|
47
|
+
const JSONL_PATH = path.join(METRICS_DIR, 'skill-invocations.jsonl');
|
|
44
48
|
|
|
45
49
|
/** Absolute path to the telemetry CLI (carries the hidden `_flush` subcommand). */
|
|
46
50
|
const TELEMETRY_CLI_PATH = path.resolve(
|
|
@@ -95,8 +99,9 @@ function readStdinJson() {
|
|
|
95
99
|
/**
|
|
96
100
|
* Non-blocking daily-fallback flush trigger. Ordered cheapest-first:
|
|
97
101
|
* 1. env kill-switches (no I/O) — DO_NOT_TRACK / SO_TELEMETRY_DISABLED.
|
|
98
|
-
* 2. shouldDailyFlush (one telemetry.json read
|
|
99
|
-
*
|
|
102
|
+
* 2. shouldDailyFlush (one telemetry.json read; a queue stat + a sessions.jsonl
|
|
103
|
+
* read only once the 24h gate has passed) — the common case (flushed within
|
|
104
|
+
* 24h) returns here WITHOUT loading owner.yaml or the roster.
|
|
100
105
|
* 3. full consent resolution — spawn a detached `telemetry _flush` only when it
|
|
101
106
|
* resolves to send.
|
|
102
107
|
*
|
|
@@ -108,6 +113,8 @@ function readStdinJson() {
|
|
|
108
113
|
* @param {number} [opts.now] Reference time epoch-ms (default Date.now()).
|
|
109
114
|
* @param {string} [opts.statePath] telemetry.json path override (test injection).
|
|
110
115
|
* @param {string} [opts.queuePath] queue path override (test injection).
|
|
116
|
+
* @param {string} [opts.metricsDir] Metrics dir for shouldDailyFlush's catch-up
|
|
117
|
+
* probe (default: this repo's metrics dir).
|
|
111
118
|
* @returns {{ spawned: boolean, reason: string }}
|
|
112
119
|
*/
|
|
113
120
|
export function maybeSpawnDailyFlush({
|
|
@@ -116,6 +123,7 @@ export function maybeSpawnDailyFlush({
|
|
|
116
123
|
now = Date.now(),
|
|
117
124
|
statePath,
|
|
118
125
|
queuePath,
|
|
126
|
+
metricsDir = METRICS_DIR,
|
|
119
127
|
} = {}) {
|
|
120
128
|
try {
|
|
121
129
|
// 1. Cheapest gate: env kill-switches, no file I/O.
|
|
@@ -123,8 +131,11 @@ export function maybeSpawnDailyFlush({
|
|
|
123
131
|
return { spawned: false, reason: 'disabled-env' };
|
|
124
132
|
}
|
|
125
133
|
|
|
126
|
-
// 2.
|
|
127
|
-
|
|
134
|
+
// 2. Backlog / catch-up check — bails out before owner.yaml load in the
|
|
135
|
+
// common case. Since #1138 this is due EITHER on a stale non-empty queue
|
|
136
|
+
// OR on a session that completed after the last successful flush, so the
|
|
137
|
+
// fallback can originate a ping instead of only retrying failed sends.
|
|
138
|
+
if (!shouldDailyFlush({ statePath, queuePath, metricsDir, now })) {
|
|
128
139
|
return { spawned: false, reason: 'not-due' };
|
|
129
140
|
}
|
|
130
141
|
|
package/monitors/monitors.json
CHANGED
|
@@ -10,5 +10,11 @@
|
|
|
10
10
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/convergence-monitor.mjs --tail",
|
|
11
11
|
"description": "Wave convergence telemetry monitor — tails .orchestrator/metrics/events.jsonl + sessions.jsonl, surfaces convergence signals (shrinking diff, pass-rate plateau, velocity).",
|
|
12
12
|
"when": "on-skill-invoke:wave-executor"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "wave-transcript-tail",
|
|
16
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}\"/scripts/lib/wave-transcript-tail.mjs --tail",
|
|
17
|
+
"description": "FA-1 wave supervision — tails live subagent transcripts of the OWN session and emits stagnation_detected(source:tail) to .orchestrator/metrics/events.jsonl (#1114)",
|
|
18
|
+
"when": "on-skill-invoke:wave-executor"
|
|
13
19
|
}
|
|
14
20
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "session-orchestrator",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.23.0",
|
|
4
4
|
"description": "Loop engineering for AI coding agents — turn ad-hoc sessions into a repeatable research → plan → wave-execute → close loop with verification gates. Runs on Claude Code, Codex CLI, Cursor, and Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://session-orchestrator.com",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Outside-in product audit as a deep session — 7 read-only roles check what the site promises against what the code does, what a user experiences, what arrives by mail, and what the data says is used. Writes a dossier; needs a per-repo journey-manifest.
|
|
3
|
+
argument-hint: "[manifest-path]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /journey-audit
|
|
7
|
+
|
|
8
|
+
Use the Session Orchestrator command definition at `commands/journey-audit.md`.
|
|
9
|
+
|
|
10
|
+
Arguments: $@
|
|
11
|
+
|
|
12
|
+
Read that command file and follow it exactly. When it references `$ARGUMENTS`, substitute the arguments above. Keep all Session Orchestrator platform fallbacks intact.
|
package/rules/_index.md
CHANGED
|
@@ -30,9 +30,17 @@ baseline project type (`nextjs-saas`, `express-service`, `docker-service`,
|
|
|
30
30
|
|
|
31
31
|
## always-on (vendored to every consumer repo)
|
|
32
32
|
|
|
33
|
-
- `always-on/parallel-sessions.md` — PSA-001/
|
|
33
|
+
- `always-on/parallel-sessions.md` — PSA-001..007 multi-session discipline (detect / pause / destructive-ops / commit / STATE.md lock / grep-verification / subagent git-write ban)
|
|
34
34
|
- `always-on/commit-discipline.md` — atomic commits, stage-by-name, no `git add .`
|
|
35
35
|
- `always-on/npm-quality-gates.md` — the typecheck + test + lint triad before commit
|
|
36
|
+
- `always-on/verification-before-completion.md` — VBC-001..005: no completion claim without fresh, quoted verification evidence
|
|
37
|
+
- `always-on/receiving-review.md` — RCR-001..009: the 6-step review-handling pattern, four-class finding triage, push-back posture
|
|
38
|
+
- `always-on/ask-via-tool.md` — AUQ-001..006: route before you ask; structured options over prose question lists
|
|
39
|
+
- `always-on/test-value.md` — TV-001..005: name the bug a test catches, or do not write it; deletion is a feature
|
|
40
|
+
- `always-on/build-value.md` — BV-001..004: the seven-rung build ladder, the four protections never simplified away
|
|
41
|
+
- `always-on/cross-session-messaging.md` — CSM-001..005: messaging is transport, not shared state; no permission laundering
|
|
42
|
+
- `always-on/loop-and-monitor.md` — LM-001..008: routing between `/goal`, Workflows, Channels, Monitor, `/loop`, and Routines
|
|
43
|
+
- `always-on/bash-harness-pitfalls.md` — six false-green shell-harness failure classes (path-scoped via its own `globs:`/`paths:` frontmatter)
|
|
36
44
|
|
|
37
45
|
## opt-in-stack (vendored on match)
|
|
38
46
|
|