projectstore-claude 0.29.3 → 0.30.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 (57) hide show
  1. package/README.md +13 -12
  2. package/bin/projectstore-claude.mjs +2 -2
  3. package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
  4. package/node_modules/projectstore/.claude-plugin/plugin.json +1 -1
  5. package/node_modules/projectstore/README.md +153 -77
  6. package/node_modules/projectstore/commands/bind.md +6 -17
  7. package/node_modules/projectstore/commands/doctor.md +12 -7
  8. package/node_modules/projectstore/commands/statusline.md +1 -1
  9. package/node_modules/projectstore/commands/story.md +15 -2
  10. package/node_modules/projectstore/docs/extending.md +11 -6
  11. package/node_modules/projectstore/docs/getting-started.md +21 -1
  12. package/node_modules/projectstore/docs/harnesses.md +13 -10
  13. package/node_modules/projectstore/docs/how-it-works.md +6 -4
  14. package/node_modules/projectstore/harnesses/claude-code.json +16 -15
  15. package/node_modules/projectstore/harnesses/codex.json +12 -13
  16. package/node_modules/projectstore/hooks/band.tsx +48 -20
  17. package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
  18. package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
  19. package/node_modules/projectstore/hooks/session-start.mjs +50 -18
  20. package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
  21. package/node_modules/projectstore/package.json +11 -2
  22. package/node_modules/projectstore/scripts/band-shared.mjs +141 -24
  23. package/node_modules/projectstore/scripts/band.mjs +72 -21
  24. package/node_modules/projectstore/scripts/binding.mjs +616 -85
  25. package/node_modules/projectstore/scripts/build-adapters.mjs +73 -29
  26. package/node_modules/projectstore/scripts/cli.mjs +1015 -185
  27. package/node_modules/projectstore/scripts/codemap.mjs +22 -7
  28. package/node_modules/projectstore/scripts/convergence.mjs +253 -83
  29. package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
  30. package/node_modules/projectstore/scripts/doctor-report.mjs +91 -23
  31. package/node_modules/projectstore/scripts/doctor.mjs +1317 -371
  32. package/node_modules/projectstore/scripts/draft.mjs +5 -2
  33. package/node_modules/projectstore/scripts/fetch-shell.mjs +188 -39
  34. package/node_modules/projectstore/scripts/graph.mjs +19 -13
  35. package/node_modules/projectstore/scripts/harness.mjs +123 -48
  36. package/node_modules/projectstore/scripts/install-harness.mjs +3532 -618
  37. package/node_modules/projectstore/scripts/kanban.mjs +2 -1
  38. package/node_modules/projectstore/scripts/lib.mjs +694 -159
  39. package/node_modules/projectstore/scripts/mcp.mjs +386 -105
  40. package/node_modules/projectstore/scripts/portable-registration.mjs +224 -51
  41. package/node_modules/projectstore/scripts/provenance.mjs +26 -10
  42. package/node_modules/projectstore/scripts/query.mjs +378 -80
  43. package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
  44. package/node_modules/projectstore/scripts/run-host.mjs +62 -17
  45. package/node_modules/projectstore/scripts/scaffold.mjs +89 -28
  46. package/node_modules/projectstore/scripts/setup.mjs +987 -0
  47. package/node_modules/projectstore/scripts/stages.mjs +5 -1
  48. package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
  49. package/node_modules/projectstore/scripts/statusline.mjs +32 -14
  50. package/node_modules/projectstore/scripts/story-section.mjs +39 -17
  51. package/node_modules/projectstore/scripts/surfaces.mjs +416 -95
  52. package/node_modules/projectstore/scripts/term.mjs +382 -97
  53. package/node_modules/projectstore/scripts/tokens.mjs +106 -28
  54. package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
  55. package/node_modules/projectstore/scripts/version-guard.mjs +74 -27
  56. package/node_modules/projectstore/scripts/worktree.mjs +16 -5
  57. package/package.json +3 -3
@@ -50,7 +50,8 @@ import { commandForm, doctorSummaryLine } from "./lib.mjs";
50
50
  // - folds: notes of this check fold on a terminal — true for all of them,
51
51
  // or a pattern for the ones it names. The offer checks (doctor.mjs's
