projectstore-claude 0.29.3 → 0.29.4

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 (53) hide show
  1. package/bin/projectstore-claude.mjs +1 -1
  2. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  3. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  4. package/node_modules/projectstore/README.md +17 -0
  5. package/node_modules/projectstore/commands/bind.md +6 -17
  6. package/node_modules/projectstore/commands/doctor.md +12 -7
  7. package/node_modules/projectstore/commands/statusline.md +1 -1
  8. package/node_modules/projectstore/commands/story.md +15 -2
  9. package/node_modules/projectstore/docs/how-it-works.md +5 -3
  10. package/node_modules/projectstore/harnesses/claude-code.json +5 -5
  11. package/node_modules/projectstore/harnesses/codex.json +4 -5
  12. package/node_modules/projectstore/hooks/band.tsx +48 -20
  13. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  14. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  15. package/node_modules/projectstore/hooks/session-start.mjs +50 -18
  16. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  17. package/node_modules/projectstore/package.json +11 -2
  18. package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
  19. package/node_modules/projectstore/scripts/band.mjs +72 -21
  20. package/node_modules/projectstore/scripts/binding.mjs +616 -85
  21. package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
  22. package/node_modules/projectstore/scripts/cli.mjs +1015 -185
  23. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  24. package/node_modules/projectstore/scripts/convergence.mjs +253 -83
  25. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  26. package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
  27. package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
  28. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  29. package/node_modules/projectstore/scripts/fetch-shell.mjs +106 -20
  30. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  31. package/node_modules/projectstore/scripts/harness.mjs +123 -48
  32. package/node_modules/projectstore/scripts/install-harness.mjs +3511 -616
  33. package/node_modules/projectstore/scripts/kanban.mjs +2 -1
  34. package/node_modules/projectstore/scripts/lib.mjs +694 -159
  35. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  36. package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
  37. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  38. package/node_modules/projectstore/scripts/query.mjs +378 -80
  39. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  40. package/node_modules/projectstore/scripts/run-host.mjs +62 -17
  41. package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
  42. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  43. package/node_modules/projectstore/scripts/stages.mjs +5 -1
  44. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  45. package/node_modules/projectstore/scripts/statusline.mjs +32 -14
  46. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  47. package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
  48. package/node_modules/projectstore/scripts/term.mjs +382 -97
  49. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  50. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  51. package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
  52. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  53. package/package.json +2 -2
@@ -55,16 +55,64 @@ import { resolve, dirname } from "node:path";
55
55
  import { fileURLToPath } from "node:url";
56
56
  import { parseArgs } from "node:util";
57
57
  import * as term from "./term.mjs";
58
- import { projectRootDeclared, childEnv, harnessIds, harnessForOverlay, pinPluginRoot, invokedAs, loadHarnesses, invocation } from "./harness.mjs";
59
- import { readConfigAt, readOverlayAt, resolveAgentModel, writeOverlayAt, overlayId, layoutRoster, commandForm, projectDirRefusal, doctorFallbackText } from "./lib.mjs";
60
- import { READ_OPERATIONS, LINEAGE_KINDS, LINEAGE_DEFAULT_DEPTH, SEARCH_DEFAULT_LIMIT, GRAPH_EDGE_CAP, DIRECTIONS } from "./query.mjs";
58
+ import {
59
+ projectRootDeclared,
60
+ childEnv,
61
+ harnessIds,
62
+ harnessForOverlay,
63
+ pinPluginRoot,
64
+ invokedAs,
65
+ shellHarnessId,
66
+ CORE_NPX,
67
+ packageCommand,
68
+ loadHarnesses,
69
+ invocation,
70
+ } from "./harness.mjs";
71
+ import {
72
+ readConfigAt,
73
+ readOverlayAt,
74
+ resolveAgentModel,
75
+ writeOverlayAt,
76
+ overlayId,
77
+ layoutRoster,
78
+ commandForm,
79
+ projectDirRefusal,
80
+ fromTerminalIn,
81
+ doctorFallbackText,
82
+ refreshSessionPointer,
83
+ } from "./lib.mjs";
84
+ import {
85
+ READ_OPERATIONS,
86
+ LINEAGE_KINDS,
87
+ LINEAGE_DEFAULT_DEPTH,
88
+ SEARCH_DEFAULT_LIMIT,
89
+ GRAPH_EDGE_CAP,
90
+ DIRECTIONS,
91
+ } from "./query.mjs";
61
92
  // binding.mjs and scaffold.mjs beneath it are write modules imported
62
93
  // statically where the install family is lazy: dependency-free, with no side
63
94
  // effects at import, so the MCP server's module graph gains nothing it could
64
95
  // trip on.
65
- import { planBind, applyBind, renderBindPlan, bindResult, bindFailed, normaliseVaultPath, shellQuote, DEFAULT_LAYOUT, DEFAULT_LANGUAGE } from "./binding.mjs";
96
+ import {
97
+ planBind,
98
+ applyBind,
99
+ renderBindPlan,
100
+ bindResult,
101
+ bindFailed,
102
+ normaliseVaultPath,
103
+ shellQuote,
104
+ DEFAULT_LAYOUT,
105
+ DEFAULT_LANGUAGE,
106
+ } from "./binding.mjs";
66
107
  import { planScaffold, applyScaffold, scaffoldResult, renderScaffoldPlan } from "./scaffold.mjs";
67
- import { planBand, applyBand, bandStatus, renderBandStatus, renderBandPlan, SUBCOMMANDS as BAND_SUBCOMMANDS } from "./band.mjs";
108
+ import {
109
+ planBand,
110
+ applyBand,
111
+ bandStatus,
112
+ renderBandStatus,
113
+ renderBandPlan,
114
+ SUBCOMMANDS as BAND_SUBCOMMANDS,
115
+ } from "./band.mjs";
68
116
 
69
117
  export const SCHEMA_VERSION = 1;
70
118
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -72,7 +120,11 @@ export const PACKAGE_ROOT = dirname(HERE);
72
120
  const script = (name) => resolve(HERE, name);
73
121
 
74
122
  export function packageVersion() {
75
- try { return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8")).version || ""); } catch { return ""; }
123
+ try {
124
+ return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, "package.json"), "utf8")).version || "");
125
+ } catch {
126
+ return "";
127
+ }
76
128
  }
77
129
 
78
130
  export function envelope(verb, project, ok, result) {
@@ -108,129 +160,428 @@ const JSON_OPT = opt("json", false, "the envelope");
108
160
  // verb: the scan knows their arity before it knows the verb (--project takes
109
161
  // a value, the rest do not). Last in every parser map, so a row cannot
110
162
  // retype one. SHORT is their one-letter forms (`-hv`: version wins).
111
- const GLOBAL_OPTS = Object.freeze([opt("project", "<dir>", "the project"), JSON_OPT, opt("help", false, "the help"), opt("version", false, "the package version")]);
163
+ const GLOBAL_OPTS = Object.freeze([
164
+ opt("project", "<dir>", "the project"),
165
+ JSON_OPT,
166
+ opt("help", false, "the help"),
167
+ opt("version", false, "the package version"),
168
+ ]);
112
169
  const SHORT = Object.freeze({ help: "h", version: "v" });
113
170
  const NO_FORMS = Object.freeze([]);
114
171
  const READ_JSON = [JSON_OPT];
115
- const HARNESS_OPT = opt("harness", "<id>", "the harness — and, without a terminal, the confirmation (a terminal is asked); there is no --yes", true);
172
+ const HARNESS_OPT = opt(
173
+ "harness",
174
+ "<id>",
175
+ "the harness — and, without a terminal, the confirmation (a terminal is asked); there is no --yes",
176
+ true,
177
+ );
116
178
  const SURFACE_OPT = opt("surface", "<key>", "one surface and those beneath it", true);
117
179
  // The layout move's remedy names this for any copy but the package's own
118
180
  // registration (the layout spec, contract 12 as amended 2026-10-03): it makes
119
181
  // "no host command" true by construction instead of by recognising the root.
120
- const NO_REGISTER_OPT = opt("no-register", false, "leave the plugin registration alone: change only this project's files");
182
+ const NO_REGISTER_OPT = opt(
183
+ "no-register",
184
+ false,
185
+ "leave the plugin registration alone: change only this project's files",
186
+ );
121
187
  const VERBOSE_OPT = opt("verbose", false, "every row's reasoning, each step's why and the host's own notes");
122
188
  // doctor's --verbose is the same flag in its own words: on a terminal, the
123
189
  // report the pipe gets — every note, every instance, each message whole.
124
- const DOCTOR_VERBOSE_OPT = opt(VERBOSE_OPT.name, VERBOSE_OPT.arg, "on a terminal too: every note, every instance, whole messages");
190
+ const DOCTOR_VERBOSE_OPT = opt(
191
+ VERBOSE_OPT.name,
192
+ VERBOSE_OPT.arg,
193
+ "on a terminal too: every note, every instance, whole messages",
194
+ );
125
195
  const INSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, NO_REGISTER_OPT, VERBOSE_OPT, JSON_OPT];
126
- const UNINSTALL_OPTS = [HARNESS_OPT, SURFACE_OPT, opt("global", false, "also remove the harness-global plugin registration"), VERBOSE_OPT, JSON_OPT];
196
+ const UNINSTALL_OPTS = [
197
+ HARNESS_OPT,
198
+ SURFACE_OPT,
199
+ opt("global", false, "also remove the harness-global plugin registration"),
200
+ VERBOSE_OPT,
201
+ JSON_OPT,
202
+ ];
203
+
204
+ // setup's own options (the setup story, rule 1): seven, beside the globals.
205
+ // `--vault <path>` takes a value here where doctor's `--vault` is a flag —
206
+ // the per-verb parse's own case.
207
+ const SETUP_OPTS = [
208
+ HARNESS_OPT,
209
+ opt(
210
+ "vault",
211
+ "<path>",
212
+ "the vault: an existing folder is bound, a missing or empty one is created (naming it confirms)",
213
+ ),
214
+ opt("layout", "<name>", `a new vault's layout (default ${DEFAULT_LAYOUT})`),
215
+ opt("language", "<code>", `a new vault's template language (default ${DEFAULT_LANGUAGE})`),
216
+ opt(
217
+ "channel",
218
+ "<npm|git>",
219
+ "with a git-marketplace copy enabled, without a terminal: npm switches to the npm install, git keeps the git marketplace",
220
+ ),
221
+ opt("dry-run", false, "the questions and the plan; nothing is written or fetched"),
222
+ VERBOSE_OPT,
223
+ ];
127
224
 
