projectstore-claude 0.29.2 → 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 (76) hide show
  1. package/README.md +1 -1
  2. package/bin/projectstore-claude.mjs +1 -1
  3. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  4. package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
  5. package/node_modules/projectstore/README.md +44 -7
  6. package/node_modules/projectstore/commands/bar.md +30 -0
  7. package/node_modules/projectstore/commands/bind.md +16 -20
  8. package/node_modules/projectstore/commands/doctor.md +12 -7
  9. package/node_modules/projectstore/commands/scaffold.md +24 -16
  10. package/node_modules/projectstore/commands/status.md +1 -1
  11. package/node_modules/projectstore/commands/statusline.md +2 -2
  12. package/node_modules/projectstore/commands/story.md +15 -2
  13. package/node_modules/projectstore/docs/alongside.md +167 -0
  14. package/node_modules/projectstore/docs/extending.md +17 -3
  15. package/node_modules/projectstore/docs/getting-started.md +18 -9
  16. package/node_modules/projectstore/docs/harnesses.md +1 -1
  17. package/node_modules/projectstore/docs/how-it-works.md +12 -3
  18. package/node_modules/projectstore/harnesses/claude-code.json +23 -8
  19. package/node_modules/projectstore/harnesses/codex.json +16 -6
  20. package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
  21. package/node_modules/projectstore/hooks/band.tsx +622 -0
  22. package/node_modules/projectstore/hooks/hooks.json +1 -0
  23. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  24. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  25. package/node_modules/projectstore/hooks/session-start.mjs +56 -20
  26. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  27. package/node_modules/projectstore/package.json +12 -2
  28. package/node_modules/projectstore/scaffold/headings.json +8 -0
  29. package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
  30. package/node_modules/projectstore/scripts/band.mjs +207 -0
  31. package/node_modules/projectstore/scripts/binding.mjs +733 -62
  32. package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
  33. package/node_modules/projectstore/scripts/cli.mjs +1463 -267
  34. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  35. package/node_modules/projectstore/scripts/convergence.mjs +638 -0
  36. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  37. package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
  38. package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
  39. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  40. package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
  41. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  42. package/node_modules/projectstore/scripts/harness.mjs +283 -37
  43. package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
  44. package/node_modules/projectstore/scripts/kanban.mjs +6 -21
  45. package/node_modules/projectstore/scripts/lib.mjs +933 -199
  46. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  47. package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
  48. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  49. package/node_modules/projectstore/scripts/query.mjs +441 -72
  50. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  51. package/node_modules/projectstore/scripts/run-host.mjs +139 -0
  52. package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
  53. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  54. package/node_modules/projectstore/scripts/stages.mjs +84 -0
  55. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  56. package/node_modules/projectstore/scripts/statusline.mjs +55 -14
  57. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  58. package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
  59. package/node_modules/projectstore/scripts/term.mjs +1027 -52
  60. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  61. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  62. package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
  63. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  64. package/node_modules/projectstore/templates/de/strings.json +63 -1
  65. package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
  66. package/node_modules/projectstore/templates/en/strings.json +63 -1
  67. package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
  68. package/node_modules/projectstore/templates/es/strings.json +63 -1
  69. package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
  70. package/node_modules/projectstore/templates/fr/strings.json +63 -1
  71. package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
  72. package/node_modules/projectstore/templates/ru/strings.json +63 -1
  73. package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
  74. package/node_modules/projectstore/templates/zh/strings.json +63 -1
  75. package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
  76. package/package.json +2 -2