52
52
  // OFFER_CHECKS: upgrade, layout-legacy) never fold, so they have no row.
53
- const row = ({ instance = null, title = null, fix = null, folds = false }) => Object.freeze({ instance, title, fix, folds });
53
+ const row = ({ instance = null, title = null, fix = null, folds = false }) =>
54
+ Object.freeze({ instance, title, fix, folds });
54
55
 
55
56
  export const CHECKS = Object.freeze({
56
57
  // Fixes: the checks whose messages name no command.
@@ -59,7 +60,7 @@ export const CHECKS = Object.freeze({
59
60
  title: "code_refs path does not resolve inside the project",
60
61
  fix: "make each code_refs path exist in the project, and keep a story's paths inside its epic's",
61
62
  }),
62
- "wikilink": row({
63
+ wikilink: row({
63
64
  instance: /^Dead wiki-link \[\[(.+)\]\]\.$/,
64
65
  title: "Dead wiki-link",
65
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",
@@ -69,24 +70,26 @@ export const CHECKS = Object.freeze({
69
70
  title: "Dead relative link",
70
71
  fix: "point each relative link at a file that exists, or remove it",
71
72
  }),
72
- "acceptance": row({ fix: "tick the criteria the work met, or move the story out of done" }),
73
+ acceptance: row({ fix: "tick the criteria the work met, or move the story out of done" }),
73
74
  "review-status": row({ fix: "set reviewed_at to the review's date, or clear review_status" }),
74
75
  "epic-status": row({ fix: "close the open stories, or move the epic out of done" }),
75
- "spec-links": row({ fix: "make each spec's stories and each story's specs name existing artifacts that list each other back, written inline as specs: [\"id\"]" }),
76
- "index": row({
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({
77
80
  instance: / is not listed in .+\/README\.md's index\.$/,
78
81
  title: "Artifact is not listed in its folder's README index",
79
82
  fix: { say: "rebuild the folder indexes from frontmatter", command: (env) => commandForm("reconcile", { env }) },
80
83
  }),
81
84
  "index-header": row({ fix: "rewrite the index header in a registered form, so reconcile can rebuild the index" }),
82
85
  // Notes that fold on a terminal while their section has something to fix.
83
- "surface": row({ folds: /— current, last written by / }),
84
- "mcp": row({ folds: true }),
85
- "statusline": row({ folds: /^Base HUD present/ }),
86
- "harness": row({ folds: true }),
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 }),
87
90
  "plugin-registration": row({ folds: / registered from the npm package / }),
88
91
  "spec-policy": row({ folds: true }),
89
- "identity": row({ folds: true }),
92
+ identity: row({ folds: true }),
90
93
  "artifact-name": row({ folds: true }),
91
94
  });
92
95
 
@@ -106,11 +109,24 @@ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
106
109
  // at a line's end with the path alone below; a space again once wrapped.
107
110
  const GLUE = "";
108
111
 
109
- const folderName = (p) => (p ? String(p).replace(/[\\/]+$/, "").split(/[\\/]/).pop() || null : null);
112
+ const folderName = (p) =>
113
+ p
114
+ ? String(p)
115
+ .replace(/[\\/]+$/, "")
116
+ .split(/[\\/]/)
117
+ .pop() || null
118
+ : null;
110
119
  const folds = (r, f) => f.level === "info" && Boolean(r.folds) && (r.folds === true || r.folds.test(f.message));
111
- const capture = (r, f) => { const m = r.instance.exec(f.message); return m && m[1] !== undefined ? m[1] : null; };
120
+ const capture = (r, f) => {
121
+ const m = r.instance.exec(f.message);
122
+ return m && m[1] !== undefined ? m[1] : null;
123
+ };
112
124
 
113
- export function report(findings, groups, { caps: c = PLAIN, verbose = false, version = null, project = null, env = process.env } = {}) {
125
+ export function report(
126
+ findings,
127
+ groups,
128
+ { caps: c = PLAIN, verbose = false, version = null, project = null, env = process.env } = {},
129
+ ) {
114
130
  const paint = painter(c);
115
131
  // The terminal's compaction (contract 11, "on a TTY only"): the cap, the
116
132
  // folds and the short instance lines. Plain output prints everything, and
@@ -122,29 +138,76 @@ export function report(findings, groups, { caps: c = PLAIN, verbose = false, ver
122
138
  // first indented two further; one line anywhere else.
123
139
  const prose = (n, text) => {
124
140
  const pad = " ".repeat(n);
125
- return wrap(text, width, pad + " ").split("\n").map((l, k) => (k ? l : pad + l).split(GLUE).join(" "));
141
+ return wrap(text, width, pad + " ")
142
+ .split("\n")
143
+ .map((l, k) => (k ? l : pad + l).split(GLUE).join(" "));
126
144
  };
127
- const withPath = (text, f) => (f.file ? `${text} ${paint("explanation", dash)}${GLUE}${paint("explanation", f.file)}` : text);
145
+ const withPath = (text, f) =>
146
+ f.file ? `${text} ${paint("explanation", dash)}${GLUE}${paint("explanation", f.file)}` : text;
128
147
 
129
148
  function entries(level, check, r, these) {
130
149
  const [, glyph, role] = LEVELS.find(([l]) => l === level);
131
150
  const lead = `${paint(role, icon(c, glyph))} [${check}]`;
132
- const matched = r.instance ? these.filter((f) => !f.about && r.instance.test(f.message)) : [];
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
+ : [];
133
157
  const grouped = matched.length >= 2 ? matched : [];
134
158
  const out = [];
135
159
  for (const f of these) {
136
- if (!grouped.includes(f)) { out.push(...prose(2, withPath(`${lead} ${f.message}`, f))); continue; }
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
+ }
137
181
  if (f !== grouped[0]) continue;
138
182
  out.push(...prose(2, `${lead} ${paint("emphasis", r.title)} (${grouped.length})`));
139
- if (!compact) { for (const g of grouped) out.push(...prose(6, withPath(g.message, g))); continue; }
183
+ if (!compact) {
184
+ for (const g of grouped) out.push(...prose(6, withPath(g.message, g)));
185
+ continue;
186
+ }
140
187
  // Each instance a row with its own glyph: its capture, then its path.
141
188
  // A path moved beneath its capture (contract 5) reads `— <path>`, as
142
189
  // plain's `message — file` does, so neither colour nor the indent is
143
190
  // all that tells it from the next capture.
144
191
  const shown = grouped.slice(0, INSTANCE_CAP);
145
- const laid = rowLines(c, shown.map((g) => { const cap = capture(r, g); return { glyph: "dot", role: "explanation", ...(cap === null ? { target: g.file || g.message } : { target: cap, note: g.file || "" }) }; }), { indent: 6 });
146
- for (const [first, ...moved] of laid) out.push(first, ...moved.map((l, k) => (k ? l : l.replace(/^ */, (pad) => `${pad}${paint("explanation", dash)} `))));
147
- if (grouped.length > shown.length) out.push(` ${paint("explanation", `+${grouped.length - shown.length} more`)}`);
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`)}`);
148
211
  }
149
212
  if (r.fix) {
150
213
  const fix = typeof r.fix === "string" ? { say: r.fix, command: null } : r.fix;
@@ -169,11 +232,16 @@ export function report(findings, groups, { caps: c = PLAIN, verbose = false, ver
169
232
  for (const check of [...new Set(atLevel.map((f) => f.check))]) {
170
233
  const r = Object.hasOwn(CHECKS, check) ? CHECKS[check] : NONE;
171
234
  let these = atLevel.filter((f) => f.check === check);
172
- if (folding) { const kept = these.filter((f) => !folds(r, f)); hidden += these.length - kept.length; these = kept; }
235
+ if (folding) {
236
+ const kept = these.filter((f) => !folds(r, f));
237
+ hidden += these.length - kept.length;
238
+ these = kept;
239
+ }
173
240
  if (these.length) out.push(...entries(level, check, r, these));
174
241
  }
175
242
  }
176
- if (hidden) out.push(` ${paint("explanation", `${plural(hidden, "note")} hidden ${icon(c, "dot")} --verbose shows them`)}`);
243
+ if (hidden)
244
+ out.push(` ${paint("explanation", `${plural(hidden, "note")} hidden ${icon(c, "dot")} --verbose shows them`)}`);
177
245
  while (out.at(-1) === "") out.pop();
178
246
  return out;
179
247
  }