128
225
  export const VERBS = Object.freeze([
129
226
  Object.freeze({
130
- verb: "doctor", summary: "Check the install wiring and the vault's consistency.", usage: NO_FORMS,
131
- module: "./doctor.mjs", wraps: "script", how: "spawn", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze(["doctor"]),
132
- options: [opt("install", false, "only the install section"), opt("vault", false, "only the vault section"), DOCTOR_VERBOSE_OPT, JSON_OPT],
227
+ verb: "setup",
228
+ summary: "Set up this project: every harness it uses, and a vault.",
229
+ usage: NO_FORMS,
230
+ module: "./setup.mjs",
231
+ wraps: "new",
232
+ how: "import",
233
+ output: "envelope",
234
+ writes: true,
235
+ requiresBinding: false,
236
+ mcp: Object.freeze([]),
237
+ options: [...SETUP_OPTS, JSON_OPT],
238
+ run: runSetupVerb,
239
+ }),
240
+ Object.freeze({
241
+ verb: "doctor",
242
+ summary: "Check the install wiring and the vault's consistency.",
243
+ usage: NO_FORMS,
244
+ module: "./doctor.mjs",
245
+ wraps: "script",
246
+ how: "spawn",
247
+ output: "envelope",
248
+ writes: false,
249
+ requiresBinding: false,
250
+ mcp: Object.freeze(["doctor"]),
251
+ options: [
252
+ opt("install", false, "only the install section"),
253
+ opt("vault", false, "only the vault section"),
254
+ DOCTOR_VERBOSE_OPT,
255
+ JSON_OPT,
256
+ ],
133
257
  run: runDoctor,
134
258
  }),
135
259
  Object.freeze({
136
- verb: "reconcile", summary: "Regenerate the derived views (kanban, code map, graph, indexes).", usage: NO_FORMS,
137
- module: "./reconcile.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: true, mcp: Object.freeze([]),
138
- options: [opt("write", false, "apply the regeneration (asks on a terminal; --only names what is written and confirms headless)"), opt("only", "<target>", "one derived view"), JSON_OPT],
260
+ verb: "reconcile",
261
+ summary: "Regenerate the derived views (kanban, code map, graph, indexes).",
262
+ usage: NO_FORMS,
263
+ module: "./reconcile.mjs",
264
+ wraps: "module",
265
+ how: "import",
266
+ output: "envelope",
267
+ writes: true,
268
+ requiresBinding: true,
269
+ mcp: Object.freeze([]),
270
+ options: [
271
+ opt(
272
+ "write",
273
+ false,
274
+ "apply the regeneration (asks on a terminal; --only names what is written and confirms headless)",
275
+ ),
276
+ opt("only", "<target>", "one derived view"),
277
+ JSON_OPT,
278
+ ],
139
279
  run: runReconcile,
140
280
  }),
141
281
  Object.freeze({
142
- verb: "plan", summary: "Show what install would write for a harness, without writing.", usage: NO_FORMS,
143
- module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze([]),
144
- options: INSTALL_OPTS, run: runInstallVerb,
282
+ verb: "plan",
283
+ summary: "Show what install would write for a harness, without writing.",
284
+ usage: NO_FORMS,
285
+ module: "./install-harness.mjs",
286
+ wraps: "module",
287
+ how: "import",
288
+ output: "envelope",
289
+ writes: false,
290
+ requiresBinding: false,
291
+ mcp: Object.freeze([]),
292
+ options: INSTALL_OPTS,
293
+ run: runInstallVerb,
145
294
  }),
146
295
  Object.freeze({
147
- verb: "install", summary: "Install projectstore's surfaces for a harness, behind a preview.", usage: NO_FORMS,
148
- module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
149
- options: INSTALL_OPTS, run: runInstallVerb,
296
+ verb: "install",
297
+ summary: "Install projectstore's surfaces for a harness, behind a preview.",
298
+ usage: NO_FORMS,
299
+ module: "./install-harness.mjs",
300
+ wraps: "module",
301
+ how: "import",
302
+ output: "envelope",
303
+ writes: true,
304
+ requiresBinding: false,
305
+ mcp: Object.freeze([]),
306
+ options: INSTALL_OPTS,
307
+ run: runInstallVerb,
150
308
  }),
151
309
  Object.freeze({
152
- verb: "uninstall", summary: "Remove what install wrote, and only that.", usage: NO_FORMS,
153
- module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
154
- options: UNINSTALL_OPTS, run: runInstallVerb,
310
+ verb: "uninstall",
311
+ summary: "Remove what install wrote, and only that.",
312
+ usage: NO_FORMS,
313
+ module: "./install-harness.mjs",
314
+ wraps: "module",
315
+ how: "import",
316
+ output: "envelope",
317
+ writes: true,
318
+ requiresBinding: false,
319
+ mcp: Object.freeze([]),
320
+ options: UNINSTALL_OPTS,
321
+ run: runInstallVerb,
155
322
  }),
156
323
  Object.freeze({
157
- verb: "upgrade", summary: "After an update, refresh what install wrote and leave the rest.", usage: NO_FORMS,
158
- module: "./install-harness.mjs", wraps: "module", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
159
- options: INSTALL_OPTS, run: runInstallVerb,
324
+ verb: "upgrade",
325
+ summary: "After an update, refresh what install wrote and leave the rest.",
326
+ usage: NO_FORMS,
327
+ module: "./install-harness.mjs",
328
+ wraps: "module",
329
+ how: "import",
330
+ output: "envelope",
331
+ writes: true,
332
+ requiresBinding: false,
333
+ mcp: Object.freeze([]),
334
+ options: INSTALL_OPTS,
335
+ run: runInstallVerb,
160
336
  }),
161
337
  Object.freeze({
162
- verb: "status", summary: "What is bound, what is in progress, and whether views are fresh.", usage: NO_FORMS,
163
- module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze(["status"]),
164
- options: READ_JSON, run: runRead("status"),
338
+ verb: "status",
339
+ summary: "What is bound, what is in progress, and whether views are fresh.",
340
+ usage: NO_FORMS,
341
+ module: "./query.mjs",
342
+ wraps: "new",
343
+ how: "import",
344
+ output: "envelope",
345
+ writes: false,
346
+ requiresBinding: false,
347
+ mcp: Object.freeze(["status"]),
348
+ options: READ_JSON,
349
+ run: runRead("status"),
165
350
  }),
166
351
  Object.freeze({
167
- verb: "orientation", summary: "The SessionStart skeleton and the facts behind it.", usage: NO_FORMS,
168
- module: "./query.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["orientation"]),
169
- options: READ_JSON, run: runRead("orientation"),
352
+ verb: "orientation",
353
+ summary: "The SessionStart skeleton and the facts behind it.",
354
+ usage: NO_FORMS,
355
+ module: "./query.mjs",
356
+ wraps: "module",
357
+ how: "import",
358
+ output: "envelope",
359
+ writes: false,
360
+ requiresBinding: true,
361
+ mcp: Object.freeze(["orientation"]),
362
+ options: READ_JSON,
363
+ run: runRead("orientation"),
170
364
  }),
171
365
  Object.freeze({
172
- verb: "search", summary: "Find a phrase in the vault's artifacts, literal and bounded.", usage: Object.freeze(["<phrase…>"]),
173
- module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["search"]),
174
- options: [opt("kind", "<type>", "only artifacts of this kind", true), opt("status", "<status>", "only artifacts in this status"), opt("limit", "<n>", `at most n matches (default ${SEARCH_DEFAULT_LIMIT}, hard cap 100)`), opt("include-derived", false, "search the derived views too"), opt("case-sensitive", false, "match case"), JSON_OPT],
366
+ verb: "search",
367
+ summary: "Find a phrase in the vault's artifacts, literal and bounded.",
368
+ usage: Object.freeze(["<phrase…>"]),
369
+ module: "./query.mjs",
370
+ wraps: "new",
371
+ how: "import",
372
+ output: "envelope",
373
+ writes: false,
374
+ requiresBinding: true,
375
+ mcp: Object.freeze(["search"]),
376
+ options: [
377
+ opt("kind", "<type>", "only artifacts of this kind", true),
378
+ opt("status", "<status>", "only artifacts in this status"),
379
+ opt("limit", "<n>", `at most n matches (default ${SEARCH_DEFAULT_LIMIT}, hard cap 100)`),
380
+ opt("include-derived", false, "search the derived views too"),
381
+ opt("case-sensitive", false, "match case"),
382
+ JSON_OPT,
383
+ ],
175
384
  run: runRead("search"),
176
385
  }),
177
386
  Object.freeze({
178
- verb: "show", summary: "One artifact's frontmatter; its body or a section on request.", usage: Object.freeze(["<path>"]),
179
- module: "./query.mjs", wraps: "module", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["get_artifact"]),
180
- options: [opt("body", false, "include the body"), opt("section", "<id>", "one section by its registry id (description, acceptance, …)"), JSON_OPT],
387
+ verb: "show",
388
+ summary: "One artifact's frontmatter; its body or a section on request.",
389
+ usage: Object.freeze(["<path>"]),
390
+ module: "./query.mjs",
391
+ wraps: "module",
392
+ how: "import",
393
+ output: "envelope",
394
+ writes: false,
395
+ requiresBinding: true,
396
+ mcp: Object.freeze(["get_artifact"]),
397
+ options: [
398
+ opt("body", false, "include the body"),
399
+ opt("section", "<id>", "one section by its registry id (description, acceptance, …)"),
400
+ JSON_OPT,
401
+ ],
181
402
  run: runRead("show"),
182
403
  }),
183
404
  Object.freeze({
184
- verb: "graph", summary: "The live link graph by vault path.", usage: Object.freeze(["neighbors <path>", "lineage <path>"]),
185
- module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["neighbors", "lineage"]),
186
- options: [opt("kind", "<edge-kind>", "only edges of this kind (lineage: one of its four)", true), opt("direction", DIRECTIONS.join("|"), "neighbors: which edges"), opt("depth", "<n>", `lineage: how far (default ${LINEAGE_DEFAULT_DEPTH})`), opt("limit", "<n>", `neighbors: cap per direction (at most ${GRAPH_EDGE_CAP})`), JSON_OPT],
405
+ verb: "graph",
406
+ summary: "The live link graph by vault path.",
407
+ usage: Object.freeze(["neighbors <path>", "lineage <path>"]),
408
+ module: "./query.mjs",
409
+ wraps: "new",
410
+ how: "import",
411
+ output: "envelope",
412
+ writes: false,
413
+ requiresBinding: true,
414
+ mcp: Object.freeze(["neighbors", "lineage"]),
415
+ options: [
416
+ opt("kind", "<edge-kind>", "only edges of this kind (lineage: one of its four)", true),
417
+ opt("direction", DIRECTIONS.join("|"), "neighbors: which edges"),
418
+ opt("depth", "<n>", `lineage: how far (default ${LINEAGE_DEFAULT_DEPTH})`),
419
+ opt("limit", "<n>", `neighbors: cap per direction (at most ${GRAPH_EDGE_CAP})`),
420
+ JSON_OPT,
421
+ ],
187
422
  run: runGraph,
188
423
  }),
189
424
  Object.freeze({
190
- verb: "codemap", summary: "The code an epic or artifact maps to, or a path's artifacts.", usage: Object.freeze(["--for <selector>"]),
191
- module: "./query.mjs", wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: true, mcp: Object.freeze(["code_refs"]),
192
- options: [opt("for", "<selector>", "an epic id, an artifact, or a repo path"), opt("reverse", false, "read the selector as a path even if it names an artifact"), JSON_OPT],
425
+ verb: "codemap",
426
+ summary: "The code an epic or artifact maps to, or a path's artifacts.",
427
+ usage: Object.freeze(["--for <selector>"]),
428
+ module: "./query.mjs",
429
+ wraps: "new",
430
+ how: "import",
431
+ output: "envelope",
432
+ writes: false,
433
+ requiresBinding: true,
434
+ mcp: Object.freeze(["code_refs"]),
435
+ options: [
436
+ opt("for", "<selector>", "an epic id, an artifact, or a repo path"),
437
+ opt("reverse", false, "read the selector as a path even if it names an artifact"),
438
+ JSON_OPT,
439
+ ],
193
440
  run: runCodemap,
194
441
  }),
195
442
  Object.freeze({
196
- verb: "agents", summary: "The harness overlay's agents block: the model each agent runs.", usage: Object.freeze(["model <name>", "show", "configure"]),
197
- module: "./lib.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
198
- options: [opt("harness", "<id>", "configure: the overlay to write — and, non-interactively, the confirmation; there is no --yes", true), opt("default", "<model>", "configure: agents.default.model (pins the clerk to sonnet unless --agent clerk=… says otherwise; an empty model clears it)"), opt("agent", "<name>=<model>", "configure: agents.per_agent.<name>.model (an empty model removes the key)", true), opt("reset", false, "configure: empty the agents block first; --default and --agent given with it apply on top"), JSON_OPT],
443
+ verb: "agents",
444
+ summary: "The harness overlay's agents block: the model each agent runs.",
445
+ usage: Object.freeze(["model <name>", "show", "configure"]),
446
+ module: "./lib.mjs",
447
+ wraps: "new",
448
+ how: "import",
449
+ output: "envelope",
450
+ writes: true,
451
+ requiresBinding: false,
452
+ mcp: Object.freeze([]),
453
+ options: [
454
+ opt(
455
+ "harness",
456
+ "<id>",
457
+ "configure: the overlay to write — and, non-interactively, the confirmation; there is no --yes",
458
+ true,
459
+ ),
460
+ opt(
461
+ "default",
462
+ "<model>",
463
+ "configure: agents.default.model (pins the clerk to sonnet unless --agent clerk=… says otherwise; an empty model clears it)",
464
+ ),
465
+ opt("agent", "<name>=<model>", "configure: agents.per_agent.<name>.model (an empty model removes the key)", true),
466
+ opt("reset", false, "configure: empty the agents block first; --default and --agent given with it apply on top"),
467
+ JSON_OPT,
468
+ ],
199
469
  run: runAgents,
200
470
  }),
201
471
  Object.freeze({
202
- verb: "band", summary: "The band above the prompt: switch it, pin an epic, ask why.", usage: Object.freeze(["on", "off", "status", "epic <ID>"]),
203
- module: "./band.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: true, mcp: Object.freeze([]),
472
+ verb: "band",
473
+ summary: "The band above the prompt: switch it, pin an epic, ask why.",
474
+ usage: Object.freeze(["on", "off", "status", "epic <ID>"]),
475
+ module: "./band.mjs",
476
+ wraps: "new",
477
+ how: "import",
478
+ output: "envelope",
479
+ writes: true,
480
+ requiresBinding: true,
481
+ mcp: Object.freeze([]),
204
482
  options: [JSON_OPT],
205
483
  run: runBand,
206
484
  }),
