thurview 0.9.0 → 0.11.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
@@ -72,19 +72,33 @@ CLI. It works with Claude Code, Codex, Cursor, OpenCode and every agent that
72
72
  reads the Agent Skills format:
73
73
 
74
74
  ```sh
75
- npx skills add Thurbeen/thurview --skill thurview
76
- npx skills add Thurbeen/thurview --skill forge-review # optional, see below
75
+ npx skills@latest add https://github.com/Thurbeen/thurview \
76
+ --skill thurview --agent universal claude-code --global --yes
77
+ npx skills@latest add https://github.com/Thurbeen/thurview \
78
+ --skill review-fix --agent universal claude-code --global --yes # optional, see below
77
79
  ```
78
80
 
81
+ `--global` installs for your user, so one install covers every repository.
82
+ `universal` puts the one real copy in `~/.agents/skills/thurview`, the directory
83
+ no single agent owns, and every other agent you name gets a symlink to it, such
84
+ as `~/.claude/skills/thurview` → `../../.agents/skills/thurview`, so an update
85
+ lands everywhere at once. Swap `claude-code` for any agent the skills CLI
86
+ supports, but keep `universal` and at least one more: with `--yes` and a single
87
+ target, the CLI copies instead of linking.
88
+
79
89
  That form tracks this repository's default branch: `skills update` takes
80
90
  whatever `main` holds, which can be ahead of the released command. To pin the
81
91
  skill to a release instead, install it from the tag, which the skill lock
82
92
  records and later updates keep:
83
93
 
84
94
  ```sh
85
- npx skills add https://github.com/Thurbeen/thurview/tree/v0.1.4/skills/thurview
95
+ npx skills@latest add https://github.com/Thurbeen/thurview/tree/v0.10.0/skills/thurview \
96
+ --agent universal claude-code --global --yes
86
97
  ```
87
98
 
