thurview 0.19.0 → 0.21.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.
Files changed (44) hide show
  1. package/README.md +3 -5
  2. package/dist/cli.js +149 -42
  3. package/dist/cli.js.map +1 -1
  4. package/dist/diff.js +3 -3
  5. package/dist/diff.js.map +1 -1
  6. package/dist/document/compile.js +1 -1
  7. package/dist/document/compile.js.map +1 -1
  8. package/dist/export.js +10 -25
  9. package/dist/export.js.map +1 -1
  10. package/dist/forge/github.js +41 -0
  11. package/dist/forge/github.js.map +1 -1
  12. package/dist/forge/gitlab.js +84 -23
  13. package/dist/forge/gitlab.js.map +1 -1
  14. package/dist/forge/types.js +4 -0
  15. package/dist/forge/types.js.map +1 -1
  16. package/dist/git.js +0 -9
  17. package/dist/git.js.map +1 -1
  18. package/dist/highlight-theme.js +37 -14
  19. package/dist/highlight-theme.js.map +1 -1
  20. package/dist/highlight.js +3 -15
  21. package/dist/highlight.js.map +1 -1
  22. package/dist/pr-review/categories.js +51 -0
  23. package/dist/pr-review/categories.js.map +1 -0
  24. package/dist/pr-review/follow.js +208 -0
  25. package/dist/pr-review/follow.js.map +1 -0
  26. package/dist/pr-review/format.js +227 -0
  27. package/dist/pr-review/format.js.map +1 -0
  28. package/dist/server/server.js +32 -47
  29. package/dist/server/server.js.map +1 -1
  30. package/dist/ui/app.css +65 -30
  31. package/dist/ui/app.js +15 -16
  32. package/dist/ui/app.js.map +2 -2
  33. package/dist/ui/index.html +21 -0
  34. package/package.json +1 -1
  35. package/skills/thurview/SKILL.md +10 -20
  36. package/skills/thurview/references/document-authoring.md +1 -2
  37. package/skills/thurview/references/lifecycle.md +0 -1
  38. package/skills/thurview-design/SKILL.md +4 -7
  39. package/skills/thurview-explain/SKILL.md +6 -7
  40. package/skills/thurview-pr-review/SKILL.md +181 -0
  41. package/skills/thurview-pr-review/references/forges.md +47 -0
  42. package/dist/theme.js +0 -290
  43. package/dist/theme.js.map +0 -1
  44. package/skills/thurview/references/theme.md +0 -89
package/README.md CHANGED
@@ -74,6 +74,8 @@ Three kinds of document, and one companion that writes none:
74
74
  - **`thurview-fix`** - no browser and no reader. It reviews, fixes what it is
75
75
  sure of behind the repository's own tests and lint, and reports the rest -
76
76
  optionally as inline comments on the change request.
77
+ - **`thurview-pr-review`** - reviews a change request on the forge and follows
78
+ it to merge: one summary edited in place, one resolvable thread per finding.
77
79
 
78
80
  <details>
79
81
  <summary><b>Other ways to install</b> - one skill at a time, pin to a release,
@@ -263,9 +265,7 @@ repository's Pages folder.
263
265
  claims a reader is there when none is: a question asked with no agent
264
266
  attached is queued, not lost, and reaches the agent the next time it checks.
265
267
  - **Revisions**: every publish is sealed; switch back to earlier ones.
266
- - **Theme**: the agent reads the project's design tokens and fonts and
267
- publishes them with the review, so each review looks like the code it
268
- explains. The default skin applies when the project has none.
268
+ - **Theme**: light or dark, following the system until you pick one in the ⋯ menu.
269
269
 
270
270
  Everything runs locally against your checkout. The server listens on
271
271
  loopback and, when present, your Tailscale address, so a phone or another
@@ -373,8 +373,6 @@ The agent writes three files in `~/.thurview/reviews/<id>/`:
373
373
  - `map.yaml`: the software map at head, optionally at base. In an explainer it
374
374
  carries the breadth the prose has no room for, and a node's `files` globs are
375
375
  what let a file count as placed rather than not examined.
376
- - `theme.yaml`: the look, derived from the reviewed project's own design
377
- system (tokens, fonts, shape, code palette). Empty means the default skin.
378
376
 