207
485
  Object.freeze({
208
- verb: "bind", summary: "Bind this project to an existing vault; naming it confirms.", usage: Object.freeze(["<vault>"]),
209
- module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
210
- options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "point an already bound project at another vault; every other setting is kept"), JSON_OPT],
486
+ verb: "session",
487
+ summary: "Refresh this session's current story from its file.",
488
+ usage: Object.freeze(["refresh <path>"]),
489
+ module: "./lib.mjs",
490
+ wraps: "new",
491
+ how: "import",
492
+ output: "envelope",
493
+ writes: true,
494
+ requiresBinding: true,
495
+ mcp: Object.freeze([]),
496
+ options: [JSON_OPT],
497
+ run: runSession,
498
+ }),
499
+ Object.freeze({
500
+ verb: "bind",
501
+ summary: "Bind this project to an existing vault; naming it confirms.",
502
+ usage: Object.freeze(["<vault>"]),
503
+ module: "./binding.mjs",
504
+ wraps: "new",
505
+ how: "import",
506
+ output: "envelope",
507
+ writes: true,
508
+ requiresBinding: false,
509
+ mcp: Object.freeze([]),
510
+ options: [
511
+ opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
512
+ opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
513
+ opt("rebind", false, "point an already bound project at another vault; every other setting is kept"),
514
+ JSON_OPT,
515
+ ],
211
516
  run: runBind(false),
212
517
  }),
213
518
  Object.freeze({
214
- verb: "init", summary: "Create a vault (folder, git repository, READMEs) and bind to it.", usage: Object.freeze(["<vault>"]),
215
- module: "./binding.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: false, mcp: Object.freeze([]),
216
- options: [opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`), opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`), opt("rebind", false, "an already bound project: create the new vault and point the project at it; every other setting is kept"), JSON_OPT],
519
+ verb: "init",
520
+ summary: "Create a vault (folder, git repository, READMEs) and bind to it.",
521
+ usage: Object.freeze(["<vault>"]),
522
+ module: "./binding.mjs",
523
+ wraps: "new",
524
+ how: "import",
525
+ output: "envelope",
526
+ writes: true,
527
+ requiresBinding: false,
528
+ mcp: Object.freeze([]),
529
+ options: [
530
+ opt("layout", "<name>", `the layout (default ${DEFAULT_LAYOUT})`),
531
+ opt("language", "<code>", `the template language (default ${DEFAULT_LANGUAGE})`),
532
+ opt(
533
+ "rebind",
534
+ false,
535
+ "an already bound project: create the new vault and point the project at it; every other setting is kept",
536
+ ),
537
+ JSON_OPT,
538
+ ],
217
539
  run: runBind(true),
218
540
  }),
219
541
  Object.freeze({
220
- verb: "scaffold", summary: "Create the vault's missing folders and READMEs; rewrite nothing.", usage: NO_FORMS,
221
- module: "./scaffold.mjs", wraps: "new", how: "import", output: "envelope", writes: true, requiresBinding: true, mcp: Object.freeze([]),
222
- options: [opt("write", false, "create them (asks at a terminal; without one, --write is the confirmation)"), JSON_OPT],
542
+ verb: "scaffold",
543
+ summary: "Create the vault's missing folders and READMEs; rewrite nothing.",
544
+ usage: NO_FORMS,
545
+ module: "./scaffold.mjs",
546
+ wraps: "new",
547
+ how: "import",
548
+ output: "envelope",
549
+ writes: true,
550
+ requiresBinding: true,
551
+ mcp: Object.freeze([]),
552
+ options: [
553
+ opt("write", false, "create them (asks at a terminal; without one, --write is the confirmation)"),
554
+ JSON_OPT,
555
+ ],
223
556
  run: runScaffold,
224
557
  }),
225
558
  Object.freeze({
226
- verb: "mcp", summary: "Serve the read tools over MCP (stdio) for the project you name.", usage: NO_FORMS,
227
- module: "./mcp.mjs", wraps: "new", how: "import", output: "text", writes: false, requiresBinding: false, mcp: Object.freeze([]),
228
- options: [], run: runMcp,
559
+ verb: "mcp",
560
+ summary: "Serve the read tools over MCP (stdio) for the project you name.",
561
+ usage: NO_FORMS,
562
+ module: "./mcp.mjs",
563
+ wraps: "new",
564
+ how: "import",
565
+ output: "text",
566
+ writes: false,
567
+ requiresBinding: false,
568
+ mcp: Object.freeze([]),
569
+ options: [],
570
+ run: runMcp,
229
571
  }),
230
572
  Object.freeze({
231
- verb: "version", summary: "Print the package version (also --version).", usage: NO_FORMS,
232
- module: null, wraps: "new", how: "import", output: "envelope", writes: false, requiresBinding: false, mcp: Object.freeze([]),
233
- options: [JSON_OPT], run: runVersion,
573
+ verb: "version",
574
+ summary: "Print the package version (also --version).",
575
+ usage: NO_FORMS,
576
+ module: null,
577
+ wraps: "new",
578
+ how: "import",
579
+ output: "envelope",
580
+ writes: false,
581
+ requiresBinding: false,
582
+ mcp: Object.freeze([]),
583
+ options: [JSON_OPT],
584
+ run: runVersion,
234
585
  }),
235
586
  ]);
236
587
 
@@ -244,11 +595,11 @@ export const PLANNED_VERBS = Object.freeze([]);
244
595
  // reading the vault, keeping it consistent. Every verb not listed here is
245
596
  // ADVANCED, one line of names in table order — so a new row is never hidden
246
597
  // by this table, and init and scaffold, which the contract names in no group,
247
- // are there too. A group lists only the verbs this run's table has. Once
248
- // setup exists, START becomes setup and uninstall, and install and bind move
249
- // to ADVANCED.
598
+ // are there too. A group lists only the verbs this run's table has. With
599
+ // setup, START is setup and uninstall, and install and bind are ADVANCED's
600
+ // (contract 10; the setup story, rule 1).
250
601
  const HELP_GROUPS = [
251
- ["START", ["install", "bind", "uninstall"]],
602
+ ["START", ["setup", "uninstall"]],
252
603
  ["VAULT", ["status", "search", "show", "graph", "codemap"]],
253
604
  ["CHECK AND REPAIR", ["doctor", "reconcile", "upgrade"]],
254
605
  ];