99
+ Releases tag without committing, so nothing moves the tag above: swap in the
100
+ [latest release](https://github.com/Thurbeen/thurview/releases/latest).
101
+
88
102
  The npm package ships the same skill, so `thurview setup skill` links the copy
89
103
  that matches the command you have installed. Use that when you want the two to
90
104
  move together.
@@ -102,7 +116,7 @@ The review reasons over a code graph thurview builds itself from the pinned
102
116
  commits with tree-sitter, so nothing else needs installing. `thurview graph`
103
117
  answers which interfaces the change moved, what it reaches, who calls a
104
118
  symbol, what tests cover it and how files cluster, for TypeScript,
105
- JavaScript, Python, Go, Rust and Java.
119
+ JavaScript, Python, Go, Rust, Java and Elixir.
106
120
 
107
121
  To run from a checkout instead:
108
122
 
@@ -178,7 +192,7 @@ bar.
178
192
  | `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
179
193
  | `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
180
194
  | `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
181
- | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at the pinned commits |
195
+ | `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
182
196
  | `thurview forge status\|prior\|submit\|reply` | Read a change request through its forge, and post the review back |
183
197
  | `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
184
198
  | `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
@@ -194,11 +208,26 @@ arguments shows live state for the current directory instead of a manual.
194
208
  `thurview <command> --help` is the fallback. Progress and diagnostics go to
195
209
  stderr.
196
210
 
197
- ## Posting the review to the forge
211
+ ## Review and fix
212
+
213
+ The `review-fix` skill reviews a branch, a commit range or a pull or merge
214
+ request, fixes what it is sure of and reports the rest, with no browser and no
215
+ approval step. For each changed symbol it asks the code graph who calls it and
216
+ which tests reach it, so a finding can name a caller the diff never shows.
217
+ Fixes that pass the repository's own tests and lint land as one local commit;
218
+ nothing is pushed unless you ask.
219
+
220
+ ```sh
221
+ thurview graph impact --head HEAD # changed symbols, the callers they left alone, the tests
222
+ thurview graph callers discount --head HEAD # every call site of one symbol
223
+ ```
224
+
225
+ `--base` and `--head` ask about two commits directly; `--head` alone diffs
226
+ from where it forked from trunk. Each caller in `impact.reach` carries the line
227
+ of its call and whether any test reaches it.
198
228
 
199
- A thurview review is read in the browser. When the change is a pull request on
200
- GitHub or a merge request on GitLab, the `forge-review` skill posts it there as
201
- well: inline comments anchored to lines, a summary, and a verdict.
229
+ With `--post`, the skill posts the findings it did not fix as inline comments
230
+ on the change request, through `thurview forge`:
202
231
 
203
232
  ```sh
204
233
  thurview forge status --change 123 # what CI actually did, and whether it is a gate at all
@@ -224,7 +253,7 @@ and gitlab.com are matched against what those CLIs are authenticated for, and
224
253
  an unmatched host is refused rather than guessed. The differences that survive
225
254
  the seam - GitLab has no changes-requested state, no atomic review and no
226
255
  multi-line comment anchor - are listed in
227
- [skills/forge-review/references/forges.md](skills/forge-review/references/forges.md).
256
+ [skills/review-fix/references/forges.md](skills/review-fix/references/forges.md).
228
257
 
229
258
  ## Authoring format
230
259
 
package/dist/cli.js CHANGED
@@ -215,22 +215,43 @@ async function ensureServer() {
215
215
  function reviewUrl(base, id, view) {
216
216
  return `${base}/review/${id}${view ? `#/${view}` : ""}`;
217
217
  }
218
+ function pinnedOf(review) {
219
+ return { worktree: review.worktree, pins: review.pins, dir: reviewDir(review.id) };
220
+ }
218
221
  /**
219
- * The interface delta at a review's pins. The graphs are cached per commit under
220
- * the review directory, so publish and `graph interfaces` build them once between
222
+ * Base and head as `--base` and `--head` name them. Head defaults to HEAD and
223
+ * base to where head forked from trunk, so a branch is diffed against what it
224
+ * branched from rather than against wherever trunk has moved since.
225
+ */
226
+ async function pinRange(worktree, base, head, usage) {
227
+ try {
228
+ const h = await g.revParse(worktree, head ?? "HEAD");
229
+ const b = base
230
+ ? await g.revParse(worktree, base)
231
+ : await g.mergeBase(worktree, await g.trunkRef(worktree), h);
232
+ return { base: b, head: h };
233
+ }
234
+ catch (e) {
235
+ throw new AxiError(e.message, "VALIDATION_ERROR", [
236
+ `Pass resolvable refs: \`${usage}\``,
237
+ ]);
238
+ }
239
+ }
240
+ /**
241
+ * The interface delta at two pinned commits. The graphs are cached per commit
242
+ * under `at.dir`, so publish and `graph interfaces` build them once between
221
243
  * them; both modules load lazily to keep tree-sitter off every other command's path.
222
244
  */
