session-orchestrator 3.21.0 → 3.22.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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor/rules/000-session-orchestrator.mdc +3 -2
  5. package/.cursor/rules/040-discovery.mdc +6 -6
  6. package/.cursor/rules/050-plan.mdc +8 -8
  7. package/CHANGELOG.md +101 -0
  8. package/README.md +10 -10
  9. package/agents/memory-proposal-collector.md +6 -4
  10. package/commands/eli5.md +33 -0
  11. package/commands/release.md +5 -3
  12. package/commands/test.md +2 -2
  13. package/docs/components.md +6 -5
  14. package/docs/scope-collision-guard.md +3 -3
  15. package/docs/session-config-reference.md +31 -8
  16. package/hooks/_lib/lock-bootstrap.mjs +19 -13
  17. package/hooks/hooks-codex.json +1 -1
  18. package/hooks/hooks.json +11 -1
  19. package/hooks/on-session-end.mjs +24 -92
  20. package/hooks/on-session-start.mjs +195 -104
  21. package/hooks/pre-auq-clarity.mjs +787 -0
  22. package/hooks/pre-bash-issue-budget.mjs +17 -18
  23. package/package.json +3 -1
  24. package/pi/prompts/eli5.md +12 -0
  25. package/scripts/auq-audit.mjs +825 -0
  26. package/scripts/autopilot.mjs +7 -8
  27. package/scripts/lib/auq/clarity.mjs +1314 -0
  28. package/scripts/lib/auq/parse.mjs +1006 -0
  29. package/scripts/lib/auq/schema.mjs +1457 -0
  30. package/scripts/lib/ci-status-banner.mjs +63 -57
  31. package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
  32. package/scripts/lib/config/vault-integration.mjs +12 -1
  33. package/scripts/lib/dispatcher/rank.mjs +4 -7
  34. package/scripts/lib/gates/gate-full.mjs +3 -3
  35. package/scripts/lib/gates/gate-helpers.mjs +17 -6
  36. package/scripts/lib/io.mjs +239 -0
  37. package/scripts/lib/issue-budget.mjs +63 -9
  38. package/scripts/lib/owner-interview.mjs +78 -32
  39. package/scripts/lib/peer-discovery.mjs +73 -22
  40. package/scripts/lib/project-hygiene.mjs +64 -4
  41. package/scripts/lib/reconcile/renderer.mjs +17 -4
  42. package/scripts/lib/resource-probe/evaluate.mjs +330 -149
  43. package/scripts/lib/resource-probe/probe-platform.mjs +35 -0
  44. package/scripts/lib/resource-probe.mjs +18 -2
  45. package/scripts/lib/spiral-carryover.mjs +23 -2
  46. package/scripts/lib/state-md/mission-status.mjs +147 -50
  47. package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
  48. package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
  49. package/scripts/lib/validate/check-rules.mjs +153 -9
  50. package/scripts/lib/vault-backfill/glab.mjs +91 -58
  51. package/scripts/lib/vault-backfill/manifest.mjs +28 -8
  52. package/scripts/lib/vcs-repo-spec.mjs +182 -13
  53. package/scripts/lib/wave-resource-gate.mjs +67 -73
  54. package/scripts/materialize-wave-scope.mjs +281 -0
  55. package/scripts/release.mjs +443 -122
  56. package/scripts/run-quality-gate.mjs +14 -0
  57. package/scripts/validate-plugin.mjs +3 -0
  58. package/scripts/validate-wave-scope.mjs +6 -1
  59. package/scripts/vault-backfill.mjs +32 -5
  60. package/skills/_shared/parallel-aware-auq.md +30 -24
  61. package/skills/_shared/parallel-aware-preamble.md +31 -2
  62. package/skills/_shared/state-ownership.md +32 -6
  63. package/skills/bootstrap/SKILL.md +2 -1
  64. package/skills/brainstorm/SKILL.md +18 -18
  65. package/skills/brainstorm/soul.md +12 -0
  66. package/skills/discovery/SKILL.md +28 -24
  67. package/skills/eli5/SKILL.md +43 -0
  68. package/skills/evolve/SKILL.md +8 -9
  69. package/skills/gitlab-ops/SKILL.md +30 -26
  70. package/skills/grill/SKILL.md +6 -6
  71. package/skills/grill/soul.md +16 -0
  72. package/skills/memory-cleanup/SKILL.md +2 -2
  73. package/skills/npm-publish/SKILL.md +4 -4
  74. package/skills/peekaboo-driver/SKILL.md +3 -3
  75. package/skills/plan/SKILL.md +18 -16
  76. package/skills/plan/mode-feature.md +1 -1
  77. package/skills/plan/mode-new.md +35 -23
  78. package/skills/plan/soul.md +12 -0
  79. package/skills/reconcile/SKILL.md +3 -3
  80. package/skills/session-end/SKILL.md +53 -20
  81. package/skills/session-end/phase-3-6-tail.md +37 -2
  82. package/skills/session-start/SKILL.md +69 -35
  83. package/skills/session-start/phase-2-5-docs-planning.md +8 -8
  84. package/skills/session-start/phase-4-5-resource-health.md +82 -19
  85. package/skills/session-start/soul.md +110 -0
  86. package/skills/test-runner/SKILL.md +2 -2
  87. package/skills/using-orchestrator/SKILL.md +1 -1
  88. package/skills/wave-executor/wave-loop.md +27 -5
  89. package/skills/write-executable-plan/SKILL.md +6 -6
  90. package/scripts/tests/fixtures/fetch-baseline/sample-rule.md +0 -8
  91. package/skills/vault-sync/tests/fixtures/archive-test-vault/90-archive/bad-archived.md +0 -8
  92. package/skills/vault-sync/tests/fixtures/archive-test-vault/_meta/.gitkeep +0 -0
  93. package/skills/vault-sync/tests/fixtures/archive-test-vault/live-note.md +0 -8
  94. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/_meta/.gitkeep +0 -0
  95. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/bad-type.md +0 -8
  96. package/skills/vault-sync/tests/fixtures/broken-frontmatter-vault/good-note.md +0 -8
  97. package/skills/vault-sync/tests/fixtures/clean-vault/.obsidian/config.md +0 -8
  98. package/skills/vault-sync/tests/fixtures/clean-vault/01-projects/foo/projects-baseline.md +0 -10
  99. package/skills/vault-sync/tests/fixtures/clean-vault/03-daily/daily-2026-04-13.md +0 -8
  100. package/skills/vault-sync/tests/fixtures/clean-vault/README.md +0 -3
  101. package/skills/vault-sync/tests/fixtures/clean-vault/hello-world.md +0 -11
  102. package/skills/vault-sync/tests/fixtures/dangling-link-vault/_meta/.gitkeep +0 -0
  103. package/skills/vault-sync/tests/fixtures/dangling-link-vault/has-dangling.md +0 -9
  104. package/skills/vault-sync/tests/fixtures/dangling-link-vault/real-target.md +0 -8
  105. package/skills/vault-sync/tests/fixtures/empty-vault/_meta/.gitkeep +0 -0
  106. package/skills/vault-sync/tests/fixtures/missing-field-vault/_meta/.gitkeep +0 -0
  107. package/skills/vault-sync/tests/fixtures/missing-field-vault/missing-id.md +0 -7
  108. package/skills/vault-sync/tests/fixtures/nested-tag-vault/03-daily/daily-2026-04-13.md +0 -9
  109. package/skills/vault-sync/tests/fixtures/nested-tag-vault/_meta/.gitkeep +0 -0
  110. package/skills/vault-sync/tests/fixtures/nested-tag-vault/nested-tags-note.md +0 -11
  111. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/README.md +0 -3
  112. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_MOC.md +0 -3
  113. package/skills/vault-sync/tests/fixtures/no-frontmatter-vault/_meta/.gitkeep +0 -0
  114. package/skills/vault-sync/tests/fixtures/with-moc-vault/_MOC.md +0 -11
  115. package/skills/vault-sync/tests/fixtures/with-moc-vault/_meta/.gitkeep +0 -0
  116. package/skills/vault-sync/tests/fixtures/with-moc-vault/hello-world.md +0 -11
  117. package/skills/vault-sync/tests/schema-drift.test.mjs +0 -133
