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
@@ -277,8 +277,22 @@ if (!existsSync(gatePath)) {
277
277
  die(`Gate script not found: ${gatePath}`);
278
278
  }
279
279
 
280
+ // `npm_config_loglevel` is INHERITED by every descendant, and the pre-push hook
281
+ // invokes this script as `npm run --silent quality-gate` — which sets it to
282
+ // `silent`. That level then reached the gate's own children: `npm pack
283
+ // --dry-run` emitted ZERO `npm notice` lines instead of 818 (measured
284
+ // 2026-08-22), so the release leakage test saw an empty listing, and several
285
+ // validate-plugin/e2e tests that shell out to npm went red the same way. Every
286
+ // one of them passes under a bare `npm test` and fails only INSIDE the gate,
287
+ // which is the hardest shape to diagnose and cost an hour of chasing phantoms.
288
+ //
289
+ // Pinned rather than deleted: an explicit level makes the gate's children
290
+ // independent of how the gate itself was invoked. `--silent` still does its real
291
+ // job — keeping THIS process's stdout to the single JSON envelope — because the
292
+ // children's output is captured by `runCheck`, never streamed.
280
293
  const env = {
281
294
  ...process.env,
295
+ npm_config_loglevel: 'notice',
282
296
  TYPECHECK_CMD,
283
297
  TEST_CMD,
284
298
  LINT_CMD,
@@ -259,6 +259,9 @@ if (runCheck('check-owner-leakage.mjs') !== 0) checkFailed = 1;
259
259
  process.stdout.write('\n');
260
260
  if (runCheck('check-rules.mjs') !== 0) checkFailed = 1;
261
261
 
262
+ process.stdout.write('\n');
263
+ if (runCheck('check-auq-clarity.mjs') !== 0) checkFailed = 1;
264
+
262
265
  process.stdout.write('\n');
263
266
  if (runCheck('check-unicode-safety.mjs') !== 0) checkFailed = 1;
264
267
 
@@ -115,6 +115,11 @@ function die(msg, code = 1) {
115
115
  * inherit it: a swallowed flag is silent (the mode never runs, and the caller
116
116
  * believes it did), whereas this refusal is loud and one line long.
117
117
  *
118
+ * An EMPTY value is the same failure class and is refused for the same reason.
119
+ * `--assert-disjoint ""` is what a failed `$(...)` capture of the materializer's
120
+ * stdout produces; since the mode is gated on a truthy path, the empty string
121
+ * silently skipped the collision check and still exited 0 (#1083).
122
+ *
118
123
  * @param {string[]} argv
119
124
  * @param {number} i - index of the FLAG token
120
125
  * @param {string} flag - the flag name, for the error message
@@ -122,7 +127,7 @@ function die(msg, code = 1) {
122
127
  */
123
128
  function flagValue(argv, i, flag) {
124
129
  const value = argv[i + 1];
125
- if (value === undefined || value.startsWith('--')) {
130
+ if (value === undefined || value === '' || value.startsWith('--')) {
126
131
  die(`${flag} requires a file-path argument`, 1);
127
132
  }
128
133
  return value;
@@ -25,12 +25,12 @@
25
25
  */
26
26
 
27
27
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
28
- import { join, resolve, dirname } from 'node:path';
28
+ import { join, resolve, dirname, relative, isAbsolute, sep } from 'node:path';
29
29
  import { createInterface } from 'node:readline';
30
30
 
31
31
  import {
32
32
  assertGlabExists, setVerbose as setGlabVerbose,
33
- listGroupRepos, checkVaultYaml, fetchRepoOwner,
33
+ listGroupRepos, checkVaultYaml,
34
34
  } from './lib/vault-backfill/glab.mjs';
35
35
 
36
36
  import {
@@ -155,11 +155,25 @@ function createVaultFolderStub(vaultDir, slug) {
155
155
 
156
156
  // ── Process one repo ──────────────────────────────────────────────────────────
157
157
 
158
+ /** Derive the complete parent namespace from a canonical GitLab project path. */
159
+ function parentNamespace(repoPath) {
160
+ if (typeof repoPath !== 'string') return 'unknown';
161
+
162
+ const segments = repoPath.split('/');
163
+ if (
164
+ segments.length < 2 ||
165
+ segments.some((segment) => !segment || segment === '.' || segment === '..')
166
+ ) {
167
+ return 'unknown';
168
+ }
169
+
170
+ return segments.slice(0, -1).join('/');
171
+ }
172
+
158
173
  function processRepo(entry, vaultDir, templateContent) {
159
174
  const { path: repoPath, slug, tier, visibility, group, id } = entry;
160
175
  const humanName = slugToHumanName(slug);
161
-
162
- const owner = applyWrites ? fetchRepoOwner(id) : 'unknown';
176
+ const owner = parentNamespace(repoPath);
163
177
 
164
178
  let renderedContent;
165
179
  try {
@@ -184,7 +198,20 @@ function processRepo(entry, vaultDir, templateContent) {
184
198
  }
185
199
 
186
200
  // Apply: write to staging directory at <out-dir>/<group>/<repo>/.vault.yaml
187
- const writePath = join(outDir, repoPath, '.vault.yaml');
201
+ const writePath = resolve(outDir, repoPath, '.vault.yaml');
202
+ const writePathRelativeToOutDir = relative(outDir, writePath);
203
+ if (
204
+ writePathRelativeToOutDir === '..' ||
205
+ writePathRelativeToOutDir.startsWith(`..${sep}`) ||
206
+ isAbsolute(writePathRelativeToOutDir)
207
+ ) {
208
+ const error = 'refusing to write outside staging directory';
209
+ emitAction('write-failed', '', { error });
210
+ process.stderr.write(`[vault-backfill] ERROR: ${error}\n`);
211
+ hadWriteError = true;
212
+ return;
213
+ }
214
+
188
215
  try {
189
216
  mkdirSync(dirname(writePath), { recursive: true });
190
217
  writeFileSync(writePath, renderedContent, 'utf8');
@@ -22,15 +22,18 @@ Fires when an `exclusive`-class session (`bootstrap`, `housekeeping`, `memory-cl
22
22
  ### Claude Code (AskUserQuestion)
23
23
 
24
24
  ```js
25
+ // Unpacked once so the question reads as one sentence instead of five key=value pairs.
26
+ const { mode, host, pid, worktreePath } = blockingSession;
27
+
25
28
  AskUserQuestion({
26
29
  questions: [{
27
- question: `An exclusive session is active in this repository (mode=${blockingSession.mode}, started ${ageHours}h ago, host=${blockingSession.host}, pid=${blockingSession.pid}, worktree=${blockingSession.worktreePath}). This blocks all other modes. How should I proceed?`,
28
- header: "Parallel-Exclusive",
30
+ question: `A ${mode} session (process ${pid} on ${host}) started ${ageHours}h ago in ${worktreePath}. What now?`,
31
+ header: "Repo belegt",
29
32
  multiSelect: false,
30
33
  options: [
31
- { label: "Warten (Recommended)", description: "Wait for the exclusive session to finish. The preamble will not retry automatically re-run the command after the other session closes." },
32
- { label: "Andere Session beenden", description: "I will close the other session myself, then re-run this command. The preamble surfaces but does NOT terminate the other session." },
33
- { label: "Abbrechen", description: "Exit cleanly. No STATE.md initialization, no lock acquired." },
34
+ { label: "Warten (Recommended)", description: "Nothing else can start here until that session closes. This command does not retry — run it again afterwards." },
35
+ { label: "Andere Session beenden", description: "You close it yourself, then run this command again. Nothing here stops the other session for you." },
36
+ { label: "Abbrechen", description: "Exit now. Nothing is written: no STATE.md, no lock." },
34
37
  ],
35
38
  }],
36
39
  });
@@ -39,19 +42,17 @@ AskUserQuestion({
39
42
  ### Codex CLI / Cursor IDE / Pi fallback (numbered Markdown list)
40
43
 
41
44
  ```
42
- Parallel-Exclusive conflict an exclusive session is active in this repository.
43
- Mode: <blockingSession.mode>
44
- Started: <ageHours>h ago (host=<host>, pid=<pid>)
45
- Worktree: <blockingSession.worktreePath>
46
- This blocks all other modes.
45
+ A <mode> session (process <pid> on <host>) started <ageHours>h ago in <worktreePath>. What now?
47
46
 
48
- 1. Warten (Recommended) — wait for the exclusive session to finish; re-run after it closes.
49
- 2. Andere Session beenden — I will close the other session myself.
50
- 3. Abbrechen — exit cleanly without initializing STATE.md.
47
+ 1. Warten (Recommended) — nothing else can start here until that session closes; this command does not retry, so run it again afterwards.
48
+ 2. Andere Session beenden — you close it yourself, then run this command again. Nothing here stops the other session for you.
49
+ 3. Abbrechen — exit now. Nothing is written: no STATE.md, no lock.
51
50
 
52
51
  Reply with the number of your choice.
53
52
  ```
54
53
 
54
+ The four slots are `blockingSession.mode`, `blockingSession.host`, `blockingSession.pid` and `blockingSession.worktreePath`; `<ageHours>` is the age of that session in hours.
55
+
55
56
  ### Outcome handling
56
57
 
57
58
  - **Warten** → exit Phase-0 cleanly with stderr note `parallel-aware: waiting on exclusive session_id=<id>`. No retry loop.
@@ -65,34 +66,39 @@ Fires when the caller is `parallel-ok`-class AND another `parallel-ok` session i
65
66
  ### Claude Code (AskUserQuestion)
66
67
 
67
68
  ```js
69
+ // Unpacked once so the question reads as one sentence instead of three key=value pairs.
70
+ const { mode, pid } = parallelPeer;
71
+
68
72
  AskUserQuestion({
69
73
  questions: [{
70
- question: `A compatible parallel session is active in this worktree (mode=${parallelPeer.mode}, started ${ageHours}h ago, pid=${parallelPeer.pid}). You can either: (a) auto-promote to a sibling worktree to run isolated, or (b) run in-place alongside the existing session (file conflicts likely). How should I proceed?`,
71
- header: "Worktree-Promo",
74
+ question: `A ${mode} session (process ${pid}) started ${ageHours}h ago in this same folder. Run separately or alongside?`,
75
+ header: "Wo starten?",
72
76
  multiSelect: false,
73
77
  options: [
74
- { label: "Worktree anlegen + starten (Recommended)", description: "Create a sibling git worktree at ../<repo-name>-<semantic-session-id>/ and start the new session there. Isolates file edits; recommended for parallel deep/feature sessions. Calls enterWorktree() from scripts/lib/autopilot/worktree-pipeline.mjs." },
75
- { label: "Manuell — in-place daneben", description: "Run in the current worktree alongside the existing session. File conflicts possible; PSA-001/002/004 discipline required. A Deviation is logged." },
76
- { label: "Abbrechen", description: "Exit cleanly. No STATE.md initialization." },
78
+ { label: "Worktree anlegen + starten (Recommended)", description: "Creates a second working folder beside this one and starts there isolates your edits, so nothing collides." },
79
+ { label: "Manuell — in-place daneben", description: "Both sessions write in this same folder conflicts are likely and you resolve them yourself. A Deviation is logged." },
80
+ { label: "Abbrechen", description: "Exit now. Nothing is written: no STATE.md, no lock." },
77
81
  ],
78
82
  }],
79
83
  });
80
84
  ```
81
85
 
86
+ The second working folder is a git worktree at `<basePath>/<repo-name>-<sessionId>/`; `enterWorktree()` from `scripts/lib/autopilot/worktree-pipeline.mjs` creates it (see Outcome handling below). Running in-place puts PSA-001/PSA-002/PSA-004 discipline on the operator.
87
+
82
88
  ### Codex CLI / Cursor IDE / Pi fallback (numbered Markdown list)
83
89
 
84
90
  ```
85
- Worktree-Promotion offer a compatible parallel session is active in this worktree.
86
- Peer mode: <parallelPeer.mode>
87
- Started: <ageHours>h ago (pid=<pid>)
91
+ A <mode> session (process <pid>) started <ageHours>h ago in this same folder. Run separately or alongside?
88
92
 
89
- 1. Worktree anlegen + starten (Recommended) — create sibling worktree and start isolated session.
90
- 2. Manuell — in-place daneben — run alongside (file conflicts possible, Deviation logged).
91
- 3. Abbrechen — exit cleanly.
93
+ 1. Worktree anlegen + starten (Recommended) — creates a second working folder beside this one and starts there; isolates your edits, so nothing collides.
94
+ 2. Manuell — in-place daneben — both sessions write in this folder; conflicts are likely and you resolve them. A Deviation is logged.
95
+ 3. Abbrechen — exit now. Nothing is written: no STATE.md, no lock.
92
96
 
93
97
  Reply with the number of your choice.
94
98
  ```
95
99
 
100
+ The two slots are `parallelPeer.mode` and `parallelPeer.pid`; `<ageHours>` is the age of that session in hours.
101
+
96
102
  ### Outcome handling
97
103
 
98
104
  - **Worktree anlegen + starten** → invoke `enterWorktree({ basePath, sessionId, branch, repoRoot })` from `scripts/lib/autopilot/worktree-pipeline.mjs`. The helper creates a sibling worktree at `<basePath>/<repo-name>-<sessionId>/`, runs idempotency + boundary checks, and logs a WARN line to stderr on fresh creation. Once the worktree exists, exit the current preamble flow — the new worktree's own session-start runs from scratch (Phase 1 onwards). On failure (`WorktreeBoundaryError` or `git worktree add` non-zero exit), emit a stderr warning `parallel-aware: enterWorktree failed: <error>; falling back to Manuell` and proceed via the Manuell path.
@@ -29,6 +29,33 @@ If an `exclusive`-class session is also active, the Exclusive-Conflict AUQ takes
29
29
  **Always-OK class** (`discovery`, `evolve`, `plan`, `repo-audit`, `portfolio`):
30
30
  The preamble passes through with zero AUQ regardless of other active sessions. Read-only modes never conflict.
31
31
 
32
+ ## Identity Binding for `findPeers` (#1085)
33
+
34
+ `mySessionId` / `callerSessionId` is a **hint for the caller's original
35
+ surface**, not a license to turn an attribution label into ownership. A native
36
+ raw id self-excludes on the discovered lock/registry surface directly. Given a
37
+ semantic hint, `findPeers` may translate it to a concrete local raw id only when
38
+ both proofs hold: `current-session.json` has the same semantic label **and** its
39
+ raw `session_id` exactly equals the readable local lock's raw `session_id`.
40
+
41
+ On a missing, malformed, or mismatched binding, `findPeers` must map nothing and
42
+ leave the discovered lock visible. The STATE.md surface always receives the
43
+ original hint and therefore compares STATE.md `session` as the attribution label
44
+ it is; it is never rewritten to a raw id. This guarded translation is only
45
+ self-exclusion for discovery, not lock/registry ownership and not a continuity
46
+ bridge across a host rotation that changes both values.
47
+
48
+ **What the binding does not prove.** Both files it reads are repo-global, so the
49
+ check establishes that they are mutually CONSISTENT — not that they describe
50
+ *this* process. Semantic labels are routinely shared between simultaneously live
51
+ sessions, and when a foreign live session wrote both files last under a label
52
+ equal to this hint, its raw id is filtered out and the peer disappears from the
53
+ result. Measured 2026-08-21: with a null hint the foreign peer is returned, with
54
+ the colliding semantic hint `peers` is empty. Treat a quiet `findPeers` result as
55
+ weaker evidence than a git or filesystem signal, and prefer passing the native
56
+ raw id whenever the caller has one. Closing this needs a per-process ownership
57
+ proof; see #1091.
58
+
32
59
  ## Preamble Algorithm
33
60
 
34
61
  Execute these steps in order. Any classification determines outcome.
@@ -152,8 +179,10 @@ findPeers(repoRoot, { mySessionId }) → peer = peers.find((p) => p.source === '
152
179
  ```js
153
180
  import { findPeers } from '../../scripts/lib/peer-discovery.mjs';
154
181
 
155
- // Inside Phase 1b, before writing STATE.md:
156
- const { peers } = await findPeers(repoRoot, { mySessionId: sessionId });
182
+ // Inside Phase 1b, before writing STATE.md. Preserve the original
183
+ // attribution-label hint for the STATE.md surface; findPeers guards any
184
+ // semantic→raw translation for discovered peers internally.
185
+ const { peers } = await findPeers(repoRoot, { mySessionId: callerSessionHint });
157
186
  const peer = peers.find((p) => p.source === 'state-md') ?? null;
158
187
  // Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
159
188
  if (peer !== null) {
@@ -17,7 +17,7 @@ current-wave: <N>
17
17
  total-waves: <N>
18
18
  # Optional fields (schema-version 1, additive for backward-compat):
19
19
  updated: <ISO 8601 UTC> # last write timestamp, touched by any writer
20
- session: <session-id> # <branch>-<YYYY-MM-DD>-<mode>-<n> (semantic, since #573); legacy UUID-v4 also accepted by parseSessionId
20
+ session: <session-label> # attribution/history label; normally semantic since #573, legacy UUID-v4 remains readable; never a lock/registry ownership key
21
21
  session-start-ref: <sha> # git ref at session start
22
22
  ---
23
23
  ```
@@ -25,7 +25,7 @@ session-start-ref: <sha> # git ref at session start
25
25
  ### Required vs. optional fields
26
26
 
27
27
  - `schema-version`, `session-type`, `branch`, `issues`, `started_at`, `status`, `current-wave`, `total-waves` — **required** in every session-owned STATE.md.
28
- - `updated`, `session`, `session-start-ref` — **optional**. Added by #184. STATE.md files without these fields remain valid and should be treated as `updated: null` / `session: null`. Writers SHOULD populate these fields but readers MUST tolerate their absence. The `session` field's value format is `<branch>-<YYYY-MM-DD>-<mode>-<n>` since #573 (Epic #568 Parallel-Aware Sessions P2.2); pre-#573 files may contain a UUID-v4 — both formats are read via `parseSessionId()` from `scripts/lib/session-id.mjs` per PRD §3 P2 row 3 (backward-compat).
28
+ - `updated`, `session`, `session-start-ref` — **optional**. Added by #184. STATE.md files without these fields remain valid and should be treated as `updated: null` / `session: null`. Writers SHOULD populate these fields but readers MUST tolerate their absence. `session` is an attribution/history label, normally `<branch>-<YYYY-MM-DD>-<mode>-<n>` since #573 (Epic #568 Parallel-Aware Sessions P2.2); pre-#573 files may contain a UUID-v4 — both formats are read via `parseSessionId()` from `scripts/lib/session-id.mjs` per PRD §3 P2 row 3 (backward-compat). Neither form grants lock or registry ownership.
29
29
 
30
30
  The `session-type: none` + `status: idle` combination is used only for bootstrap-scaffolded placeholder files (no active session).
31
31
 
@@ -79,6 +79,32 @@ A log of unresolved, user-facing questions surfaced by wave agents during a sess
79
79
 
80
80
  Helpers: `readOpenQuestions` (pure), `appendOpenQuestion` (pure), `markOpenQuestionAnswered` (pure), `appendOpenQuestionOnDisk` (lock-guarded write), `markOpenQuestionAnsweredOnDisk` (lock-guarded write) — all exported from `scripts/lib/state-md.mjs`.
81
81
 
82
+ ## Session Identity and Lock Ownership (#1085)
83
+
84
+ This contract distinguishes a physical live-session key from labels that make a
85
+ session intelligible to people and history readers. It does not add an identity
86
+ layer.
87
+
88
+ - **`session_id` is the only live ownership key.** It is the native raw identity
89
+ supplied by the active harness, or a generated UUID when no trustworthy raw
90
+ identity is available. Lock acquisition, registry membership, self-exclusion,
91
+ proof checks, and lock release use this physical key.
92
+ - **`semantic_session_id` and STATE.md `session` are attribution/history
93
+ labels, never ownership.** They may describe the same work to a human, but
94
+ equality of either label cannot acquire, refresh, release, or reclaim a lock.
95
+ A legacy UUID in STATE.md remains readable only as historical data.
96
+ - **Never bridge a raw mismatch with a label or a proof.** If the current raw
97
+ id and a live lock's raw id differ, ownership is ambiguous. Leave the live
98
+ lock visible and let its TTL/Reaper lifecycle resolve it; do not substitute a
99
+ semantic match, STATE.md `session` match, or owner-proof match.
100
+ - **There is no `logical_session_id`.** A true cross-harness restart-continuity
101
+ contract requires a trusted native resume identifier and remains a follow-up.
102
+ In particular, a host rotation that changes both raw and semantic values has
103
+ no guaranteed continuity.
104
+
105
+ The peer-discovery and issue-budget procedures below apply these rules at their
106
+ narrow surfaces; neither creates a second ownership model.
107
+
82
108
  ## CCU-009 — Status = Index, Never History (#730/H6)
83
109
 
84
110
  > Adopted from an external-repo fleet-mining finding (2026-07-02): narrative
@@ -174,8 +200,8 @@ The `.orchestrator/session.lock` file is written mechanically by `hooks/_lib/loc
174
200
 
175
201
  ```json
176
202
  {
177
- "session_id": "<UUID-v4 OR semantic-id>",
178
- "semantic_session_id": "<branch>-<YYYY-MM-DD>-<mode>-<n>",
203
+ "session_id": "<native-raw-id OR generated-UUID>",
204
+ "semantic_session_id": "<attribution-label>",
179
205
  "started_at": "<ISO-8601 UTC>",
180
206
  "last_heartbeat": "<ISO-8601 UTC>",
181
207
  "mode": "deep|feature|housekeeping|session|...",
@@ -189,8 +215,8 @@ The `.orchestrator/session.lock` file is written mechanically by `hooks/_lib/loc
189
215
 
190
216
  | Field | Required since | Description |
191
217
  |---|---|---|
192
- | `session_id` | v1 | The session identifier (UUID-v4 on Claude Code, semantic on Codex/Cursor). |
193
- | `semantic_session_id` | v2 (Epic #583) | The semantic form (`<branch>-<YYYY-MM-DD>-<mode>-<n>`) **always present**, even when `session_id` is a UUID. Closes D4 gap: semantic-id branch was previously dead code on Claude Code (stdin always provides UUID). |
218
+ | `session_id` | v1 | The physical live lock/registry ownership key: a native raw harness identity, or a generated UUID when no trustworthy raw identity exists. Never use a semantic label here. |
219
+ | `semantic_session_id` | v2 (Epic #583) | An attribution/history label, normally `<branch>-<YYYY-MM-DD>-<mode>-<n>`, surfaced alongside the raw key. It never establishes lock or registry ownership, including when it equals STATE.md `session`. |
194
220
  | `started_at` | v1 | ISO-8601 timestamp when the lock was written. |
195
221
  | `last_heartbeat` | v2 (Epic #583) | ISO-8601 timestamp updated by the `SessionStart` hook and by `PostToolBatch`/`Stop` hooks. **Basis for liveness determination** — replaces PID-liveness (see below). |
196
222
  | `mode` | v1 | Session mode consulted by exclusivity-matrix. May be `"unknown"` in the provisional lock written by the hook before Session Config + AUQ have settled. |
@@ -79,7 +79,6 @@ AskUserQuestion({
79
79
  question: "Leeres Repo erkannt. Basierend auf '<HEURISTIC_REASON>' empfehle ich **<RECOMMENDED_TIER>**. Passt das?",
80
80
  header: "Bootstrap",
81
81
  options: [
82
- { label: "<RECOMMENDED_TIER> (Empfohlen)", description: "<one-line description of what this tier scaffolds>" },
83
82
  { label: "fast", description: "Nur CLAUDE.md + .gitignore + README. Für Demos, Spikes, Playgrounds." },
84
83
  { label: "standard", description: "Fast + package.json/Manifest + TypeScript + Linting + Tests. Für MVPs und echte Produkte." },
85
84
  { label: "deep", description: "Standard + CI + CODEOWNERS + CHANGELOG. Für Production, Team, Langlebige Repos." },
@@ -90,6 +89,8 @@ AskUserQuestion({
90
89
  })
91
90
  ```
92
91
 
92
+ Before rendering: append ` (Empfohlen)` to whichever of the three tier labels equals `<RECOMMENDED_TIER>`, and move that option to position 1. The recommended tier is one of the three — listing it a fourth time as its own option made five options, one more than `AskUserQuestion` accepts, and repeated the same choice twice.
93
+
93
94
  If user selects "Abbrechen": stop. Report "Bootstrap abgebrochen. Kein Kommando wird ausgeführt." Do not continue.
94
95
 
95
96
  Store confirmed tier as `CONFIRMED_TIER`.
@@ -48,12 +48,12 @@ Run a single AUQ to let the user characterize the problem in their own words:
48
48
  ```
49
49
  AskUserQuestion({
50
50
  questions: [{
51
- question: "Describe the feature or problem you want to brainstorm. What's the core pain point it addresses, and what's still ambiguous to you?",
52
- header: "Feature Frame",
51
+ question: "What do you want to brainstorm, and what is still unclear about it?",
52
+ header: "Frame",
53
53
  options: [
54
- { label: "UX / user-facing surface is unclear", description: "You know what you want to build but not how it should work for the user." },
55
- { label: "Scope / boundaries are unclear", description: "You're not sure how much to build, or where one feature ends and another begins." },
56
- { label: "Multiple approaches exist need to pick one", description: "You see 2-3 ways to solve this and aren't sure which is best." },
54
+ { label: "The user-facing surface", description: "You know what to build but not how it should work for the user." },
55
+ { label: "Scope / boundaries", description: "You're not sure how much to build, or where one feature ends and another begins." },
56
+ { label: "Several approaches — pick one", description: "You see 2-3 ways to solve this and aren't sure which is best." },
57
57
  { label: "Other / describe below", description: "Free-form: describe the ambiguity in the next prompt." }
58
58
  ],
59
59
  multiSelect: false
@@ -98,9 +98,9 @@ Example round (adapt to the actual feature):
98
98
  AskUserQuestion({
99
99
  questions: [{
100
100
  question: "Who is the primary user of this feature, and how do they trigger it?",
101
- header: "User Surface — Round 2",
101
+ header: "User surface",
102
102
  options: [
103
- { label: "Authenticated user via UI action (Recommended)", description: "Pro: fits existing session model. Con: requires UI component work." },
103
+ { label: "Signed-in user, UI action (Recommended)", description: "Pro: fits existing session model. Con: requires UI component work." },
104
104
  { label: "Automated trigger (webhook, cron, event)", description: "Pro: no manual user step. Con: harder to debug and test." },
105
105
  { label: "Admin-only operation", description: "Pro: simpler access control. Con: limits who can self-serve." },
106
106
  { label: "Other / describe below", description: "Describe the trigger mechanism." }
@@ -128,12 +128,12 @@ Once the dialogue has enough signal, synthesize 2-3 concrete implementation appr
128
128
  ```
129
129
  AskUserQuestion({
130
130
  questions: [{
131
- question: "Based on your answers, here are the viable approaches. Which fits best?",
132
- header: "Design Approach",
131
+ question: "Which of these approaches fits best?",
132
+ header: "Approach",
133
133
  options: [
134
- { label: "Approach A — [1-sentence summary] (Recommended)", description: "Trade-offs: [key pro]. [key con]. Complexity: low/medium/high." },
135
- { label: "Approach B — [1-sentence summary]", description: "Trade-offs: [key pro]. [key con]. Complexity: low/medium/high." },
136
- { label: "Approach C — [1-sentence summary]", description: "Trade-offs: [key pro]. [key con]. Complexity: low/medium/high." }
134
+ { label: "A — [short summary] (Recommended)", description: "Best balance of value, reversibility and scope, because [key pro]. Cost: [key con]. Complexity: low/medium/high." },
135
+ { label: "B — [short summary]", description: "Trade-offs: [key pro]. Cost: [key con]. Complexity: low/medium/high." },
136
+ { label: "C — [short summary]", description: "Trade-offs: [key pro]. Cost: [key con]. Complexity: low/medium/high." }
137
137
  ],
138
138
  multiSelect: false
139
139
  }]
@@ -231,13 +231,13 @@ Present the spec path and summary to the user. Then ask via AUQ:
231
231
  ```
232
232
  AskUserQuestion({
233
233
  questions: [{
234
- question: "The design spec has been written to docs/specs/YYYY-MM-DD-<slug>-design.md. How do you want to proceed?",
235
- header: "Design Hand-off",
234
+ question: "The spec is at docs/specs/YYYY-MM-DD-<slug>-design.md. How do you want to proceed?",
235
+ header: "Hand-off",
236
236
  options: [
237
- { label: "Proceed to /plan feature (Recommended)", description: "Formalize this spec into a PRD with acceptance criteria and issue creation." },
238
- { label: "Proceed to /write-executable-plan", description: "Skip the formal PRD and go straight to an executable plan (issue #39, once shipped)." },
239
- { label: "Revise the spec", description: "I have feedback describe what to change and I'll update the spec." },
240
- { label: "Done for now", description: "Keep the spec as a reference; no immediate next step." }
237
+ { label: "Proceed to /plan feature (Recommended)", description: "Turns the spec into a PRD with acceptance criteria, then files the issues. Cost: one more question round." },
238
+ { label: "Proceed to /write-executable-plan", description: "Skips the formal PRD and writes a step-by-step executable plan instead — tracked in #39 (a numbered ticket, not yet released)." },
239
+ { label: "Revise the spec", description: "Describe what to change; the spec is updated and comes back here." },
240
+ { label: "Done for now", description: "The spec stays on disk as a reference. Nothing else runs." }
241
241
  ],
242
242
  multiSelect: false
243
243
  }]
@@ -62,6 +62,18 @@ The active level is `efficiency.output-level` in `~/.config/session-orchestrator
62
62
  - Shape: explain WHY each option is on the table, name the paths you already discarded and why, define unfamiliar terms on first use.
63
63
  - Escalation: `expand <topic>` — see § Escalation above.
64
64
 
65
+ ### Register — how a sentence reads
66
+
67
+ The budgets above set *how much* you say; the register sets *how*. It is
68
+ defined once, in `skills/session-start/soul.md` § "Register — how a sentence
69
+ reads", and binds here unchanged: the frame ("write for someone who knows this
70
+ project but has not seen what you just saw"), the plain-words test with its
71
+ five worked cases, and its precedence over § "Never traded for brevity" above.
72
+ Read it there. It is not repeated here on purpose — the § Output Levels intro
73
+ sentence already exists in four copies across the four souls with nothing
74
+ checking their parity, and a fifth copied rule would drift the same way. A
75
+ pointer cannot.
76
+
65
77
  ### Companion dials
66
78
 
67
79
  Same file, same lookup, same fallback-to-default rule:
@@ -104,13 +104,13 @@ When the gate above is satisfied, present exactly this AskUserQuestion (AUQ-003
104
104
  ```
105
105
  AskUserQuestion({
106
106
  questions: [{
107
- question: "Scope `feature` was requested. How should this run handle feature discovery?",
108
- header: "Feature Scope",
107
+ question: "Scope `feature` was requested. How should this run handle it?",
108
+ header: "Scope",
109
109
  options: [
110
- { label: "Grounded scan (Recommended)", description: "Run the evidence-anchored feature probes (intent-drift, stubbed-dead-feature) on the probe→verify→triage rails." },
111
- { label: "Also judgment topics", description: "Grounded scan PLUS collect judgment-based product questions (opportunity framing, personas). A follow-up prompt after Phase 5 offers inline synthesis or hand-off to /brainstorm or /plan feature — judgment items never enter the verified-findings pipeline." },
112
- { label: "Route out", description: "No scan; hand off to /brainstorm (product ideation) or /grill (assumption stress-test) instead." },
113
- { label: "Skip", description: "Drop `feature` from this run's scope set." }
110
+ { label: "Grounded scan (Recommended)", description: "Every finding is tied to a file and line and is verified before it can become an issue. Cost: two extra probes (intent-drift, stubbed-dead-feature)." },
111
+ { label: "Also judgment topics", description: "Same scan, plus open product questions (opportunity framing, personas) kept as notes. They never become issues; after Phase 5 you pick where they go." },
112
+ { label: "Route out", description: "No scan at all. You get a pointer to /brainstorm (product ideation) or /grill (assumption stress-test) instead." },
113
+ { label: "Skip", description: "Drops `feature` (the probes for half-built and drifted features) from this run; the other scopes still run." }
114
114
  ],
115
115
  multiSelect: false
116
116
  }]
@@ -125,12 +125,12 @@ AskUserQuestion({
125
125
  ```
126
126
  AskUserQuestion({
127
127
  questions: [{
128
- question: "Judgment topics were collected alongside the grounded scan. How should they be handled?",
129
- header: "Judgment Topics",
128
+ question: "Where should the collected judgment topics go?",
129
+ header: "Topics",
130
130
  options: [
131
- { label: "Inline synthesis (Recommended)", description: "Sketch a lightweight OST/persona pass directly in the report's `### Judgment Topics (non-verified)` appendix explicitly marked non-verified, no separate skill invocation needed." },
132
- { label: "Route to /brainstorm", description: "Hand the collected topics off as pre-filled context to /brainstorm for a full Socratic ideation dialogue." },
133
- { label: "Route to /plan feature", description: "Hand the collected topics off as pre-filled context to /plan feature for feature-PRD scoping." }
131
+ { label: "Inline synthesis (Recommended)", description: "Sketches an outcome/persona pass into `### Judgment Topics (non-verified)` (a report section that never becomes issues). Cost: no second run." },
132
+ { label: "Route to /brainstorm", description: "Hands the topics to /brainstorm as its opening context, for a full question-and-answer design dialogue." },
133
+ { label: "Route to /plan feature", description: "Hands the topics to /plan feature as its opening context, for feature-PRD scoping." }
134
134
  ],
135
135
  multiSelect: false
136
136
  }]
@@ -370,14 +370,16 @@ For each Critical or High finding, use AskUserQuestion (on Codex CLI where AskUs
370
370
  ```
371
371
  AskUserQuestion({
372
372
  questions: [{
373
- question: "<finding title>\n\n<file_path>:<line_number>\n```\n<matched_text with +/-3 lines context>\n```\n\n<description>\n\nRecommended fix: <recommended_fix>",
374
- header: "<severity>",
373
+ question: "<severity> finding in <file_path> what should happen with it?",
374
+ header: "Finding",
375
375
  options: [
376
- { label: "Create issue (<severity>)", description: "Create a priority::<severity> issue for this finding" },
377
- { label: "Adjust priority", description: "Create issue with different priority" },
378
- { label: "Dismiss -- intentional", description: "This is by design, skip" },
379
- { label: "Dismiss -- false positive", description: "Detection was wrong, skip" }
380
- ]
376
+ { label: "Create issue (<severity>)", description: "Files it as priority::<severity>, so it is tracked outside this session. The code below is copied into the issue body.",
377
+ preview: "<finding title>\n\n<file_path>:<line_number>\n```\n<matched_text with +/-3 lines context>\n```\n\n<description>\n\nRecommended fix: <recommended_fix>" },
378
+ { label: "Adjust priority", description: "Same issue, a priority you pick — this question then comes back with the new label." },
379
+ { label: "Dismiss -- intentional", description: "The code is deliberate. Nothing is filed, and the finding stays only in this run's report." },
380
+ { label: "Dismiss -- false positive", description: "The probe misread the code. Nothing is filed; worth reporting if the same probe misfires again." }
381
+ ],
382
+ multiSelect: false
381
383
  }]
382
384
  })
383
385
  ```
@@ -391,13 +393,15 @@ Group remaining findings by category. For each category with medium/low findings
391
393
  ```
392
394
  AskUserQuestion({
393
395
  questions: [{
394
- question: "[N] medium/low findings in [category]:\n\n1. [title] -- [file_path]:[line] ([severity])\n2. [title] -- [file_path]:[line] ([severity])\n...",
395
- header: "[Category]",
396
+ question: "Create issues for all [N] medium/low findings in [category]?",
397
+ header: "Findings",
396
398
  options: [
397
- { label: "Accept all (Recommended)", description: "Create issues for all [N] findings" },
398
- { label: "Review individually", description: "Walk through each finding one by one" },
399
- { label: "Dismiss all", description: "Skip all medium/low findings in this category" }
400
- ]
399
+ { label: "Accept all (Recommended)", description: "Medium and low findings are cheap to file and cheap to close. Cost: [N] issues, roughly one second apart.",
400
+ preview: "1. [title] -- [file_path]:[line] ([severity])\n2. [title] -- [file_path]:[line] ([severity])\n..." },
401
+ { label: "Review individually", description: "One question per finding, same options as the critical ones. Cost: [N] more prompts." },
402
+ { label: "Dismiss all", description: "Nothing is filed for this category. The findings stay in this run's report only." }
403
+ ],
404
+ multiSelect: false
401
405
  }]
402
406
  })
403
407
  ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: eli5
3
+ description: Explain a topic like I'm a 5 year old — restate my last output, or a named topic, in plain words without dropping a single fact. Use when the user types /eli5 [topic], or says an answer was too technical, too long, or unclear about what he now has to do.
4
+ model: inherit
5
+ tools: Read, Grep, Glob, Bash
6
+ ---
7
+
8
+ # eli5
9
+
10
+ Say it again in plain words. Same facts, in the order he needs them.
11
+
12
+ ## The frame
13
+
14
+ **Write for someone who knows this project but has not seen what you just saw.**
15
+
16
+ He owns this repo. He is not missing knowledge, he is missing observation — he did not watch the command run or read the file it points at. Write across, not down. `.claude/rules/ask-via-tool.md` § AUQ-006 and `skills/session-start/soul.md` § Register are the canonical statement of this register; apply them, do not restate them.
17
+
18
+ ## What to restate
19
+
20
+ - `/eli5` — my last substantial output in this conversation. If there is none yet, say so instead of inventing a topic.
21
+ - `/eli5 <topic>` — that topic (`$ARGUMENTS`), grounded in what THIS session already measured: name the file, command, or record it came from. If it was never measured here, say that rather than filling the gap from memory.
22
+
23
+ Answer in the operator's language: `owner.language` in `~/.config/session-orchestrator/owner.yaml`, falling back to `en` when that file is missing, unreadable, or the key is absent — and follow the operator's own language the moment he writes in another one.
24
+
25
+ ## Two limits, and they are the whole skill
26
+
27
+ **1. Say more simply what actually happens — introduce nothing that does not exist.**
28
+ Test: delete every noun the system does not contain. Sentence still true and complete → it was no analogy. Sentence collapses → describe what actually happens instead.
29
+ ✓ "Waiting means the other session finishes first." ✗ "Think of the session as a level crossing."
30
+
31
+ **2. Simplifying removes words, never facts.** If a path, a number, an error code, an identifier, or an instruction to act disappears, that is data loss, not simplification. The mechanical decider: **could the token you are about to cut ever appear in a `grep`? Then it stays.** `skills/session-start/soul.md` § "Never traded for brevity" outranks every brevity instruction in this file.
32
+
33
+ ## Shape
34
+
35
+ Line 1 answers the question he actually has: **do I have to do something now, and what happens if I don't?** Then the facts, in the order he needs them — not the order you found them.
36
+
37
+ This is a terminal, so there is no picture to draw. The second channel is the `preview` field on an `AskUserQuestion` option: when the options differ in something literal — a diff, a title, a config block, a file list — put that text there and he reads the thing instead of a description of it.
38
+
39
+ ## What this is not
40
+
41
+ - **Not shorter by default.** Eight lines before may be eight lines after. Reorder first; cut only filler.
42
+ - **Not a children's explanation.** The label is a poster, the reader is an expert who was not in the room.
43
+ - **Not a second attempt at the answer.** If the first answer was wrong, fix the answer — `/eli5` restates, it never re-derives.