223
- async function deltaFor(review, base, head) {
245
+ async function deltaFor(at, base, head) {
224
246
  const graph = await import("./graph.js");
225
247
  const { interfaceDelta } = await import("./interfaces.js");
226
- const dir = reviewDir(review.id);
227
- const b = base ?? (await graph.graphAt(review.worktree, review.pins.base, dir));
228
- const h = head ?? (await graph.graphAt(review.worktree, review.pins.head, dir));
229
- const changes = await g.lineChanges(review.worktree, review.pins.base, review.pins.head);
230
- const changed = await g.changedFiles(review.worktree, review.pins.base, review.pins.head);
248
+ const b = base ?? (await graph.graphAt(at.worktree, at.pins.base, at.dir));
249
+ const h = head ?? (await graph.graphAt(at.worktree, at.pins.head, at.dir));
250
+ const changes = await g.lineChanges(at.worktree, at.pins.base, at.pins.head);
251
+ const changed = await g.changedFiles(at.worktree, at.pins.base, at.pins.head);
231
252
  return interfaceDelta({
232
- cwd: review.worktree,
233
- pins: review.pins,
253
+ cwd: at.worktree,
254
+ pins: at.pins,
234
255
  base: b,
235
256
  head: h,
236
257
  impact: graph.impact(b, h, changes, 1),
@@ -438,12 +459,18 @@ const SPECS = {
438
459
  args: "interfaces|impact|callers <name>|tests-for <name>|architecture",
439
460
  flags: {
440
461
  review: { kind: "string", help: "review id prefix" },
462
+ base: {
463
+ kind: "string",
464
+ help: "instead of --review: base revision (default: trunk fork point)",
465
+ },
466
+ head: { kind: "string", help: "instead of --review: head revision (default: HEAD)" },
441
467
  graph: { kind: "string", help: "callers, tests-for: head or base", default: "head" },
442
468
  depth: { kind: "string", help: "how many caller hops to follow", default: "2" },
443
469
  },
444
470
  examples: [
445
471
  "thurview graph interfaces",
446
472
  "thurview graph impact",
473
+ "thurview graph impact --base main --head HEAD",
447
474
  "thurview graph callers login",
448
475
  "thurview graph tests-for login --graph base",
449
476
  "thurview graph architecture",
@@ -590,17 +617,7 @@ const commands = {
590
617
  const [bb, hh] = b?.kind === "range" && !str(p, "base") && !str(p, "head")
591
618
  ? b.name.split("..")
592
619
  : [str(p, "base"), str(p, "head")];
593
- try {
594
- head = await g.revParse(worktree, hh ?? "HEAD");
595
- base = bb
596
- ? await g.revParse(worktree, bb)
597
- : await g.mergeBase(worktree, await g.trunkRef(worktree), head);
598
- }
599
- catch (e) {
600
- throw new AxiError(e.message, "VALIDATION_ERROR", [
601
- "Pass resolvable refs: `thurview scaffold --base <ref> --head <ref>`",
602
- ]);
603
- }
620
+ ({ base, head } = await pinRange(worktree, bb, hh, "thurview scaffold --base <ref> --head <ref>"));
604
621
  binding = { kind: "range", name: `${base.slice(0, 12)}..${head.slice(0, 12)}` };
605
622
  }
606
623
  else {
@@ -896,7 +913,7 @@ const commands = {
896
913
  let interfaces = null;
897
914
  if (kind === "review") {
898
915
  try {
899
- interfaces = await deltaFor(review);
916
+ interfaces = await deltaFor(pinnedOf(review));
900
917
  }
901
918
  catch (e) {
902
919
  diags.push({
@@ -1229,17 +1246,37 @@ const commands = {
1229
1246
  const side = str(p, "graph") ?? "head";
1230
1247
  if (side !== "head" && side !== "base")
1231
1248
  throw new AxiError("--graph must be head or base", "VALIDATION_ERROR", help);
1249
+ const baseRef = str(p, "base");
1250
+ const headRef = str(p, "head");
1251
+ const commits = baseRef !== undefined || headRef !== undefined;
1252
+ if (commits && str(p, "review"))
1253
+ throw new AxiError("pass --review or --base/--head, not both", "VALIDATION_ERROR", help);
1232
1254
  const graph = await import("./graph.js");
1233
- const review = await resolveReview(str(p, "review"));
1234
- if (kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
1255
+ const review = commits ? null : await resolveReview(str(p, "review"));
1256
+ let t;
1257
+ if (review)
1258
+ t = pinnedOf(review);
1259
+ else {
1260
+ const worktree = await worktreeOf(process.cwd());
1261
+ if (!worktree)
1262
+ throw new AxiError("not inside a git repository", "VALIDATION_ERROR", [
1263
+ "Run inside the source worktree, or pass --review <id>",
1264
+ ]);
1265
+ // No review directory owns these graphs, and a commit's graph is the same
1266
+ // whoever asks, so they share one cache under the thurview home.
1267
+ const pins = await pinRange(worktree, baseRef, headRef, "thurview graph impact --base <ref>");
1268
+ t = { worktree, pins, dir: home() };
1269
+ }
1270
+ // A next step has to name the same commits, or it answers about another change.
1271
+ const again = review ? "" : ` --base ${short(t.pins.base)} --head ${short(t.pins.head)}`;
1272
+ if (review && kindOf(review) === "explainer" && (sub === "interfaces" || sub === "impact"))
1235
1273
  throw new AxiError(`graph ${sub} compares two commits; an explainer is pinned to one`, "VALIDATION_ERROR", [
1236
1274
  `Run \`thurview graph architecture --review ${short(review.id)}\` for the structure at that commit`,
1237
1275
  `Run \`thurview graph callers <name> --review ${short(review.id)}\` to follow one symbol`,
1238
1276
  ]);
1239
- const dir = reviewDir(review.id);
1240
- const at = (commit) => graph.graphAt(review.worktree, commit, dir);
1277
+ const at = (commit) => graph.graphAt(t.worktree, commit, t.dir);
1241
1278
  if (sub === "callers" || sub === "tests-for") {
1242
- const g = await at(side === "base" ? review.pins.base : review.pins.head);
1279
+ const g = await at(side === "base" ? t.pins.base : t.pins.head);
1243
1280
  const pins = {
1244
1281
  graph: side,
1245
1282
  commit: short(g.commit),
@@ -1251,25 +1288,25 @@ const commands = {
1251
1288
  ...pins,
1252
1289
  symbol: name,
1253
1290
  callers: graph.callers(g, name),
1254
- help: [`Run \`thurview graph tests-for ${name}\` to see what exercises it`],
1291
+ help: [`Run \`thurview graph tests-for ${name}${again}\` to see what exercises it`],
1255
1292
  };
1256
1293
  return {
1257
1294
  ...pins,
1258
1295
  symbol: name,
1259
1296
  depth,
1260
1297
  tests: graph.testsFor(g, name, depth),
1261
- help: [`Run \`thurview graph callers ${name}\` for every reference`],
1298
+ help: [`Run \`thurview graph callers ${name}${again}\` for every reference`],
1262
1299
  };
1263
1300
  }
1264
- const base = await at(review.pins.base);
1265
- const head = await at(review.pins.head);
1301
+ const base = await at(t.pins.base);
1302
+ const head = await at(t.pins.head);
1266
1303
  const pins = {
1267
1304
  base: short(base.commit),
1268
1305
  head: short(head.commit),
1269
1306
  languages: graph.LANGUAGES.join(","),
1270
1307
  };
1271
1308
  if (sub === "interfaces") {
1272
- const delta = await deltaFor(review, base, head);
1309
+ const delta = await deltaFor(t, base, head);
1273
1310
  return {
1274
1311
  ...pins,
1275
1312
  verdict: delta.verdict,
@@ -1287,29 +1324,31 @@ const commands = {
1287
1324
  unreadable: delta.unreadable,
1288
1325
  truncated: delta.truncated,
1289
1326
  help: [
1290
- "Write one capability line per entry in data.yaml under `interfaces`, keyed by id",
1291
- "Run `thurview graph callers <name>` to see who a removed or changed interface reached",
1327
+ ...(review
1328
+ ? ["Write one capability line per entry in data.yaml under `interfaces`, keyed by id"]
1329
+ : []),
1330
+ `Run \`thurview graph callers <name>${again}\` to see who a removed or changed interface reached`,
1292
1331
  ],
1293
1332
  };
1294
1333
  }
1295
1334
  if (sub === "impact") {
1296
- const changes = await g.lineChanges(review.worktree, review.pins.base, review.pins.head);
1335
+ const changes = await g.lineChanges(t.worktree, t.pins.base, t.pins.head);
1297
1336
  return {
1298
1337
  ...pins,
1299
1338
  depth,
1300
1339
  ...graph.impact(base, head, changes, depth),
1301
1340
  help: [
1302
- "Run `thurview graph callers <name>` to follow one symbol",
1303
- "Run `thurview graph architecture` for the module structure and its diff",
1341
+ `Run \`thurview graph callers <name>${again}\` to follow one symbol`,
1342
+ `Run \`thurview graph architecture${again}\` for the module structure and its diff`,
1304
1343
  ],
1305
1344
  };
1306
1345
  }
1307
1346
  // An explainer is scoped to a path, so its structure is that path's, not the
1308
1347
  // repository's: the same bound the Coverage tab accounts for.
1309
- if (kindOf(review) === "explainer") {
1348
+ if (review && kindOf(review) === "explainer") {
1310
1349
  const scope = review.binding.name;
1311
1350
  const g0 = scopeGraph(head, scope);
1312
- const allFiles = await g.listFiles(review.worktree, review.pins.head);
1351
+ const allFiles = await g.listFiles(t.worktree, t.pins.head);
1313
1352
  const { diff: _diff, truncated: _truncated, ...rest } = graph.architecture(g0, g0);
1314
1353
  return {
1315
1354
  commit: short(head.commit),
@@ -1787,7 +1826,7 @@ const commands = {
1787
1826
  return {
1788
1827
  skill: installed,
1789
1828
  help: [
1790
- "Invoke them as /thurview and /forge-review in Claude Code, or by name in other agents",
1829
+ "Invoke them as /thurview and /review-fix in Claude Code, or by name in other agents",
1791
1830
  "Run `thurview setup hooks` for ambient context at session start",
1792
1831
  ],
1793
1832
  };