@@ -259,20 +610,41 @@ const HELP_GROUPS = [
259
610
  const VERB_COLUMN = 12;
260
611
  export const SUMMARY_MAX = 80 - (2 + VERB_COLUMN + 1);
261
612
 
262
- // Examples per verb. `{cmd}` is how this run was invoked (a shell's own name
263
- // when PROJECTSTORE_SHELL says so, else the core — a shell passes the read
264
- // verbs through, so its name serves them too), `{h}` the harness argument the
265
- // core needs and a shell fixes. A `{core}` example is the core's alone: under
266
- // a shell `{h}` is empty, so it would repeat the line above it and say what
267
- // the shell's own run does not. `{switch.surface}`, `{switch.host}` and
268
- // `{switch.doctor}` name the harness whose project can switch channels — the
269
- // one with a host plugin registration — from its manifest: its surface key,
270
- // its display name and its doctor form (switchHarness, below); an example
271
- // naming them is left out when no manifest has one.
613
+ // Examples per verb. They are what to run next, so they are the core's form
614
+ // through a shell too (the story "User-facing text says where to run and
615
+ // names no project path and no shell", rule 6): `{cmd}` is the core through
616
+ // npx, and `{h}` the harness argument — ` --harness <id>` from the core, and
617
+ // through a shell ` --harness <the shell's own harness>` (shellHarnessId),
618
+ // never a session's identity. `{fixed}` is that argument through a shell
619
+ // only: the shell inserts --harness for every verb that declares it
620
+ // (packaging/shells.mjs, HARNESS_VERBS), so an example the core runs bare —
621
+ // setup, which asks; agents show, which reads the session's overlay — names
622
+ // the shell's harness through it and stays bare from the core. A `{core}`
623
+ // example is the core's alone: a shell's help leaves it out, since it says
624
+ // what the shell's own harness does not (every harness, the way back from a
625
+ // switch). `{switch.surface}`,
626
+ // `{switch.host}` and `{switch.doctor}` name the harness whose project can
627
+ // switch channels — the one with a host plugin registration — from its
628
+ // manifest: its surface key, its display name and its doctor form
629
+ // (switchHarness, below); an example naming them is left out when no
630
+ // manifest has one.
272
631
  const EXAMPLES = {
632
+ setup: [
633
+ "{cmd} setup{fixed}",
634
+ "{cmd} setup{fixed} --dry-run # the questions and the plan; nothing is written or fetched",
635
+ "{cmd} setup{h} --vault ~/vaults/my-project # without a terminal: --harness confirms, --vault names the vault",
636
+ ],
273
637
  install: ["{cmd} install{h}", "{cmd} plan{h} # the same plan; nothing is written"],
274
- upgrade: ["{cmd}@<version> upgrade{h} # the version you name is the version that runs", "{cmd} upgrade{h} --verbose"],
275
- uninstall: ["{core}{cmd} uninstall # every harness this project uses", "{cmd} uninstall{h}", "{cmd} uninstall{h} --surface statusline", "{core}{cmd} uninstall{h} --surface {switch.surface} # back to the git marketplace after a switch, then restart {switch.host} and run {switch.doctor} in it"],
638
+ upgrade: [
639
+ "{cmd}@<version> upgrade{h} # the version you name is the version that runs",
640
+ "{cmd} upgrade{h} --verbose",
641
+ ],
642
+ uninstall: [
643
+ "{core}{cmd} uninstall # every harness this project uses",
644
+ "{cmd} uninstall{h}",
645
+ "{cmd} uninstall{h} --surface statusline",
646
+ "{core}{cmd} uninstall{h} --surface {switch.surface} # back to the git marketplace after a switch, then restart {switch.host} and run {switch.doctor} in it",
647
+ ],
276
648
  plan: ["{cmd} plan{h}", "{cmd} plan{h} --json # one envelope, for scripts and agents"],
277
649
  status: ["{cmd} status", "{cmd} status --json"],
278
650
  orientation: ["{cmd} orientation --json"],
@@ -285,13 +657,20 @@ const EXAMPLES = {
285
657
  bind: ["{cmd} bind ~/vaults/my-project", "{cmd} bind ~/vaults/other --rebind"],
286
658
  init: ["{cmd} init ~/vaults/new-project --language ru"],
287
659
  scaffold: ["{cmd} scaffold # what would be created; nothing is written", "{cmd} scaffold --write"],
288
- agents: ["{cmd} agents show", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
289
- band: ["{cmd} band status", "{cmd} band off", "{cmd} band epic PS-CORE # a session with no epic of its own shows this one"],
660
+ agents: ["{cmd} agents show{fixed}", "{cmd} agents configure{h} --default opus --agent clerk=sonnet"],
661
+ band: [
662
+ "{cmd} band status",
663
+ "{cmd} band off",
664
+ "{cmd} band epic PS-CORE # a session with no epic of its own shows this one",
665
+ ],
666
+ session: ["{cmd} session refresh epics/PS-CORE/stories/story-x.md --json # after a write no hook saw"],
290
667
  mcp: ['{cmd} mcp --project "$PWD"'],
291
668
  };
292
669
 
293
- // How this run was invoked is invokedAs (harness.mjs), the one spelling,
294
- // which status's and bind's next commands print too.
670
+ // How this run was invoked is invokedAs (harness.mjs), the one spelling of
671
+ // the binary that ran: the help's first line, its Usage lines, the
672
+ // fixed-harness line and the usage errors. What a person runs next is the
673
+ // core's (CORE_NPX, packageCommand), as status's and bind's Next lines are.
295
674
 
296
675
  // The harness whose project can switch channels (the converge story's rule
297
676
  // 4): the one whose manifest declares a host plugin registration. Its surface
@@ -299,8 +678,15 @@ const EXAMPLES = {
299
678
  // harness.mjs, never a literal; null when no manifest has one.
300
679
  function switchHarness() {
301
680
  for (const m of loadHarnesses().values()) {
302
- const row = Object.entries(m.surfaces || {}).find(([k, s]) => !k.startsWith("_") && s?.format === "host-plugin-registration");
303
- if (row) return { "switch.surface": row[0], "switch.host": m.display_name, "switch.doctor": invocation(m, "doctor", { args: "--fix" }) };
681
+ const row = Object.entries(m.surfaces || {}).find(
682
+ ([k, s]) => !k.startsWith("_") && s?.format === "host-plugin-registration",
683
+ );
684
+ if (row)
685
+ return {
686
+ "switch.surface": row[0],
687
+ "switch.host": m.display_name,
688
+ "switch.doctor": invocation(m, "doctor", { args: "--fix" }),
689
+ };
304
690
  }
305
691
  return null;
306
692
  }
@@ -310,7 +696,10 @@ function switchHarness() {
310
696
  // it. On a live terminal a description wraps with its column as the hanging
311
697
  // indent; anywhere else it stays one line, whole, for a reader that greps.
312
698
  function optionLines(options, c = term.PLAIN) {
313
- const rows = options.map((o) => [term.glyphText(c, `--${o.name}${o.arg ? " " + o.arg : ""}`), term.glyphText(c, `${o.summary}${o.multiple ? " (repeatable)" : ""}`)]);
699
+ const rows = options.map((o) => [
700
+ term.glyphText(c, `--${o.name}${o.arg ? " " + o.arg : ""}`),
701
+ term.glyphText(c, `${o.summary}${o.multiple ? " (repeatable)" : ""}`),
702
+ ]);
314
703
  const w = Math.min(28, Math.max(0, ...rows.map(([l]) => l.length)));
315
704
  const pad = " ".repeat(2 + w + 2);
316
705
  const text = (r) => term.wrap(r, c.live ? c.width : 0, pad);
@@ -321,7 +710,8 @@ const EXIT_CODES = "0 ok, 1 findings or a refusal, 2 usage, 3 not bound, 130 can
321
710
  // The facts that close a help screen (`Harnesses`, `Exit codes`): the key in
322
711
  // the explanation role, its value at one column for both.
323
712
  const FACT_KEY = "Exit codes".length;
324
- const fact = (c, key, value) => `${term.painter(c)("explanation", key)}${" ".repeat(FACT_KEY - key.length + 2)}${value}`;
713
+ const fact = (c, key, value) =>
714
+ `${term.painter(c)("explanation", key)}${" ".repeat(FACT_KEY - key.length + 2)}${value}`;
325
715
 
326
716
  // What the bin is, in the help's badge: one line with the name and version,
327
717
  // short enough that a prerelease version (0.30.0-rc.12) still fits 80 columns.
@@ -357,7 +747,10 @@ export function usage(env = process.env, verbs = VERBS, { caps: c = term.PLAIN,
357
747
  term.badge(c, "projectstore", [version, PURPOSE]),
358
748
  "",
359
749
  term.heading(c, "Usage"),
360
- ...term.rows(c, [{ target: `${cmd} <verb> [options]`, note: "run it inside your code project" }, { target: `${cmd} <verb> --help`, note: "one verb's options and examples" }]),
750
+ ...term.rows(c, [
751
+ { target: `${cmd} <verb> [options]`, note: "run it inside your code project" },
752
+ { target: `${cmd} <verb> --help`, note: "one verb's options and examples" },
753
+ ]),
361
754
  ];
362
755
  const listed = new Set(HELP_GROUPS.flatMap(([, names]) => names));
363
756
  for (const [title, names] of HELP_GROUPS) {
@@ -369,24 +762,48 @@ export function usage(env = process.env, verbs = VERBS, { caps: c = term.PLAIN,
369
762
  lines.push(lead + term.wrap(term.glyphText(c, v.summary), width, " ".repeat(term.plain(lead).length)));
370
763
  }
371
764
  }
765
+ // ADVANCED is one line of verbs, broken at 80 columns in every mode: with
766
+ // install and bind among them (contract 10, once setup exists) it no longer
767
+ // fits one row, and a pipe gets the same lines a terminal does.
372
768
  const advanced = verbs.filter((v) => !listed.has(v.verb));
373
- if (advanced.length) lines.push("", term.heading(c, "ADVANCED"), " " + term.wrap(advanced.map((v) => paint("action", v.verb)).join(` ${term.icon(c, "dot")} `), width, " "));
769
+ if (advanced.length)
770
+ lines.push(
771
+ "",
772
+ term.heading(c, "ADVANCED"),
773
+ " " + term.wrap(advanced.map((v) => paint("action", v.verb)).join(` ${term.icon(c, "dot")} `), 80, " "),
774
+ );
374
775
  if (PLANNED_VERBS.length) lines.push("", `Planned: ${PLANNED_VERBS.map((v) => `${v.verb} (${v.lands})`).join(", ")}`);
375
- // A command is offered only for a verb this table has.
376
- const h = shell ? "" : " --harness <id>";
776
+ // A command is offered only for a verb this table has. The examples and the
777
+ // tips' plan are what to run next: the core's form, through a shell with
778
+ // the shell's own harness (rule 6 of the story "User-facing text says
779
+ // where to run…"); setup names it only through a shell, as EXAMPLES'
780
+ // `{fixed}` does.
781
+ const h = ` --harness ${(shell && shellHarnessId(env)) || "<id>"}`;
377
782
  const has = (verb) => verbs.some((v) => v.verb === verb);
378
- const examples = [["install", `${cmd} install${h}`], ["status", `${cmd} status`], ["doctor", `${cmd} doctor`]].filter(([v]) => has(v)).map(([, x]) => x);
783
+ const examples = [
784
+ ["setup", `${CORE_NPX} setup${shell ? h : ""}`],
785
+ ["status", `${CORE_NPX} status`],
786
+ ["doctor", `${CORE_NPX} doctor`],
787
+ ]
788
+ .filter(([v]) => has(v))
789
+ .map(([, x]) => x);
379
790
  lines.push("", term.heading(c, "Options"), ...optionLines(EVERY_VERB, c));
380
791
  if (examples.length) lines.push("", term.heading(c, "Examples"), ...term.commandBlock(c, examples));
381
792
  lines.push(
382
793
  "",
383
794
  term.heading(c, "Tips"),
384
- prose("install, upgrade and uninstall show their plan, then ask at a terminal."),
385
- prose(`Without a terminal, ${shell ? "the verb is its own" : "--harness is the"} confirmation ${term.icon(c, "dash")} there is no --yes.`),
795
+ prose("setup, install, upgrade and uninstall show a plan, then ask at a terminal."),
796
+ prose(
797
+ `Without a terminal, ${shell ? "the verb is its own" : "--harness is the"} confirmation ${term.icon(c, "dash")} there is no --yes.`,
798
+ ),
386
799
  prose("Unattended in a terminal: pass --json, set CI=1, or give stdin </dev/null."),
387
800
  prose("--verbose on those verbs adds every row's reasoning and the host's notes."),
388
801
  );
389
- if (has("plan")) lines.push(prose("plan prints the same plan and writes nothing:"), ...term.commandBlock(c, [`${cmd} plan${h}`]));
802
+ if (has("plan"))
803
+ lines.push(
804
+ prose("plan prints the same plan and writes nothing:"),
805
+ ...term.commandBlock(c, [`${CORE_NPX} plan${h}`]),
806
+ );
390
807
  lines.push("", fact(c, "Harnesses", harnessIds().join(", ")), fact(c, "Exit codes", EXIT_CODES));
391
808
  return lines.join("\n");
392
809
  }
@@ -404,14 +821,26 @@ export function verbHelp(row, env = process.env, { caps: c = term.PLAIN } = {})
404
821
  const prose = (text) => " " + term.wrap(term.glyphText(c, text), c.live ? c.width : 0, " ");
405
822
  const head = `${paint("emphasis", `${cmd} ${row.verb}`)} ${term.icon(c, "dash")} ${term.glyphText(c, row.summary)}`;
406
823
  const lines = [term.wrap(head, c.live ? c.width : 0, " "), "", term.heading(c, "Usage")];
407
- for (const f of row.usage.length ? row.usage : [""]) lines.push(` ${cmd} ${row.verb}${f ? " " + term.glyphText(c, f) : ""}${opts.length ? " [options]" : ""}`);
824
+ for (const f of row.usage.length ? row.usage : [""])
825
+ lines.push(` ${cmd} ${row.verb}${f ? " " + term.glyphText(c, f) : ""}${opts.length ? " [options]" : ""}`);
408
826
  if (NOTES[row.verb]) lines.push("", prose(NOTES[row.verb]));
409
827
  if (opts.length) lines.push("", term.heading(c, "Options"), ...optionLines(opts, c));
410
- if (fixed) lines.push("", term.wrap(`${shell} names the harness itself: without a terminal, the verb is its own confirmation.`, c.live ? c.width : 0, " "));
828
+ if (fixed)
829
+ lines.push(
830
+ "",
831
+ term.wrap(
832
+ `${shell} names the harness itself: without a terminal, the verb is its own confirmation.`,
833
+ c.live ? c.width : 0,
834
+ " ",
835
+ ),
836
+ );
411
837
  else if (row.options.some((o) => o.name === "harness")) lines.push("", fact(c, "Harnesses", harnessIds().join(", ")));
412
838
  const sw = switchHarness();
413
- const ex = (EXAMPLES[row.verb] || []).filter((e) => !(fixed && e.startsWith("{core}")) && (sw || !e.includes("{switch."))).map((e) => e.replace(/^\{core\}/, "").replace(/\{(switch\.\w+)\}/g, (_, k) => sw[k]));
414
- const h = fixed ? "" : " --harness <id>";
839
+ const ex = (EXAMPLES[row.verb] || [])
840
+ .filter((e) => !(fixed && e.startsWith("{core}")) && (sw || !e.includes("{switch.")))
841
+ .map((e) => e.replace(/^\{core\}/, "").replace(/\{(switch\.\w+)\}/g, (_, k) => sw[k]));
842
+ // What to run next is the core's form: through a shell, its own harness.
843
+ const h = ` --harness ${(fixed && shellHarnessId(env)) || "<id>"}`;
415
844
  // Each example command stands alone on its line, after a blank one
416
845
  // (contract 6): nothing after it, so a paste takes exactly the command — a
417
846
  // trailing `# note` broke a paste into a shell that reads no comments. A
@@ -419,13 +848,26 @@ export function verbHelp(row, env = process.env, { caps: c = term.PLAIN } = {})
419
848
  // introduced command does (DONE's tip, doctor's fix, bind's Next);
420
849
  // commands with no note share one block.
421
850
  const blocks = [];
422
- for (const [x, note] of ex.map((e) => e.split("{cmd}").join(cmd).split("{h}").join(h).split(" # "))) {
851
+ const render = (e) =>
852
+ e
853
+ .split("{cmd}")
854
+ .join(CORE_NPX)
855
+ .split("{h}")
856
+ .join(h)
857
+ .split("{fixed}")
858
+ .join(fixed ? h : "");
859
+ for (const [x, note] of ex.map((e) => render(e).split(" # "))) {
423
860
  const last = blocks.at(-1);
424
861
  if (!note && last && !last.note) last.cmds.push(x);
425
862
  else blocks.push({ note: note ? `${note[0].toUpperCase()}${note.slice(1)}:` : null, cmds: [x] });
426
863
  }
427
864
  if (blocks.length) lines.push("", term.heading(c, "Examples"));
428
- for (const b of blocks) lines.push("", ...(b.note ? [prose(paint("explanation", b.note)), ""] : []), ...b.cmds.map((x) => ` ${paint("action", x)}`));
865
+ for (const b of blocks)
866
+ lines.push(
867
+ "",
868
+ ...(b.note ? [prose(paint("explanation", b.note)), ""] : []),
869
+ ...b.cmds.map((x) => ` ${paint("action", x)}`),
870
+ );
429
871
  lines.push("", fact(c, "Exit codes", EXIT_CODES));
430
872
  return lines.join("\n");
431
873
  }
@@ -435,13 +877,19 @@ export function nearest(word, names) {
435
877
  const d = (a, b) => {
436
878
  const m = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
437
879
  for (let j = 1; j <= b.length; j++) m[0][j] = j;
438
- for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
880
+ for (let i = 1; i <= a.length; i++)
881
+ for (let j = 1; j <= b.length; j++)
882
+ m[i][j] = Math.min(m[i - 1][j] + 1, m[i][j - 1] + 1, m[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
439
883
  return m[a.length][b.length];
440
884
  };
441
- let best = null, score = 3;
885
+ let best = null,
886
+ score = 3;
442
887
  for (const n of names) {
443
888
  const k = n.startsWith(word) && word.length >= 3 ? 0 : d(word, n);
444
- if (k < score) { best = n; score = k; }
889
+ if (k < score) {
890
+ best = n;
891
+ score = k;
892
+ }
445
893
  }
446
894
  return best;
447
895
  }
@@ -469,7 +917,10 @@ function scanArgv(argv, verbs) {
469
917
  for (let i = 0; i < argv.length; i++) {
470
918
  const a = argv[i];
471
919
  if (a === "--") {
472
- if (scan.at === -1 && i + 1 < argv.length) { scan.verb = argv[i + 1]; scan.at = i + 1; }
920
+ if (scan.at === -1 && i + 1 < argv.length) {
921
+ scan.verb = argv[i + 1];
922
+ scan.at = i + 1;
923
+ }
473
924
  break;
474
925
  }
475
926
  if (a.length > 1 && a.startsWith("-")) {
@@ -484,12 +935,22 @@ function scanArgv(argv, verbs) {
484
935
  const name = a.startsWith("--") ? a.slice(2).split("=")[0] : null;
485
936
  const inline = a.includes("=");
486
937
  if (!inline && (name === "project" || (row && takesValue(row, name)))) i++;
487
- else if (scan.at === -1 && !GLOBAL_OPTS.some((o) => o.name === name)) scan.misplaced.push({ name, raw: a, at: i, inline, value: undefined });
938
+ else if (scan.at === -1 && !GLOBAL_OPTS.some((o) => o.name === name))
939
+ scan.misplaced.push({ name, raw: a, at: i, inline, value: undefined });
488
940
  continue;
489
941
  }
490
942
  if (scan.at !== -1) continue;
491
943
  const prev = scan.misplaced[scan.misplaced.length - 1];
492
- if (!verbs.some((v) => v.verb === a) && prev && prev.at === i - 1 && !prev.inline && verbs.some((v) => takesValue(v, prev.name))) { prev.value = a; continue; }
944
+ if (
945
+ !verbs.some((v) => v.verb === a) &&
946
+ prev &&
947
+ prev.at === i - 1 &&
948
+ !prev.inline &&
949
+ verbs.some((v) => takesValue(v, prev.name))
950
+ ) {
951
+ prev.value = a;
952
+ continue;
953
+ }
493
954
  scan.verb = a;
494
955
  scan.at = i;
495
956
  row = verbs.find((v) => v.verb === a) || null;
@@ -504,7 +965,12 @@ const optionsOf = (row) => [...(row ? row.options : []), ...GLOBAL_OPTS];
504
965
  // Rule 2: the row is the parser — arg is the type, multiple the repetition.
505
966
  function parserOptions(row) {
506
967
  const options = {};
507
- for (const o of optionsOf(row)) options[o.name] = { type: o.arg ? "string" : "boolean", ...(o.multiple ? { multiple: true } : {}), ...(SHORT[o.name] ? { short: SHORT[o.name] } : {}) };
968
+ for (const o of optionsOf(row))
969
+ options[o.name] = {
970
+ type: o.arg ? "string" : "boolean",
971
+ ...(o.multiple ? { multiple: true } : {}),
972
+ ...(SHORT[o.name] ? { short: SHORT[o.name] } : {}),
973
+ };
508
974
  return options;
509
975
  }
510
976
 
@@ -527,7 +993,10 @@ function unknownOption(row, raw, verbs) {
527
993
  function parseFailure(e, row, verbs) {
528
994
  const raw = e.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ? /'(-{1,2}[^']+)'/.exec(e.message)?.[1] : null;
529
995
  if (raw) return unknownOption(row, raw, verbs);
530
- const name = e.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE" ? /'(?:-[^-], )?(--[^' ]+)(?: <value>)?'/.exec(e.message)?.[1] : null;
996
+ const name =
997
+ e.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE"
998
+ ? /'(?:-[^-], )?(--[^' ]+)(?: <value>)?'/.exec(e.message)?.[1]
999
+ : null;
531
1000
  const o = name ? optionsOf(row).findLast((x) => `--${x.name}` === name) : null;
532
1001
  if (!o) return e.message;
533
1002
  return o.arg ? `\`--${o.name}\` needs a value: \`--${o.name} ${o.arg}\`` : `\`--${o.name}\` takes no value`;
@@ -570,7 +1039,18 @@ function matchForm(row, args, values) {
570
1039
 
571
1040
  // `verbs` is the table this run answers for: VERBS, or a test's own — the
572
1041
  // scan, the help, the verb lookup and every hint read it, never VERBS behind it.
573
- export async function run(argv, { verbs = VERBS, env = process.env, cwd = process.cwd(), stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, ask = null } = {}) {
1042
+ export async function run(
1043
+ argv,
1044
+ {
1045
+ verbs = VERBS,
1046
+ env = process.env,
1047
+ cwd = process.cwd(),
1048
+ stdin = process.stdin,
1049
+ stdout = process.stdout,
1050
+ stderr = process.stderr,
1051
+ ask = null,
1052
+ } = {},
1053
+ ) {
574
1054
  const scan = scanArgv(argv, verbs);
575
1055
  const row = verbs.find((v) => v.verb === scan.verb) || null;
576
1056
  // A failure before a verb runs still answers in the envelope under --json:
@@ -578,7 +1058,8 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
578
1058
  // verb is the scan's, whichever step failed (rule 7) — never the first
579
1059
  // bare word, which may be --project's value.
580
1060
  const fail = (message, code, { project = null, help = false } = {}) => {
581
- if (scan.json) stdout.write(JSON.stringify(envelope(scan.verb, project, false, { error: message, exit: code }), null, 2) + "\n");
1061
+ if (scan.json)
1062
+ stdout.write(JSON.stringify(envelope(scan.verb, project, false, { error: message, exit: code }), null, 2) + "\n");
582
1063
  stderr.write(message + "\n" + (help ? usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n" : ""));
583
1064
  return code;
584
1065
  };
@@ -608,7 +1089,11 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
608
1089
  // as before — not "unknown verb: codex"; otherwise the globals alone are
609
1090
  // parsed, so a bad value of one (`--project` last) is named.
610
1091
  if (!scan.misplaced.length) {
611
- try { parseArgs({ args: argv, options: parserOptions(null), strict: true, allowPositionals: true }); } catch (e) { return refuse(parseFailure(e, null, verbs)); }
1092
+ try {
1093
+ parseArgs({ args: argv, options: parserOptions(null), strict: true, allowPositionals: true });
1094
+ } catch (e) {
1095
+ return refuse(parseFailure(e, null, verbs));
1096
+ }
612
1097
  }
613
1098
  stderr.write(usage(env, verbs, { caps: term.caps(stderr, env) }) + "\n");
614
1099
  return 2;
@@ -616,9 +1101,18 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
616
1101
  const verb = scan.verb;
617
1102
  if (!row) {
618
1103
  const planned = PLANNED_VERBS.find((v) => v.verb === verb);
619
- const guess = planned ? null : nearest(verb, verbs.map((v) => v.verb));
1104
+ const guess = planned
1105
+ ? null
1106
+ : nearest(
1107
+ verb,
1108
+ verbs.map((v) => v.verb),
1109
+ );
620
1110
  if (guess) return fail(`unknown verb: ${verb} — did you mean ${guess}?\nRun --help for every verb.`, 2);
621
- return fail((planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`), 2, { help: true });
1111
+ return fail(
1112
+ planned ? `${verb} lands with roadmap ${planned.lands}; not in this release.` : `unknown verb: ${verb}`,
1113
+ 2,
1114
+ { help: true },
1115
+ );
622
1116
  }
623
1117
  // Rule 3: only the globals come before the verb (git's model). A verb's
624
1118
  // own option there is shown in its place; one the verb does not declare is
@@ -633,7 +1127,9 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
633
1127
  // The rest of the line follows, so the command shown is the one to run.
634
1128
  const { shell, cmd } = invokedAs(env);
635
1129
  const moved = shell && o.name === "harness" ? [] : [m.raw, ...(o.arg && m.value !== undefined ? [m.value] : [])];
636
- return refuse(`\`--${o.name}\` belongs after the verb: \`${[cmd, verb, ...moved, ...argv.slice(scan.at + 1)].join(" ")}\``);
1130
+ return refuse(
1131
+ `\`--${o.name}\` belongs after the verb: \`${[cmd, verb, ...moved, ...argv.slice(scan.at + 1)].join(" ")}\``,
1132
+ );
637
1133
  }
638
1134
  // Rule 2: one strict parse with this row's options plus the globals, on the
639
1135
  // full argv so a token's index is the user's (rule 4's hints read them).
@@ -648,20 +1144,55 @@ export async function run(argv, { verbs = VERBS, env = process.env, cwd = proces
648
1144
  // hint says how it became one: the flag before it takes no value, or a
649
1145
  // "--" made everything after it an argument.
650
1146
  const args = tokens.filter((t) => t.kind === "positional" && t.index !== scan.at);
651
- const form = matchForm(row, args.map((t) => t.value), values);
1147
+ const form = matchForm(
1148
+ row,
1149
+ args.map((t) => t.value),
1150
+ values,
1151
+ );
652
1152
  if (form && args.length > form.bound) {
653
1153
  const extra = args[form.bound];
654
1154
  const prev = tokens.find((t) => t.index === extra.index - 1);
655
1155
  const end = tokens.find((t) => t.kind === "option-terminator");
656
- const why = prev && prev.kind === "option" && prev.value === undefined ? ` — \`--${prev.name}\` takes no value`
657
- : end && extra.index > end.index ? " — everything after `--` is an argument" : "";
658
- return refuse(`${form.bound ? `\`${verb} ${form.form}\` takes no further argument` : `${verb} takes no argument`} \`${extra.value}\`${why}`);
1156
+ const why =
1157
+ prev && prev.kind === "option" && prev.value === undefined
1158
+ ? ` — \`--${prev.name}\` takes no value`
1159
+ : end && extra.index > end.index
1160
+ ? " — everything after `--` is an argument"
1161
+ : "";
1162
+ return refuse(
1163
+ `${form.bound ? `\`${verb} ${form.form}\` takes no further argument` : `${verb} takes no argument`} \`${extra.value}\`${why}`,
1164
+ );
659
1165
  }
660
1166
  const project = resolveProject({ project: values.project, env, cwd });
661
1167
  const cfg = readConfigAt(project);
662
- if (row.requiresBinding && !cfg) return fail(`${project} is not bound to a vault — run ${commandForm("bind", { args: "<vault>", env })} in a session, or \`projectstore bind <vault>\` (\`projectstore init <vault>\` also creates the vault).`, 3, { project });
1168
+ // Each form alone on its line after the lead, the terminal's in the core's
1169
+ // form, the directory in the lead (the story "User-facing text says where to
1170
+ // run and names no project path and no shell", rules 1, 4 and 6).
1171
+ if (row.requiresBinding && !cfg)
1172
+ return fail(
1173
+ [
1174
+ `${project} is not bound to a vault. Bind it in a session, or ${fromTerminalIn(project)}, where init creates the vault too:`,
1175
+ commandForm("bind", { args: "<vault>", env }),
1176
+ packageCommand([], "bind", { args: "<vault>" }),
1177
+ packageCommand([], "init", { args: "<vault>" }),
1178
+ ].join("\n"),
1179
+ 3,
1180
+ { project },
1181
+ );
663
1182
  try {
664
- return await row.run({ row, values, positionals: positionals.slice(1), cfg, project, env, cwd, stdin, stdout, stderr, ask });
1183
+ return await row.run({
1184
+ row,
1185
+ values,
1186
+ positionals: positionals.slice(1),
1187
+ cfg,
1188
+ project,
1189
+ env,
1190
+ cwd,
1191
+ stdin,
1192
+ stdout,
1193
+ stderr,
1194
+ ask,
1195
+ });
665
1196
  } catch (e) {
666
1197
  // An internal failure is not "findings" (1) — it is 2, with an envelope
667
1198
  // when one was asked for.
@@ -716,8 +1247,16 @@ function runRead(name) {
716
1247
  let result;
717
1248
  if (name === "status") result = op.fn(cfg, { project });
718
1249
  else if (name === "orientation") result = await op.fn(cfg);
719
- else if (name === "search") result = op.fn(cfg, positionals.join(" "), { kinds: values.kind || null, status: values.status ?? null, limit: values.limit, includeDerived: Boolean(values["include-derived"]), caseSensitive: Boolean(values["case-sensitive"]) });
720
- else if (name === "show") result = op.fn(cfg, positionals[0], { body: Boolean(values.body), section: values.section ?? null });
1250
+ else if (name === "search")
1251
+ result = op.fn(cfg, positionals.join(" "), {
1252
+ kinds: values.kind || null,
1253
+ status: values.status ?? null,
1254
+ limit: values.limit,
1255
+ includeDerived: Boolean(values["include-derived"]),
1256
+ caseSensitive: Boolean(values["case-sensitive"]),
1257
+ });
1258
+ else if (name === "show")
1259
+ result = op.fn(cfg, positionals[0], { body: Boolean(values.body), section: values.section ?? null });
721
1260
  return emitRead({ verb: row.verb, ...ctx }, op, result);
722
1261
  } catch (e) {
723
1262
  if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
@@ -758,9 +1297,22 @@ function runBind(init) {
758
1297
  return async (ctx) => {
759
1298
  const { row, values, positionals, project, env, stdout, stderr } = ctx;
760
1299
  const vault = positionals[0];
761
- if (!vault) return usageFail(Object.assign(new Error(`${row.verb} takes the vault path`), { code: "USAGE" }), { verb: row.verb, ...ctx });
1300
+ if (!vault)
1301
+ return usageFail(Object.assign(new Error(`${row.verb} takes the vault path`), { code: "USAGE" }), {
1302
+ verb: row.verb,
1303
+ ...ctx,
1304
+ });
762
1305
  // The author is the caller's to find: the plan reads nothing ambient.
763
- const plan = planBind(project, { vault, layout: values.layout ?? null, language: values.language ?? null, rebind: Boolean(values.rebind), init, author: gitAuthor(project, env), env, root: PACKAGE_ROOT });
1306
+ const plan = planBind(project, {
1307
+ vault,
1308
+ layout: values.layout ?? null,
1309
+ language: values.language ?? null,
1310
+ rebind: Boolean(values.rebind),
1311
+ init,
1312
+ author: gitAuthor(project, env),
1313
+ env,
1314
+ root: PACKAGE_ROOT,
1315
+ });
764
1316
  const usage = plan.refusals.find((r) => r.code === "USAGE");
765
1317
  if (usage) return usageFail(Object.assign(new Error(usage.message), { code: "USAGE" }), { verb: row.verb, ...ctx });
766
1318
  let done = null;
@@ -796,21 +1348,38 @@ async function runScaffold(ctx) {
796
1348
  // readConfigAt hands back the stored path raw: `~`, relative, a trailing slash.
797
1349
  const norm = normaliseVaultPath(cfg.vault_path, project);
798
1350
  const vault = typeof norm === "string" ? norm : null;
1351
+ // Each refusal ends with its command alone on its line, in the core's form,
1352
+ // the directory in the lead (the story "User-facing text says where to
1353
+ // run…", rules 1, 4 and 6). In a session, bind asks before a rebind.
799
1354
  if (!vault) {
800
- const why = `the binding names no usable vault path${norm && norm.error ? ` (${norm.error})` : ""} — rebind with \`projectstore bind <vault> --rebind\``;
1355
+ const why = [
1356
+ `the binding names no usable vault path${norm && norm.error ? ` (${norm.error})` : ""}. Rebind it in a session, or ${fromTerminalIn(project)}:`,
1357
+ commandForm("bind", { args: "<vault>", env }),
1358
+ packageCommand([], "bind", { args: "<vault> --rebind" }),
1359
+ ].join("\n");
801
1360
  return emit(false, { error: why }, why + "\n", 1);
802
1361
  }
803
1362
  if (!existsSync(vault)) {
804
- const why = `${vault} does not exist — the binding names a vault directory that is not there. \`projectstore init ${shellQuote(vault)}\` creates it under this binding: the directory, its git repository and the layout's folders.`;
1363
+ const why = [
1364
+ `${vault} does not exist: the binding names a vault directory that is not there. init creates it under this binding — the directory, its git repository and the layout's folders — ${fromTerminalIn(project)}:`,
1365
+ packageCommand([], "init", { args: shellQuote(vault) }),
1366
+ ].join("\n");
805
1367
  return emit(false, { error: why, vault_path: vault }, why + "\n", 1);
806
1368
  }
807
- const plan = planScaffold(vault, { layout: cfg.layout || DEFAULT_LAYOUT, language: cfg.language || DEFAULT_LANGUAGE, root: PACKAGE_ROOT });
1369
+ const plan = planScaffold(vault, {
1370
+ layout: cfg.layout || DEFAULT_LAYOUT,
1371
+ language: cfg.language || DEFAULT_LANGUAGE,
1372
+ root: PACKAGE_ROOT,
1373
+ });
808
1374
  if (!plan.ok) return emit(false, scaffoldResult(plan), renderScaffoldPlan(plan, null, { env }), 1);
809
- if (!values.write || plan.creates === 0) return emit(true, scaffoldResult(plan), renderScaffoldPlan(plan, null, { env }), 0);
1375
+ if (!values.write || plan.creates === 0)
1376
+ return emit(true, scaffoldResult(plan), renderScaffoldPlan(plan, null, { env, project }), 0);
810
1377
  // An injected `ask` is a terminal, as for install. Otherwise the installer's
811
1378
  // definition of one (TTYs, CI, a host session's markers) is the one
812
1379
  // contract 9 names — imported lazily, as runInstallVerb imports it.
813
- const interactive = !values.json && (ask ? true : (await import("./install-harness.mjs")).isInteractive({ stdin, stdout, env, json: false }));
1380
+ const interactive =
1381
+ !values.json &&
1382
+ (ask ? true : (await import("./install-harness.mjs")).isInteractive({ stdin, stdout, env, json: false }));
814
1383
  if (interactive) {
815
1384
  stdout.write(renderScaffoldPlan(plan, null, { env, asking: true }));
816
1385
  if (!(await term.askApply(plan.creates, { stdin, stdout, ask, caps: term.caps(stdout, env) }))) {
@@ -834,21 +1403,32 @@ async function runAgents(ctx) {
834
1403
  if (!["model", "show", "configure"].includes(sub)) return usage("agents takes model <name>, show, or configure");
835
1404
  // The flag IS the confirmation, so it names exactly one overlay: two are a
836
1405
  // question, not an answer.
837
- if (values.harness && values.harness.length > 1) return usage("--harness names exactly one overlay (it is the confirmation); given twice");
1406
+ if (values.harness && values.harness.length > 1)
1407
+ return usage("--harness names exactly one overlay (it is the confirmation); given twice");
838
1408
  const named = values.harness && values.harness[0];
839
- if (named && !harnessIds().includes(named)) return usage(`unknown harness: ${named} — known: ${harnessIds().join(", ")}`);
1409
+ if (named && !harnessIds().includes(named))
1410
+ return usage(`unknown harness: ${named} — known: ${harnessIds().join(", ")}`);
840
1411
  const harness = named || overlayId(env);
841
1412
  const forConfigure = ["default", "agent", "reset"].filter((o) => values[o] !== undefined);
842
1413
  if (sub !== "configure" && forConfigure.length) return usage(`--${forConfigure[0]} is an option of agents configure`);
843
1414
  // A refusal goes to stderr, as bind's does; --json keeps its envelope on stdout.
844
- const emit = (verb, ok, result, text) => { if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n"); else (ok ? stdout : stderr).write(text); return ok ? 0 : 1; };
1415
+ const emit = (verb, ok, result, text) => {
1416
+ if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
1417
+ else (ok ? stdout : stderr).write(text);
1418
+ return ok ? 0 : 1;
1419
+ };
845
1420
  const cfg = readConfigAt(project);
846
1421
  const roster = layoutRoster(cfg);
847
1422
  if (sub === "model") {
848
1423
  const name = positionals[1];
849
1424
  if (!name) return usage("agents model takes the agent's bare name (critic, planner, …)");
850
1425
  const r = resolveAgentModel(project, name, { harness });
851
- return emit("agents model", true, r, `${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}\n`);
1426
+ return emit(
1427
+ "agents model",
1428
+ true,
1429
+ r,
1430
+ `${name}: ${r.model ? `${r.model} (${r.source}, ${r.overlay})` : "no model configured — the agent's frontmatter decides"}\n`,
1431
+ );
852
1432
  }
853
1433
  const before = readOverlayAt(project, harness);
854
1434
  const inBinding = Boolean(cfg && cfg.agents && typeof cfg.agents === "object");
@@ -856,34 +1436,72 @@ async function runAgents(ctx) {
856
1436
  // Resolved per roster agent (per configured name when no layout loads), so
857
1437
  // a reader reports what would run without restating the two-term rule.
858
1438
  const names = [...new Set([...(roster || []), ...Object.keys(before.agents.per_agent)])].sort();
859
- const resolved = Object.fromEntries(names.map((n) => { const r = resolveAgentModel(project, n, { harness }); return [n, { model: r.model, source: r.source }]; }));
1439
+ const resolved = Object.fromEntries(
1440
+ names.map((n) => {
1441
+ const r = resolveAgentModel(project, n, { harness });
1442
+ return [n, { model: r.model, source: r.source }];
1443
+ }),
1444
+ );
860
1445
  const unknown = roster ? Object.keys(before.agents.per_agent).filter((n) => !roster.includes(n)) : [];
861
- const lines = [`overlay: ${before.path || "(none: no harness detected and none named)"}${before.path && !before.present ? " (absent)" : ""}`];
1446
+ const lines = [
1447
+ `overlay: ${before.path || "(none: no harness detected and none named)"}${before.path && !before.present ? " (absent)" : ""}`,
1448
+ ];
862
1449
  if (before.unparseable) lines.push(" not valid JSON");
863
1450
  if (before.agents.default) lines.push(` default: ${before.agents.default}`);
864
- for (const n of names) lines.push(` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`);
1451
+ for (const n of names)
1452
+ lines.push(
1453
+ ` ${n}: ${resolved[n].model ? `${resolved[n].model} (${resolved[n].source})` : "— (the agent's frontmatter)"}${unknown.includes(n) ? " — not in the roster: nothing runs under this name" : ""}`,
1454
+ );
865
1455
  if (before.rejected.length) lines.push(` ignored (not an overlay key): ${before.rejected.join(", ")}`);
866
1456
  if (inBinding) lines.push(" the binding still carries an agents block — a pre-0.28 leftover; run upgrade");
867
- return emit("agents show", true, { harness, path: before.path, present: before.present, unparseable: before.unparseable, agents: before.agents, resolved, roster, unknown, rejected: before.rejected, agents_in_binding: inBinding }, lines.join("\n") + "\n");
1457
+ return emit(
1458
+ "agents show",
1459
+ true,
1460
+ {
1461
+ harness,
1462
+ path: before.path,
1463
+ present: before.present,
1464
+ unparseable: before.unparseable,
1465
+ agents: before.agents,
1466
+ resolved,
1467
+ roster,
1468
+ unknown,
1469
+ rejected: before.rejected,
1470
+ agents_in_binding: inBinding,
1471
+ },
1472
+ lines.join("\n") + "\n",
1473
+ );
868
1474
  }
869
1475
  // configure — in a project that exists (the install spec, contract 19;
870
1476
  // the layout spec, contract 4): refused before the preview and the gate.
871
1477
  const missingProject = projectDirRefusal(project);
872
1478
  if (missingProject) return emit("agents configure", false, { error: missingProject }, `${missingProject}\n`);
873
1479
  if (!harness) return usage("no harness detected and none named: agents configure names --harness <id>");
874
- if (before.unparseable) return emit("agents configure", false, { error: `${before.path} is not valid JSON; fix or remove it first`, harness, path: before.path }, `${before.path} is not valid JSON; fix or remove it first\n`);
875
- if (!values.reset && values.default === undefined && !(values.agent || []).length) return usage("agents configure takes --default <model>, --agent <name>=<model> (repeatable) or --reset");
1480
+ if (before.unparseable)
1481
+ return emit(
1482
+ "agents configure",
1483
+ false,
1484
+ { error: `${before.path} is not valid JSON; fix or remove it first`, harness, path: before.path },
1485
+ `${before.path} is not valid JSON; fix or remove it first\n`,
1486
+ );
1487
+ if (!values.reset && values.default === undefined && !(values.agent || []).length)
1488
+ return usage("agents configure takes --default <model>, --agent <name>=<model> (repeatable) or --reset");
876
1489
  // --reset empties the block first; --default and --agent then apply on top of
877
1490
  // the emptied block — "reset, then set" is one call, and nothing given is
878
1491
  // silently dropped.
879
1492
  const base = values.reset ? { default: null, per_agent: {} } : before.agents;
880
- const next = { default: values.default !== undefined ? (values.default || null) : base.default, per_agent: { ...base.per_agent } };
1493
+ const next = {
1494
+ default: values.default !== undefined ? values.default || null : base.default,
1495
+ per_agent: { ...base.per_agent },
1496
+ };
881
1497
  for (const spec of values.agent || []) {
882
1498
  const m = /^([a-z][a-z0-9-]*)=(.*)$/.exec(spec);
883
1499
  if (!m) return usage(`--agent takes <name>=<model>, got ${spec}`);
884
1500
  // A name outside the roster would be written, listed, and never run.
885
- if (roster && !roster.includes(m[1])) return usage(`no agent named ${m[1]} in the ${cfg.layout} roster — known: ${roster.join(", ")}`);
886
- if (m[2]) next.per_agent[m[1]] = m[2]; else delete next.per_agent[m[1]];
1501
+ if (roster && !roster.includes(m[1]))
1502
+ return usage(`no agent named ${m[1]} in the ${cfg.layout} roster — known: ${roster.join(", ")}`);
1503
+ if (m[2]) next.per_agent[m[1]] = m[2];
1504
+ else delete next.per_agent[m[1]];
887
1505
  }
888
1506
  // The clerk transcribes; a strong default must not lift it (commands/agents.md,
889
1507
  // ADR-008). WHICH model it is pinned to is the harness's vocabulary, so the
@@ -902,29 +1520,77 @@ async function runAgents(ctx) {
902
1520
  const clerkAsked = (values.agent || []).some((s) => s.startsWith("clerk="));
903
1521
  if (next.default && !next.per_agent.clerk && !clerkAsked) {
904
1522
  const cheap = harnessForOverlay(harness)?.agent_translation?.cheap_model || null;
905
- if (cheap) { next.per_agent.clerk = cheap; pinnedClerk = true; }
906
- else clerkPinUnavailable = true;
1523
+ if (cheap) {
1524
+ next.per_agent.clerk = cheap;
1525
+ pinnedClerk = true;
1526
+ } else clerkPinUnavailable = true;
907
1527
  }
908
1528
  // Keys inside the agents block that the allowlist rejected (an `effort`, a
909
1529
  // stray shape) are rewritten out by the write and announced here; keys
910
1530
  // outside the block are kept, and stay doctor's to name.
911
1531
  const dropped = before.rejected.filter((k) => k.startsWith("agents."));
912
- const same = !dropped.length && JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) === JSON.stringify({ d: next.default, p: next.per_agent });
913
- const preview = [`agents configure — ${harness} — ${before.path}`, ` default: ${before.agents.default || "—"} → ${next.default || "—"}`];
1532
+ const same =
1533
+ !dropped.length &&
1534
+ JSON.stringify({ d: before.agents.default, p: before.agents.per_agent }) ===
1535
+ JSON.stringify({ d: next.default, p: next.per_agent });
1536
+ const preview = [
1537
+ `agents configure — ${harness} — ${before.path}`,
1538
+ ` default: ${before.agents.default || "—"} → ${next.default || "—"}`,
1539
+ ];
914
1540
  const names = new Set([...Object.keys(before.agents.per_agent), ...Object.keys(next.per_agent)]);
915
- for (const n of [...names].sort()) preview.push(` ${n}: ${before.agents.per_agent[n] || "—"} → ${next.per_agent[n] || "—"}${pinnedClerk && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`);
1541
+ for (const n of [...names].sort())
1542
+ preview.push(
1543
+ ` ${n}: ${before.agents.per_agent[n] || "—"} → ${next.per_agent[n] || "—"}${pinnedClerk && n === "clerk" ? " (pinned: the clerk stays cheap under a strong default)" : ""}`,
1544
+ );
916
1545
  // Silence here would read as "the clerk is fine", and it is not: it takes the
917
1546
  // strong default, which is the one thing ADR-008 says must not happen to it.
918
- if (clerkPinUnavailable) preview.push(` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`);
919
- if (dropped.length) preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
920
- if (same) return emit("agents configure", true, { harness, path: before.path, wrote: false, agents: next, pinnedClerk, clerkPinUnavailable, dropped }, preview.join("\n") + "\n Nothing to change.\n");
1547
+ if (clerkPinUnavailable)
1548
+ preview.push(
1549
+ ` clerk: takes the default — ${harness} declares no cheap model to pin it to, so name one with --agent clerk=<model>`,
1550
+ );
1551
+ if (dropped.length)
1552
+ preview.push(` drops from the agents block (an overlay carries only models): ${dropped.join(", ")}`);
1553
+ if (same)
1554
+ return emit(
1555
+ "agents configure",
1556
+ true,
1557
+ { harness, path: before.path, wrote: false, agents: next, pinnedClerk, clerkPinUnavailable, dropped },
1558
+ preview.join("\n") + "\n Nothing to change.\n",
1559
+ );
921
1560
  // The gate: a named harness confirms; otherwise ask, and refuse without a terminal.
922
1561
  let confirmed = Boolean(named);
923
- if (!confirmed) { const a = await confirmWrite(preview.join("\n") + `\nWrite ${before.path}? [y/N] `, { stdin, stdout, ask }); if (a === null) return usage("a non-interactive agents configure names --harness <id> to confirm; there is no --yes"); confirmed = a; }
924
- if (!confirmed) return emit("agents configure", false, { harness, path: before.path, wrote: false, declined: true, agents: next, dropped }, preview.join("\n") + "\n nothing written.\n");
1562
+ if (!confirmed) {
1563
+ const a = await confirmWrite(preview.join("\n") + `\nWrite ${before.path}? [y/N] `, { stdin, stdout, ask });
1564
+ if (a === null)
1565
+ return usage("a non-interactive agents configure names --harness <id> to confirm; there is no --yes");
1566
+ confirmed = a;
1567
+ }
1568
+ if (!confirmed)
1569
+ return emit(
1570
+ "agents configure",
1571
+ false,
1572
+ { harness, path: before.path, wrote: false, declined: true, agents: next, dropped },
1573
+ preview.join("\n") + "\n nothing written.\n",
1574
+ );
925
1575
  const path = writeOverlayAt(project, harness, next);
926
1576
  const after = readOverlayAt(project, harness);
927
- return emit("agents configure", true, { harness, path, wrote: true, agents: after.agents, pinnedClerk, clerkPinUnavailable, dropped, rejected: after.rejected, agents_in_binding: inBinding }, preview.join("\n") + `\n wrote ${path}.${inBinding ? " The binding still carries an agents block (pre-0.28); run upgrade to move it." : ""}\n`);
1577
+ return emit(
1578
+ "agents configure",
1579
+ true,
1580
+ {
1581
+ harness,
1582
+ path,
1583
+ wrote: true,
1584
+ agents: after.agents,
1585
+ pinnedClerk,
1586
+ clerkPinUnavailable,
1587
+ dropped,
1588
+ rejected: after.rejected,
1589
+ agents_in_binding: inBinding,
1590
+ },
1591
+ preview.join("\n") +
1592
+ `\n wrote ${path}.${inBinding ? " The binding still carries an agents block (pre-0.28); run upgrade to move it." : ""}\n`,
1593
+ );
928
1594
  }
929
1595
 
930
1596
  // The band's switch (the band spec, contract 13): one atomic binding write per
@@ -936,41 +1602,107 @@ async function runBand(ctx) {
936
1602
  const sub = positionals[0];
937
1603
  const arg = positionals[1];
938
1604
  const usage = (m) => usageFail(Object.assign(new Error(m), { code: "USAGE" }), { verb: row.verb, ...ctx });
939
- if (!BAND_SUBCOMMANDS.includes(sub)) return usage(`band takes ${BAND_SUBCOMMANDS.join(", ")} (epic takes an epic id, or none to release the pin)`);
940
- const emit = (verb, ok, result, text) => { if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n"); else (ok ? stdout : stderr).write(text); return ok ? 0 : 1; };
1605
+ if (!BAND_SUBCOMMANDS.includes(sub))
1606
+ return usage(`band takes ${BAND_SUBCOMMANDS.join(", ")} (epic takes an epic id, or none to release the pin)`);
1607
+ const emit = (verb, ok, result, text) => {
1608
+ if (values.json) stdout.write(JSON.stringify(envelope(verb, project, ok, result), null, 2) + "\n");
1609
+ else (ok ? stdout : stderr).write(text);
1610
+ return ok ? 0 : 1;
1611
+ };
941
1612
  if (sub === "status") {
942
1613
  const s = bandStatus(project, cfg, { env, root: PACKAGE_ROOT });
943
1614
  return emit("band status", true, s, renderBandStatus(s));
944
1615
  }
945
1616
  const plan = planBand(project, sub, arg, { env, root: PACKAGE_ROOT });
946
- if (plan.refusal) return emit(`band ${sub}`, false, { error: plan.refusal.message, code: plan.refusal.code, path: plan.path }, `${plan.refusal.message}\n`);
1617
+ if (plan.refusal)
1618
+ return emit(
1619
+ `band ${sub}`,
1620
+ false,
1621
+ { error: plan.refusal.message, code: plan.refusal.code, path: plan.path },
1622
+ `${plan.refusal.message}\n`,
1623
+ );
947
1624
  const preview = renderBandPlan(plan, sub, arg);
948
- if (!plan.changed) return emit(`band ${sub}`, true, { path: plan.path, wrote: false, band: plan.after }, preview + " Nothing to change.\n");
1625
+ if (!plan.changed)
1626
+ return emit(
1627
+ `band ${sub}`,
1628
+ true,
1629
+ { path: plan.path, wrote: false, band: plan.after },
1630
+ preview + " Nothing to change.\n",
1631
+ );
949
1632
  let confirmed = Boolean(values.json);
950
1633
  if (!confirmed) {
951
1634
  const a = await confirmWrite(preview + `Write ${plan.path}? [y/N] `, { stdin, stdout, ask });
952
1635
  if (a === null) return usage("a non-interactive band write passes --json; there is no --yes");
953
1636
  confirmed = a;
954
1637
  }
955
- if (!confirmed) return emit(`band ${sub}`, false, { path: plan.path, wrote: false, declined: true, band: plan.after }, preview + " nothing written.\n");
1638
+ if (!confirmed)
1639
+ return emit(
1640
+ `band ${sub}`,
1641
+ false,
1642
+ { path: plan.path, wrote: false, declined: true, band: plan.after },
1643
+ preview + " nothing written.\n",
1644
+ );
956
1645
  const done = applyBand(plan);
957
- return emit(`band ${sub}`, true, { path: plan.path, wrote: done.wrote, band: plan.after }, preview + ` wrote ${plan.path}. The band follows within a few seconds; no restart.\n`);
1646
+ return emit(
1647
+ `band ${sub}`,
1648
+ true,
1649
+ { path: plan.path, wrote: done.wrote, band: plan.after },
1650
+ preview + ` wrote ${plan.path}. The band follows within a few seconds; no restart.\n`,
1651
+ );
1652
+ }
1653
+
1654
+ // `session refresh <path>`: this session's pointer from a story file, after a
1655
+ // write no hook saw (the close ceremony's copy). Machine-local state, like the
1656
+ // hooks' own writes — no confirmation. No session: nothing written, exit 0.
1657
+ async function runSession(ctx) {
1658
+ const { row, values, positionals, cfg, project, env, stdout, stderr } = ctx;
1659
+ if (positionals[0] !== "refresh" || !positionals[1] || positionals.length > 2)
1660
+ return usageFail(
1661
+ Object.assign(new Error("session takes refresh <path> (a story file in the vault)"), { code: "USAGE" }),
1662
+ {
1663
+ verb: row.verb,
1664
+ ...ctx,
1665
+ },
1666
+ );
1667
+ const r = refreshSessionPointer(project, cfg, positionals[1], { env });
1668
+ const ok = !r.refusal;
1669
+ const result = ok ? r : { error: r.refusal.message, code: r.refusal.code };
1670
+ if (values.json) stdout.write(JSON.stringify(envelope("session refresh", project, ok, result), null, 2) + "\n");
1671
+ else if (!ok) stderr.write(`${r.refusal.message}\n`);
1672
+ else if (!r.refreshed) stdout.write("No session id from the host: nothing written.\n");
1673
+ else {
1674
+ const p = r.pointer;
1675
+ stdout.write(
1676
+ `This session's story: ${p.story_title || p.active_story}${p.story_status ? ` (${p.story_status})` : ""}.\n`,
1677
+ );
1678
+ }
1679
+ return ok ? 0 : 1;
958
1680
  }
959
1681
 
960
1682
  async function runGraph(ctx) {
961
1683
  const { row, values, positionals, cfg } = ctx;
962
1684
  const sub = positionals[0];
963
1685
  const path = positionals[1];
964
- if (!["neighbors", "lineage"].includes(sub)) return usageFail(Object.assign(new Error("graph takes neighbors <path> or lineage <path>"), { code: "USAGE" }), { verb: row.verb, ...ctx });
1686
+ if (!["neighbors", "lineage"].includes(sub))
1687
+ return usageFail(Object.assign(new Error("graph takes neighbors <path> or lineage <path>"), { code: "USAGE" }), {
1688
+ verb: row.verb,
1689
+ ...ctx,
1690
+ });
965
1691
  const op = READ_OPERATIONS[sub];
966
1692
  try {
967
1693
  // The row declares the union; each mode takes its own — an option the
968
1694
  // mode would ignore is a usage error, not a silent no-op.
969
1695
  const notFor = sub === "neighbors" ? ["depth"] : ["direction", "limit"];
970
- for (const o of notFor) if (values[o] !== undefined) throw Object.assign(new Error(`--${o} is not an option of graph ${sub}`), { code: "USAGE" });
971
- const result = sub === "neighbors"
972
- ? op.fn(cfg, path, { kinds: values.kind || null, direction: values.direction, limit: values.limit })
973
- : op.fn(cfg, path, { depth: values.depth, kinds: values.kind && values.kind.length ? values.kind : LINEAGE_KINDS });
1696
+ for (const o of notFor)
1697
+ if (values[o] !== undefined)
1698
+ throw Object.assign(new Error(`--${o} is not an option of graph ${sub}`), { code: "USAGE" });
1699
+ const result =
1700
+ sub === "neighbors"
1701
+ ? op.fn(cfg, path, { kinds: values.kind || null, direction: values.direction, limit: values.limit })
1702
+ : op.fn(cfg, path, {
1703
+ depth: values.depth,
1704
+ kinds: values.kind && values.kind.length ? values.kind : LINEAGE_KINDS,
1705
+ });
974
1706
  return emitRead({ verb: `graph ${sub}`, ...ctx }, op, result);
975
1707
  } catch (e) {
976
1708
  if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
@@ -982,7 +1714,10 @@ async function runCodemap(ctx) {
982
1714
  const { row, values, cfg } = ctx;
983
1715
  const op = READ_OPERATIONS.codeRefs;
984
1716
  try {
985
- if (!values.for) throw Object.assign(new Error("codemap takes --for <selector>; regeneration is reconcile --only codemap"), { code: "USAGE" });
1717
+ if (!values.for)
1718
+ throw Object.assign(new Error("codemap takes --for <selector>; regeneration is reconcile --only codemap"), {
1719
+ code: "USAGE",
1720
+ });
986
1721
  return emitRead({ verb: row.verb, ...ctx }, op, op.fn(cfg, values.for, { reverse: Boolean(values.reverse) }));
987
1722
  } catch (e) {
988
1723
  if (e && e.code === "USAGE") return usageFail(e, { verb: row.verb, ...ctx });
@@ -992,14 +1727,22 @@ async function runCodemap(ctx) {
992
1727
 
993
1728
  function runVersion({ values, stdout }) {
994
1729
  const version = packageVersion();
995
- stdout.write(values.json ? JSON.stringify(envelope("version", null, true, { version }), null, 2) + "\n" : version + "\n");
1730
+ stdout.write(
1731
+ values.json ? JSON.stringify(envelope("version", null, true, { version }), null, 2) + "\n" : version + "\n",
1732
+ );
996
1733
  return 0;
997
1734
  }
998
1735
 
999
1736
  function spawnDoctor(project, env, flags) {
1000
1737
  // A project that does not exist is still a project doctor can report on
1001
1738
  // ("not bound") — so the cwd is only set when it can be.
1002
- return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], { encoding: "utf8", cwd: existsSync(project) ? project : undefined, env: ownEnv(env, project), timeout: 60000, maxBuffer: 1 << 24 });
1739
+ return spawnSync(process.execPath, [script("doctor.mjs"), ...flags], {
1740
+ encoding: "utf8",
1741
+ cwd: existsSync(project) ? project : undefined,
1742
+ env: ownEnv(env, project),
1743
+ timeout: 60000,
1744
+ maxBuffer: 1 << 24,
1745
+ });
1003
1746
  }
1004
1747
 
1005
1748
  // Spawned, never imported (MCP ADR decision 2): doctor's main writes to
@@ -1021,13 +1764,27 @@ async function runDoctor({ values, project, env, stdout, stderr }) {
1021
1764
  if (r.error) return fail(r.error.message);
1022
1765
  if (r.status !== 0 && !r.stdout) return fail((r.stderr || "").trim() || `exit ${r.status}`);
1023
1766
  let findings;
1024
- try { findings = JSON.parse(r.stdout); } catch { return fail("unparseable output"); }
1767
+ try {
1768
+ findings = JSON.parse(r.stdout);
1769
+ } catch {
1770
+ return fail("unparseable output");
1771
+ }
1025
1772
  const ok = !findings.some((f) => f.level === "issue");
1026
1773
  if (values.json) stdout.write(JSON.stringify(envelope("doctor", project, ok, findings), null, 2) + "\n");
1027
1774
  else {
1028
1775
  // The sections as doctor.mjs derives them: no flag is both.
1029
- const groups = sections.length ? [values.install && "install", values.vault && "vault"].filter(Boolean) : ["install", "vault"];
1030
- stdout.write(await doctorText(findings, groups, { caps: term.caps(stdout, env), verbose: Boolean(values.verbose), project, env: ownEnv(env, project), stderr }));
1776
+ const groups = sections.length
1777
+ ? [values.install && "install", values.vault && "vault"].filter(Boolean)
1778
+ : ["install", "vault"];
1779
+ stdout.write(
1780
+ await doctorText(findings, groups, {
1781
+ caps: term.caps(stdout, env),
1782
+ verbose: Boolean(values.verbose),
1783
+ project,
1784
+ env: ownEnv(env, project),
1785
+ stderr,
1786
+ }),
1787
+ );
1031
1788
  }
1032
1789
  return ok ? 0 : 1;
1033
1790
  }
@@ -1038,12 +1795,20 @@ async function runDoctor({ values, project, env, stdout, stderr }) {
1038
1795
  // reader the findings: they are printed one per line, `level check message
1039
1796
  // — file`, and the Summary line still ends them (lib.mjs). `load` stands in
1040
1797
  // for the import in a test.
1041
- export async function doctorText(findings, groups, { caps, verbose = false, project = null, env = process.env, stderr = null } = {}, { load = () => import("./doctor-report.mjs") } = {}) {
1798
+ export async function doctorText(
1799
+ findings,
1800
+ groups,
1801
+ { caps, verbose = false, project = null, env = process.env, stderr = null } = {},
1802
+ { load = () => import("./doctor-report.mjs") } = {},
1803
+ ) {
1042
1804
  try {
1043
1805
  const { report } = await load();
1044
1806
  return report(findings, groups, { caps, verbose, version: packageVersion(), project, env });
1045
1807
  } catch (e) {
1046
- if (stderr) stderr.write(`doctor: the report could not be drawn (${e && e.message ? e.message : e}); its findings follow, one per line.\n`);
1808
+ if (stderr)
1809
+ stderr.write(
1810
+ `doctor: the report could not be drawn (${e && e.message ? e.message : e}); its findings follow, one per line.\n`,
1811
+ );
1047
1812
  return doctorFallbackText(findings, { env });
1048
1813
  }
1049
1814
  }
@@ -1054,13 +1819,21 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
1054
1819
  // The gate: --only names what is written and confirms headless; a bare
1055
1820
  // --write asks on a terminal and refuses without one.
1056
1821
  if (write && !only) {
1057
- const yes = await confirmWrite(`Regenerate every derived view of ${project}'s vault? [y/N] `, { stdin, stdout, ask });
1822
+ const yes = await confirmWrite(`Regenerate every derived view of ${project}'s vault? [y/N] `, {
1823
+ stdin,
1824
+ stdout,
1825
+ ask,
1826
+ });
1058
1827
  if (yes === null) {
1059
1828
  stderr.write("a bare reconcile --write in a non-TTY refuses; name what is written to confirm: --only <target>\n");
1060
- if (values.json) stdout.write(JSON.stringify(envelope("reconcile", project, false, { refused: "non-tty" }), null, 2) + "\n");
1829
+ if (values.json)
1830
+ stdout.write(JSON.stringify(envelope("reconcile", project, false, { refused: "non-tty" }), null, 2) + "\n");
1061
1831
  return 1;
1062
1832
  }
1063
- if (!yes) { stdout.write("nothing written.\n"); return 0; }
1833
+ if (!yes) {
1834
+ stdout.write("nothing written.\n");
1835
+ return 0;
1836
+ }
1064
1837
  }
1065
1838
  const { runReconcile: core } = await import("./reconcile.mjs");
1066
1839
  let out;
@@ -1069,7 +1842,8 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
1069
1842
  } catch (e) {
1070
1843
  const msg = e && e.message ? e.message : String(e);
1071
1844
  stderr.write(msg + "\n");
1072
- if (values.json) stdout.write(JSON.stringify(envelope("reconcile", project, false, { error: msg }), null, 2) + "\n");
1845
+ if (values.json)
1846
+ stdout.write(JSON.stringify(envelope("reconcile", project, false, { error: msg }), null, 2) + "\n");
1073
1847
  return e && e.code === "UNBOUND" ? 3 : 2;
1074
1848
  }
1075
1849
  const failed = write && out.summary && out.summary.failed > 0;
@@ -1077,14 +1851,54 @@ async function runReconcile({ values, project, env, stdin, stdout, stderr, ask }
1077
1851
  return failed ? 1 : 0;
1078
1852
  }
1079
1853
 
1854
+ // setup (the setup story): its own module, imported lazily as the install
1855
+ // family is, run with this package as its root and in the environment every
1856
+ // installer run gets (ownEnv, for the project setup settles on). The envelope
1857
+ // is written here, as every verb's is: its result is the write verbs' keys
1858
+ // plus `setup`. A usage error is exit 2 with its envelope.
1859
+ async function runSetupVerb(ctx) {
1860
+ const { runSetup } = await import("./setup.mjs");
1861
+ const { values, env, stdout } = ctx;
1862
+ const r = await runSetup({
1863
+ ...ctx,
1864
+ root: PACKAGE_ROOT,
1865
+ ownEnv: (p) => ownEnv(env, p),
1866
+ author: (p) => gitAuthor(p, env),
1867
+ });
1868
+ if (r.error) return usageFail(Object.assign(new Error(r.error), { code: "USAGE" }), { verb: "setup", ...ctx });
1869
+ if (values.json && r.result)
1870
+ stdout.write(JSON.stringify(envelope("setup", r.project, r.ok, r.result), null, 2) + "\n");
1871
+ return r.code;
1872
+ }
1873
+
1080
1874
  async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, ask }) {
1081
1875
  const ih = await import("./install-harness.mjs");
1082
- const opts = { harnesses: values.harness || [], surfaces: values.surface && values.surface.length ? values.surface : null, globalRemoval: Boolean(values.global), register: !values["no-register"], root: PACKAGE_ROOT, env: ownEnv(env, project), cwd, stdin, stdout, ask, json: Boolean(values.json), verbose: Boolean(values.verbose) };
1876
+ const opts = {
1877
+ harnesses: values.harness || [],
1878
+ surfaces: values.surface && values.surface.length ? values.surface : null,
1879
+ globalRemoval: Boolean(values.global),
1880
+ register: !values["no-register"],
1881
+ root: PACKAGE_ROOT,
1882
+ env: ownEnv(env, project),
1883
+ cwd,
1884
+ stdin,
1885
+ stdout,
1886
+ ask,
1887
+ json: Boolean(values.json),
1888
+ verbose: Boolean(values.verbose),
1889
+ };
1083
1890
  // plan goes through the installer's own planOptions, as its main() does, so
1084
1891
  // the two entry points read the same root version for a dry-run row.
1085
1892
  if (row.verb === "plan") {
1086
1893
  const p = ih.plan(project, ih.planOptions(opts));
1087
- if (values.json) stdout.write(JSON.stringify(envelope("plan", project, p.ok && !p.incomplete, { ...p, items: p.items.map(ih.publicItem) }), null, 2) + "\n");
1894
+ if (values.json)
1895
+ stdout.write(
1896
+ JSON.stringify(
1897
+ envelope("plan", project, p.ok && !p.incomplete, { ...p, items: p.items.map(ih.publicItem) }),
1898
+ null,
1899
+ 2,
1900
+ ) + "\n",
1901
+ );
1088
1902
  else {
1089
1903
  const c = term.caps(stdout, env);
1090
1904
  stdout.write(ih.renderPreview(p, { verbose: opts.verbose, verb: "plan", caps: c }));
@@ -1100,12 +1914,28 @@ async function runInstallVerb({ row, values, project, env, cwd, stdin, stdout, a
1100
1914
  if (values.json) {
1101
1915
  // `fetched` is additive: the shells a run fetched before its plan, [] for
1102
1916
  // a run that fetched none (uninstall always).
1103
- stdout.write(JSON.stringify(envelope(row.verb, project, ok, { gate: r.gate, applied: r.applied, failed: r.failed, incomplete: r.plan.incomplete, plannedAgainst: r.plan.plannedAgainst, items: r.plan.items.map(ih.publicItem), refusals: r.plan.refusals, reports: r.plan.reports, fetched: r.fetched || [] }), null, 2) + "\n");
1917
+ stdout.write(
1918
+ JSON.stringify(
1919
+ envelope(row.verb, project, ok, {
1920
+ gate: r.gate,
1921
+ applied: r.applied,
1922
+ failed: r.failed,
1923
+ incomplete: r.plan.incomplete,
1924
+ plannedAgainst: r.plan.plannedAgainst,
1925
+ items: r.plan.items.map(ih.publicItem),
1926
+ refusals: r.plan.refusals,
1927
+ reports: r.plan.reports,
1928
+ fetched: r.fetched || [],
1929
+ }),
1930
+ null,
1931
+ 2,
1932
+ ) + "\n",
1933
+ );
1104
1934
  } else {
1105
1935
  // The installer's own words for a gate that wrote nothing — a bare run
1106
1936
  // without a terminal, a declined question, a channel switch without a
1107
1937
  // terminal — shared with its main(), so the two entries say the same.
1108
- stdout.write(ih.gateText(r, { verb: row.verb, harnesses: opts.harnesses, surfaces: opts.surfaces, caps: term.caps(stdout, env) }));
1938
+ stdout.write(ih.gateText(r, { verb: row.verb, harnesses: opts.harnesses, caps: term.caps(stdout, env) }));
1109
1939
  }
1110
1940
  return ok ? 0 : 1;
1111
1941
  }