session-orchestrator 3.22.0 → 3.24.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/remote-offload/SKILL.md +13 -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 +446 -0
- package/README.md +22 -17
- package/agents/AGENTS.md +23 -4
- package/agents/code-implementer.md +2 -1
- package/agents/db-specialist.md +2 -2
- 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 +194 -25
- package/docs/codex-setup.md +64 -0
- package/docs/components.md +7 -7
- package/docs/cursor-setup.md +26 -47
- package/docs/events-schema.md +120 -10
- 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 +89 -9
- package/docs/session-config-template.md +38 -7
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +52 -1
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/lock-bootstrap.mjs +8 -4
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/vcs-create-matcher.mjs +397 -38
- package/hooks/cwd-change-restore.mjs +9 -29
- package/hooks/enforce-scope.mjs +93 -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 +486 -19
- package/hooks/on-session-start.mjs +263 -12
- package/hooks/on-stop.mjs +392 -24
- package/hooks/post-bash-write-verify.mjs +104 -4
- package/hooks/post-subagent-discovery-validator.mjs +182 -21
- package/hooks/post-tool-batch-wave-signal.mjs +165 -42
- package/hooks/post-tool-failure-corrective-context.mjs +9 -32
- package/hooks/pre-bash-issue-budget.mjs +117 -4
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- 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/hooks/subagent-telemetry.mjs +11 -26
- 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 +31 -12
- package/scripts/backfill-abandoned-sessions.mjs +80 -11
- package/scripts/backfill-evidence-digest.mjs +376 -0
- package/scripts/cursor-install.mjs +89 -48
- package/scripts/emit-event.mjs +10 -2
- 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/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/autopilot/worktree-pipeline.mjs +318 -18
- package/scripts/lib/build-live-signals.mjs +49 -27
- package/scripts/lib/ci-status-banner.mjs +158 -11
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/block-header.mjs +55 -0
- package/scripts/lib/config/discovery-validator.mjs +7 -2
- package/scripts/lib/config/health-endpoints.mjs +383 -0
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/remote-hosts.mjs +233 -0
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config-schema.mjs +9 -1
- package/scripts/lib/config.mjs +87 -8
- 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/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +238 -5
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- 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/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- 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 +25 -10
- package/scripts/lib/memory-paths.mjs +15 -6
- package/scripts/lib/mode-selector/scoring.mjs +53 -6
- package/scripts/lib/peer-discovery.mjs +20 -2
- 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 +517 -18
- 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 +126 -0
- package/scripts/lib/session-close-backfill.mjs +427 -37
- package/scripts/lib/session-discovery.mjs +69 -5
- package/scripts/lib/session-end/phase-skip.mjs +38 -5
- 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 +220 -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-canonical.mjs +446 -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/schema.mjs +74 -8
- package/scripts/lib/telemetry/sync.mjs +91 -16
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- 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 +25 -65
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- 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-skill-script-paths.mjs +436 -0
- package/scripts/lib/validate/check-test-value-bans.mjs +165 -17
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +333 -32
- package/scripts/lib/validate/check-validator-registration.mjs +248 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- 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-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +381 -141
- package/scripts/lib/vault-status/narrative-mirror.mjs +190 -27
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-executor/remote-dispatch.mjs +504 -0
- package/scripts/lib/wave-resource-gate.mjs +127 -7
- package/scripts/lib/wave-transcript-tail.mjs +889 -0
- package/scripts/materialize-wave-scope.mjs +228 -15
- package/scripts/mcp-server.sh +11 -2
- package/scripts/memory-propose.mjs +132 -8
- package/scripts/parse-config.mjs +65 -0
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +29 -0
- package/scripts/validate-wave-scope.mjs +67 -0
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +305 -51
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +19 -4
- 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/contract-version-bump/SKILL.md +1 -1
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +38 -1
- package/skills/journey-audit/SKILL.md +270 -0
- package/skills/peekaboo-driver/SKILL.md +15 -3
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +46 -3
- package/skills/remote-offload/SKILL.md +89 -0
- 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 +20 -9
- 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 +54 -17
- 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 +5 -3
- package/skills/wave-executor/circuit-breaker.md +34 -9
- package/skills/wave-executor/wave-loop.md +143 -22
- package/templates/_shared/journey-manifest.md +110 -0
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -9,12 +9,24 @@
|
|
|
9
9
|
* each bare `files` array first, then writes the complete record array as the
|
|
10
10
|
* aggregate sidecar consumed by validate-wave-scope's --assert-disjoint and
|
|
11
11
|
* --union modes.
|
|
12
|
+
*
|
|
13
|
+
* After the aggregate is published, per-agent declarations left behind by an
|
|
14
|
+
* earlier materialization of the SAME wave are reconciled away (#1103) — but
|
|
15
|
+
* only against a proven session owner. See {@link reconcileOrphans}.
|
|
16
|
+
*
|
|
17
|
+
* PEER RECORDS (`peer-session-*`, #1195) get NO per-agent file: shape (a) is
|
|
18
|
+
* `$AGENT_FILESCOPE_JSON`, addressed by an agent id at dispatch time, and no
|
|
19
|
+
* agent is ever dispatched for a peer session — nothing reads it. They are
|
|
20
|
+
* still written into the aggregate, where `--assert-disjoint` and
|
|
21
|
+
* `hooks/post-bash-write-verify.mjs` do read them; `--union` excludes them so
|
|
22
|
+
* `allowedPaths` never grants a peer's territory to this wave's agents.
|
|
12
23
|
*/
|
|
13
24
|
|
|
14
|
-
import { readFileSync, unlinkSync } from 'node:fs';
|
|
25
|
+
import { readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
15
26
|
import { resolve } from 'node:path';
|
|
16
27
|
import { fileURLToPath } from 'node:url';
|
|
17
28
|
import { writeJsonAtomicSync } from './lib/io.mjs';
|
|
29
|
+
import { isPeerRecordId } from './lib/scope-gate.mjs';
|
|
18
30
|
|
|
19
31
|
const HELP = `Usage: node scripts/materialize-wave-scope.mjs --state-dir <dir> --wave <positive-int> [--json]
|
|
20
32
|
|
|
@@ -27,7 +39,12 @@ Required:
|
|
|
27
39
|
Positive wave number used in filescopes/wave-N/.
|
|
28
40
|
|
|
29
41
|
Options:
|
|
30
|
-
--
|
|
42
|
+
--session <id> This session's id (session_id or its semantic twin). Used
|
|
43
|
+
ONLY to prove ownership before an orphaned per-agent
|
|
44
|
+
declaration of the same wave is removed (#1103). Without
|
|
45
|
+
it, orphans are reported and RETAINED, never deleted.
|
|
46
|
+
--json Emit {ok, aggregatePath, perAgentPaths, removedOrphans,
|
|
47
|
+
retainedOrphans} to stdout.
|
|
31
48
|
-h, --help Show this help and exit 0.
|
|
32
49
|
|
|
33
50
|
Output:
|
|
@@ -41,8 +58,17 @@ Examples:
|
|
|
41
58
|
|
|
42
59
|
Writes:
|
|
43
60
|
<state-dir>/filescopes/wave-N/<id>.json Bare string[] for each record
|
|
61
|
+
EXCEPT a peer-session-* record (#1195), which is
|
|
62
|
+
aggregate-only: no agent is dispatched for it, so nothing
|
|
63
|
+
would read its per-agent file.
|
|
44
64
|
<state-dir>/filescopes/wave-N.scopes.json Aggregate [{id, files}, ...]
|
|
45
65
|
|
|
66
|
+
Removes (only with a proven owner — see --session):
|
|
67
|
+
<state-dir>/filescopes/wave-N/<stale-id>.json Per-agent declarations of this
|
|
68
|
+
wave whose id is absent from the new record array. An
|
|
69
|
+
orphan that cannot be proven owned is named on stderr and
|
|
70
|
+
LEFT IN PLACE; that is a WARN, never a failure.
|
|
71
|
+
|
|
46
72
|
Exit codes:
|
|
47
73
|
0 All declaration files and the aggregate sidecar were written.
|
|
48
74
|
1 Usage or input validation error; no write was attempted.
|
|
@@ -55,11 +81,12 @@ class WriteError extends Error {}
|
|
|
55
81
|
|
|
56
82
|
/**
|
|
57
83
|
* @param {string[]} argv
|
|
58
|
-
* @returns {{ stateDir: string, wave: number, json: boolean, help: boolean }}
|
|
84
|
+
* @returns {{ stateDir: string, wave: number, session: string|null, json: boolean, help: boolean }}
|
|
59
85
|
*/
|
|
60
86
|
export function parseCliArgs(argv) {
|
|
61
87
|
let stateDir;
|
|
62
88
|
let waveRaw;
|
|
89
|
+
let session = null;
|
|
63
90
|
let json = false;
|
|
64
91
|
let help = false;
|
|
65
92
|
|
|
@@ -73,7 +100,7 @@ export function parseCliArgs(argv) {
|
|
|
73
100
|
help = true;
|
|
74
101
|
continue;
|
|
75
102
|
}
|
|
76
|
-
if (arg === '--state-dir' || arg === '--wave') {
|
|
103
|
+
if (arg === '--state-dir' || arg === '--wave' || arg === '--session') {
|
|
77
104
|
const value = argv[index + 1];
|
|
78
105
|
if (value === undefined || value.startsWith('--')) {
|
|
79
106
|
throw new InputError(`${arg} requires a value`);
|
|
@@ -81,6 +108,12 @@ export function parseCliArgs(argv) {
|
|
|
81
108
|
if (arg === '--state-dir') {
|
|
82
109
|
if (stateDir !== undefined) throw new InputError('--state-dir may be specified only once');
|
|
83
110
|
stateDir = value;
|
|
111
|
+
} else if (arg === '--session') {
|
|
112
|
+
if (session !== null) throw new InputError('--session may be specified only once');
|
|
113
|
+
// An empty or whitespace-only id proves nothing and must not be read as
|
|
114
|
+
// an owner: it would make every orphan deletable by any caller.
|
|
115
|
+
if (value.trim().length === 0) throw new InputError('--session must be a non-empty id');
|
|
116
|
+
session = value.trim();
|
|
84
117
|
} else {
|
|
85
118
|
if (waveRaw !== undefined) throw new InputError('--wave may be specified only once');
|
|
86
119
|
waveRaw = value;
|
|
@@ -91,7 +124,7 @@ export function parseCliArgs(argv) {
|
|
|
91
124
|
throw new InputError(`unknown argument: ${arg}`);
|
|
92
125
|
}
|
|
93
126
|
|
|
94
|
-
if (help) return { stateDir: '', wave: 0, json, help: true };
|
|
127
|
+
if (help) return { stateDir: '', wave: 0, session, json, help: true };
|
|
95
128
|
if (stateDir === undefined) throw new InputError('--state-dir is required');
|
|
96
129
|
if (waveRaw === undefined) throw new InputError('--wave is required');
|
|
97
130
|
if (stateDir.length === 0 || /[\0\r\n]/.test(stateDir)) {
|
|
@@ -103,7 +136,7 @@ export function parseCliArgs(argv) {
|
|
|
103
136
|
|
|
104
137
|
const wave = Number(waveRaw);
|
|
105
138
|
if (!Number.isSafeInteger(wave)) throw new InputError('--wave must be a safe positive integer');
|
|
106
|
-
return { stateDir, wave, json, help: false };
|
|
139
|
+
return { stateDir, wave, session, json, help: false };
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/**
|
|
@@ -183,6 +216,141 @@ export function validateScopeRecords(value) {
|
|
|
183
216
|
return value;
|
|
184
217
|
}
|
|
185
218
|
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Read the session ids the sibling manifest `<state-dir>/wave-scope.json`
|
|
222
|
+
* declares for this state directory (#1123 wrote both `session` — the raw
|
|
223
|
+
* `session_id` — and its human-readable twin `semantic_session`).
|
|
224
|
+
*
|
|
225
|
+
* A caller may legitimately hold either spelling, so BOTH are returned and a
|
|
226
|
+
* match against either proves ownership. Any failure to read or parse the
|
|
227
|
+
* manifest returns an empty list, which the caller must treat as "ownership NOT
|
|
228
|
+
* established" — never as "no owner, therefore mine".
|
|
229
|
+
*
|
|
230
|
+
* @param {string} stateDir
|
|
231
|
+
* @param {typeof readFileSync} [readFile]
|
|
232
|
+
* @returns {string[]}
|
|
233
|
+
*/
|
|
234
|
+
export function manifestSessionIds(stateDir, readFile = readFileSync) {
|
|
235
|
+
let manifest;
|
|
236
|
+
try {
|
|
237
|
+
manifest = JSON.parse(readFile(resolve(stateDir, 'wave-scope.json'), 'utf8'));
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
if (!isRecord(manifest)) return [];
|
|
242
|
+
return ['session', 'semantic_session']
|
|
243
|
+
.map((key) => manifest[key])
|
|
244
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
245
|
+
.map((value) => value.trim());
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reconcile per-agent declarations left behind by an earlier materialization of
|
|
250
|
+
* the SAME wave (#1103).
|
|
251
|
+
*
|
|
252
|
+
* The write loop is a pure upsert over the new id set, so a file whose id was
|
|
253
|
+
* dropped from the plan survives: it is in no aggregate — `--assert-disjoint`
|
|
254
|
+
* and `--union` cannot see it — while every by-id consumer (FILE-SCOPE
|
|
255
|
+
* injection, the Learnings-Index, `--assert-subset`) still reads it. A live
|
|
256
|
+
* scope claim with zero aggregate coverage.
|
|
257
|
+
*
|
|
258
|
+
* ## Why this is NOT a directory wipe
|
|
259
|
+
*
|
|
260
|
+
* The wave number is not a session-unique key: two sessions sharing one working
|
|
261
|
+
* copy both call their first wave `wave-1` and both resolve to
|
|
262
|
+
* `<state-dir>/filescopes/wave-1/`. Clearing the directory would convert a
|
|
263
|
+
* stale-read bug into cross-session data loss — the class
|
|
264
|
+
* `.claude/rules/parallel-sessions.md` PSA-003 forbids by name ("Did I create
|
|
265
|
+
* this file? If not, it is not mine to touch"). So removal needs a two-part
|
|
266
|
+
* test, and only the second part is about the file:
|
|
267
|
+
*
|
|
268
|
+
* 1. the id is absent from the new record array (it is an orphan), AND
|
|
269
|
+
* 2. this invocation can PROVE it owns the state directory — `sessionId`
|
|
270
|
+
* matches an id the sibling manifest declares.
|
|
271
|
+
*
|
|
272
|
+
* Failing (2) is not an error and never blocks: the orphan is returned in
|
|
273
|
+
* `retained` WITH its reason so the caller can name the file. A silent skip is
|
|
274
|
+
* the one outcome forbidden here, because it is byte-identical to a clean run.
|
|
275
|
+
*
|
|
276
|
+
* ## Named ceiling (BV-004)
|
|
277
|
+
*
|
|
278
|
+
* Bounded by WHEN materialization runs, not by what it inspects: a session that
|
|
279
|
+
* re-materializes wave N *while its own agents are still in flight* would delete
|
|
280
|
+
* the scope files those agents are reading. That is safe today only because
|
|
281
|
+
* `skills/wave-executor/wave-loop.md` § Scope Manifest 3.2 places
|
|
282
|
+
* (re-)materialization strictly PRE-dispatch, so no reader exists yet. REVISIT
|
|
283
|
+
* TRIGGER: any caller that materializes a wave after its dispatch has begun — a
|
|
284
|
+
* mid-wave scope amendment, a repair pass reusing the same wave number, or a
|
|
285
|
+
* dispatch loop that re-runs the materializer per agent. Ownership does not
|
|
286
|
+
* protect against that case; the ordering does.
|
|
287
|
+
*
|
|
288
|
+
* @param {object} params
|
|
289
|
+
* @param {string} params.scopeDir `<state-dir>/filescopes/wave-N`
|
|
290
|
+
* @param {string[]} params.keepIds ids present in the new record array
|
|
291
|
+
* @param {string[]} params.ownerIds session ids the manifest declares
|
|
292
|
+
* @param {string|null} params.sessionId this invocation's session id
|
|
293
|
+
* @param {typeof readdirSync} [params.readDir]
|
|
294
|
+
* @param {typeof unlinkSync} [params.removeFile]
|
|
295
|
+
* @returns {{removed: string[], retained: Array<{file: string, reason: string}>}}
|
|
296
|
+
*/
|
|
297
|
+
export function reconcileOrphans({
|
|
298
|
+
scopeDir,
|
|
299
|
+
keepIds,
|
|
300
|
+
ownerIds,
|
|
301
|
+
sessionId,
|
|
302
|
+
readDir = readdirSync,
|
|
303
|
+
removeFile = unlinkSync,
|
|
304
|
+
}) {
|
|
305
|
+
let entries;
|
|
306
|
+
try {
|
|
307
|
+
entries = readDir(scopeDir, { withFileTypes: true });
|
|
308
|
+
} catch {
|
|
309
|
+
// No directory yet (first materialization) or unreadable — nothing to
|
|
310
|
+
// reconcile. Not a failure: the aggregate is already published.
|
|
311
|
+
return { removed: [], retained: [] };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Case-INSENSITIVE keep set. validateScopeRecords already rejects two ids that
|
|
315
|
+
// differ only in case within one input, so this cannot hide a real orphan —
|
|
316
|
+
// but on a case-insensitive filesystem `A2.json` and `a2.json` are ONE file,
|
|
317
|
+
// and deleting the "orphan" would delete the declaration just written.
|
|
318
|
+
const keep = new Set(keepIds.map((id) => `${id}.json`.toLowerCase()));
|
|
319
|
+
const orphans = [];
|
|
320
|
+
for (const entry of entries) {
|
|
321
|
+
if (typeof entry?.isFile === 'function' && !entry.isFile()) continue;
|
|
322
|
+
const name = typeof entry === 'string' ? entry : entry?.name;
|
|
323
|
+
if (typeof name !== 'string' || !name.endsWith('.json')) continue;
|
|
324
|
+
if (keep.has(name.toLowerCase())) continue;
|
|
325
|
+
orphans.push(name);
|
|
326
|
+
}
|
|
327
|
+
if (orphans.length === 0) return { removed: [], retained: [] };
|
|
328
|
+
|
|
329
|
+
const proven = typeof sessionId === 'string' && sessionId.length > 0 && ownerIds.includes(sessionId);
|
|
330
|
+
if (!proven) {
|
|
331
|
+
const reason = sessionId === null || sessionId === undefined
|
|
332
|
+
? 'no --session given, so this state directory has no provable owner'
|
|
333
|
+
: ownerIds.length === 0
|
|
334
|
+
? 'wave-scope.json declares no session, so ownership cannot be established'
|
|
335
|
+
: `wave-scope.json is owned by a different session (${ownerIds.join(' / ')})`;
|
|
336
|
+
return { removed: [], retained: orphans.map((file) => ({ file, reason })) };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const removed = [];
|
|
340
|
+
const retained = [];
|
|
341
|
+
for (const file of orphans) {
|
|
342
|
+
try {
|
|
343
|
+
removeFile(resolve(scopeDir, file));
|
|
344
|
+
removed.push(file);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// A failed unlink leaves a live orphan behind — report it, never throw:
|
|
347
|
+
// the aggregate is already published and the materialization succeeded.
|
|
348
|
+
retained.push({ file, reason: `could not remove: ${error instanceof Error ? error.message : String(error)}` });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { removed, retained };
|
|
352
|
+
}
|
|
353
|
+
|
|
186
354
|
/**
|
|
187
355
|
* Materialize validated declarations in their required write order.
|
|
188
356
|
*
|
|
@@ -193,13 +361,30 @@ export function validateScopeRecords(value) {
|
|
|
193
361
|
* has succeeded.
|
|
194
362
|
*
|
|
195
363
|
* @param {Array<{id: string, files: string[]}>} records
|
|
196
|
-
* @param {{ stateDir: string, wave: number,
|
|
197
|
-
*
|
|
364
|
+
* @param {{ stateDir: string, wave: number, session?: string|null,
|
|
365
|
+
* writeJson?: typeof writeJsonAtomicSync,
|
|
366
|
+
* readDir?: typeof readdirSync, removeFile?: typeof unlinkSync,
|
|
367
|
+
* readFile?: typeof readFileSync }} options
|
|
368
|
+
* @returns {{ aggregatePath: string, perAgentPaths: string[],
|
|
369
|
+
* removedOrphans: string[], retainedOrphans: Array<{file: string, reason: string}> }}
|
|
198
370
|
*/
|
|
199
|
-
export function materializeWaveScope(records, {
|
|
371
|
+
export function materializeWaveScope(records, {
|
|
372
|
+
stateDir,
|
|
373
|
+
wave,
|
|
374
|
+
session = null,
|
|
375
|
+
writeJson = writeJsonAtomicSync,
|
|
376
|
+
readDir = readdirSync,
|
|
377
|
+
removeFile = unlinkSync,
|
|
378
|
+
readFile = readFileSync,
|
|
379
|
+
}) {
|
|
200
380
|
const scopeDir = resolve(stateDir, 'filescopes', `wave-${wave}`);
|
|
201
381
|
const aggregatePath = resolve(stateDir, 'filescopes', `wave-${wave}.scopes.json`);
|
|
202
|
-
|
|
382
|
+
// #1195 — peer records are aggregate-only (see the header note): no reader
|
|
383
|
+
// exists for a per-agent file that no dispatch will ever address. Excluding
|
|
384
|
+
// them from `keepIds` below also lets a stale `peer-session-*.json` written
|
|
385
|
+
// before this rule be reconciled away like any other orphan.
|
|
386
|
+
const agentRecords = records.filter((r) => !isPeerRecordId(r.id));
|
|
387
|
+
const perAgentPaths = agentRecords.map(({ id }) => resolve(scopeDir, `${id}.json`));
|
|
203
388
|
|
|
204
389
|
try {
|
|
205
390
|
unlinkSync(aggregatePath);
|
|
@@ -209,8 +394,8 @@ export function materializeWaveScope(records, { stateDir, wave, writeJson = writ
|
|
|
209
394
|
}
|
|
210
395
|
}
|
|
211
396
|
|
|
212
|
-
for (let index = 0; index <
|
|
213
|
-
const result = writeJson(perAgentPaths[index],
|
|
397
|
+
for (let index = 0; index < agentRecords.length; index++) {
|
|
398
|
+
const result = writeJson(perAgentPaths[index], agentRecords[index].files, { tmpPrefix: '.materialize-wave-scope' });
|
|
214
399
|
if (!result?.ok) {
|
|
215
400
|
throw new WriteError(`cannot write per-agent declaration ${perAgentPaths[index]}: ${result?.error ?? 'unknown write failure'}`);
|
|
216
401
|
}
|
|
@@ -220,7 +405,21 @@ export function materializeWaveScope(records, { stateDir, wave, writeJson = writ
|
|
|
220
405
|
if (!aggregateResult?.ok) {
|
|
221
406
|
throw new WriteError(`cannot write aggregate declaration ${aggregatePath}: ${aggregateResult?.error ?? 'unknown write failure'}`);
|
|
222
407
|
}
|
|
223
|
-
|
|
408
|
+
|
|
409
|
+
// #1103 — strictly AFTER the aggregate write. The aggregate is this command's
|
|
410
|
+
// publication marker (see the write-order note above), so reconciling before
|
|
411
|
+
// it would remove a live declaration while the run could still fail and leave
|
|
412
|
+
// no aggregate at all — deleting coverage that nothing replaced.
|
|
413
|
+
const { removed, retained } = reconcileOrphans({
|
|
414
|
+
scopeDir,
|
|
415
|
+
keepIds: agentRecords.map(({ id }) => id),
|
|
416
|
+
ownerIds: manifestSessionIds(stateDir, readFile),
|
|
417
|
+
sessionId: session,
|
|
418
|
+
readDir,
|
|
419
|
+
removeFile,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return { aggregatePath, perAgentPaths, removedOrphans: removed, retainedOrphans: retained };
|
|
224
423
|
}
|
|
225
424
|
|
|
226
425
|
/**
|
|
@@ -256,10 +455,24 @@ export function main() {
|
|
|
256
455
|
return;
|
|
257
456
|
}
|
|
258
457
|
const records = validateScopeRecords(readStdinJson());
|
|
259
|
-
const { aggregatePath, perAgentPaths } =
|
|
458
|
+
const { aggregatePath, perAgentPaths, removedOrphans, retainedOrphans } =
|
|
459
|
+
materializeWaveScope(records, args);
|
|
460
|
+
|
|
461
|
+
// stderr carries ONLY the anomalous cases. Measured constraint, not taste:
|
|
462
|
+
// the corpus pins byte-empty stderr on this command's success path
|
|
463
|
+
// (tests/scripts/materialize-wave-scope.test.mjs and
|
|
464
|
+
// tests/integration/wave-scope-producer.test.mjs), and a wave with no
|
|
465
|
+
// orphans IS the success path. Both lists always reach --json.
|
|
466
|
+
for (const file of removedOrphans) {
|
|
467
|
+
process.stderr.write(`materialize-wave-scope: removed orphaned declaration ${file} (id absent from this wave's records)\n`);
|
|
468
|
+
}
|
|
469
|
+
for (const { file, reason } of retainedOrphans) {
|
|
470
|
+
process.stderr.write(`materialize-wave-scope: WARN orphaned declaration ${file} RETAINED — ${reason}\n`);
|
|
471
|
+
}
|
|
472
|
+
|
|
260
473
|
process.stdout.write(
|
|
261
474
|
args.json
|
|
262
|
-
? `${JSON.stringify({ ok: true, aggregatePath, perAgentPaths })}\n`
|
|
475
|
+
? `${JSON.stringify({ ok: true, aggregatePath, perAgentPaths, removedOrphans, retainedOrphans })}\n`
|
|
263
476
|
: `${aggregatePath}\n`,
|
|
264
477
|
);
|
|
265
478
|
} catch (error) {
|
package/scripts/mcp-server.sh
CHANGED
|
@@ -40,9 +40,18 @@ text_content() {
|
|
|
40
40
|
# ---------------------------------------------------------------------------
|
|
41
41
|
# Verify jq is available (fatal — we cannot parse JSON without it)
|
|
42
42
|
# ---------------------------------------------------------------------------
|
|
43
|
+
# The diagnostic goes to STDERR, never stdout. A `{"id":null,"error":...}` line
|
|
44
|
+
# on stdout is NOT a valid `initialize` response, so the client reports the same
|
|
45
|
+
# opaque "connection closed: initialize response" it reports when the script
|
|
46
|
+
# cannot be found at all — the two failure modes were indistinguishable
|
|
47
|
+
# (GH Kanevry/session-orchestrator#64). A named, actionable stderr line keeps
|
|
48
|
+
# them apart. Same shape as hooks/run-node.sh's Node-missing diagnostic.
|
|
43
49
|
if ! command -v jq >/dev/null 2>&1; then
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
{
|
|
51
|
+
echo "session-orchestrator: 'jq' not found in PATH — the MCP server did not start."
|
|
52
|
+
echo " Fix: install jq (macOS: brew install jq · Debian/Ubuntu: apt-get install jq), then restart the client."
|
|
53
|
+
echo " (MCP server shells do not source ~/.zshrc, so a jq that works in your terminal can still be invisible here.)"
|
|
54
|
+
} >&2
|
|
46
55
|
exit 1
|
|
47
56
|
fi
|
|
48
57
|
|
|
@@ -46,6 +46,8 @@ import { dirname, join, isAbsolute } from 'node:path';
|
|
|
46
46
|
import { fileURLToPath } from 'node:url';
|
|
47
47
|
|
|
48
48
|
import { isWaveAgentContext, WAVE_AGENT_ENV_VAR, WAVE_AGENT_ENV_VALUE } from './lib/wave-context.mjs';
|
|
49
|
+
import { readProcessLocalSessionIds } from './lib/session-identity/own-session.mjs';
|
|
50
|
+
import { readLockDetailed } from './lib/session-lock.mjs';
|
|
49
51
|
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
51
53
|
// Constants & defaults
|
|
@@ -259,11 +261,14 @@ const confidence = confidenceVal;
|
|
|
259
261
|
|
|
260
262
|
let frontmatter = {};
|
|
261
263
|
let stateMdReadOk = false;
|
|
264
|
+
/** Directory holding STATE.md — the same state-dir the wave manifest lives in (#1166). */
|
|
265
|
+
let stateDir = null;
|
|
262
266
|
|
|
263
267
|
try {
|
|
264
268
|
// resolveStateMdPath from state-md.mjs: falls back to .claude/STATE.md
|
|
265
269
|
const stateMdMod = await import('./lib/state-md.mjs');
|
|
266
270
|
const stateMdPath = stateMdMod.resolveStateMdPath(process.cwd());
|
|
271
|
+
stateDir = dirname(stateMdPath);
|
|
267
272
|
|
|
268
273
|
if (!existsSync(stateMdPath)) {
|
|
269
274
|
if (!dryRun) {
|
|
@@ -352,17 +357,136 @@ if (!dryRun && (currentWaveRaw === undefined || currentWaveRaw === null || curre
|
|
|
352
357
|
}
|
|
353
358
|
|
|
354
359
|
// ---------------------------------------------------------------------------
|
|
355
|
-
// Step 3 — Build wave ID
|
|
356
|
-
// when !dryRun; falls back to a 'W-dryrun' placeholder under --dry-run when
|
|
357
|
-
// STATE.md was absent/unparseable/missing the field — #741.3)
|
|
360
|
+
// Step 3 — Build the RUNNING wave ID (#1166)
|
|
358
361
|
// ---------------------------------------------------------------------------
|
|
362
|
+
//
|
|
363
|
+
// #1166 — two writers, two semantics: `skills/wave-executor/wave-loop.md` § 3a
|
|
364
|
+
// writes STATE.md `current-wave` = the JUST-COMPLETED wave, while the
|
|
365
|
+
// coordinator's `<state-dir>/wave-scope.json` carries `wave` = the RUNNING
|
|
366
|
+
// wave. Stamping proposals off `current-wave` verbatim filed every wave-N+1
|
|
367
|
+
// proposal into the W<N> quota bucket. Priority: the manifest ONLY when it is
|
|
368
|
+
// bound to THIS session, else `current-wave + 1`. An UNBOUND manifest is not
|
|
369
|
+
// trusted (#1177 FX1) — since #1123 both writers stamp the binding, so a
|
|
370
|
+
// binding-less manifest is a peer's or a stale artefact.
|
|
371
|
+
//
|
|
372
|
+
// The session binding compares `manifest.semantic_session` — the SEMANTIC label
|
|
373
|
+
// — against the semantic id looked up in `session.lock`, and that lookup is only
|
|
374
|
+
// honoured when this PROCESS's own raw session id equals the lock's raw
|
|
375
|
+
// `session_id` (#1188). STATE.md's `session` field is NOT a witness here: it is
|
|
376
|
+
// written by the lock owner, so under a peer's lock both "sides" name the peer.
|
|
377
|
+
|
|
378
|
+
/** @returns {string|undefined} */
|
|
379
|
+
function resolveRunningWaveId() {
|
|
380
|
+
if (stateDir !== null) {
|
|
381
|
+
try {
|
|
382
|
+
const manifestPath = join(stateDir, 'wave-scope.json');
|
|
383
|
+
if (existsSync(manifestPath)) {
|
|
384
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
385
|
+
const boundSession = manifest?.semantic_session ?? manifest?.session;
|
|
386
|
+
const unbound =
|
|
387
|
+
boundSession === undefined || boundSession === null || boundSession === '';
|
|
388
|
+
// BOTH sides must be non-empty strings. `undefined === undefined` is
|
|
389
|
+
// true, so a STATE.md WITHOUT a `session` key previously "matched" any
|
|
390
|
+
// manifest whose `semantic_session` was also absent-but-present-in-the
|
|
391
|
+
// -comparison — adopting a foreign coordinator's wave number as its own.
|
|
392
|
+
const mineSide = manifest?.semantic_session;
|
|
393
|
+
// #1188 — STATE.md is a working-copy artefact of the LOCK OWNER, not a
|
|
394
|
+
// process-local witness: when a peer holds the lock the peer wrote BOTH
|
|
395
|
+
// STATE.md and the manifest, so the two "independent" sides agree about
|
|
396
|
+
// the PEER. Same class as #1177 FX1; same shape as attributionForRecord
|
|
397
|
+
// (scripts/lib/events.mjs): the lock is a raw->semantic LOOKUP,
|
|
398
|
+
// authorised by a process-local match on the RAW id. Measured
|
|
399
|
+
// 2026-09-02: a wave-agent process carries the COORDINATOR's raw uuid in
|
|
400
|
+
// CLAUDE_CODE_SESSION_ID and no semantic id at all, so comparing the
|
|
401
|
+
// process-local id against `semantic_session` directly never matches.
|
|
402
|
+
// CEILING (BV-004): a harness exporting no session id (Codex CLI,
|
|
403
|
+
// Cursor) resolves no process-local id -> binding unprovable ->
|
|
404
|
+
// current-wave + 1 fallback with a stderr line. REVISIT if that fallback
|
|
405
|
+
// rate is measured non-trivial in events.jsonl.
|
|
406
|
+
const lock = readLockDetailed({ repoRoot: process.cwd() });
|
|
407
|
+
const processLocal = readProcessLocalSessionIds();
|
|
408
|
+
const lockRaw =
|
|
409
|
+
lock.status === 'ok' ? String(lock.lock.session_id ?? '').trim() : '';
|
|
410
|
+
const authorised = lockRaw !== '' && processLocal.includes(lockRaw);
|
|
411
|
+
const oursSide = authorised ? (lock.lock.semantic_session_id ?? '') : '';
|
|
412
|
+
if (!authorised) {
|
|
413
|
+
// A SILENT non-match is the exact failure mode this fix exists to make
|
|
414
|
+
// visible: without this line, a proposal bucketed by the fallback is
|
|
415
|
+
// indistinguishable from one bucketed by a trusted manifest.
|
|
416
|
+
process.stderr.write(
|
|
417
|
+
`⚠ memory-propose: session binding unprovable (session.lock ${lock.status}` +
|
|
418
|
+
`${lockRaw ? `, raw ${lockRaw}` : ''}; process-local ids: ` +
|
|
419
|
+
`${processLocal.length > 0 ? processLocal.join(', ') : 'none'}) — ` +
|
|
420
|
+
'ignoring wave-scope.json, falling back to current-wave + 1\n',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const mine =
|
|
424
|
+
typeof mineSide === 'string' && mineSide.length > 0 &&
|
|
425
|
+
typeof oursSide === 'string' && oursSide.length > 0 &&
|
|
426
|
+
mineSide === oursSide;
|
|
427
|
+
// An UNBOUND manifest is NOT trusted (#1177 FX1). It used to be, as
|
|
428
|
+
// "legacy" — but since #1123 BOTH writers stamp the binding, so a
|
|
429
|
+
// manifest without one today is a PEER's or a stale artefact, and
|
|
430
|
+
// trusting it files this session's proposals into a foreign quota
|
|
431
|
+
// bucket. Unbound → say so once on stderr and fall back.
|
|
432
|
+
if (unbound) {
|
|
433
|
+
process.stderr.write(
|
|
434
|
+
'memory-propose: wave-scope.json unbound (no semantic_session) — ignored\n',
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
if (mine) {
|
|
438
|
+
const raw = manifest?.wave;
|
|
439
|
+
const num =
|
|
440
|
+
typeof raw === 'number'
|
|
441
|
+
? raw
|
|
442
|
+
: typeof raw === 'string' && raw.trim() !== ''
|
|
443
|
+
? Number(raw)
|
|
444
|
+
: NaN;
|
|
445
|
+
if (Number.isInteger(num)) return `W${num}`;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch (err) {
|
|
449
|
+
// Unreadable / malformed manifest → fall through to the +1 fallback, but
|
|
450
|
+
// SAY SO. A swallowed parse error makes a wrongly-bucketed proposal look
|
|
451
|
+
// like ordinary fallback behaviour; the operator then has no signal that
|
|
452
|
+
// the coordinator's own manifest is corrupt. Diagnostics on stderr keeps
|
|
453
|
+
// the stdout JSON contract intact (cli-design.md § JSON-First Output).
|
|
454
|
+
process.stderr.write(
|
|
455
|
+
`⚠ memory-propose: cannot read ${join(stateDir, 'wave-scope.json')} ` +
|
|
456
|
+
`(${err?.message ?? String(err)}) — falling back to current-wave + 1\n`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (currentWaveRaw === undefined || currentWaveRaw === null || currentWaveRaw === '') {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
const completed = Number(currentWaveRaw);
|
|
465
|
+
if (!Number.isFinite(completed)) return undefined; // never 'WNaN'
|
|
466
|
+
// CEILING (BV-004): `+1` assumes waves run consecutively and that STATE.md is
|
|
467
|
+
// maintained — it is WRONG when a wave is skipped, when the plan is adapted
|
|
468
|
+
// mid-session, or under `persistence: false` where `current-wave` never
|
|
469
|
+
// advances. It is the fallback precisely because wave-scope.json is the only
|
|
470
|
+
// artefact that KNOWS the running wave.
|
|
471
|
+
// REVISIT when a wave-scope.json manifest is present in every dispatch (then
|
|
472
|
+
// delete the fallback and reject instead), or when a proposal is observed in
|
|
473
|
+
// the wrong quota bucket while the manifest was readable.
|
|
474
|
+
return `W${completed + 1}`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const resolvedWaveId = resolveRunningWaveId();
|
|
359
478
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
479
|
+
if (!dryRun && resolvedWaveId === undefined) {
|
|
480
|
+
exit(
|
|
481
|
+
{
|
|
482
|
+
status: STATUS.REJECTED_WRONG_CONTEXT,
|
|
483
|
+
detail: `Cannot resolve the running wave: no usable wave-scope.json and STATE.md 'current-wave' is not numeric (got '${currentWaveRaw}')`,
|
|
484
|
+
},
|
|
485
|
+
3,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
364
488
|
|
|
365
|
-
const waveId = dryRun ? (
|
|
489
|
+
const waveId = dryRun ? (resolvedWaveId ?? 'W-dryrun') : resolvedWaveId;
|
|
366
490
|
|
|
367
491
|
// ---------------------------------------------------------------------------
|
|
368
492
|
// Step 4 — Read Session Config (quota + floor)
|
package/scripts/parse-config.mjs
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Output: Single JSON object to stdout with ALL config fields (defaults applied).
|
|
10
10
|
* Exit codes: 0 success, 1 error (message to stderr)
|
|
11
11
|
*
|
|
12
|
+
* Unparsable lines inside `## Session Config` (#1097) are reported per line on
|
|
13
|
+
* stderr under `enforcement: warn` (the default) and refuse the run under
|
|
14
|
+
* `enforcement: strict`; `off` is silent. stdout is unchanged for any
|
|
15
|
+
* well-formed block — a warning never alters the emitted JSON.
|
|
16
|
+
*
|
|
12
17
|
* Environment:
|
|
13
18
|
* SO_CONFIG_FILE — override filename (e.g. "AGENTS.md") resolved from project root
|
|
14
19
|
* SO_SKIP_CONFIG_VALIDATION — set to "1" to bypass validate-config.mjs
|
|
@@ -19,6 +24,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
19
24
|
import { join, dirname, resolve, parse as parsePath } from 'node:path';
|
|
20
25
|
import { fileURLToPath } from 'node:url';
|
|
21
26
|
import { parseSessionConfig } from './lib/config.mjs';
|
|
27
|
+
import { collectUnparsableLines } from './lib/config/section-extractor.mjs';
|
|
28
|
+
import { ENFORCEMENT_VALUES } from './lib/config-schema.mjs';
|
|
22
29
|
|
|
23
30
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
24
31
|
|
|
@@ -108,6 +115,64 @@ try {
|
|
|
108
115
|
process.exit(1);
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Unparsable-line gate (#1097) — never a silent default
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
//
|
|
122
|
+
// A line inside `## Session Config` that no parser can read is simply absent
|
|
123
|
+
// from the KV map, and every consumer then applies its own default — `false`
|
|
124
|
+
// for the booleans. A broken key therefore reads exactly like a deliberately
|
|
125
|
+
// disabled feature, in every log, forever. This gate is the only place that
|
|
126
|
+
// difference is ever stated out loud.
|
|
127
|
+
//
|
|
128
|
+
// It runs BEFORE the validator so a malformed block is reported in terms of the
|
|
129
|
+
// FILE (line number + text) rather than in terms of the defaults it silently
|
|
130
|
+
// produced. `off` stays silent by definition — that is what turning enforcement
|
|
131
|
+
// off means; `warn` (the default) reports and proceeds; `strict` refuses.
|
|
132
|
+
|
|
133
|
+
const unparsableLines = collectUnparsableLines(content);
|
|
134
|
+
|
|
135
|
+
if (unparsableLines.length > 0) {
|
|
136
|
+
// Belt-and-braces, not a live branch: `parseSessionConfig` already REFUSES
|
|
137
|
+
// an out-of-vocabulary enforcement value above (measured: `enforcement:
|
|
138
|
+
// banana` exits 1 with "must be strict|warn|off" and never reaches here), so
|
|
139
|
+
// `config.enforcement` is one of the three by the time this line runs. The
|
|
140
|
+
// fallback exists for the direction that matters if that ever loosens — an
|
|
141
|
+
// unknown value must degrade to `warn`, never arm the `strict` refusal.
|
|
142
|
+
const enforcement = ENFORCEMENT_VALUES.has(config.enforcement) ? config.enforcement : 'warn';
|
|
143
|
+
|
|
144
|
+
if (enforcement !== 'off') {
|
|
145
|
+
// Ceiling: 20 named lines. A whole prose section pasted into the block is
|
|
146
|
+
// one defect, not 200, and the strict path below exits immediately after
|
|
147
|
+
// writing — Node's stderr is async on a pipe, so an unbounded list is the
|
|
148
|
+
// write-then-exit truncation class this repo has already paid for once.
|
|
149
|
+
// Revisit if a legitimate config block ever carries >20 broken lines.
|
|
150
|
+
const SHOWN = 20;
|
|
151
|
+
for (const { line, text } of unparsableLines.slice(0, SHOWN)) {
|
|
152
|
+
process.stderr.write(
|
|
153
|
+
`parse-config.mjs: WARN unparsable Session Config line ${line}: ${text}\n`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (unparsableLines.length > SHOWN) {
|
|
157
|
+
process.stderr.write(
|
|
158
|
+
`parse-config.mjs: WARN … and ${unparsableLines.length - SHOWN} more unparsable line(s)\n`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(
|
|
162
|
+
`parse-config.mjs: ${unparsableLines.length} unparsable line(s) in ${configFile} — ` +
|
|
163
|
+
'those keys fall back to their defaults, which for booleans is `false`.\n',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (enforcement === 'strict') {
|
|
168
|
+
process.stderr.write(
|
|
169
|
+
'parse-config.mjs: enforcement: strict — refusing to emit config parsed from an ' +
|
|
170
|
+
'unparsable Session Config block.\n',
|
|
171
|
+
);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
111
176
|
// jq -n produces pretty-printed JSON without a trailing newline — match that format
|
|
112
177
|
const assembledJson = JSON.stringify(config, null, 2);
|
|
113
178
|
|