@@ -64,6 +64,116 @@ The active level is `efficiency.output-level` in `~/.config/session-orchestrator
64
64
  - Shape: explain the WHY behind each recommendation, name the alternatives you rejected and why, spell out unfamiliar terms on first use.
65
65
  - Escalation: `expand <topic>` — see § Escalation above.
66
66
 
67
+ ### Register — how a sentence reads
68
+
69
+ The budgets above set *how much* you say. This sets *how*. It binds at every
70
+ level and is not itself a budget: applying it changes word order and word
71
+ choice, not line count. It is the canonical statement for this repo — the
72
+ other three souls (`plan`, `brainstorm`, `grill`) point here rather than
73
+ copying it.
74
+
75
+ **Write for someone who knows this project but has not seen what you just saw.**
76
+ What he needs to decide stands in the text, not in the file it points at — in
77
+ the AUQ payload and in every finding you post.
78
+
79
+ This is not "explain it like he is five". The operator owns this repo. He is
80
+ not missing knowledge, he is missing **observation** — he did not watch the
81
+ command you just ran or read the file you just opened. A knowledge framing
82
+ would be factually wrong and condescending at the same time. Write across, not
83
+ down: same expertise as yours, minus your last ten minutes.
84
+
85
+ #### Plain words, real things
86
+
87
+ > **Say more simply what actually happens — and introduce nothing that does not exist.**
88
+ >
89
+ > **The test:** delete every noun the system does not contain. If the sentence
90
+ > is still true and complete, it was no analogy. If it collapses, the analogy
91
+ > was load-bearing — replace it with a description of what actually happens.
92
+
93
+ Five worked cases, in rising difficulty:
94
+
95
+ 1. "Waiting means the other session finishes first." — **allowed.** Sessions
96
+ and waiting both exist; nothing foreign was introduced.
97
+ 2. "Think of the session as a level crossing." — **forbidden.** Delete "level
98
+ crossing" and nothing is left. Say what happens instead: one session holds
99
+ `.orchestrator/session.lock`, the other waits for it.
100
+ 3. "The token budget is used up." — **allowed.** `TOKEN_BUDGET_EXCEEDED` is a
101
+ real identifier and "budget" is the system's own word. Adding "…like a tank
102
+ of fuel" would be forbidden — the tank does not exist.
103
+ 4. "Think of the kill-switches as a fuse box." — **forbidden, and wrong on the
104
+ facts.** Fuses trip on overload; the kill-switches also test elapsed time and
105
+ confidence. The image sounds helpful and is not. A wrong picture costs more
106
+ than no picture, because the operator reasons from it.
107
+ 5. **Dead metaphors.** A proper name may itself be a metaphor —
108
+ `pre-bash-destructive-guard` is called a guard — and you use the name as
109
+ given. Reviving the image is the violation: "the guard will not let it
110
+ through" invites the operator to picture a guard and then reason from the
111
+ picture instead of from the hook. Name the identifier, then say what it
112
+ does: the hook denies the Bash call.
113
+
114
+ #### Precedence over § "Never traded for brevity"
115
+
116
+ There is a real collision above: "say it more simply" can water down a precise
117
+ error message. Resolve it in three steps.
118
+
119
+ 1. **Simplifying removes words, never facts.** If a path, a number, an error
120
+ code, an identifier, or an instruction to act disappears, that is data loss,
121
+ not simplification — and § "Never traded for brevity" already forbids it.
122
+ 2. **When both will not fit in one sentence: precision in the sentence,
123
+ plainness in the one beside it.** The exact term is never replaced, only
124
+ accompanied. It is what the operator greps, quotes, and pastes into an issue.
125
+ 3. **The mechanical tie-breaker:** could the token you are about to cut ever
126
+ appear in a `grep`? Then it stays.
127
+
128
+ Measured 2026-08-22 at `a4f93cf`: of 191 option descriptions in this repo, the
129
+ 20 that match the safety lexicon (`SAFETY_PATTERN` in
130
+ `scripts/lib/auq/schema.mjs`) run 26–108 codepoints — all of them under both K6
131
+ length thresholds (`descriptionCharsWarn` 120, `descriptionCharsFail` 150). The
132
+ collision therefore does not occur today. This precedence rule is a precaution,
133
+ not a repair.
134
+
135
+ #### Worked example — an operator-visible message
136
+
137
+ `formatBlockReason()` in `scripts/lib/issue-budget.mjs` is what the operator
138
+ sees when the issue cap blocks a creation. Rendered with the collector-issue
139
+ sink, before:
140
+
141
+ ```
142
+ issue-budget: session cap reached — 12/12 issues already created.
143
+ This request was NOT created. It is parked as overflow entry #3 in:
144
+ .orchestrator/runtime/issue-budget-overflow.jsonl
145
+ session-end Phase 5 will fold all overflow entries into ONE collector issue `[Backlog-Sammel] <session-id>, N zurückgestellte Punkte`. Nothing is lost.
146
+ Exempt from the cap: priority::critical, the carryover class (SPIRAL/FAILED, [Carryover]),
147
+ and broken-window closure issues — those are never deferred.
148
+ To raise the cap for this repo, edit `issue-budget.max-per-session` in the Session Config;
149
+ `mode: warn` reports without blocking, `mode: off` disables the gate.
150
+ ```
151
+
152
+ After:
153
+
154
+ ```
155
+ Nothing is lost — the issue is parked, and nothing needs doing right now.
156
+ issue-budget: session cap reached — 12/12 issues already created, so this one was NOT created.
157
+ It is parked as overflow entry #3 in:
158
+ .orchestrator/runtime/issue-budget-overflow.jsonl
159
+ session-end Phase 5 folds all overflow entries into ONE collector issue `[Backlog-Sammel] <session-id>, N zurückgestellte Punkte`.
160
+ Exempt from the cap: priority::critical, the carryover class (SPIRAL/FAILED, [Carryover]),
161
+ and broken-window closure issues — those are never deferred.
162
+ To raise the cap for this repo, edit `issue-budget.max-per-session` in the Session Config; `mode: warn` reports without blocking, `mode: off` disables the gate.
163
+ ```
164
+
165
+ Three changes, and only these three: the operator's own question — *must I do
166
+ something?* — moved to line 1, carrying "Nothing is lost" up from line 4 where
167
+ he used to reach it last; `will fold` became the active `folds`; and the first
168
+ two lines merged on a causal `so`, which is why "This request" is now "this
169
+ one" — the same subject, already named in the sentence.
170
+
171
+ The whole word-level diff is four dropped tokens: `This`, `request`, `will`,
172
+ `fold`. Not one of them is a path, a count, a label, a config key, or a mode
173
+ value; every one of those survives character for character. Eight lines before,
174
+ eight lines after — **this register is not a diet.** It is the same facts, in
175
+ the order the reader needs them.
176
+
67
177
  ### Companion dials
68
178
 
69
179
  Same file, same lookup, same fallback-to-default rule:
@@ -216,11 +216,11 @@ Group `medium` and `low` findings and present via a single `AskUserQuestion` cal
216
216
  AskUserQuestion({
217
217
  questions: [{
218
218
  question: `<N> medium/low findings to triage. How to handle?`,
219
- header: "Test-runner triage",
219
+ header: "Triage",
220
220
  options: [
221
221
  {
222
222
  label: "Create all (Recommended)",
223
- description: "File <N> new issues, all with label from:test-runner"
223
+ description: "Files <N> new issues at once, all with label from:test-runner — fastest, and you can still close any of them afterwards."
224
224
  },
225
225
  {
226
226
  label: "Review each",
@@ -67,7 +67,7 @@ When two or more candidates score ≥ 0.85 with delta < 0.15, call `AskUserQuest
67
67
  AskUserQuestion({
68
68
  questions: [{
69
69
  question: "Your message matches multiple workflows. Which one did you mean?",
70
- header: "Skill Dispatch",
70
+ header: "Dispatch",
71
71
  multiSelect: false,
72
72
  options: [
73
73
  { label: "<top-skill display name> (Recommended)", description: "<one-line description of what it does>" },
@@ -1114,20 +1114,40 @@ Before each wave dispatch:
1114
1114
  ```
1115
1115
  The `gates` field (optional) mirrors `enforcement-gates` from Session Config (#77). When present, hooks check each gate individually via `gate_enabled()`. Missing gate entries default to enabled, preserving default behavior.
1116
1116
  2. Validate by piping through `node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs"` (where `$PLUGIN_ROOT` is `$CLAUDE_PLUGIN_ROOT`, `$CODEX_PLUGIN_ROOT`, or `$CURSOR_RULES_DIR` per platform — see `skills/_shared/config-reading.md`). If validation fails (exit 1), fix the JSON based on stderr errors and retry.
1117
- 3. **`allowedPaths` is COMPUTED from per-agent scope files — never hand-transcribed (#1020).** Transcribing the union by hand produced 5 scope divergences in ONE session. Three steps, in this order; none of them is a judgement call. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
1117
+ 3. **`allowedPaths` is COMPUTED from one canonical declaration array — never hand-transcribed (#1020/#1083).** Transcribing either declaration shape or the union by hand produced scope divergences. Globs stay verbatim (`scripts/*.sh`) — the enforcement hook resolves them at check time.
1118
1118
 
1119
- **3.1 — one file per agent.** Write each agent's "Files:" scope from the session plan, verbatim, as a JSON array of strings to `<state-dir>/filescopes/wave-<N>/<agent-id>.json`. That path IS `$AGENT_FILESCOPE_JSON` — the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and the File-Scope Injection (#1020) already consume. Do not write a second copy anywhere, and **never to a `$TMPDIR` temp path**: the injector and `hooks/pre-task-scope-disjoint.mjs` need an addressable, wave-keyed location that a temp file cannot be. Reading `$AGENT_FILESCOPE_JSON` as "some temp file" is the one failure that costs no error — the injector finds nothing, no `FILE-SCOPE` block reaches the prompt, `extractScopeFromPrompt` returns `[]`, and the dispatch is ALLOWed exactly as it was before #1020, signal-free. The coordinator's OWN planned direct edits go into `<state-dir>/filescopes/wave-<N>/coordinator.json` in the identical form and take part in both steps below: 2 of those 5 divergences were coordinator-direct edits, for which no agent scope file exists by construction, and the commit guard caught them only at the commit boundary.
1119
+ **3.1 — materialize both declaration shapes once.** Build one JSON array from the session plan, one `{id, files}` record for every agent plus exactly one `coordinator` record for the coordinator's planned direct edits. `files` arrays, their entries and their order are the plan's verbatim declarations. Materialize it ONCE and capture the aggregate-sidecar path:
1120
1120
 
1121
- > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs BEFORE the union of 3.3 exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
1121
+ ```bash
1122
+ WAVE_SCOPE_RECORDS='[{"id":"W3-I1","files":["scripts/example.mjs"]},{"id":"coordinator","files":["skills/wave-executor/wave-loop.md"]}]'
1123
+ WAVE_SCOPES_SIDECAR="$(
1124
+ printf '%s' "$WAVE_SCOPE_RECORDS" | node "$PLUGIN_ROOT/scripts/materialize-wave-scope.mjs" \
1125
+ --state-dir "$STATE_DIR" --wave "$WAVE"
1126
+ )"
1127
+ [ -n "$WAVE_SCOPES_SIDECAR" ] || { echo "materialize-wave-scope produced no sidecar path" >&2; exit 1; }
1128
+ ```
1129
+
1130
+ The non-empty check is not decoration. The materializer sends every diagnostic
1131
+ to stderr, so a failure leaves `$WAVE_SCOPES_SIDECAR` empty, and an empty path
1132
+ is what step 3.2 would then pass to `--assert-disjoint`. That combination used
1133
+ to exit 0 with the collision gate never run — the same signal-free-ALLOW shape
1134
+ #1083 exists to close. `validate-wave-scope.mjs` now refuses an empty flag
1135
+ value as well, so this guard and that refusal are belt and braces.
1136
+
1137
+ `materialize-wave-scope.mjs` validates the COMPLETE input before writing; it writes `<state-dir>/filescopes/wave-<N>/<agent-id>.json` as each bare `files` array first, then writes `<state-dir>/filescopes/wave-<N>.scopes.json` as the unchanged aggregate record array last. Its human stdout is only that final sidecar path, so the command substitution above is the canonical `$WAVE_SCOPES_SIDECAR`. On error, do not continue with a partial declaration set; correct the plan and run the one command again.
1122
1138
 
1123
- **3.2 — assert disjointness BEFORE computing the union.** Build the sidecar an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), one record per file written in 3.1, `coordinator.json` included and run:
1139
+ The per-agent path IS `$AGENT_FILESCOPE_JSON` the same file `--assert-subset` (#796 below), Grounding Injection (#85), the Learnings-Index (#1014) and File-Scope Injection (#1020) consume. Never write a `$TMPDIR` copy: it degrades to a signal-free allow when an injector cannot find the addressable wave-keyed file. The coordinator's record is materialized as `coordinator.json` and included in the aggregate, so its direct edits are covered by the two checks below.
1140
+
1141
+ > **`<state-dir>/filescopes/` is control state, like `wave-scope.json` itself — never a wave territory.** Step 3.1 necessarily runs before the union exists, so writing these files reports `bash-write-verify: N file(s) changed by a Bash call OUTSIDE the wave's allowedPaths` naming `filescopes/wave-<N>/*.json`. Expected once per wave rollover at this step; it is information, not a scope violation. Never widen `allowedPaths` to silence it — that would grant agents write access to the deconfliction record itself.
1142
+
1143
+ **3.2 — assert disjointness BEFORE computing the union.** The materialized aggregate is an ARRAY of `{id, files}` records (never an object map: a duplicated agent id must stay visible), including `coordinator.json`. Run:
1124
1144
 
1125
1145
  ```bash
1126
1146
  node "$PLUGIN_ROOT/scripts/validate-wave-scope.mjs" \
1127
1147
  --assert-disjoint "$WAVE_SCOPES_SIDECAR" < <state-dir>/wave-scope.json
1128
1148
  ```
1129
1149
 
1130
- Exit 1 (one stderr message per collision) means two agents were handed the same file: fix the session plan, rewrite the affected 3.1 files, re-assert. Never widen the union to make it pass. This runs **before** 3.3 because a union computed over colliding scopes launders the defect into the very artefact meant to prevent it — `allowedPaths` then grants the file and every later gate sees a legal write.
1150
+ Exit 1 (one stderr message per collision) means two agents were handed the same file: fix the session plan, re-materialize, re-assert. Never widen the union to make it pass. This runs **before** 3.3 because a union computed over colliding scopes launders the defect into the very artefact meant to prevent it — `allowedPaths` then grants the file and every later gate sees a legal write.
1131
1151
 
1132
1152
  **3.3 — compute the union.** `--union` is a QUERY MODE that still requires a schema-valid manifest on stdin, so write the skeleton first with `"allowedPaths": []`, then:
1133
1153
 
@@ -1138,6 +1158,8 @@ Before each wave dispatch:
1138
1158
 
1139
1159
  It prints the computed `allowedPaths` array as JSON on stdout **instead of** the manifest echo — one JSON document per run, the flag decides which. Insert that array as `allowedPaths`, then write the final `wave-scope.json`. It already applies the Test-Sibling Expansion below (`expandTestSiblings(unionFileScopes(scopes), { role })`, role read from the manifest), so do not also run the helper by hand.
1140
1160
 
1161
+ **Artifact production, disjointness and union computation are mechanized. Native prompt injection is a separate follow-up.** The materializer creates the durable declarations; the validator proves disjointness and computes the union. It does not install or prove the platform's prompt-injection transport, which remains independently responsible for reading `$AGENT_FILESCOPE_JSON` before dispatch.
1162
+
1141
1163
  **The `--assert-subset` assertion (#796, below) stays unchanged and keeps running.** It checks a DIFFERENT property — each agent's scope ⊆ the union — and a double assignment is structurally invisible to it: a file claimed twice is a subset twice over. `--assert-disjoint` is an addition, never a replacement.
1142
1164
 
1143
1165
  **Test-Sibling Expansion (#970):** an `allowedPaths` entry that names a production file but NOT its test sibling makes the wave's own regression test unwritable — the scope guard then mechanically enforces exactly the inconsistency the quality gate exists to catch. Cross-repo evidence, three occurrences in ONE session: a migrations glob without the SQL-test directory (the regression test could not be written); a lone `.actions.ts` file (the wave's cross-tenant security test stayed red); a dead-export deletion whose importing test lay outside every scope (the suite ended red). Do NOT hand-derive the sibling paths — step 3.3's `--union` runs `expandTestSiblings(…, { role })` for you, so the hook, the validator and this prose state one rule.
@@ -163,11 +163,11 @@ Any hit → surface to user via AUQ:
163
163
  ```
164
164
  AskUserQuestion({
165
165
  questions: [{
166
- question: "The placeholder linter found forbidden strings in the draft plan:\n\n[list each hit with Task number, Step number, and matched text]\n\nHow do you want to proceed?",
167
- header: "Placeholder Linter",
166
+ question: "The draft plan still contains forbidden placeholder strings. How do you want to proceed?",
167
+ header: "Platzhalter",
168
168
  options: [
169
- { label: "Fix automatically (Recommended)", description: "I will resolve each hit by filling in the missing specifics before writing the plan." },
170
- { label: "Show me each hit interactively", description: "Walk me through each one so I can provide the missing detail." }
169
+ { label: "Fix automatically (Recommended)", description: "I fill in the missing specifics myself, then write the plan fastest, and you review the result at hand-off.", preview: "[each hit: Task number, Step number, matched text]" },
170
+ { label: "Show me each hit interactively", description: "Walk me through each one so I can provide the missing detail.", preview: "[the same hits, one at a time, starting with the first]" }
171
171
  ],
172
172
  multiSelect: false
173
173
  }]
@@ -202,8 +202,8 @@ Present the plan path and task count to the user via AUQ:
202
202
  ```
203
203
  AskUserQuestion({
204
204
  questions: [{
205
- question: "Executable plan written to docs/plans/YYYY-MM-DD-<slug>.md ([N] tasks, ~[total] min estimated).\n\nHow do you want to execute it?",
206
- header: "Plan Hand-off",
205
+ question: "Plan written to docs/plans/YYYY-MM-DD-<slug>.md [N] tasks, ~[total] min. How to execute?",
206
+ header: "Plan fertig",
207
207
  options: [
208
208
  { label: "Dispatch via wave-executor (Recommended)", description: "Parallel agents execute all tasks simultaneously. Fastest for file-disjoint tasks." },
209
209
  { label: "Execute coordinator-direct", description: "One task at a time in this session. Safer for tasks with shared state." },
@@ -1,8 +0,0 @@
1
- # Sample Baseline Rule (test fixture)
2
-
3
- This file is used by scripts/tests/fetch-baseline.bats as the mocked response body
4
- for the GitLab API raw-file endpoint. It has no real content — presence is enough
5
- to verify the fetch + cache path.
6
-
7
- - Rule ID: TEST-001
8
- - Purpose: end-to-end fetch verification
@@ -1,8 +0,0 @@
1
- ---
2
- id: bad-archived
3
- type: memo
4
- created: not-a-date
5
- updated: not-a-date
6
- ---
7
-
8
- Bad frontmatter — but inside 90-archive, so must be ignored.
@@ -1,8 +0,0 @@
1
- ---
2
- id: live-note
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- Lives outside archive — must be validated.
@@ -1,8 +0,0 @@
1
- ---
2
- id: bad-type
3
- type: memo
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- `type: memo` is not in the enum.
@@ -1,8 +0,0 @@
1
- ---
2
- id: good-note
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- OK.
@@ -1,8 +0,0 @@
1
- ---
2
- id: bogus
3
- type: memo
4
- created: not-a-date
5
- updated: not-a-date
6
- ---
7
-
8
- If the validator crawls .obsidian/, this file would FAIL. It must be excluded.
@@ -1,10 +0,0 @@
1
- ---
2
- id: projects-baseline
3
- type: project
4
- created: 2026-04-01
5
- updated: 2026-04-13
6
- status: active
7
- tags: [meta, infra]
8
- ---
9
-
10
- Project overview.
@@ -1,8 +0,0 @@
1
- ---
2
- id: daily-2026-04-13
3
- type: daily
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- Today: wrote vault-sync validator.
@@ -1,3 +0,0 @@
1
- # Clean Vault
2
-
3
- No frontmatter here — README-style files must be skipped, not errored.
@@ -1,11 +0,0 @@
1
- ---
2
- id: hello-world
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- title: Hello World
7
- tags: [meta, intro]
8
- status: active
9
- ---
10
-
11
- A minimal valid note that links to [[daily-2026-04-13]] and [[projects-baseline]].
@@ -1,9 +0,0 @@
1
- ---
2
- id: has-dangling
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- This note links to [[non-existent-target]] which should be flagged as a warning.
9
- It also links to [[real-target]] which exists.
@@ -1,8 +0,0 @@
1
- ---
2
- id: real-target
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- ---
7
-
8
- Real.
@@ -1,7 +0,0 @@
1
- ---
2
- type: note
3
- created: 2026-04-13
4
- updated: 2026-04-13
5
- ---
6
-
7
- Missing `id`.
@@ -1,9 +0,0 @@
1
- ---
2
- id: daily-2026-04-13
3
- type: daily
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- tags: [area/infrastructure/cleanup, meta/session-log]
7
- ---
8
-
9
- Daily note using deep-nested Obsidian tags.
@@ -1,11 +0,0 @@
1
- ---
2
- id: nested-tags-note
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- title: Nested Tags Note
7
- tags: [meta/schema, status/in-progress, area/infrastructure, session/2026-04-13]
8
- status: active
9
- ---
10
-
11
- A note that uses Obsidian nested-tag conventions (slash-separated hierarchy).
@@ -1,3 +0,0 @@
1
- # README only
2
-
3
- No frontmatter. Must be counted as `files_skipped_no_frontmatter` and not error.
@@ -1,3 +0,0 @@
1
- # Map of Content
2
-
3
- No frontmatter — this is a plain index.
@@ -1,11 +0,0 @@
1
- ---
2
- title: Map of Content
3
- ---
4
-
5
- # Map of Content
6
-
7
- This is an index file that intentionally lacks the full note frontmatter
8
- (no `id`, `type`, `created`, or `updated`). Validator should report errors
9
- unless this file is excluded via `--exclude "**/_MOC.md"`.
10
-
11
- - [[hello-world]]
@@ -1,11 +0,0 @@
1
- ---
2
- id: hello-world
3
- type: note
4
- created: 2026-04-13
5
- updated: 2026-04-13
6
- title: Hello World
7
- tags: [meta, intro]
8
- status: active
9
- ---
10
-
11
- A valid note that coexists with a broken `_MOC.md` index file.
@@ -1,133 +0,0 @@
1
- /**
2
- * schema-drift.test.mjs — Integration tests for sync-vault-schema.mjs
3
- *
4
- * Verifies that the vendored schema block in validator.mjs stays in lockstep
5
- * with the canonical TypeScript source. Tests run the actual script against
6
- * actual files — no mocks.
7
- *
8
- * Run from session-orchestrator root:
9
- * pnpm exec vitest run skills/vault-sync/tests/schema-drift.test.mjs
10
- */
11
-
12
- import { describe, it, expect } from 'vitest';
13
- import { execFileSync } from 'node:child_process';
14
- import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
15
- import { join, dirname } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
17
- import { tmpdir } from 'node:os';
18
-
19
- const __dirname = dirname(fileURLToPath(import.meta.url));
20
-
21
- // Paths relative to this test file
22
- const SYNC_SCRIPT = join(__dirname, '../../../scripts/sync-vault-schema.mjs');
23
- const VALIDATOR_MJS = join(__dirname, '../validator.mjs');
24
- const CANONICAL = join(
25
- __dirname,
26
- '../../../../projects-baseline/packages/zod-schemas/src/vault-frontmatter.ts',
27
- );
28
- // Tests below shell out to sync-vault-schema.mjs, which by default reads
29
- // the canonical schema from a sibling projects-baseline checkout. In CI
30
- // (and on any contributor machine without that checkout) the canonical
31
- // file is absent, so we skip these integration tests. The schema-drift
32
- // CI stage runs the script directly against a freshly cloned canonical.
33
- const HAS_CANONICAL = existsSync(CANONICAL);
34
-
35
- const SENTINEL_BEGIN = 'BEGIN GENERATED SCHEMA';
36
- const SENTINEL_END = 'END GENERATED SCHEMA';
37
-
38
- /**
39
- * Run sync-vault-schema.mjs with given args.
40
- * Returns { status, stdout, stderr }.
41
- * Never throws — captures non-zero exits via try/catch.
42
- */
43
- function runScript(args = []) {
44
- try {
45
- const stdout = execFileSync(process.execPath, [SYNC_SCRIPT, ...args], {
46
- encoding: 'utf-8',
47
- stdio: ['ignore', 'pipe', 'pipe'],
48
- });
49
- return { status: 0, stdout, stderr: '' };
50
- } catch (err) {
51
- return {
52
- status: err.status ?? 1,
53
- stdout: err.stdout ?? '',
54
- stderr: err.stderr ?? '',
55
- };
56
- }
57
- }
58
-
59
- describe.skipIf(!HAS_CANONICAL)('sync-vault-schema.mjs', () => {
60
- it('idempotency — running --write twice produces no diff between runs', () => {
61
- // Read validator.mjs before any writes
62
- const before = readFileSync(VALIDATOR_MJS, 'utf-8');
63
-
64
- // First --write
65
- const first = runScript(['--write']);
66
- expect(first.status, `first --write failed: ${first.stderr}`).toBe(0);
67
-
68
- // Read after first write
69
- const afterFirst = readFileSync(VALIDATOR_MJS, 'utf-8');
70
- expect(afterFirst).toBe(before);
71
-
72
- // Second --write
73
- const second = runScript(['--write']);
74
- expect(second.status, `second --write failed: ${second.stderr}`).toBe(0);
75
-
76
- // Read after second write
77
- const afterSecond = readFileSync(VALIDATOR_MJS, 'utf-8');
78
- expect(afterSecond).toBe(before);
79
- });
80
-
81
- it('check-mode-clean — --check exits 0 when canonical and vendored are in sync', () => {
82
- const result = runScript(['--check']);
83
- expect(result.status, `--check failed unexpectedly: ${result.stderr}`).toBe(0);
84
- expect(result.stdout).toContain('no drift');
85
- });
86
-
87
- it('check-mode-detects-drift — --check exits 1 with diff when canonical differs', () => {
88
- // Build a temporary fake canonical with a different max for the id field
89
- const rand = Math.random().toString(36).slice(2);
90
- const tmpDir = join(tmpdir(), `sync-vault-schema-test-${rand}`);
91
- mkdirSync(tmpDir, { recursive: true });
92
- const fakeCanonical = join(tmpDir, 'vault-frontmatter.ts');
93
-
94
- // Read the real canonical and alter it: change .max(128) to .max(256) for id
95
- const realCanonical = readFileSync(CANONICAL, 'utf-8');
96
- const altered = realCanonical.replace('.max(128)', '.max(256)');
97
- writeFileSync(fakeCanonical, altered, 'utf-8');
98
-
99
- try {
100
- const result = runScript(['--check', '--canonical', fakeCanonical]);
101
- expect(result.status, 'expected exit 1 for drift').toBe(1);
102
- // Diff should mention the changed value
103
- expect(result.stderr).toMatch(/256|drift/);
104
- } finally {
105
- rmSync(tmpDir, { recursive: true, force: true });
106
- }
107
- });
108
-
109
- it('failure-mode-missing-canonical — exits 2 with clear message when canonical not found', () => {
110
- const rand = Math.random().toString(36).slice(2);
111
- const fakePath = join(tmpdir(), `does-not-exist-${rand}.ts`);
112
-
113
- const result = runScript(['--check', '--canonical', fakePath]);
114
- expect(result.status, 'expected exit 2 for missing file').toBe(2);
115
- expect(result.stderr).toContain(fakePath);
116
- });
117
-
118
- it('sentinel-presence — validator.mjs contains both sentinel markers exactly once in correct order', () => {
119
- const content = readFileSync(VALIDATOR_MJS, 'utf-8');
120
-
121
- // Count occurrences
122
- const beginCount = (content.match(/BEGIN GENERATED SCHEMA/g) ?? []).length;
123
- const endCount = (content.match(/END GENERATED SCHEMA/g) ?? []).length;
124
-
125
- expect(beginCount, 'BEGIN GENERATED SCHEMA must appear exactly once').toBe(1);
126
- expect(endCount, 'END GENERATED SCHEMA must appear exactly once').toBe(1);
127
-
128
- // BEGIN must appear before END
129
- const beginPos = content.indexOf(SENTINEL_BEGIN);
130
- const endPos = content.indexOf(SENTINEL_END);
131
- expect(beginPos, 'BEGIN sentinel must appear before END sentinel').toBeLessThan(endPos);
132
- });
133
- });