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
@@ -25,11 +25,13 @@ import { readConfig, readStdinJson, adoptHookInput, roleForm, sharedRoleForm } f
25
25
  // told once that both name one role — otherwise the second rule below would
26
26
  // have it report the block as a contradiction every session.
27
27
  function rules() {
28
- const critic = roleForm("critic"), reviewer = roleForm("reviewer");
28
+ const critic = roleForm("critic"),
29
+ reviewer = roleForm("reviewer");
29
30
  const shared = sharedRoleForm("critic");
30
- const bridge = shared === critic
31
- ? ""
32
- : `\n\nThe \`AGENTS.md\` block may name roles as \`${shared}\`; that is the same role, called \`${critic}\` here.`;
31
+ const bridge =
32
+ shared === critic
33
+ ? ""
34
+ : `\n\nThe \`AGENTS.md\` block may name roles as \`${shared}\`; that is the same role, called \`${critic}\` here.`;
33
35
  return `# projectstore — standing rules for this session
34
36
 
35
37
  **Artifact-first order.** A feature-sized request opens a vault artifact before
@@ -46,9 +48,11 @@ contradiction becomes invisible to the person who could have settled it.${bridge
46
48
  }
47
49
 
48
50
  function emit(additionalContext) {
49
- process.stdout.write(JSON.stringify({
50
- hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
51
- }) + "\n");
51
+ process.stdout.write(
52
+ JSON.stringify({
53
+ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext },
54
+ }) + "\n",
55
+ );
52
56
  }
53
57
 
54
58
  function main() {
@@ -70,6 +74,8 @@ function main() {
70
74
  emit(rules());
71
75
  }
72
76
 
73
- try { main(); } catch {
77
+ try {
78
+ main();
79
+ } catch {
74
80
  // A hook must never break session startup.
75
81
  }
@@ -13,7 +13,7 @@
13
13
  // open. Bounded and O(1) in vault size by construction.
14
14
 
15
15
  import { existsSync, writeFileSync, mkdirSync } from "node:fs";
16
- import { join, dirname } from "node:path";
16
+ import { dirname } from "node:path";
17
17
  import {
18
18
  readConfig,
19
19
  gatherVaultFacts,
@@ -27,9 +27,10 @@ import {
27
27
  layoutPaths,
28
28
  pickExisting,
29
29
  activeHarnessId,
30
- ensureRuntimeDir,
30
+ ensureStateRoot,
31
31
  syncStatusLine,
32
32
  cleanupStaleSessionState,
33
+ refreshStoryPointer,
33
34
  armReminder,
34
35
  truncEnd,
35
36
  truncFront,
@@ -64,9 +65,10 @@ function welcomedMarkerWritePath(proj) {
64
65
  // the one instruction it carries branches on the binding; everything else is
65
66
  // the same message.
66
67
  function buildWelcome(cfg = null) {
67
- const start = cfg && cfg.vault_path
68
- ? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want — the agent picks up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation, and you approve every write. If the vault has no folders yet, \`${commandForm("scaffold")}\` lays them out.`
69
- : `**To start using it**: run \`${commandForm("bind", { args: "<vault-path>" })}\` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation; you only approve the writes.`;
68
+ const start =
69
+ cfg && cfg.vault_path
70
+ ? `**Already bound**: this project's vault is \`${truncFront(String(cfg.vault_path), PATH_CELL)}\`. Ask for what you want — the agent picks up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation, and you approve every write. If the vault has no folders yet, \`${commandForm("scaffold")}\` lays them out.`
71
+ : `**To start using it**: run \`${commandForm("bind", { args: "<vault-path>" })}\` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like \`${commandForm("adr")}\` and \`${commandForm("epic")}\` from the conversation; you only approve the writes.`;
70
72
  return [
71
73
  "# 👋 projectstore is loaded for the first time in this project",
72
74
  "",
@@ -90,7 +92,11 @@ function showWelcomeOnce(proj, cfg = null) {
90
92
  const text = buildWelcome(cfg);
91
93
  try {
92
94
  const marker = welcomedMarkerWritePath(proj);
93
- ensureRuntimeDir(proj); // .projectstore/.gitignore ignores state/; the marker is not a session file
95
+ // The state directory's own .gitignore, header included, even in an
96
+ // unbound project — and no sessions/ there: it is how a project uninstall
97
+ // recognises this marker as ours (install spec contract 13, as amended by
98
+ // the shell-fetch story).
99
+ ensureStateRoot(proj);
94
100
  mkdirSync(dirname(marker), { recursive: true });
95
101
  writeFileSync(marker, new Date().toISOString() + "\n", "utf8");
96
102
  } catch {}
@@ -174,15 +180,25 @@ async function main() {
174
180
  // an unbound one that is not a worktree pays a single ~10 ms probe.
175
181
  let binding = null;
176
182
  if (!cfg) {
177
- try { binding = resolveBinding(proj); } catch {}
183
+ try {
184
+ binding = resolveBinding(proj);
185
+ } catch {}
178
186
  }
179
187
  const welcome = showWelcomeOnce(proj, cfg);
180
188
  // The person's channel carries one instruction, and it has to be the right
181
- // one: a bound project is told what it is bound to, not to bind again.
189
+ // one: a bound project is told what it is bound to, not to bind again. A
190
+ // command stands alone on its line, the update hint on lines of its own
191
+ // after it (the story "User-facing text says where to run…", rules 4 and 5).
182
192
  const welcomeSystemMessage = welcome
183
- ? (cfg && cfg.vault_path
193
+ ? cfg && cfg.vault_path
184
194
  ? `👋 projectstore: first-run welcome shown. Bound to ${cfg.vault_path}. ${updateHint().line}`
185
- : `👋 projectstore: first-run welcome shown. Start with ${commandForm("bind", { args: "<vault-path>" })}. ${updateHint().line}`)
195
+ : [
196
+ "👋 projectstore: first-run welcome shown. Start with:",
197
+ commandForm("bind", { args: "<vault-path>" }),
198
+ updateHint().line,
199
+ ]
200
+ .filter(Boolean)
201
+ .join("\n")
186
202
  : null;
187
203
 
188
204
  if (!cfg) {
@@ -192,8 +208,10 @@ async function main() {
192
208
  // whose parent is already bound. Ordering is the cheap fix; the small
193
209
  // redundancy is accepted rather than papered over with copy that will rot.
194
210
  const offer = binding && binding.state === "inheritable" ? bindingOfferText(binding) : "";
211
+ // What binding does first, then the command alone on its line: an
212
+ // in-session form that can carry the parent's vault path (rule 4).
195
213
  const offerSystemMessage = offer
196
- ? `projectstore: this worktree is unbound — ${inheritForm(binding.vaultPath, { layout: binding.layout, language: binding.language })} ${bindInherits() ? "adopts the binding of" : "binds it to the vault, layout and language of"} the checkout it was forked from.`
214
+ ? `projectstore: this worktree is unbound. ${bindInherits() ? "Adopt the binding of" : "Bind it to the vault, layout and language of"} the checkout it was forked from, in the session:\n${inheritForm(binding.vaultPath, { layout: binding.layout, language: binding.language })}`
197
215
  : null;
198
216
  const body = offer + (welcome || "");
199
217
  if (body) {
@@ -209,10 +227,23 @@ async function main() {
209
227
  // Opt-in status line: keep settings.local.json pointed at this plugin
210
228
  // version's statusline.mjs (self-heals on update). Best-effort; a settings
211
229
  // write must never break session-context injection.
212
- try { syncStatusLine(cfg, proj); } catch {}
230
+ try {
231
+ syncStatusLine(cfg, proj);
232
+ } catch {}
213
233
  // Statusline-feature housekeeping, like syncStatusLine — must run even when
214
234
  // auto_inject=false (touch-session writes pointers regardless of it).
215
- try { cleanupStaleSessionState(proj); } catch {}
235
+ try {
236
+ cleanupStaleSessionState(proj);
237
+ } catch {}
238
+ // This session's story fields from the story file, once (ADR-006, amended
239
+ // 2026-10-12): a pointer written before a close, or a story closed elsewhere
240
+ // since, stops naming finished work. A bounded read; a lost race writes
241
+ // nothing.
242
+ if (sid) {
243
+ try {
244
+ await refreshStoryPointer(proj, cfg.vault_path, sid);
245
+ } catch {}
246
+ }
216
247
 
217
248
  // Arm BEFORE the auto_inject gate. The entry reminder's markers must be
218
249
  // re-armed after a compaction whether or not this session injects context —
@@ -227,7 +258,9 @@ async function main() {
227
258
  // context while its marker persists on disk. Arming lets it fire once more;
228
259
  // the cap of two is enforced by the election, not here.
229
260
  if (sid && (input?.source === "compact" || input?.source === "clear")) {
230
- try { armReminder(proj, sid); } catch {}
261
+ try {
262
+ armReminder(proj, sid);
263
+ } catch {}
231
264
  }
232
265
 
233
266
  if (cfg.auto_inject === false) {
@@ -268,23 +301,26 @@ async function main() {
268
301
  }
269
302
  }
270
303
 
271
- // Cheap install-only doctor subset (ADR-005): one line, only when N > 0;
272
- // aborted past its budget rather than reporting a false "clean".
304
+ // Cheap install-only doctor subset (ADR-005): one part, only when N > 0;
305
+ // aborted past its budget rather than reporting a false "clean". Its doctor
306
+ // form stands alone on its line, as every command of this line does.
273
307
  let doctorMsg = null;
274
308
  let offers = [];
275
309
  try {
276
310
  const r = runStartupChecks(cfg, proj);
277
311
  if (r.skipped) {
278
- doctorMsg = `projectstore doctor: startup checks skipped — run ${commandForm("doctor")}`;
312
+ doctorMsg = `projectstore doctor: startup checks skipped. Run them in the session:\n${commandForm("doctor")}`;
279
313
  } else if (r.count > 0) {
280
- doctorMsg = `projectstore doctor: ${r.count} install issue(s) — run ${commandForm("doctor")}`;
314
+ doctorMsg = `projectstore doctor: ${r.count} install issue(s). See them in the session:\n${commandForm("doctor")}`;
281
315
  }
282
316
  // Offers (doctor's OFFER_CHECKS): one-time steps a user should see once,
283
317
  // e.g. the re-stamp after a plugin update — not issues, not silent.
284
318
  offers = (r.offers || []).map((m) => `projectstore: ${m}`);
285
319
  } catch {}
286
- const systemMessage =
287
- [welcomeSystemMessage, doctorMsg, ...offers].filter(Boolean).join(" · ") || null;
320
+ // Each part on lines of its own: an offer ends with its command alone on
321
+ // its line, and a part joined after it with ` · ` would put text after the
322
+ // command (the story "User-facing text says where to run…", rule 4).
323
+ const systemMessage = [welcomeSystemMessage, doctorMsg, ...offers].filter(Boolean).join("\n") || null;
288
324
 
289
325
  if (gatherError) {
290
326
  emit(
@@ -71,12 +71,14 @@ async function main() {
71
71
 
72
72
  if (!electEmitter(proj, sid)) return;
73
73
  appendEntryLog(proj, { at: new Date().toISOString(), session_id: sid, score, via: "Stop" });
74
- process.stdout.write(JSON.stringify({
75
- hookSpecificOutput: {
76
- hookEventName: "Stop",
77
- additionalContext: entryReminderText(score),
78
- },
79
- }) + "\n");
74
+ process.stdout.write(
75
+ JSON.stringify({
76
+ hookSpecificOutput: {
77
+ hookEventName: "Stop",
78
+ additionalContext: entryReminderText(score),
79
+ },
80
+ }) + "\n",
81
+ );
80
82
  }
81
83
 
82
84
  main().catch(() => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectstore",
3
- "version": "0.29.2",
3
+ "version": "0.29.4",
4
4
  "description": "Your agent runs the project through a verified loop: task → artifact (ADR / spec / epic / story) → adversarial critic → backlog → planner → reviewer → done. Plain markdown in git — any model can pick the project up tomorrow.",
5
5
  "keywords": [
6
6
  "project-management",
@@ -41,6 +41,7 @@
41
41
  "files": [
42
42
  "bin/",
43
43
  ".claude-plugin/",
44
+ "!.claude-plugin/types/",
44
45
  "agents/",
45
46
  "commands/",
46
47
  "harnesses/",
@@ -65,6 +66,15 @@
65
66
  "shells": "node packaging/shells.mjs --write",
66
67
  "shells:check": "node packaging/shells.mjs --check",
67
68
  "shells:build": "node packaging/shells.mjs --build --out dist",
68
- "release:check": "npm run adapters:check && npm run guard && npm run shells:check && npm test && npm pack --dry-run"
69
+ "release:check": "npm run adapters:check && npm run guard && npm run shells:check && npm run lint && npm run format:check && npm test && npm pack --dry-run",
70
+ "lint": "eslint --max-warnings 0 bin scripts \"hooks/*.mjs\" \"packaging/**/*.mjs\" \"tests/**/*.mjs\" eslint.config.mjs",
71
+ "format": "prettier --write bin scripts \"hooks/*.mjs\" \"packaging/**/*.mjs\" \"tests/**/*.mjs\" eslint.config.mjs",
72
+ "format:check": "prettier --check bin scripts \"hooks/*.mjs\" \"packaging/**/*.mjs\" \"tests/**/*.mjs\" eslint.config.mjs"
73
+ },
74
+ "devDependencies": {
75
+ "@eslint/js": "10.0.1",
76
+ "eslint": "10.12.0",
77
+ "eslint-plugin-n": "18.4.1",
78
+ "prettier": "3.9.10"
69
79
  }
70
80
  }
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_description": "Language-independent registry of section headings, inline keywords and index-table column names recognized by the deterministic scripts (doctor, reconcile, story-section). Per id, per language: an ARRAY of accepted forms; the FIRST form of the bound language is the canonical form used when writing. Matching accepts every form of every language, so a ru-headed file lints in an en-bound vault. New sections/keywords added by any story MUST register a form for every bundled language (en, ru, es, de, fr, zh) here (PS-SPEC story-002).",
3
3
  "headings": {
4
+ "goal": {
5
+ "en": ["Goal"],
6
+ "ru": ["Цель"],
7
+ "es": ["Objetivo"],
8
+ "de": ["Ziel"],
9
+ "fr": ["Objectif"],
10
+ "zh": ["目标"]
11
+ },
4
12
  "description": {
5
13
  "en": ["Description"],
6
14
  "ru": ["Описание"],