379
377
  `security` is a review's own dimension, shown to the reader under the interface
380
378
  delta rather than left to a section the agent might not write. It is
package/dist/cli.js CHANGED
@@ -12,8 +12,6 @@ import * as g from "./git.js";
12
12
  import { SCHEMA, home, newId, now, readReview, writeReview, listReviews, reviewsFor, reviewDir, revisionDir, passFile, readThreads, readText, writeText, writeJson, readJson, serverStateFile, deleteReview, kindOf, } from "./store.js";
13
13
  import { compileDocument, compileMap, globToRegExp } from "./document/compile.js";
14
14
  import { computeCoverage, scopeGlob } from "./coverage.js";
15
- import { parseTheme, compileTheme } from "./theme.js";
16
- import { registerTheme } from "./highlight.js";
17
15
  import { replyThread, setThreadStatus, needsAgent } from "./threads.js";
18
16
  import { targetLabel, truncate } from "./thread-state.js";
19
17
  import { attach } from "./presence.js";
@@ -23,6 +21,9 @@ import { parseFlags, helpFor, str, bool } from "./flags.js";
23
21
  import { forgeFor, repoOf, summariseCi, } from "./forge/index.js";
24
22
  import { parseSubmission, longComments, buildPass } from "./forge/submission.js";
25
23
  import { recordForgeFacts, ciFacts } from "./queue.js";
24
+ import { parsePass } from "./pr-review/format.js";
25
+ import { CATEGORIES, CONFIDENCE } from "./pr-review/categories.js";
26
+ import { readState, startReview, stopReview, sync, waitForEvent, STOP_LABEL, } from "./pr-review/follow.js";
26
27
  import { VERSION } from "./version.js";
27
28
  const execFileP = promisify(execFile);
28
29
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -277,18 +278,6 @@ const TEMPLATE_MAP = `# Software map: where this change landed in the system, an
277
278
  nodes: []
278
279
  edges: []
279
280
  `;
