thurview 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,8 +37,8 @@ back with changes requested.
37
37
 
38
38
  <details>
39
39
  <summary><b>Other ways to install</b> - pin the skill to a release, install the
40
- command from npm, run from a checkout, session hooks, the review-fix
41
- skill</summary>
40
+ command from npm, run from a checkout, session hooks, the thurview-design and
41
+ thurview-fix skills</summary>
42
42
 
43
43
  `--global` installs for your user, so one install covers every repository.
44
44
  `universal` puts the one real copy in `~/.agents/skills/thurview`, the directory
@@ -92,14 +92,33 @@ with the reviews of its working directory. `thurview setup skill` links the
92
92
  skill from this checkout instead of the `skills` CLI copy; use one or the
93
93
  other.
94
94
 
95
- Also available: `review-fix`, the browserless companion skill described
96
- [below](#review-and-fix).
95
+ Also available: `thurview-design`, which authors the design kind described
96
+ [below](#how-it-works), and `thurview-fix`, the browserless companion skill
97
+ described [below](#review-and-fix).
97
98
 
98
99
  ```sh
99
100
  npx skills@latest add https://github.com/Thurbeen/thurview \
100
- --skill review-fix --agent universal claude-code --global --yes
101
+ --skill thurview-design --agent universal claude-code --global --yes
102
+ npx skills@latest add https://github.com/Thurbeen/thurview \
103
+ --skill thurview-fix --agent universal claude-code --global --yes
101
104
  ```
102
105
 
106
+ `thurview-fix` was called `review-fix`. A skill name is an address, so nothing
107
+ updates the old one in place: an install made before the rename keeps answering
108
+ to `/review-fix` from a copy that will never change again, and one made with
109
+ `thurview setup skill` leaves a symlink pointing at a directory this repository
110
+ no longer ships. Remove it and add the skill under its new name:
111
+
112
+ ```sh
113
+ npx skills@latest remove --global review-fix
114
+ npx skills@latest add https://github.com/Thurbeen/thurview \
115
+ --skill thurview-fix --agent universal claude-code --global --yes
116
+ ```
117
+
118
+ For a `thurview setup skill` install, delete the stale link -
119
+ `~/.agents/skills/review-fix` and the same path under `~/.claude` and
120
+ `~/.cursor` - and run `thurview setup skill` again.
121
+
103
122
  </details>
104
123
 
105
124
  ## How it works
@@ -115,6 +134,12 @@ architecture well enough to spot design problems themselves. It has no diff and
115
134
  nothing to approve, it surfaces structure rather than grading it, and it states
116
135
  what it did not examine.
117
136
 
137
+ And it reads a plan. A **design** is the same document over a change that is
138
+ not written yet: one pinned commit — the code it argues from — anchors on the
139
+ code as it stands, and what it would build declared as proposals, each attached
140
+ to the code it lands in today. An anchor never points at code that does not
141
+ exist; the reader approves the design or sends it back.
142
+
118
143
  It does not review the code for you. It helps you understand it fast enough
119
144
  to review it yourself.
120
145
 
@@ -201,20 +226,21 @@ bar.
201
226
 
202
227
  ## CLI
203
228
 
204
- | Command | Purpose |
205
- | --------------------------------------------------------------------- | ----------------------------------------------------------------- |
206
- | `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
207
- | `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
208
- | `thurview info [--all]` | Reviews bound to this worktree |
209
- | `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
210
- | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
211
- | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
212
- | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
213
- | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
214
- | `thurview forge status\|prior\|pass\|submit\|reply` | Read a change request through its forge, and post the review back |
215
- | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
216
- | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
217
- | `thurview update` | Self-update from npm |
229
+ | Command | Purpose |
230
+ | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
231
+ | `thurview scaffold [--pr N \| --base R --head R]` | Create a review pinned to exact commits (`--update` re-pins) |
232
+ | `thurview explain [<path>] [--commit R]` | Create a code explainer of a codebase or subsystem at one commit |
233
+ | `thurview design [<path>] [--commit R]` | Create a design of what to build, pinned to the commit it argues from |
234
+ | `thurview info [--all]` | Reviews, explainers and designs bound to this worktree |
235
+ | `thurview publish --review ID [--view T] [--open]` | Validate the document and map, seal a revision |
236
+ | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
237
+ | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
238
+ | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
239
+ | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
240
+ | `thurview forge status\|prior\|pass\|submit\|reply` | Read a change request through its forge, and post the review back |
241
+ | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
242
+ | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
243
+ | `thurview update` | Self-update from npm |
218
244
 
219
245
  thurview is an [AXI](https://axi.md): built for agents that drive it through a
220
246
  shell. Output is [TOON](https://toonformat.dev) on stdout, errors are
@@ -228,7 +254,7 @@ stderr.
228
254
 
229
255
  ## Review and fix
230
256
 
231
- The `review-fix` skill reviews a branch, a commit range or a pull or merge
257
+ The `thurview-fix` skill reviews a branch, a commit range or a pull or merge
232
258
  request, fixes what it is sure of and reports the rest, with no browser and no
233
259
  approval step. For each changed symbol it asks the code graph who calls it and
234
260
  which tests reach it, so a finding can name a caller the diff never shows.
@@ -280,7 +306,7 @@ and gitlab.com are matched against what those CLIs are authenticated for, and
280
306
  an unmatched host is refused rather than guessed. The differences that survive
281
307
  the seam - GitLab has no changes-requested state, no atomic review and no
282
308
  multi-line comment anchor - are listed in
283
- [skills/review-fix/references/forges.md](skills/review-fix/references/forges.md).
309
+ [skills/thurview-fix/references/forges.md](skills/thurview-fix/references/forges.md).
284
310
 
285
311
  ## Authoring format
286
312
 
@@ -302,12 +328,23 @@ An explainer writes the same files, minus `interfaces`: there is no change to
302
328
  derive a delta from, and `graph: base` on an anchor is an error because there
303
329
  is one commit.
304
330
 
331
+ A design writes the same files, and `interfaces` means something else in it:
332
+ each entry is a **proposal** — what the design would add, change or remove,
333
+ with the anchor of the code that proposal lands in, replaces or plugs into
334
+ today. `graph: base` is an error for the same reason as in an explainer, a
335
+ `symbol:` entry is an error because no diff derived one, and a design that
336
+ proposes nothing is refused: that document is an explainer. In its `map.yaml`,
337
+ `base` is the structure as it stands and `nodes` the structure it proposes, so
338
+ a proposed part may own files that do not exist yet while a `base` node may
339
+ not.
340
+
305
341
  `thurview publish` rejects an anchor whose file or lines do not exist at the
306
342
  pinned commit, a call stack frame that claims an added or removed call the
307
343
  diff does not show, a storage operation on an unknown field, a map edge
308
344
  to an unknown node, an interface annotation for a symbol the change did not
309
345
  move, a declared interface whose anchor holds no added or deleted line, and an
310
- explainer that anchors nothing at all. The full format is in
346
+ explainer that anchors nothing at all, and a design that proposes nothing or
347
+ whose proposal names no site in the code as it stands. The full format is in
311
348
  [skills/thurview/references](skills/thurview/references).
312
349
 
313
350
  Optional guidance for the agent: `~/.thurview/THURVIEW.md` for you,
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import { parseSubmission, longComments, buildPass } from "./forge/submission.js"
24
24
  import { VERSION } from "./version.js";
25
25
  const execFileP = promisify(execFile);
26
26
  const HERE = dirname(fileURLToPath(import.meta.url));
27
- const DESCRIPTION = "Guided, evidence-anchored reviews of a change and explainers of a codebase, read and answered in the browser";
27
+ const DESCRIPTION = "Guided, evidence-anchored reviews of a change, explainers of a codebase and designs of what to build, read and answered in the browser";
28
28
  function note(msg) {
29
29
  process.stderr.write(msg + "\n");
30
30
  }
@@ -134,9 +134,9 @@ async function reviewRow(r, fields) {
134
134
  row["binding"] = bindingLabel(r.binding);
135
135
  if (fields.has("all") || fields.has("pins"))
136
136
  row["pins"] =
137
- kindOf(r) === "explainer"
138
- ? r.pins.head.slice(0, 12)
139
- : `${r.pins.base.slice(0, 12)}..${r.pins.head.slice(0, 12)}`;
137
+ kindOf(r) === "review"
138
+ ? `${r.pins.base.slice(0, 12)}..${r.pins.head.slice(0, 12)}`
139
+ : r.pins.head.slice(0, 12);
140
140
  if (fields.has("all") || fields.has("worktree"))
141
141
  row["worktree"] = r.worktree;
142
142
  if (fields.has("all") || fields.has("inSync"))
@@ -323,6 +323,150 @@ const TEMPLATE_EXPLAIN_MAP = `# The structure of the code at the pinned commit:
323
323
  nodes: []
324
324
  edges: []
325
325
  `;
326
+ const TEMPLATE_DESIGN_MD = (title) => `# ${title}
327
+
328
+ **Summary**
329
+
330
+ - The agent is still writing this design. The page offers the new revision
331
+ when the proposal lands.
332
+ `;
333
+ const TEMPLATE_DESIGN_DATA = `# Typed inputs for the design: actors, anchors, stores, and the proposals it
334
+ # makes. A design is pinned to ONE commit - the code as it stands, which the
335
+ # design changes - so every anchor reads that commit and \`graph: base\` is an
336
+ # error.
337
+ #
338
+ # An anchor is evidence, never a proposal. It points at code that exists today:
339
+ # what the design changes, and what constrains it.
340
+ #
341
+ # anchors:
342
+ # login:
343
+ # title: where a request is authenticated today
344
+ # peek: { file: src/auth.ts, from: 41, to: 58 }
345
+ #
346
+ # interfaces holds what the design WOULD add, change or remove. Each entry names
347
+ # the interface, what it would let a consumer do, and the anchor of the code it
348
+ # lands in, replaces or plugs into today. A design with no entry here proposes
349
+ # nothing, and publish refuses it: that document is a code explainer.
350
+ #
351
+ # interfaces:
352
+ # strict:
353
+ # name: auth.login --strict
354
+ # change: added
355
+ # capability: Rejects an empty user instead of answering false.
356
+ # anchor: login
357
+ actors: {}
358
+ anchors: {}
359
+ stores: {}
360
+ interfaces: {}
361
+ `;
362
+ const TEMPLATE_DESIGN_MAP = `# The structure the design proposes, with the structure as it stands under
363
+ # \`base\`, so the Map tab shows what it adds, changes and removes. A node under
364
+ # \`nodes\` may own files that do not exist yet - that is a proposed part. A node
365
+ # under \`base\` may not: it is a claim about today, and publish warns.
366
+ # Seed base from \`thurview graph architecture\`.
367
+ nodes: []
368
+ edges: []
369
+ `;
370
+ const EXPLAINER = {
371
+ kind: "explainer",
372
+ command: "explain",
373
+ title: (scope, worktree) => scope === "**" ? worktree.split("/").pop() || "Codebase" : scope.replace(/\/\*\*$/, ""),
374
+ templates: { md: TEMPLATE_EXPLAIN_MD, data: TEMPLATE_EXPLAIN_DATA, map: TEMPLATE_EXPLAIN_MAP },
375
+ };
376
+ const DESIGN = {
377
+ kind: "design",
378
+ command: "design",
379
+ title: (scope) => (scope === "**" ? "Design" : `${scope.replace(/\/\*\*$/, "")} design`),
380
+ templates: { md: TEMPLATE_DESIGN_MD, data: TEMPLATE_DESIGN_DATA, map: TEMPLATE_DESIGN_MAP },
381
+ };
382
+ /** The kind with its article, so a generated sentence reads as one: "an explainer". */
383
+ function kindWord(kind) {
384
+ return kind === "explainer" ? "an explainer" : kind === "design" ? "a design" : "a review";
385
+ }
386
+ /** The command that re-pins a document of this kind, for a message that offers it. */
387
+ function repinCommand(kind) {
388
+ return kind === "review"
389
+ ? "thurview scaffold"
390
+ : `thurview ${kind === "design" ? "design" : "explain"}`;
391
+ }
392
+ async function pinOneCommit(p, k) {
393
+ const worktree = await worktreeOf(process.cwd());
394
+ if (!worktree)
395
+ throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
396
+ `Run \`thurview ${k.command}\` inside the source worktree`,
397
+ ]);
398
+ const existing = bool(p, "update") || str(p, "review") ? await resolveReview(str(p, "review")) : null;
399
+ if (existing && kindOf(existing) !== k.kind) {
400
+ const other = kindOf(existing);
401
+ throw new AxiError(`${short(existing.id)} is ${kindWord(other)}, not ${kindWord(k.kind)}`, "VALIDATION_ERROR", [
402
+ `Run \`${repinCommand(other)} --update --review ${short(existing.id)}\` to re-pin that ${other}`,
403
+ `Run \`thurview ${k.command}\` with no --review to start ${kindWord(k.kind)}`,
404
+ ]);
405
+ }
406
+ const scope = scopeGlob(p.positional[0] ?? existing?.binding.name);
407
+ let commit;
408
+ try {
409
+ commit = await g.revParse(worktree, str(p, "commit") ?? "HEAD");
410
+ }
411
+ catch (e) {
412
+ throw new AxiError(e.message, "VALIDATION_ERROR", [
413
+ `Pass a resolvable ref: \`thurview ${k.command} --commit <ref>\``,
414
+ ]);
415
+ }
416
+ // A scope that matches nothing is a typo, and a document of nothing would
417
+ // still publish and still state honest-looking coverage of zero files.
418
+ const files = await g.listFiles(worktree, commit);
419
+ const inScope = scope === "**" ? files : files.filter((f) => globToRegExp(scope).test(f));
420
+ if (!inScope.length)
421
+ throw new AxiError(`no file matches "${scope}" at ${commit.slice(0, 12)}`, "VALIDATION_ERROR", [
422
+ `Pass a path that exists at that commit: \`thurview ${k.command} src/server\``,
423
+ `Run \`thurview ${k.command}\` with no scope for the whole repository`,
424
+ ]);
425
+ const binding = { kind: "codebase", name: scope };
426
+ if (existing) {
427
+ existing.pins = { base: commit, head: commit };
428
+ existing.binding = binding;
429
+ if (str(p, "title"))
430
+ existing.title = str(p, "title");
431
+ await writeReview(existing);
432
+ return { review: existing, reused: false, scope, commit, worktree, inScope };
433
+ }
434
+ const match = bool(p, "new")
435
+ ? []
436
+ : (await reviewsFor(worktree)).filter((r) => kindOf(r) === k.kind &&
437
+ r.binding.name === scope &&
438
+ r.status !== "accepted" &&
439
+ r.status !== "closed");
440
+ if (match.length) {
441
+ const review = match[0];
442
+ review.pins = { base: commit, head: commit };
443
+ await writeReview(review);
444
+ return { review, reused: true, scope, commit, worktree, inScope };
445
+ }
446
+ const id = newId();
447
+ const review = {
448
+ schema: SCHEMA,
449
+ id,
450
+ kind: k.kind,
451
+ title: str(p, "title") || k.title(scope, worktree),
452
+ worktree,
453
+ repoRoot: worktree,
454
+ binding,
455
+ pins: { base: commit, head: commit },
456
+ status: "draft",
457
+ revision: 0,
458
+ dismissed: false,
459
+ createdAt: now(),
460
+ updatedAt: now(),
461
+ };
462
+ await mkdir(reviewDir(id), { recursive: true });
463
+ await writeText(join(reviewDir(id), "review.md"), k.templates.md(review.title));
464
+ await writeText(join(reviewDir(id), "data.yaml"), k.templates.data);
465
+ await writeText(join(reviewDir(id), "map.yaml"), k.templates.map);
466
+ await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
467
+ await writeReview(review);
468
+ return { review, reused: false, scope, commit, worktree, inScope };
469
+ }
326
470
  // ---- commands ----