@@ -0,0 +1,253 @@
1
+ // projectstore — doctor-report.mjs: doctor's text report, drawn from its
2
+ // findings (the presentation spec, *Terminal presentation of the projectstore
3
+ // CLI: layout, glyphs, colour roles, questions and live lines*, contract 11).
4
+ //
5
+ // A renderer, not a second doctor: it imports no check and is pure — it
6
+ // returns text and writes nothing. That is why doctor.mjs itself stays
7
+ // spawned, never imported (the MCP ADR's decision 2 table), while this module
8
+ // is imported by both readers of the findings: the bin, which renders the
9
+ // findings of its one `--json` spawn in its own process (so a terminal gets
10
+ // colour), and doctor.mjs's own text mode, through a dynamic import. The
11
+ // dynamic import keeps the SessionStart hook's static graph — which reaches
12
+ // doctor.mjs — as it was; this module imports only term.mjs and lib.mjs.
13
+ //
14
+ // report(findings, groups, { caps, verbose, version, project, env }) → text
15
+ // CHECKS → the check table
16
+ //
17
+ // The layout: a badge with the version and the project's folder name; per
18
+ // section (INSTALL, VAULT) a heading with its counts and a rule; issues, then
19
+ // warnings, then notes; the Summary line last, verbatim — the write ceremony
20
+ // reads it (commands/story.md step 5a, agents/clerk.md). That line is
21
+ // lib.mjs's doctorSummaryLine, so a caller's fallback ends with it too.
22
+ //
23
+ // Two readers, one layout:
24
+ // - plain (a pipe, an agent's Bash tool) prints every finding, its message
25
+ // whole on one line with its path, and every note. Agents read it, and the
26
+ // vocabulary suite's about-filter cuts a finding out by its exact message;
27
+ // - a terminal gets the same report compacted: a cause with many instances
28
+ // lists each by its capture and path, at most eight, and the notes of the
29
+ // folding rows fold into one line. `--verbose` turns the compaction off.
30
+ // Every entry keeps its `[check]` id: the `--fix` walk of commands/doctor.md
31
+ // keys on it.
32
+
33
+ import { PLAIN, painter, icon, badge, heading, rule, rowLines, wrap, commandBlock } from "./term.mjs";
34
+ import { commandForm, doctorSummaryLine } from "./lib.mjs";
35
+
36
+ // ─── The check table ─────────────────────────────────────────────────────
37
+ //
38
+ // One row per check id that is grouped, fixed or folded; a check with no row
39
+ // prints each finding as it is. A row is { instance, title, fix, folds }:
40
+ // - instance: a pattern over the message, with zero or one capture. Two or
41
+ // more findings of one check and one level that match it form one entry:
42
+ // the title once, then each finding's capture and path. A finding that
43
+ // does not match — and a finding about another harness (`about`), whose
44
+ // exact message the vocabulary suite cuts out — prints on its own.
45
+ // - fix: what to do, listed once under the check's entries at each level,
46
+ // only for a check whose messages carry no command (contract 11): a
47
+ // sentence, or { say, command } where the command is composed through
48
+ // commandForm and stands on its own line (contract 6). Each is worded to
49
+ // be true for every message its check can carry.
50
+ // - folds: notes of this check fold on a terminal — true for all of them,
51
+ // or a pattern for the ones it names. The offer checks (doctor.mjs's
52
+ // OFFER_CHECKS: upgrade, layout-legacy) never fold, so they have no row.
53
+ const row = ({ instance = null, title = null, fix = null, folds = false }) =>
54
+ Object.freeze({ instance, title, fix, folds });
55
+
56
+ export const CHECKS = Object.freeze({
57
+ // Fixes: the checks whose messages name no command.
58
+ "code-refs": row({
59
+ instance: /^code_refs path "([^"]+)" does not resolve/,
60
+ title: "code_refs path does not resolve inside the project",
61
+ fix: "make each code_refs path exist in the project, and keep a story's paths inside its epic's",
62
+ }),
63
+ wikilink: row({
64
+ instance: /^Dead wiki-link \[\[(.+)\]\]\.$/,
65
+ title: "Dead wiki-link",
66
+ fix: "make each [[link]] name exactly one existing artifact (correct the name, or qualify it with its folder); if the check was skipped, fix the error it names",
67
+ }),
68
+ "rel-link": row({
69
+ instance: /^Dead relative link \((.+)\)\.$/,
70
+ title: "Dead relative link",
71
+ fix: "point each relative link at a file that exists, or remove it",
72
+ }),
73
+ acceptance: row({ fix: "tick the criteria the work met, or move the story out of done" }),
74
+ "review-status": row({ fix: "set reviewed_at to the review's date, or clear review_status" }),
75
+ "epic-status": row({ fix: "close the open stories, or move the epic out of done" }),
76
+ "spec-links": row({
77
+ fix: "make each spec's stories and each story's specs name existing artifacts that list each other back, written inline as specs: [\"id\"]",
78
+ }),
79
+ index: row({
80
+ instance: / is not listed in .+\/README\.md's index\.$/,
81
+ title: "Artifact is not listed in its folder's README index",
82
+ fix: { say: "rebuild the folder indexes from frontmatter", command: (env) => commandForm("reconcile", { env }) },
83
+ }),
84
+ "index-header": row({ fix: "rewrite the index header in a registered form, so reconcile can rebuild the index" }),
85
+ // Notes that fold on a terminal while their section has something to fix.
86
+ surface: row({ folds: /— current, last written by / }),
87
+ mcp: row({ folds: true }),
88
+ statusline: row({ folds: /^Base HUD present/ }),
89
+ harness: row({ folds: true }),
90
+ "plugin-registration": row({ folds: / registered from the npm package / }),
91
+ "spec-policy": row({ folds: true }),
92
+ identity: row({ folds: true }),
93
+ "artifact-name": row({ folds: true }),
94
+ });
95
+
96
+ // The instance list on a terminal: at most this many, then `+N more`.
97
+ export const INSTANCE_CAP = 8;
98
+
99
+ const NONE = row({});
100
+ const LEVELS = Object.freeze([
101
+ // [level, glyph name, role, count word]
102
+ ["issue", "issue", "error", "issue"],
103
+ ["warn", "warning", "attention", "warning"],
104
+ // A note is not a warning: its own glyph, so the two differ without colour.
105
+ ["info", "info", "explanation", "note"],
106
+ ]);
107
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
108
+ // Joins the dash to the path after it, so wrapping never leaves a bare dash
109
+ // at a line's end with the path alone below; a space again once wrapped.
110
+ const GLUE = "";
111
+
112
+ const folderName = (p) =>
113
+ p
114
+ ? String(p)
115
+ .replace(/[\\/]+$/, "")
116
+ .split(/[\\/]/)
117
+ .pop() || null
118
+ : null;
119
+ const folds = (r, f) => f.level === "info" && Boolean(r.folds) && (r.folds === true || r.folds.test(f.message));
120
+ const capture = (r, f) => {
121
+ const m = r.instance.exec(f.message);
122
+ return m && m[1] !== undefined ? m[1] : null;
123
+ };
124
+
125
+ export function report(
126
+ findings,
127
+ groups,
128
+ { caps: c = PLAIN, verbose = false, version = null, project = null, env = process.env } = {},
129
+ ) {
130
+ const paint = painter(c);
131
+ // The terminal's compaction (contract 11, "on a TTY only"): the cap, the
132
+ // folds and the short instance lines. Plain output prints everything, and
133
+ // --verbose asks a terminal for the same.
134
+ const compact = c.live && !verbose;
135
+ const width = c.live ? c.width : 0;
136
+ const dash = icon(c, "dash");
137
+ // Prose at an indent: wrapped on a live terminal, every line after the
138
+ // first indented two further; one line anywhere else.
139
+ const prose = (n, text) => {
140
+ const pad = " ".repeat(n);
141
+ return wrap(text, width, pad + " ")
142
+ .split("\n")
143
+ .map((l, k) => (k ? l : pad + l).split(GLUE).join(" "));
144
+ };
145
+ const withPath = (text, f) =>
146
+ f.file ? `${text} ${paint("explanation", dash)}${GLUE}${paint("explanation", f.file)}` : text;
147
+
148
+ function entries(level, check, r, these) {
149
+ const [, glyph, role] = LEVELS.find(([l]) => l === level);
150
+ const lead = `${paint(role, icon(c, glyph))} [${check}]`;
151
+ // A message with further lines (leads, commands) is never grouped: an
152
+ // instance row draws the message whole with its path after it, which
153
+ // would put the path after a command.
154
+ const matched = r.instance
155
+ ? these.filter((f) => !f.about && !String(f.message).includes("\n") && r.instance.test(f.message))
156
+ : [];
157
+ const grouped = matched.length >= 2 ? matched : [];
158
+ const out = [];
159
+ for (const f of these) {
160
+ // A message with further lines (a remedy, the silenced copy's way back
161
+ // and `setup --channel npm`): its first line is prose, drawn with the
162
+ // path as one entry; the path goes before that line's closing colon, so
163
+ // the colon introduces what follows, not the path. After it, a line
164
+ // ending in `:` is a lead, drawn as prose after a blank line at the
165
+ // entry's text indent; any other line is a command, drawn alone after a
166
+ // blank line, six spaces in (contract 6; the story "User-facing text
167
+ // says where to run…", rule 4 as amended).
168
+ const [said, ...further] = String(f.message).split("\n");
169
+ if (!grouped.includes(f) && further.length) {
170
+ const colon = said.endsWith(":") ? ":" : "";
171
+ out.push(...prose(2, withPath(`${lead} ${colon ? said.slice(0, -1) : said}`, f) + colon));
172
+ for (const l of further)
173
+ out.push(...(l.endsWith(":") ? ["", ...prose(2, l)] : commandBlock(c, [l], { indent: 6 })));
174
+ out.push("");
175
+ continue;
176
+ }
177
+ if (!grouped.includes(f)) {
178
+ out.push(...prose(2, withPath(`${lead} ${f.message}`, f)));
179
+ continue;
180
+ }
181
+ if (f !== grouped[0]) continue;
182
+ out.push(...prose(2, `${lead} ${paint("emphasis", r.title)} (${grouped.length})`));
183
+ if (!compact) {
184
+ for (const g of grouped) out.push(...prose(6, withPath(g.message, g)));
185
+ continue;
186
+ }
187
+ // Each instance a row with its own glyph: its capture, then its path.
188
+ // A path moved beneath its capture (contract 5) reads `— <path>`, as
189
+ // plain's `message — file` does, so neither colour nor the indent is
190
+ // all that tells it from the next capture.
191
+ const shown = grouped.slice(0, INSTANCE_CAP);
192
+ const laid = rowLines(
193
+ c,
194
+ shown.map((g) => {
195
+ const cap = capture(r, g);
196
+ return {
197
+ glyph: "dot",
198
+ role: "explanation",
199
+ ...(cap === null ? { target: g.file || g.message } : { target: cap, note: g.file || "" }),
200
+ };
201
+ }),
202
+ { indent: 6 },
203
+ );
204
+ for (const [first, ...moved] of laid)
205
+ out.push(
206
+ first,
207
+ ...moved.map((l, k) => (k ? l : l.replace(/^ */, (pad) => `${pad}${paint("explanation", dash)} `))),
208
+ );
209
+ if (grouped.length > shown.length)
210
+ out.push(` ${paint("explanation", `+${grouped.length - shown.length} more`)}`);
211
+ }
212
+ if (r.fix) {
213
+ const fix = typeof r.fix === "string" ? { say: r.fix, command: null } : r.fix;
214
+ out.push(...prose(4, `${paint("action", "fix:")} ${fix.say}${fix.command ? ":" : "."}`));
215
+ if (fix.command) out.push(...commandBlock(c, [fix.command(env)], { indent: 6 }), "");
216
+ }
217
+ return out;
218
+ }
219
+
220
+ function section(group) {
221
+ const fs = findings.filter((f) => f.group === group);
222
+ const n = Object.fromEntries(LEVELS.map(([l]) => [l, fs.filter((f) => f.level === l).length]));
223
+ // Each count in its role; a zero issue or warning count is the done role.
224
+ const counts = LEVELS.map(([l, , role, word]) => [plural(n[l], word), l === "info" ? role : n[l] ? role : "done"]);
225
+ const out = [heading(c, group.toUpperCase(), counts), rule(c)];
226
+ if (!fs.length) return [...out, ` ${paint("done", icon(c, "ok"))} clean`];
227
+ // A note folds only while its section has something to fix.
228
+ const folding = compact && n.issue + n.warn > 0;
229
+ let hidden = 0;
230
+ for (const [level] of LEVELS) {
231
+ const atLevel = fs.filter((f) => f.level === level);
232
+ for (const check of [...new Set(atLevel.map((f) => f.check))]) {
233
+ const r = Object.hasOwn(CHECKS, check) ? CHECKS[check] : NONE;
234
+ let these = atLevel.filter((f) => f.check === check);
235
+ if (folding) {
236
+ const kept = these.filter((f) => !folds(r, f));
237
+ hidden += these.length - kept.length;
238
+ these = kept;
239
+ }
240
+ if (these.length) out.push(...entries(level, check, r, these));
241
+ }
242
+ }
243
+ if (hidden)
244
+ out.push(` ${paint("explanation", `${plural(hidden, "note")} hidden ${icon(c, "dot")} --verbose shows them`)}`);
245
+ while (out.at(-1) === "") out.pop();
246
+ return out;
247
+ }
248
+
249
+ const lines = [badge(c, "projectstore doctor", [version, folderName(project)])];
250
+ for (const g of groups) lines.push("", ...section(g));
251
+ lines.push("", doctorSummaryLine(findings, { env }));
252
+ return lines.join("\n") + "\n";
253
+ }