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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +3 -2
- package/.cursor/rules/040-discovery.mdc +6 -6
- package/.cursor/rules/050-plan.mdc +8 -8
- package/CHANGELOG.md +101 -0
- package/README.md +10 -10
- package/agents/memory-proposal-collector.md +6 -4
- package/commands/eli5.md +33 -0
- package/commands/release.md +5 -3
- package/commands/test.md +2 -2
- package/docs/components.md +6 -5
- package/docs/scope-collision-guard.md +3 -3
- package/docs/session-config-reference.md +31 -8
- package/hooks/_lib/lock-bootstrap.mjs +19 -13
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +11 -1
- package/hooks/on-session-end.mjs +24 -92
- package/hooks/on-session-start.mjs +195 -104
- package/hooks/pre-auq-clarity.mjs +787 -0
- package/hooks/pre-bash-issue-budget.mjs +17 -18
- package/package.json +3 -1
- package/pi/prompts/eli5.md +12 -0
- package/scripts/auq-audit.mjs +825 -0
- package/scripts/autopilot.mjs +7 -8
- 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/ci-status-banner.mjs +63 -57
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +32 -9
- package/scripts/lib/config/vault-integration.mjs +12 -1
- package/scripts/lib/dispatcher/rank.mjs +4 -7
- package/scripts/lib/gates/gate-full.mjs +3 -3
- package/scripts/lib/gates/gate-helpers.mjs +17 -6
- package/scripts/lib/io.mjs +239 -0
- package/scripts/lib/issue-budget.mjs +63 -9
- package/scripts/lib/owner-interview.mjs +78 -32
- package/scripts/lib/peer-discovery.mjs +73 -22
- package/scripts/lib/project-hygiene.mjs +64 -4
- package/scripts/lib/reconcile/renderer.mjs +17 -4
- 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/spiral-carryover.mjs +23 -2
- package/scripts/lib/state-md/mission-status.mjs +147 -50
- package/scripts/lib/validate/check-auq-clarity.mjs +274 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +30 -0
- package/scripts/lib/validate/check-rules.mjs +153 -9
- package/scripts/lib/vault-backfill/glab.mjs +91 -58
- package/scripts/lib/vault-backfill/manifest.mjs +28 -8
- package/scripts/lib/vcs-repo-spec.mjs +182 -13
- package/scripts/lib/wave-resource-gate.mjs +67 -73
- package/scripts/materialize-wave-scope.mjs +281 -0
- package/scripts/release.mjs +443 -122
- package/scripts/run-quality-gate.mjs +14 -0
- package/scripts/validate-plugin.mjs +3 -0
- package/scripts/validate-wave-scope.mjs +6 -1
- package/scripts/vault-backfill.mjs +32 -5
- package/skills/_shared/parallel-aware-auq.md +30 -24
- package/skills/_shared/parallel-aware-preamble.md +31 -2
- package/skills/_shared/state-ownership.md +32 -6
- package/skills/bootstrap/SKILL.md +2 -1
- package/skills/brainstorm/SKILL.md +18 -18
- package/skills/brainstorm/soul.md +12 -0
- package/skills/discovery/SKILL.md +28 -24
- 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/memory-cleanup/SKILL.md +2 -2
- package/skills/npm-publish/SKILL.md +4 -4
- package/skills/peekaboo-driver/SKILL.md +3 -3
- 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 +3 -3
- package/skills/session-end/SKILL.md +53 -20
- package/skills/session-end/phase-3-6-tail.md +37 -2
- package/skills/session-start/SKILL.md +69 -35
- 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/soul.md +110 -0
- package/skills/test-runner/SKILL.md +2 -2
- package/skills/using-orchestrator/SKILL.md +1 -1
- package/skills/wave-executor/wave-loop.md +27 -5
- package/skills/write-executable-plan/SKILL.md +6 -6
- 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
|
@@ -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,
|
|
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 =
|
|
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: `
|
|
28
|
-
header: "
|
|
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: "
|
|
32
|
-
{ label: "Andere Session beenden", description: "
|
|
33
|
-
{ label: "Abbrechen", description: "Exit
|
|
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
|
-
|
|
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) —
|
|
49
|
-
2. Andere Session beenden —
|
|
50
|
-
3. Abbrechen — exit
|
|
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
|
|
71
|
-
header: "
|
|
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: "
|
|
75
|
-
{ label: "Manuell — in-place daneben", description: "
|
|
76
|
-
{ label: "Abbrechen", description: "Exit
|
|
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
|
-
|
|
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) —
|
|
90
|
-
2. Manuell — in-place daneben —
|
|
91
|
-
3. Abbrechen — exit
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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": "<
|
|
178
|
-
"semantic_session_id": "<
|
|
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
|
|
193
|
-
| `semantic_session_id` | v2 (Epic #583) |
|
|
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: "
|
|
52
|
-
header: "
|
|
51
|
+
question: "What do you want to brainstorm, and what is still unclear about it?",
|
|
52
|
+
header: "Frame",
|
|
53
53
|
options: [
|
|
54
|
-
{ label: "
|
|
55
|
-
{ label: "Scope / boundaries
|
|
56
|
-
{ label: "
|
|
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
|
|
101
|
+
header: "User surface",
|
|
102
102
|
options: [
|
|
103
|
-
{ label: "
|
|
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: "
|
|
132
|
-
header: "
|
|
131
|
+
question: "Which of these approaches fits best?",
|
|
132
|
+
header: "Approach",
|
|
133
133
|
options: [
|
|
134
|
-
{ label: "
|
|
135
|
-
{ label: "
|
|
136
|
-
{ label: "
|
|
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
|
|
235
|
-
header: "
|
|
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: "
|
|
238
|
-
{ label: "Proceed to /write-executable-plan", description: "
|
|
239
|
-
{ label: "Revise the spec", description: "
|
|
240
|
-
{ label: "Done for now", description: "
|
|
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
|
|
108
|
-
header: "
|
|
107
|
+
question: "Scope `feature` was requested. How should this run handle it?",
|
|
108
|
+
header: "Scope",
|
|
109
109
|
options: [
|
|
110
|
-
{ label: "Grounded scan (Recommended)", description: "
|
|
111
|
-
{ label: "Also judgment topics", description: "
|
|
112
|
-
{ label: "Route out", description: "No scan
|
|
113
|
-
{ label: "Skip", description: "
|
|
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: "
|
|
129
|
-
header: "
|
|
128
|
+
question: "Where should the collected judgment topics go?",
|
|
129
|
+
header: "Topics",
|
|
130
130
|
options: [
|
|
131
|
-
{ label: "Inline synthesis (Recommended)", description: "
|
|
132
|
-
{ label: "Route to /brainstorm", description: "
|
|
133
|
-
{ label: "Route to /plan feature", description: "
|
|
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
|
|
374
|
-
header: "
|
|
373
|
+
question: "<severity> finding in <file_path> — what should happen with it?",
|
|
374
|
+
header: "Finding",
|
|
375
375
|
options: [
|
|
376
|
-
{ label: "Create issue (<severity>)", description: "
|
|
377
|
-
|
|
378
|
-
{ label: "
|
|
379
|
-
{ label: "Dismiss --
|
|
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]
|
|
395
|
-
header: "
|
|
396
|
+
question: "Create issues for all [N] medium/low findings in [category]?",
|
|
397
|
+
header: "Findings",
|
|
396
398
|
options: [
|
|
397
|
-
{ label: "Accept all (Recommended)", description: "
|
|
398
|
-
|
|
399
|
-
{ label: "
|
|
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.
|