327
471
  const SPECS = {
328
472
  scaffold: {
@@ -368,8 +512,25 @@ const SPECS = {
368
512
  "thurview explain --update --review <id>",
369
513
  ],
370
514
  },
515
+ design: {
516
+ description: "Create a design or architecture document: what to build, argued against the code as it stands",
517
+ args: "[<path scope>]",
518
+ flags: {
519
+ commit: { kind: "string", help: "commit to pin (default: HEAD)" },
520
+ title: { kind: "string", help: "initial title" },
521
+ new: { kind: "boolean", help: "create another design even if one matches the scope" },
522
+ update: { kind: "boolean", help: "re-pin an existing design to a new commit" },
523
+ review: { kind: "string", help: "design to update (id prefix)" },
524
+ },
525
+ examples: [
526
+ "thurview design",
527
+ "thurview design src/server",
528
+ "thurview design --title 'Queue the forge pass'",
529
+ "thurview design --update --review <id>",
530
+ ],
531
+ },
371
532
  info: {
372
- description: "Reviews and explainers bound to this worktree (or all of them with --all)",
533
+ description: "Reviews, explainers and designs bound to this worktree (or all of them with --all)",
373
534
  flags: {
374
535
  all: { kind: "boolean", help: "every review, not only this worktree" },
375
536
  fields: {
@@ -553,6 +714,7 @@ async function homeView() {
553
714
  "Run `thurview scaffold` to create a review of the current branch",
554
715
  "Run `thurview scaffold --pr <number>` for a pull request",
555
716
  "Run `thurview explain [<path>]` to explain the codebase at HEAD instead",
717
+ "Run `thurview design [<path>]` to design a change before writing it",
556
718
  ],
557
719
  };
558
720
  const reviews = [];
@@ -719,87 +881,8 @@ const commands = {
719
881
  },
720
882
  async explain(args) {
721
883
  const p = parseFlags("explain", args, spec("explain").flags, 1);
722
- const cwd = process.cwd();
723
- const worktree = await worktreeOf(cwd);
724
- if (!worktree)
725
- throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
726
- "Run `thurview explain` inside the source worktree",
727
- ]);
728
- const existing = bool(p, "update") || str(p, "review") ? await resolveReview(str(p, "review")) : null;
729
- if (existing && kindOf(existing) !== "explainer")
730
- throw new AxiError(`${short(existing.id)} is a review, not an explainer`, "VALIDATION_ERROR", [
731
- "Run `thurview scaffold --update --review <id>` to re-pin a review",
732
- "Run `thurview explain` with no --review to start an explainer",
733
- ]);
734
- const scope = scopeGlob(p.positional[0] ?? existing?.binding.name);
735
- let commit;
736
- try {
737
- commit = await g.revParse(worktree, str(p, "commit") ?? "HEAD");
738
- }
739
- catch (e) {
740
- throw new AxiError(e.message, "VALIDATION_ERROR", [
741
- "Pass a resolvable ref: `thurview explain --commit <ref>`",
742
- ]);
743
- }
744
- // A scope that matches nothing is a typo, and an explainer of nothing would
745
- // still publish and still state honest-looking coverage of zero files.
746
- const files = await g.listFiles(worktree, commit);
747
- const inScope = scope === "**" ? files : files.filter((f) => globToRegExp(scope).test(f));
748
- if (!inScope.length)
749
- throw new AxiError(`no file matches "${scope}" at ${commit.slice(0, 12)}`, "VALIDATION_ERROR", [
750
- "Pass a path that exists at that commit: `thurview explain src/server`",
751
- "Run `thurview explain` with no scope for the whole repository",
752
- ]);
753
- const binding = { kind: "codebase", name: scope };
754
- const defaultTitle = scope === "**" ? worktree.split("/").pop() || "Codebase" : scope.replace(/\/\*\*$/, "");
755
- let review;
756
- let reused = false;
757
- if (existing) {
758
- review = existing;
759
- review.pins = { base: commit, head: commit };
760
- review.binding = binding;
761
- if (str(p, "title"))
762
- review.title = str(p, "title");
763
- await writeReview(review);
764
- }
765
- else {
766
- const match = bool(p, "new")
767
- ? []
768
- : (await reviewsFor(worktree)).filter((r) => kindOf(r) === "explainer" &&
769
- r.binding.name === scope &&
770
- r.status !== "accepted" &&
771
- r.status !== "closed");
772
- if (match.length) {
773
- review = match[0];
774
- reused = true;
775
- review.pins = { base: commit, head: commit };
776
- await writeReview(review);
777
- }
778
- else {
779
- const id = newId();
780
- review = {
781
- schema: SCHEMA,
782
- id,
783
- kind: "explainer",
784
- title: str(p, "title") || defaultTitle,
785
- worktree,
786
- repoRoot: worktree,
787
- binding,
788
- pins: { base: commit, head: commit },
789
- status: "draft",
790
- revision: 0,
791
- dismissed: false,
792
- createdAt: now(),
793
- updatedAt: now(),
794
- };
795
- await mkdir(reviewDir(id), { recursive: true });
796
- await writeText(join(reviewDir(id), "review.md"), TEMPLATE_EXPLAIN_MD(review.title));
797
- await writeText(join(reviewDir(id), "data.yaml"), TEMPLATE_EXPLAIN_DATA);
798
- await writeText(join(reviewDir(id), "map.yaml"), TEMPLATE_EXPLAIN_MAP);
799
- await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
800
- await writeReview(review);
801
- }
802
- }
884
+ const pinned = await pinOneCommit(p, EXPLAINER);
885
+ const { review, scope, commit, worktree } = pinned;
803
886
  const dir = reviewDir(review.id);
804
887
  return {
805
888
  explainer: {
@@ -812,7 +895,7 @@ const commands = {
812
895
  scope,
813
896
  commit,
814
897
  worktree,
815
- reused,
898
+ reused: pinned.reused,
816
899
  dir,
817
900
  },
818
901
  files: {
@@ -821,7 +904,7 @@ const commands = {
821
904
  map: join(dir, "map.yaml"),
822
905
  theme: join(dir, "theme.yaml"),
823
906
  },
824
- scale: { filesInScope: inScope.length },
907
+ scale: { filesInScope: pinned.inScope.length },
825
908
  guidance: await guidanceFiles(worktree),
826
909
  help: [
827
910
  `Run \`thurview graph architecture --review ${short(review.id)}\` for the clusters, their hubs and the links between them`,
@@ -830,6 +913,40 @@ const commands = {
830
913
  ],
831
914
  };
832
915
  },
916
+ async design(args) {
917
+ const p = parseFlags("design", args, spec("design").flags, 1);
918
+ const pinned = await pinOneCommit(p, DESIGN);
919
+ const { review, scope, commit, worktree } = pinned;
920
+ const dir = reviewDir(review.id);
921
+ return {
922
+ design: {
923
+ id: short(review.id),
924
+ uuid: review.id,
925
+ kind: "design",
926
+ title: review.title,
927
+ status: review.status,
928
+ rev: review.revision,
929
+ scope,
930
+ commit,
931
+ worktree,
932
+ reused: pinned.reused,
933
+ dir,
934
+ },
935
+ files: {
936
+ document: join(dir, "review.md"),
937
+ data: join(dir, "data.yaml"),
938
+ map: join(dir, "map.yaml"),
939
+ theme: join(dir, "theme.yaml"),
940
+ },
941
+ scale: { filesInScope: pinned.inScope.length },
942
+ guidance: await guidanceFiles(worktree),
943
+ help: [
944
+ `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure the design has to fit`,
945
+ `Declare in ${join(dir, "data.yaml")} what the design would add, change or remove, each anchored to the code it lands in today`,
946
+ `Edit ${join(dir, "review.md")}, then run \`thurview publish --review ${short(review.id)}\``,
947
+ ],
948
+ };
949
+ },
833
950
  async info(args) {
834
951
  const p = parseFlags("info", args, spec("info").flags);
835
952
  const fields = new Set((str(p, "fields") ?? "").split(",").filter(Boolean));
@@ -1003,7 +1120,7 @@ const commands = {
1003
1120
  if (review.binding.kind === "codebase") {
1004
1121
  const tip = await g.revParse(review.worktree, "HEAD").catch(() => null);
1005
1122
  if (tip && tip !== review.pins.head)
1006
- warnings.push(`HEAD has moved past the pinned commit; run \`thurview explain --update --review ${short(review.id)}\` to re-pin`);
1123
+ warnings.push(`HEAD has moved past the pinned commit; run \`${repinCommand(kind)} --update --review ${short(review.id)}\` to re-pin`);
1007
1124
  }
1008
1125
  if (review.binding.kind === "branch") {
1009
1126
  const tip = await g.revParse(review.worktree, review.binding.name).catch(() => null);
@@ -1058,7 +1175,9 @@ const commands = {
1058
1175
  map: !!map,
1059
1176
  ...(kind === "explainer"
1060
1177
  ? { coverage: coverage ? coverage.verdict : "(unavailable)" }
1061
- : { interfaces: doc.document.interfaces?.verdict ?? "(unavailable)" }),
1178
+ : kind === "design"
1179
+ ? { proposes: doc.document.interfaces?.verdict ?? "(unavailable)" }
1180
+ : { interfaces: doc.document.interfaces?.verdict ?? "(unavailable)" }),
1062
1181
  theme: theme?.name ?? "default",
1063
1182
  url: url ?? "(server not running)",
1064
1183
  },
@@ -1263,8 +1382,8 @@ const commands = {
1263
1382
  }
1264
1383
  // A next step has to name the same commits, or it answers about another change.
1265
1384
  const again = review ? "" : ` --base ${short(t.pins.base)} --head ${short(t.pins.head)}`;
1266
- if (review && kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
1267
- throw new AxiError(`graph ${sub} compares two commits; an explainer is pinned to one`, "VALIDATION_ERROR", [
1385
+ if (review && kindOf(review) !== "review" && (sub === "interfaces" || sub === "impact"))
1386
+ throw new AxiError(`graph ${sub} compares two commits; ${kindOf(review) === "design" ? "a design" : "an explainer"} is pinned to one`, "VALIDATION_ERROR", [
1268
1387
  `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
1269
1388
  `Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
1270
1389
  ]);
@@ -1337,9 +1456,11 @@ const commands = {
1337
1456
  ],
1338
1457
  };
1339
1458
  }
1340
- // An explainer is scoped to a path, so its structure is that path's, not the
1341
- // repository's: the same bound the Coverage tab accounts for.
1342
- if (review && kindOf(review) === "explainer") {
1459
+ // An explainer and a design are both scoped to a path and pinned to one
1460
+ // commit, so the structure they get back is that path's, not the
1461
+ // repository's: for an explainer, the same bound the Coverage tab accounts
1462
+ // for; for a design, the structure it has to fit.
1463
+ if (review && kindOf(review) !== "review") {
1343
1464
  const scope = review.binding.name;
1344
1465
  const g0 = scopeGraph(head, scope);
1345
1466
  const allFiles = await g.listFiles(t.worktree, t.pins.head);
@@ -1885,7 +2006,7 @@ const commands = {
1885
2006
  return {
1886
2007
  skill: installed,
1887
2008
  help: [
1888
- "Invoke them as /thurview and /review-fix in Claude Code, or by name in other agents",
2009
+ `Invoke them as ${names.map((n) => `/${n}`).join(", ")} in Claude Code, or by name in other agents`,
1889
2010
  "Run `thurview setup hooks` for ambient context at session start",
1890
2011
  ],
1891
2012
  };