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.
- package/README.md +1 -1
- package/bin/projectstore-claude.mjs +1 -1
- package/node_modules/projectstore/.claude-plugin/marketplace.json +1 -1
- package/node_modules/projectstore/.claude-plugin/plugin.json +2 -1
- package/node_modules/projectstore/README.md +44 -7
- package/node_modules/projectstore/commands/bar.md +30 -0
- package/node_modules/projectstore/commands/bind.md +16 -20
- package/node_modules/projectstore/commands/doctor.md +12 -7
- package/node_modules/projectstore/commands/scaffold.md +24 -16
- package/node_modules/projectstore/commands/status.md +1 -1
- package/node_modules/projectstore/commands/statusline.md +2 -2
- package/node_modules/projectstore/commands/story.md +15 -2
- package/node_modules/projectstore/docs/alongside.md +167 -0
- package/node_modules/projectstore/docs/extending.md +17 -3
- package/node_modules/projectstore/docs/getting-started.md +18 -9
- package/node_modules/projectstore/docs/harnesses.md +1 -1
- package/node_modules/projectstore/docs/how-it-works.md +12 -3
- package/node_modules/projectstore/harnesses/claude-code.json +23 -8
- package/node_modules/projectstore/harnesses/codex.json +16 -6
- package/node_modules/projectstore/hooks/band-state.d.ts +44 -0
- package/node_modules/projectstore/hooks/band.tsx +622 -0
- package/node_modules/projectstore/hooks/hooks.json +1 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +7 -10
- package/node_modules/projectstore/hooks/session-rules.mjs +14 -8
- package/node_modules/projectstore/hooks/session-start.mjs +56 -20
- package/node_modules/projectstore/hooks/session-stop.mjs +8 -6
- package/node_modules/projectstore/package.json +12 -2
- package/node_modules/projectstore/scaffold/headings.json +8 -0
- package/node_modules/projectstore/scripts/band-shared.mjs +446 -0
- package/node_modules/projectstore/scripts/band.mjs +207 -0
- package/node_modules/projectstore/scripts/binding.mjs +733 -62
- package/node_modules/projectstore/scripts/build-adapters.mjs +98 -35
- package/node_modules/projectstore/scripts/cli.mjs +1463 -267
- package/node_modules/projectstore/scripts/codemap.mjs +22 -7
- package/node_modules/projectstore/scripts/convergence.mjs +638 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +14 -8
- package/node_modules/projectstore/scripts/doctor-report.mjs +253 -0
- package/node_modules/projectstore/scripts/doctor.mjs +1384 -420
- package/node_modules/projectstore/scripts/draft.mjs +5 -2
- package/node_modules/projectstore/scripts/fetch-shell.mjs +369 -0
- package/node_modules/projectstore/scripts/graph.mjs +19 -13
- package/node_modules/projectstore/scripts/harness.mjs +283 -37
- package/node_modules/projectstore/scripts/install-harness.mjs +4701 -575
- package/node_modules/projectstore/scripts/kanban.mjs +6 -21
- package/node_modules/projectstore/scripts/lib.mjs +933 -199
- package/node_modules/projectstore/scripts/mcp.mjs +386 -105
- package/node_modules/projectstore/scripts/portable-registration.mjs +276 -56
- package/node_modules/projectstore/scripts/provenance.mjs +26 -10
- package/node_modules/projectstore/scripts/query.mjs +441 -72
- package/node_modules/projectstore/scripts/reconcile.mjs +55 -18
- package/node_modules/projectstore/scripts/run-host.mjs +139 -0
- package/node_modules/projectstore/scripts/scaffold.mjs +244 -0
- package/node_modules/projectstore/scripts/setup.mjs +987 -0
- package/node_modules/projectstore/scripts/stages.mjs +84 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +36 -11
- package/node_modules/projectstore/scripts/statusline.mjs +55 -14
- package/node_modules/projectstore/scripts/story-section.mjs +39 -17
- package/node_modules/projectstore/scripts/surfaces.mjs +485 -99
- package/node_modules/projectstore/scripts/term.mjs +1027 -52
- package/node_modules/projectstore/scripts/tokens.mjs +106 -28
- package/node_modules/projectstore/scripts/touch-session.mjs +109 -70
- package/node_modules/projectstore/scripts/version-guard.mjs +79 -27
- package/node_modules/projectstore/scripts/worktree.mjs +16 -5
- package/node_modules/projectstore/templates/de/strings.json +63 -1
- package/node_modules/projectstore/templates/de/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/en/strings.json +63 -1
- package/node_modules/projectstore/templates/en/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/es/strings.json +63 -1
- package/node_modules/projectstore/templates/es/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/fr/strings.json +63 -1
- package/node_modules/projectstore/templates/fr/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/ru/strings.json +63 -1
- package/node_modules/projectstore/templates/ru/vault-readme.md.tmpl +13 -0
- package/node_modules/projectstore/templates/zh/strings.json +63 -1
- package/node_modules/projectstore/templates/zh/vault-readme.md.tmpl +13 -0
- 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
|
+
}
|