280
- const TEMPLATE_THEME = `# Look of this review, derived from the reviewed project's own design system.
281
- # Leave this file empty (or delete it) for the default skin. See the thurview skill
282
- # reference (references/theme.md) for every key. Example:
283
- #
284
- # name: acme-web
285
- # source: tailwind.config.ts, src/styles/tokens.css
286
- # mode: light
287
- # colors: { bg: "#ffffff", bg2: "#f6f7f9", fg: "#111827", fg2: "#4b5563", muted: "#9ca3af", line: "#e5e7eb", accent: "#2563eb", link: "#2563eb", ok: "#16a34a", warn: "#d97706", del: "#dc2626" }
288
- # fonts: { display: "Inter, sans-serif", body: "Inter, sans-serif", mono: "'JetBrains Mono', monospace", stylesheets: ["https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"] }
289
- # shape: { radius: 8px, bevel: false, glow: false, scanlines: false, headingTransform: none }
290
- # code: { keyword: "#7c3aed", string: "#15803d", function: "#b45309", variable: "#0369a1", comment: "#9ca3af" }
291
- `;
292
281
  const TEMPLATE_EXPLAIN_MD = (title) => `# ${title}
293
282
 
294
283
  **Summary**
@@ -465,7 +454,6 @@ async function pinOneCommit(p, k) {
465
454
  await writeText(join(reviewDir(id), "review.md"), k.templates.md(review.title));
466
455
  await writeText(join(reviewDir(id), "data.yaml"), k.templates.data);
467
456
  await writeText(join(reviewDir(id), "map.yaml"), k.templates.map);
468
- await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
469
457
  await writeReview(review);
470
458
  return { review, reused: false, scope, commit, worktree, inScope };
471
459
  }
@@ -543,7 +531,7 @@ const SPECS = {
543
531
  examples: ["thurview info", "thurview info --all --fields pins,inSync"],
544
532
  },
545
533
  publish: {
546
- description: "Validate review.md, data.yaml, map.yaml and theme.yaml against the pins and seal a revision",
534
+ description: "Validate review.md, data.yaml and map.yaml against the pins and seal a revision",
547
535
  flags: {
548
536
  review: {
549
537
  kind: "string",
@@ -664,6 +652,33 @@ const SPECS = {
664
652
  'thurview forge reply <threadId> --body "<answer>" --resolve --at <sha>',
665
653
  ],
666
654
  },
655
+ "pr-review": {
656
+ description: "Follow a pull or merge request until it merges: one summary edited in place, one resolvable thread per finding",
657
+ args: "status|sync --file <pass.json>|wait|stop|start",
658
+ flags: {
659
+ change: { kind: "string", help: "change request number or URL" },
660
+ review: { kind: "string", help: "review id prefix; its binding names the change request" },
661
+ forge: {
662
+ kind: "string",
663
+ help: "github or gitlab, when the host is not one of the two known ones",
664
+ },
665
+ repo: { kind: "string", help: "host/path, when `origin` is not the repository to post to" },
666
+ file: { kind: "string", help: "sync: the JSON pass to post" },
667
+ "dry-run": { kind: "boolean", help: "sync: validate and render, post nothing" },
668
+ interval: { kind: "string", help: "wait: seconds between polls", default: "120" },
669
+ timeout: {
670
+ kind: "string",
671
+ help: "wait: seconds before answering `none`, so one call fits a tool limit",
672
+ default: "540",
673
+ },
674
+ },
675
+ examples: [
676
+ "thurview pr-review status --change 123",
677
+ "thurview pr-review sync --change 123 --file pass.json --dry-run",
678
+ "thurview pr-review wait --change 123",
679
+ "thurview pr-review stop --change 123",
680
+ ],
681
+ },
667
682
  delete: {
668
683
  description: "Delete a review and everything stored for it (the code is untouched)",
669
684
  flags: { review: { kind: "string", help: "review id prefix (required)" } },
@@ -845,7 +860,6 @@ const commands = {
845
860
  await writeText(join(reviewDir(id), "review.md"), TEMPLATE_MD(review.title));
846
861
  await writeText(join(reviewDir(id), "data.yaml"), TEMPLATE_DATA);
847
862
  await writeText(join(reviewDir(id), "map.yaml"), TEMPLATE_MAP);
848
- await writeText(join(reviewDir(id), "theme.yaml"), TEMPLATE_THEME);
849
863
  await writeReview(review);
850
864
  }
851
865
  }
@@ -871,7 +885,6 @@ const commands = {
871
885
  document: join(dir, "review.md"),
872
886
  data: join(dir, "data.yaml"),
873
887
  map: join(dir, "map.yaml"),
874
- theme: join(dir, "theme.yaml"),
875
888
  },
876
889
  change: stat,
877
890
  guidance: await guidanceFiles(worktree),
@@ -907,7 +920,6 @@ const commands = {
907
920
  document: join(dir, "review.md"),
908
921
  data: join(dir, "data.yaml"),
909
922
  map: join(dir, "map.yaml"),
910
- theme: join(dir, "theme.yaml"),
911
923
  },
912
924
  scale: { filesInScope: pinned.inScope.length },
913
925
  guidance: await guidanceFiles(worktree),
@@ -941,7 +953,6 @@ const commands = {
941
953
  document: join(dir, "review.md"),
942
954
  data: join(dir, "data.yaml"),
943
955
  map: join(dir, "map.yaml"),
944
- theme: join(dir, "theme.yaml"),
945
956
  },
946
957
  scale: { filesInScope: pinned.inScope.length },
947
958
  guidance: await guidanceFiles(worktree),
@@ -1007,22 +1018,13 @@ const commands = {
1007
1018
  }
1008
1019
  }
1009
1020
  const diags = [];
1010
- let theme = null;
1011
- if (themeYaml) {
1012
- const t = parseTheme(themeYaml);
1013
- diags.push(...t.diagnostics);
1014
- if (t.theme) {
1015
- theme = compileTheme(t.theme, (p) => `/api/reviews/${review.id}/blob?path=${encodeURIComponent(p)}`);
1016
- for (const f of t.theme.fonts.files)
1017
- if (!(await g.fileExists(review.worktree, review.pins.head, f.path)))
1018
- diags.push({
1019
- level: "error",
1020
- file: "theme.yaml",
1021
- message: `fonts.files: ${f.path} does not exist at the pinned head commit`,
1022
- });
1023
- }
1024
- }
1025
- const themeName = theme ? await registerTheme(theme.shiki) : undefined;
1021
+ // an older scaffold wrote a comments-only template; only a theme someone wrote is worth a warning
1022
+ if (themeYaml?.replace(/^\s*#.*$/gm, "").trim())
1023
+ diags.push({
1024
+ level: "warning",
1025
+ file: "theme.yaml",
1026
+ message: "ignored: a review no longer restyles thurview's own light and dark theme; delete the file",
1027
+ });
1026
1028
  const kind = kindOf(review);
1027
1029
  const doc = await compileDocument({
1028
1030
  cwd: review.worktree,
@@ -1030,7 +1032,6 @@ const commands = {
1030
1032
  reviewMd,
1031
1033
  dataYaml: dataYaml ?? "",
1032
1034
  kind,
1033
- ...(themeName ? { themeName } : {}),
1034
1035
  });
1035
1036
  diags.push(...doc.diagnostics);
1036
1037
  let map = null;
@@ -1127,9 +1128,6 @@ const commands = {
1127
1128
  await writeJson(join(rdir, "map.json"), map);
1128
1129
  await writeJson(join(rdir, "changes.json"), changes);
1129
1130
  await writeJson(join(rdir, "coverage.json"), coverage);
1130
- if (themeYaml !== null)
1131
- await cp(join(dir, "theme.yaml"), join(rdir, "theme.yaml"));
1132
- await writeJson(join(rdir, "theme.json"), theme);
1133
1131
  await writeJson(join(rdir, "meta.json"), {
1134
1132
  revision: n,
1135
1133
  at: now(),
@@ -1137,7 +1135,6 @@ const commands = {
1137
1135
  pins: review.pins,
1138
1136
  kind,
1139
1137
  hasMap: !!map,
1140
- theme: theme?.name ?? "default",
1141
1138
  });
1142
1139
  review.title = doc.document.title;
1143
1140
  review.revision = n;
@@ -1168,7 +1165,6 @@ const commands = {
1168
1165
  interfaces: doc.document.interfaces?.verdict ?? "(unavailable)",
1169
1166
  security: doc.document.security?.verdict ?? "(unavailable)",
1170
1167
  }),
1171
- theme: theme?.name ?? "default",
1172
1168
  url: url ?? "(server not running)",
1173
1169
  },
1174
1170
  };
@@ -1910,6 +1906,117 @@ const commands = {
1910
1906
  help: ["Run `thurview setup hooks` or `thurview setup skill` to install what is missing"],
1911
1907
  };
1912
1908
  },
1909
+ async "pr-review"(args) {
1910
+ const sub = args[0];
1911
+ const subs = ["status", "sync", "wait", "stop", "start"];
1912
+ if (!sub || !subs.includes(sub))
1913
+ throw new AxiError(`unknown pr-review command${sub ? ` ${sub}` : ""}`, "VALIDATION_ERROR", subs.map((x) => `thurview pr-review ${x} --change <ref>`));
1914
+ const s = spec("pr-review").flags;
1915
+ const p = parseFlags(`pr-review ${sub}`, args.slice(1), s);
1916
+ const ctx = await forgeContext(p);
1917
+ const where = `${ctx.repo.path}#${ctx.cr.number}`;
1918
+ const again = `thurview pr-review wait --change ${ctx.cr.number}`;
1919
+ if (sub === "status") {
1920
+ const st = await readState(ctx);
1921
+ const open = st.findings.filter((f) => f.open);
1922
+ return {
1923
+ change: {
1924
+ url: ctx.cr.url,
1925
+ state: ctx.cr.state,
1926
+ author: ctx.cr.author,
1927
+ head: ctx.cr.head,
1928
+ base: ctx.cr.base,
1929
+ baseBranch: ctx.cr.baseBranch,
1930
+ },
1931
+ review: st.summary
1932
+ ? {
1933
+ state: st.summary.marker.state,
1934
+ reviewedHead: st.summary.marker.head,
1935
+ pushedSince: st.summary.marker.head !== ctx.cr.head,
1936
+ }
1937
+ : "none yet (the first pass reviews the whole change)",
1938
+ ...(st.stop ? { stopRequested: st.stop } : {}),
1939
+ open: open.length
1940
+ ? open.map((f) => ({
1941
+ id: f.id,
1942
+ category: f.category,
1943
+ severity: f.severity,
1944
+ at: f.path ? `${f.path}:${f.line ?? ""}` : "",
1945
+ title: f.title,
1946
+ }))
1947
+ : "0 (no open finding)",
1948
+ categories: Object.fromEntries(Object.entries(CATEGORIES).map(([k, v]) => [k, v.definition])),
1949
+ confidence: CONFIDENCE,
1950
+ help: [
1951
+ st.summary && st.summary.marker.head !== ctx.cr.head
1952
+ ? `Review only \`git diff ${st.summary.marker.head.slice(0, 12)} ${ctx.cr.head.slice(0, 12)}\`, then decide each open finding: still valid, or fixed`
1953
+ : `Review \`git diff ${ctx.cr.base.slice(0, 12)} ${ctx.cr.head.slice(0, 12)}\``,
1954
+ `Write the pass file and run \`thurview pr-review sync --change ${ctx.cr.number} --file <pass.json> --dry-run\``,
1955
+ ],
1956
+ };
1957
+ }
1958
+ if (sub === "sync") {
1959
+ const file = str(p, "file");
1960
+ if (!file)
1961
+ throw new AxiError("--file is required", "VALIDATION_ERROR", [
1962
+ "thurview pr-review sync --change <ref> --file <pass.json>",
1963
+ ]);
1964
+ const text = await readText(resolve(process.cwd(), file));
1965
+ if (text === null)
1966
+ throw new AxiError(`${file} not found`, "NOT_FOUND", []);
1967
+ const dry = bool(p, "dry-run");
1968
+ const r = await sync(ctx, parsePass(text, file), { dryRun: dry });
1969
+ return {
1970
+ pass: {
1971
+ change: where,
1972
+ head: r.head,
1973
+ summary: r.summary,
1974
+ open: r.open,
1975
+ blocking: r.blocking,
1976
+ },
1977
+ posted: r.posted.length ? r.posted : "0 (no new finding)",
1978
+ resolved: r.resolved.length ? r.resolved : "0 (no finding fixed)",
1979
+ duplicates: r.duplicates.length
1980
+ ? r.duplicates
1981
+ : "0 (no finding already open was found again)",
1982
+ ...(dry ? { body: r.body } : {}),
1983
+ help: dry
1984
+ ? ["Nothing was posted; re-run without --dry-run to post it"]
1985
+ : [
1986
+ `Run \`${again}\` to block until the next push, merge or stop`,
1987
+ "Never approve, merge or push to the change request; the verdict is the summary",
1988
+ ],
1989
+ };
1990
+ }
1991
+ if (sub === "wait") {
1992
+ const interval = Number(str(p, "interval"));
1993
+ const timeout = Number(str(p, "timeout"));
1994
+ if (!(interval > 0) || !(timeout > 0))
1995
+ throw new AxiError("--interval and --timeout take seconds", "VALIDATION_ERROR", []);
1996
+ const ev = await waitForEvent(ctx.forge, ctx.repo, ctx.cr.number, { interval, timeout });
1997
+ const help = ev.event === "push"
1998
+ ? [
1999
+ ev.since
2000
+ ? `Review only \`git diff ${ev.since.slice(0, 12)} ${ev.head.slice(0, 12)}\`, then sync`
2001
+ : "Nothing posted yet: review the whole change, then sync",
2002
+ ]
2003
+ : ev.event === "none"
2004
+ ? [`Nothing new; run \`${again}\` again`]
2005
+ : ["The follow loop ends here; the summary says why"];
2006
+ return { event: { ...ev, change: where }, help };
2007
+ }
2008
+ if (sub === "stop") {
2009
+ await stopReview(ctx);
2010
+ return {
2011
+ stopped: where,
2012
+ help: [
2013
+ `Resume with \`thurview pr-review start --change ${ctx.cr.number}\`; a reader stops with the ${STOP_LABEL} label or a /thurview stop comment`,
2014
+ ],
2015
+ };
2016
+ }
2017
+ await startReview(ctx);
2018
+ return { started: where, help: [`Run \`${again}\``] };
2019
+ },
1913
2020
  async skill(args) {
1914
2021
  parseFlags("skill", args, {});
1915
2022
  const skills = {};