thurview 0.9.0 → 0.10.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 +22 -7
- package/dist/cli.js +79 -40
- package/dist/cli.js.map +1 -1
- package/dist/graph.js +32 -2
- package/dist/graph.js.map +1 -1
- package/package.json +1 -1
- package/skills/review-fix/SKILL.md +165 -0
- package/skills/thurview/SKILL.md +3 -3
- package/skills/forge-review/SKILL.md +0 -276
- package/skills/forge-review/references/security-surfaces.md +0 -54
- /package/skills/{forge-review → review-fix}/references/forges.md +0 -0
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ reads the Agent Skills format:
|
|
|
73
73
|
|
|
74
74
|
```sh
|
|
75
75
|
npx skills add Thurbeen/thurview --skill thurview
|
|
76
|
-
npx skills add Thurbeen/thurview --skill
|
|
76
|
+
npx skills add Thurbeen/thurview --skill review-fix # optional, see below
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
That form tracks this repository's default branch: `skills update` takes
|
|
@@ -178,7 +178,7 @@ bar.
|
|
|
178
178
|
| `thurview open --review ID [--view T]` | Start the server if needed and open the browser |
|
|
179
179
|
| `thurview wait --review ID [--timeout S]` | Block until the reader needs the agent |
|
|
180
180
|
| `thurview threads list\|get\|reply\|resolve` | Read and answer threads |
|
|
181
|
-
| `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at
|
|
181
|
+
| `thurview graph interfaces\|impact\|callers\|tests-for\|architecture` | Ask the code graph at a review's pins, or at `--base`/`--head` |
|
|
182
182
|
| `thurview forge status\|prior\|submit\|reply` | Read a change request through its forge, and post the review back |
|
|
183
183
|
| `thurview serve` / `thurview stop` | Run the server in the foreground / stop the background one |
|
|
184
184
|
| `thurview setup hooks\|skill\|status` | Session hooks, agent skill, install state |
|
|
@@ -194,11 +194,26 @@ arguments shows live state for the current directory instead of a manual.
|
|
|
194
194
|
`thurview <command> --help` is the fallback. Progress and diagnostics go to
|
|
195
195
|
stderr.
|
|
196
196
|
|
|
197
|
-
##
|
|
197
|
+
## Review and fix
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
The `review-fix` skill reviews a branch, a commit range or a pull or merge
|
|
200
|
+
request, fixes what it is sure of and reports the rest, with no browser and no
|
|
201
|
+
approval step. For each changed symbol it asks the code graph who calls it and
|
|
202
|
+
which tests reach it, so a finding can name a caller the diff never shows.
|
|
203
|
+
Fixes that pass the repository's own tests and lint land as one local commit;
|
|
204
|
+
nothing is pushed unless you ask.
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
thurview graph impact --head HEAD # changed symbols, the callers they left alone, the tests
|
|
208
|
+
thurview graph callers discount --head HEAD # every call site of one symbol
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`--base` and `--head` ask about two commits directly; `--head` alone diffs
|
|
212
|
+
from where it forked from trunk. Each caller in `impact.reach` carries the line
|
|
213
|
+
of its call and whether any test reaches it.
|
|
214
|
+
|
|
215
|
+
With `--post`, the skill posts the findings it did not fix as inline comments
|
|
216
|
+
on the change request, through `thurview forge`:
|
|
202
217
|
|
|
203
218
|
```sh
|
|
204
219
|
thurview forge status --change 123 # what CI actually did, and whether it is a gate at all
|
|
@@ -224,7 +239,7 @@ and gitlab.com are matched against what those CLIs are authenticated for, and
|
|
|
224
239
|
an unmatched host is refused rather than guessed. The differences that survive
|
|
225
240
|
the seam - GitLab has no changes-requested state, no atomic review and no
|
|
226
241
|
multi-line comment anchor - are listed in
|
|
227
|
-
[skills/
|
|
242
|
+
[skills/review-fix/references/forges.md](skills/review-fix/references/forges.md).
|
|
228
243
|
|
|
229
244
|
## Authoring format
|
|
230
245
|
|
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
|
-
*
|
|
220
|
-
*
|
|
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(
|
|
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
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
const
|
|
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:
|
|
233
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
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" ?
|
|
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(
|
|
1265
|
-
const head = await at(
|
|
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(
|
|
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
|
-
|
|
1291
|
-
|
|
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(
|
|
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
|
-
|
|
1303
|
-
|
|
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(
|
|
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 /
|
|
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
|
};
|