session-orchestrator 3.21.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 +11 -2
- 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/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +10 -10
- 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 +269 -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/memory-proposal-collector.md +6 -4
- 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/eli5.md +33 -0
- package/commands/go.md +1 -1
- package/commands/journey-audit.md +43 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- 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 +7 -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 +52 -5
- package/docs/session-config-reference.md +57 -12
- package/docs/session-config-template.md +4 -3
- package/docs/telemetry.md +22 -0
- package/hooks/_lib/lock-bootstrap.mjs +26 -16
- 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 +12 -2
- package/hooks/on-session-end.mjs +237 -104
- package/hooks/on-session-start.mjs +404 -110
- 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-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +133 -21
- 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 +3 -1
- package/pi/prompts/eli5.md +12 -0
- 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/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +12 -8
- 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/auq/clarity.mjs +1314 -0
- package/scripts/lib/auq/parse.mjs +1006 -0
- package/scripts/lib/auq/schema.mjs +1457 -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 +221 -68
- package/scripts/lib/command-blocker.mjs +70 -0
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/reconcile.mjs +79 -4
- package/scripts/lib/config/section-extractor.mjs +235 -36
- package/scripts/lib/config/vault-integration.mjs +12 -1
- 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/dispatcher/rank.mjs +4 -7
- package/scripts/lib/express-path.mjs +327 -0
- package/scripts/lib/file-lock.mjs +22 -4
- package/scripts/lib/gates/gate-full.mjs +84 -11
- package/scripts/lib/gates/gate-helpers.mjs +93 -21
- 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/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +284 -31
- 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/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/platform.mjs +72 -9
- package/scripts/lib/plugin-root.mjs +143 -19
- package/scripts/lib/project-hygiene.mjs +104 -4
- 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 +165 -7
- package/scripts/lib/reconcile/sanitize.mjs +40 -17
- package/scripts/lib/reconcile/writer.mjs +415 -84
- package/scripts/lib/resource-probe/evaluate.mjs +330 -149
- package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
- package/scripts/lib/resource-probe.mjs +18 -2
- 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 +111 -9
- package/scripts/lib/state-md/frontmatter-mutators.mjs +41 -8
- package/scripts/lib/state-md/mission-status.mjs +467 -72
- 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-auq-clarity.mjs +274 -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 +59 -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-rules.mjs +153 -9
- 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-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- 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/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +832 -0
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/lib/wave-transcript-tail.mjs +869 -0
- package/scripts/materialize-wave-scope.mjs +478 -0
- package/scripts/mcp-server.sh +11 -2
- package/scripts/parse-config.mjs +65 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/token-audit.sh +9 -2
- package/scripts/validate-plugin.mjs +6 -0
- package/scripts/validate-wave-scope.mjs +73 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/scripts/vault-mirror.mjs +203 -34
- package/skills/_shared/monitor-patterns.md +31 -5
- package/skills/_shared/parallel-aware-auq.md +31 -25
- package/skills/_shared/parallel-aware-preamble.md +35 -4
- package/skills/_shared/platform-tools.md +11 -5
- package/skills/_shared/state-ownership.md +61 -8
- package/skills/autopilot/SKILL.md +5 -1
- package/skills/bootstrap/SKILL.md +5 -4
- 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/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/claude-md-drift-check/checker.mjs +39 -11
- package/skills/discovery/SKILL.md +28 -24
- package/skills/dispatcher/SKILL.md +1 -1
- package/skills/eli5/SKILL.md +43 -0
- package/skills/evolve/SKILL.md +8 -9
- package/skills/gitlab-ops/SKILL.md +30 -26
- package/skills/grill/SKILL.md +6 -6
- package/skills/grill/soul.md +16 -0
- package/skills/journey-audit/SKILL.md +269 -0
- package/skills/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +18 -6
- package/skills/persona-panel/SKILL.md +1 -1
- package/skills/plan/SKILL.md +18 -16
- package/skills/plan/mode-feature.md +1 -1
- package/skills/plan/mode-new.md +35 -23
- package/skills/plan/soul.md +12 -0
- package/skills/reconcile/SKILL.md +44 -4
- package/skills/session-end/SKILL.md +70 -24
- package/skills/session-end/metrics-collection.md +7 -4
- package/skills/session-end/phase-3-6-tail.md +48 -5
- 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 +96 -48
- package/skills/session-start/phase-2-5-docs-planning.md +8 -8
- package/skills/session-start/phase-4-5-resource-health.md +82 -19
- 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/session-start/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- 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 +129 -24
- package/skills/write-executable-plan/SKILL.md +6 -6
- package/templates/_shared/journey-manifest.md +110 -0
- package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
- package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
- package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
- package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
- package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
- package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
- package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
- package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
- package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
- package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
- package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
- package/templates/_shared/rules/parallel-sessions.md +0 -77
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Materialize both wave file-scope declaration shapes from one canonical array.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/materialize-wave-scope.mjs --state-dir <dir> --wave <positive-int> [--json] < scopes.json
|
|
7
|
+
*
|
|
8
|
+
* The stdin document is an array of `{ id, files }` records. The command writes
|
|
9
|
+
* each bare `files` array first, then writes the complete record array as the
|
|
10
|
+
* aggregate sidecar consumed by validate-wave-scope's --assert-disjoint and
|
|
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
|
+
|
|
18
|
+
import { readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { writeJsonAtomicSync } from './lib/io.mjs';
|
|
22
|
+
|
|
23
|
+
const HELP = `Usage: node scripts/materialize-wave-scope.mjs --state-dir <dir> --wave <positive-int> [--json]
|
|
24
|
+
|
|
25
|
+
Read one JSON array of {id, files} records from stdin and materialize both
|
|
26
|
+
wave-scope declaration shapes from that canonical record array.
|
|
27
|
+
|
|
28
|
+
Required:
|
|
29
|
+
--state-dir <dir> State directory that owns filescopes/.
|
|
30
|
+
--wave <positive-int>
|
|
31
|
+
Positive wave number used in filescopes/wave-N/.
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--session <id> This session's id (session_id or its semantic twin). Used
|
|
35
|
+
ONLY to prove ownership before an orphaned per-agent
|
|
36
|
+
declaration of the same wave is removed (#1103). Without
|
|
37
|
+
it, orphans are reported and RETAINED, never deleted.
|
|
38
|
+
--json Emit {ok, aggregatePath, perAgentPaths, removedOrphans,
|
|
39
|
+
retainedOrphans} to stdout.
|
|
40
|
+
-h, --help Show this help and exit 0.
|
|
41
|
+
|
|
42
|
+
Output:
|
|
43
|
+
Human mode prints only the aggregate sidecar path. --json prints one JSON
|
|
44
|
+
object. Diagnostics are written only to stderr.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
printf '%s' '[{"id":"W1-I1","files":["scripts/example.mjs"]},{"id":"coordinator","files":[]}]' | \\
|
|
48
|
+
node scripts/materialize-wave-scope.mjs --state-dir .claude --wave 1
|
|
49
|
+
node scripts/materialize-wave-scope.mjs --state-dir .claude --wave 1 --json < scopes.json
|
|
50
|
+
|
|
51
|
+
Writes:
|
|
52
|
+
<state-dir>/filescopes/wave-N/<id>.json Bare string[] for each record
|
|
53
|
+
<state-dir>/filescopes/wave-N.scopes.json Aggregate [{id, files}, ...]
|
|
54
|
+
|
|
55
|
+
Removes (only with a proven owner — see --session):
|
|
56
|
+
<state-dir>/filescopes/wave-N/<stale-id>.json Per-agent declarations of this
|
|
57
|
+
wave whose id is absent from the new record array. An
|
|
58
|
+
orphan that cannot be proven owned is named on stderr and
|
|
59
|
+
LEFT IN PLACE; that is a WARN, never a failure.
|
|
60
|
+
|
|
61
|
+
Exit codes:
|
|
62
|
+
0 All declaration files and the aggregate sidecar were written.
|
|
63
|
+
1 Usage or input validation error; no write was attempted.
|
|
64
|
+
2 Filesystem or write error; earlier per-agent writes are retained and any
|
|
65
|
+
previous aggregate is invalidated before per-agent writes begin.
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
class InputError extends Error {}
|
|
69
|
+
class WriteError extends Error {}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string[]} argv
|
|
73
|
+
* @returns {{ stateDir: string, wave: number, session: string|null, json: boolean, help: boolean }}
|
|
74
|
+
*/
|
|
75
|
+
export function parseCliArgs(argv) {
|
|
76
|
+
let stateDir;
|
|
77
|
+
let waveRaw;
|
|
78
|
+
let session = null;
|
|
79
|
+
let json = false;
|
|
80
|
+
let help = false;
|
|
81
|
+
|
|
82
|
+
for (let index = 0; index < argv.length; index++) {
|
|
83
|
+
const arg = argv[index];
|
|
84
|
+
if (arg === '--json') {
|
|
85
|
+
json = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (arg === '--help' || arg === '-h') {
|
|
89
|
+
help = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === '--state-dir' || arg === '--wave' || arg === '--session') {
|
|
93
|
+
const value = argv[index + 1];
|
|
94
|
+
if (value === undefined || value.startsWith('--')) {
|
|
95
|
+
throw new InputError(`${arg} requires a value`);
|
|
96
|
+
}
|
|
97
|
+
if (arg === '--state-dir') {
|
|
98
|
+
if (stateDir !== undefined) throw new InputError('--state-dir may be specified only once');
|
|
99
|
+
stateDir = value;
|
|
100
|
+
} else if (arg === '--session') {
|
|
101
|
+
if (session !== null) throw new InputError('--session may be specified only once');
|
|
102
|
+
// An empty or whitespace-only id proves nothing and must not be read as
|
|
103
|
+
// an owner: it would make every orphan deletable by any caller.
|
|
104
|
+
if (value.trim().length === 0) throw new InputError('--session must be a non-empty id');
|
|
105
|
+
session = value.trim();
|
|
106
|
+
} else {
|
|
107
|
+
if (waveRaw !== undefined) throw new InputError('--wave may be specified only once');
|
|
108
|
+
waveRaw = value;
|
|
109
|
+
}
|
|
110
|
+
index++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
throw new InputError(`unknown argument: ${arg}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (help) return { stateDir: '', wave: 0, session, json, help: true };
|
|
117
|
+
if (stateDir === undefined) throw new InputError('--state-dir is required');
|
|
118
|
+
if (waveRaw === undefined) throw new InputError('--wave is required');
|
|
119
|
+
if (stateDir.length === 0 || /[\0\r\n]/.test(stateDir)) {
|
|
120
|
+
throw new InputError('--state-dir must be a non-empty path without NUL or newline characters');
|
|
121
|
+
}
|
|
122
|
+
if (!/^[1-9]\d*$/.test(waveRaw)) {
|
|
123
|
+
throw new InputError('--wave must be a positive integer');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const wave = Number(waveRaw);
|
|
127
|
+
if (!Number.isSafeInteger(wave)) throw new InputError('--wave must be a safe positive integer');
|
|
128
|
+
return { stateDir, wave, session, json, help: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {unknown} value
|
|
133
|
+
* @returns {value is Record<string, unknown>}
|
|
134
|
+
*/
|
|
135
|
+
function isRecord(value) {
|
|
136
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A path is scope data rather than an output target, so absolute paths remain
|
|
141
|
+
* valid for sanctioned out-of-repository grants. Traversal/control characters
|
|
142
|
+
* are rejected because no scope consumer can safely interpret them verbatim.
|
|
143
|
+
*
|
|
144
|
+
* @param {unknown} value
|
|
145
|
+
* @param {number} recordIndex
|
|
146
|
+
* @param {number} fileIndex
|
|
147
|
+
*/
|
|
148
|
+
function validateScopePath(value, recordIndex, fileIndex) {
|
|
149
|
+
if (typeof value !== 'string' || value.length === 0 || value.trim().length === 0) {
|
|
150
|
+
throw new InputError(`record #${recordIndex} files[${fileIndex}] must be a non-empty string`);
|
|
151
|
+
}
|
|
152
|
+
if (/[\0\r\n]/.test(value)) {
|
|
153
|
+
throw new InputError(`record #${recordIndex} files[${fileIndex}] must not contain NUL or newline characters`);
|
|
154
|
+
}
|
|
155
|
+
if (value.split(/[\\/]+/).includes('..')) {
|
|
156
|
+
throw new InputError(`record #${recordIndex} files[${fileIndex}] must not contain path traversal`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Validate the canonical record array completely before a write is attempted.
|
|
162
|
+
* The original values are returned without sorting, deduplication, or expansion.
|
|
163
|
+
*
|
|
164
|
+
* @param {unknown} value
|
|
165
|
+
* @returns {Array<{id: string, files: string[]}>}
|
|
166
|
+
*/
|
|
167
|
+
export function validateScopeRecords(value) {
|
|
168
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
169
|
+
throw new InputError('stdin must be a non-empty JSON array of {id, files} records');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const seenIds = new Set();
|
|
173
|
+
let coordinatorCount = 0;
|
|
174
|
+
for (let recordIndex = 0; recordIndex < value.length; recordIndex++) {
|
|
175
|
+
const record = value[recordIndex];
|
|
176
|
+
if (!isRecord(record)) {
|
|
177
|
+
throw new InputError(`record #${recordIndex} must be an object with id and files`);
|
|
178
|
+
}
|
|
179
|
+
if (typeof record.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(record.id)) {
|
|
180
|
+
throw new InputError(`record #${recordIndex} has an unsafe id`);
|
|
181
|
+
}
|
|
182
|
+
const idKey = record.id.toLowerCase();
|
|
183
|
+
if (seenIds.has(idKey)) {
|
|
184
|
+
throw new InputError(`record #${recordIndex} has duplicate id (case-insensitive): ${record.id}`);
|
|
185
|
+
}
|
|
186
|
+
seenIds.add(idKey);
|
|
187
|
+
if (idKey === 'coordinator') {
|
|
188
|
+
if (record.id !== 'coordinator') {
|
|
189
|
+
throw new InputError(`record #${recordIndex} id must be exactly coordinator (lowercase)`);
|
|
190
|
+
}
|
|
191
|
+
coordinatorCount++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!Array.isArray(record.files)) {
|
|
195
|
+
throw new InputError(`record #${recordIndex} (${record.id}) must have a files string array`);
|
|
196
|
+
}
|
|
197
|
+
for (let fileIndex = 0; fileIndex < record.files.length; fileIndex++) {
|
|
198
|
+
validateScopePath(record.files[fileIndex], recordIndex, fileIndex);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (coordinatorCount !== 1) {
|
|
203
|
+
throw new InputError(`input must contain exactly one coordinator record; found ${coordinatorCount}`);
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Read the session ids the sibling manifest `<state-dir>/wave-scope.json`
|
|
211
|
+
* declares for this state directory (#1123 wrote both `session` — the raw
|
|
212
|
+
* `session_id` — and its human-readable twin `semantic_session`).
|
|
213
|
+
*
|
|
214
|
+
* A caller may legitimately hold either spelling, so BOTH are returned and a
|
|
215
|
+
* match against either proves ownership. Any failure to read or parse the
|
|
216
|
+
* manifest returns an empty list, which the caller must treat as "ownership NOT
|
|
217
|
+
* established" — never as "no owner, therefore mine".
|
|
218
|
+
*
|
|
219
|
+
* @param {string} stateDir
|
|
220
|
+
* @param {typeof readFileSync} [readFile]
|
|
221
|
+
* @returns {string[]}
|
|
222
|
+
*/
|
|
223
|
+
export function manifestSessionIds(stateDir, readFile = readFileSync) {
|
|
224
|
+
let manifest;
|
|
225
|
+
try {
|
|
226
|
+
manifest = JSON.parse(readFile(resolve(stateDir, 'wave-scope.json'), 'utf8'));
|
|
227
|
+
} catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
if (!isRecord(manifest)) return [];
|
|
231
|
+
return ['session', 'semantic_session']
|
|
232
|
+
.map((key) => manifest[key])
|
|
233
|
+
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
234
|
+
.map((value) => value.trim());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reconcile per-agent declarations left behind by an earlier materialization of
|
|
239
|
+
* the SAME wave (#1103).
|
|
240
|
+
*
|
|
241
|
+
* The write loop is a pure upsert over the new id set, so a file whose id was
|
|
242
|
+
* dropped from the plan survives: it is in no aggregate — `--assert-disjoint`
|
|
243
|
+
* and `--union` cannot see it — while every by-id consumer (FILE-SCOPE
|
|
244
|
+
* injection, the Learnings-Index, `--assert-subset`) still reads it. A live
|
|
245
|
+
* scope claim with zero aggregate coverage.
|
|
246
|
+
*
|
|
247
|
+
* ## Why this is NOT a directory wipe
|
|
248
|
+
*
|
|
249
|
+
* The wave number is not a session-unique key: two sessions sharing one working
|
|
250
|
+
* copy both call their first wave `wave-1` and both resolve to
|
|
251
|
+
* `<state-dir>/filescopes/wave-1/`. Clearing the directory would convert a
|
|
252
|
+
* stale-read bug into cross-session data loss — the class
|
|
253
|
+
* `.claude/rules/parallel-sessions.md` PSA-003 forbids by name ("Did I create
|
|
254
|
+
* this file? If not, it is not mine to touch"). So removal needs a two-part
|
|
255
|
+
* test, and only the second part is about the file:
|
|
256
|
+
*
|
|
257
|
+
* 1. the id is absent from the new record array (it is an orphan), AND
|
|
258
|
+
* 2. this invocation can PROVE it owns the state directory — `sessionId`
|
|
259
|
+
* matches an id the sibling manifest declares.
|
|
260
|
+
*
|
|
261
|
+
* Failing (2) is not an error and never blocks: the orphan is returned in
|
|
262
|
+
* `retained` WITH its reason so the caller can name the file. A silent skip is
|
|
263
|
+
* the one outcome forbidden here, because it is byte-identical to a clean run.
|
|
264
|
+
*
|
|
265
|
+
* ## Named ceiling (BV-004)
|
|
266
|
+
*
|
|
267
|
+
* Bounded by WHEN materialization runs, not by what it inspects: a session that
|
|
268
|
+
* re-materializes wave N *while its own agents are still in flight* would delete
|
|
269
|
+
* the scope files those agents are reading. That is safe today only because
|
|
270
|
+
* `skills/wave-executor/wave-loop.md` § Scope Manifest 3.2 places
|
|
271
|
+
* (re-)materialization strictly PRE-dispatch, so no reader exists yet. REVISIT
|
|
272
|
+
* TRIGGER: any caller that materializes a wave after its dispatch has begun — a
|
|
273
|
+
* mid-wave scope amendment, a repair pass reusing the same wave number, or a
|
|
274
|
+
* dispatch loop that re-runs the materializer per agent. Ownership does not
|
|
275
|
+
* protect against that case; the ordering does.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} params
|
|
278
|
+
* @param {string} params.scopeDir `<state-dir>/filescopes/wave-N`
|
|
279
|
+
* @param {string[]} params.keepIds ids present in the new record array
|
|
280
|
+
* @param {string[]} params.ownerIds session ids the manifest declares
|
|
281
|
+
* @param {string|null} params.sessionId this invocation's session id
|
|
282
|
+
* @param {typeof readdirSync} [params.readDir]
|
|
283
|
+
* @param {typeof unlinkSync} [params.removeFile]
|
|
284
|
+
* @returns {{removed: string[], retained: Array<{file: string, reason: string}>}}
|
|
285
|
+
*/
|
|
286
|
+
export function reconcileOrphans({
|
|
287
|
+
scopeDir,
|
|
288
|
+
keepIds,
|
|
289
|
+
ownerIds,
|
|
290
|
+
sessionId,
|
|
291
|
+
readDir = readdirSync,
|
|
292
|
+
removeFile = unlinkSync,
|
|
293
|
+
}) {
|
|
294
|
+
let entries;
|
|
295
|
+
try {
|
|
296
|
+
entries = readDir(scopeDir, { withFileTypes: true });
|
|
297
|
+
} catch {
|
|
298
|
+
// No directory yet (first materialization) or unreadable — nothing to
|
|
299
|
+
// reconcile. Not a failure: the aggregate is already published.
|
|
300
|
+
return { removed: [], retained: [] };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Case-INSENSITIVE keep set. validateScopeRecords already rejects two ids that
|
|
304
|
+
// differ only in case within one input, so this cannot hide a real orphan —
|
|
305
|
+
// but on a case-insensitive filesystem `A2.json` and `a2.json` are ONE file,
|
|
306
|
+
// and deleting the "orphan" would delete the declaration just written.
|
|
307
|
+
const keep = new Set(keepIds.map((id) => `${id}.json`.toLowerCase()));
|
|
308
|
+
const orphans = [];
|
|
309
|
+
for (const entry of entries) {
|
|
310
|
+
if (typeof entry?.isFile === 'function' && !entry.isFile()) continue;
|
|
311
|
+
const name = typeof entry === 'string' ? entry : entry?.name;
|
|
312
|
+
if (typeof name !== 'string' || !name.endsWith('.json')) continue;
|
|
313
|
+
if (keep.has(name.toLowerCase())) continue;
|
|
314
|
+
orphans.push(name);
|
|
315
|
+
}
|
|
316
|
+
if (orphans.length === 0) return { removed: [], retained: [] };
|
|
317
|
+
|
|
318
|
+
const proven = typeof sessionId === 'string' && sessionId.length > 0 && ownerIds.includes(sessionId);
|
|
319
|
+
if (!proven) {
|
|
320
|
+
const reason = sessionId === null || sessionId === undefined
|
|
321
|
+
? 'no --session given, so this state directory has no provable owner'
|
|
322
|
+
: ownerIds.length === 0
|
|
323
|
+
? 'wave-scope.json declares no session, so ownership cannot be established'
|
|
324
|
+
: `wave-scope.json is owned by a different session (${ownerIds.join(' / ')})`;
|
|
325
|
+
return { removed: [], retained: orphans.map((file) => ({ file, reason })) };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const removed = [];
|
|
329
|
+
const retained = [];
|
|
330
|
+
for (const file of orphans) {
|
|
331
|
+
try {
|
|
332
|
+
removeFile(resolve(scopeDir, file));
|
|
333
|
+
removed.push(file);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// A failed unlink leaves a live orphan behind — report it, never throw:
|
|
336
|
+
// the aggregate is already published and the materialization succeeded.
|
|
337
|
+
retained.push({ file, reason: `could not remove: ${error instanceof Error ? error.message : String(error)}` });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return { removed, retained };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Materialize validated declarations in their required write order.
|
|
345
|
+
*
|
|
346
|
+
* The optional writer is a narrow seam for deterministic write-failure tests;
|
|
347
|
+
* production always delegates to writeJsonAtomicSync. There is intentionally no
|
|
348
|
+
* rollback: a prior aggregate is invalidated before per-agent writes, then the
|
|
349
|
+
* aggregate publication marker is written only after every per-agent declaration
|
|
350
|
+
* has succeeded.
|
|
351
|
+
*
|
|
352
|
+
* @param {Array<{id: string, files: string[]}>} records
|
|
353
|
+
* @param {{ stateDir: string, wave: number, session?: string|null,
|
|
354
|
+
* writeJson?: typeof writeJsonAtomicSync,
|
|
355
|
+
* readDir?: typeof readdirSync, removeFile?: typeof unlinkSync,
|
|
356
|
+
* readFile?: typeof readFileSync }} options
|
|
357
|
+
* @returns {{ aggregatePath: string, perAgentPaths: string[],
|
|
358
|
+
* removedOrphans: string[], retainedOrphans: Array<{file: string, reason: string}> }}
|
|
359
|
+
*/
|
|
360
|
+
export function materializeWaveScope(records, {
|
|
361
|
+
stateDir,
|
|
362
|
+
wave,
|
|
363
|
+
session = null,
|
|
364
|
+
writeJson = writeJsonAtomicSync,
|
|
365
|
+
readDir = readdirSync,
|
|
366
|
+
removeFile = unlinkSync,
|
|
367
|
+
readFile = readFileSync,
|
|
368
|
+
}) {
|
|
369
|
+
const scopeDir = resolve(stateDir, 'filescopes', `wave-${wave}`);
|
|
370
|
+
const aggregatePath = resolve(stateDir, 'filescopes', `wave-${wave}.scopes.json`);
|
|
371
|
+
const perAgentPaths = records.map(({ id }) => resolve(scopeDir, `${id}.json`));
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
unlinkSync(aggregatePath);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error?.code !== 'ENOENT') {
|
|
377
|
+
throw new WriteError(`cannot invalidate aggregate declaration ${aggregatePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (let index = 0; index < records.length; index++) {
|
|
382
|
+
const result = writeJson(perAgentPaths[index], records[index].files, { tmpPrefix: '.materialize-wave-scope' });
|
|
383
|
+
if (!result?.ok) {
|
|
384
|
+
throw new WriteError(`cannot write per-agent declaration ${perAgentPaths[index]}: ${result?.error ?? 'unknown write failure'}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const aggregateResult = writeJson(aggregatePath, records, { tmpPrefix: '.materialize-wave-scope' });
|
|
389
|
+
if (!aggregateResult?.ok) {
|
|
390
|
+
throw new WriteError(`cannot write aggregate declaration ${aggregatePath}: ${aggregateResult?.error ?? 'unknown write failure'}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// #1103 — strictly AFTER the aggregate write. The aggregate is this command's
|
|
394
|
+
// publication marker (see the write-order note above), so reconciling before
|
|
395
|
+
// it would remove a live declaration while the run could still fail and leave
|
|
396
|
+
// no aggregate at all — deleting coverage that nothing replaced.
|
|
397
|
+
const { removed, retained } = reconcileOrphans({
|
|
398
|
+
scopeDir,
|
|
399
|
+
keepIds: records.map(({ id }) => id),
|
|
400
|
+
ownerIds: manifestSessionIds(stateDir, readFile),
|
|
401
|
+
sessionId: session,
|
|
402
|
+
readDir,
|
|
403
|
+
removeFile,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return { aggregatePath, perAgentPaths, removedOrphans: removed, retainedOrphans: retained };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* @param {string} message
|
|
411
|
+
* @param {number} code
|
|
412
|
+
*/
|
|
413
|
+
function fail(message, code) {
|
|
414
|
+
process.stderr.write(`materialize-wave-scope: ${message}\n`);
|
|
415
|
+
process.exitCode = code;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function readStdinJson() {
|
|
419
|
+
let raw;
|
|
420
|
+
try {
|
|
421
|
+
raw = readFileSync(0, 'utf8');
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw new WriteError(`cannot read stdin: ${error instanceof Error ? error.message : String(error)}`);
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
return JSON.parse(raw);
|
|
427
|
+
} catch {
|
|
428
|
+
throw new InputError('stdin is not valid JSON');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Execute the CLI, mapping usage/input and I/O failures to its exit contract. */
|
|
433
|
+
export function main() {
|
|
434
|
+
let args;
|
|
435
|
+
try {
|
|
436
|
+
args = parseCliArgs(process.argv.slice(2));
|
|
437
|
+
if (args.help) {
|
|
438
|
+
process.stdout.write(HELP);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const records = validateScopeRecords(readStdinJson());
|
|
442
|
+
const { aggregatePath, perAgentPaths, removedOrphans, retainedOrphans } =
|
|
443
|
+
materializeWaveScope(records, args);
|
|
444
|
+
|
|
445
|
+
// stderr carries ONLY the anomalous cases. Measured constraint, not taste:
|
|
446
|
+
// the corpus pins byte-empty stderr on this command's success path
|
|
447
|
+
// (tests/scripts/materialize-wave-scope.test.mjs and
|
|
448
|
+
// tests/integration/wave-scope-producer.test.mjs), and a wave with no
|
|
449
|
+
// orphans IS the success path. Both lists always reach --json.
|
|
450
|
+
for (const file of removedOrphans) {
|
|
451
|
+
process.stderr.write(`materialize-wave-scope: removed orphaned declaration ${file} (id absent from this wave's records)\n`);
|
|
452
|
+
}
|
|
453
|
+
for (const { file, reason } of retainedOrphans) {
|
|
454
|
+
process.stderr.write(`materialize-wave-scope: WARN orphaned declaration ${file} RETAINED — ${reason}\n`);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
process.stdout.write(
|
|
458
|
+
args.json
|
|
459
|
+
? `${JSON.stringify({ ok: true, aggregatePath, perAgentPaths, removedOrphans, retainedOrphans })}\n`
|
|
460
|
+
: `${aggregatePath}\n`,
|
|
461
|
+
);
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (error instanceof InputError) {
|
|
464
|
+
fail(error.message, 1);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (error instanceof WriteError) {
|
|
468
|
+
fail(error.message, 2);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
fail(`unexpected system error: ${error instanceof Error ? error.message : String(error)}`, 2);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : null;
|
|
476
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
477
|
+
main();
|
|
478
|
+
}
|
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
|
|
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
|
|