projectstore-codex 0.0.1 → 0.28.1

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 (185) hide show
  1. package/.codex-plugin/plugin.json +48 -0
  2. package/README.md +15 -7
  3. package/bin/projectstore-codex.mjs +88 -0
  4. package/hooks/hooks.json +59 -0
  5. package/node_modules/projectstore/.claude-plugin/marketplace.json +40 -0
  6. package/node_modules/projectstore/.claude-plugin/plugin.json +23 -0
  7. package/node_modules/projectstore/.mcp.json +14 -0
  8. package/node_modules/projectstore/AGENTS.md +26 -0
  9. package/node_modules/projectstore/LICENSE +21 -0
  10. package/node_modules/projectstore/README.md +284 -0
  11. package/node_modules/projectstore/agents/archaeologist.md +76 -0
  12. package/node_modules/projectstore/agents/clerk.md +93 -0
  13. package/node_modules/projectstore/agents/critic.md +94 -0
  14. package/node_modules/projectstore/agents/librarian.md +81 -0
  15. package/node_modules/projectstore/agents/planner.md +80 -0
  16. package/node_modules/projectstore/agents/reviewer.md +98 -0
  17. package/node_modules/projectstore/bin/projectstore.mjs +7 -0
  18. package/node_modules/projectstore/commands/adr.md +57 -0
  19. package/node_modules/projectstore/commands/agents.md +180 -0
  20. package/node_modules/projectstore/commands/bind.md +128 -0
  21. package/node_modules/projectstore/commands/codemap.md +50 -0
  22. package/node_modules/projectstore/commands/concept.md +17 -0
  23. package/node_modules/projectstore/commands/doctor.md +166 -0
  24. package/node_modules/projectstore/commands/epic.md +40 -0
  25. package/node_modules/projectstore/commands/graph.md +56 -0
  26. package/node_modules/projectstore/commands/kanban.md +40 -0
  27. package/node_modules/projectstore/commands/meeting.md +17 -0
  28. package/node_modules/projectstore/commands/reconcile.md +73 -0
  29. package/node_modules/projectstore/commands/research.md +17 -0
  30. package/node_modules/projectstore/commands/review.md +89 -0
  31. package/node_modules/projectstore/commands/runbook.md +17 -0
  32. package/node_modules/projectstore/commands/scaffold.md +23 -0
  33. package/node_modules/projectstore/commands/search.md +22 -0
  34. package/node_modules/projectstore/commands/spec.md +91 -0
  35. package/node_modules/projectstore/commands/status.md +27 -0
  36. package/node_modules/projectstore/commands/statusline.md +46 -0
  37. package/node_modules/projectstore/commands/story.md +113 -0
  38. package/node_modules/projectstore/docs/extending.md +172 -0
  39. package/node_modules/projectstore/docs/getting-started.md +133 -0
  40. package/node_modules/projectstore/docs/harnesses.md +163 -0
  41. package/node_modules/projectstore/docs/how-it-works.md +263 -0
  42. package/node_modules/projectstore/docs/images/loop-light.svg +94 -0
  43. package/node_modules/projectstore/docs/images/loop.svg +93 -0
  44. package/node_modules/projectstore/docs/images/statusline-hud.png +0 -0
  45. package/node_modules/projectstore/docs/images/team-light.svg +79 -0
  46. package/node_modules/projectstore/docs/images/team.svg +79 -0
  47. package/node_modules/projectstore/harnesses/claude-code.json +483 -0
  48. package/node_modules/projectstore/harnesses/codex.json +332 -0
  49. package/node_modules/projectstore/hooks/hooks.json +59 -0
  50. package/node_modules/projectstore/hooks/pre-compact.mjs +121 -0
  51. package/node_modules/projectstore/hooks/session-rules.mjs +63 -0
  52. package/node_modules/projectstore/hooks/session-start.mjs +301 -0
  53. package/node_modules/projectstore/hooks/session-stop.mjs +84 -0
  54. package/node_modules/projectstore/package.json +70 -0
  55. package/node_modules/projectstore/scaffold/checklists.json +88 -0
  56. package/node_modules/projectstore/scaffold/headings.json +171 -0
  57. package/node_modules/projectstore/scaffold/layouts/engineering.json +85 -0
  58. package/node_modules/projectstore/scripts/binding.mjs +165 -0
  59. package/node_modules/projectstore/scripts/build-adapters.mjs +264 -0
  60. package/node_modules/projectstore/scripts/cli.mjs +595 -0
  61. package/node_modules/projectstore/scripts/codemap.mjs +99 -0
  62. package/node_modules/projectstore/scripts/diff-refs.mjs +127 -0
  63. package/node_modules/projectstore/scripts/doctor.mjs +2127 -0
  64. package/node_modules/projectstore/scripts/draft.mjs +261 -0
  65. package/node_modules/projectstore/scripts/graph.mjs +219 -0
  66. package/node_modules/projectstore/scripts/harness.mjs +608 -0
  67. package/node_modules/projectstore/scripts/install-harness.mjs +1387 -0
  68. package/node_modules/projectstore/scripts/kanban.mjs +174 -0
  69. package/node_modules/projectstore/scripts/lib.mjs +3085 -0
  70. package/node_modules/projectstore/scripts/mcp.mjs +391 -0
  71. package/node_modules/projectstore/scripts/portable-registration.mjs +198 -0
  72. package/node_modules/projectstore/scripts/provenance.mjs +375 -0
  73. package/node_modules/projectstore/scripts/query.mjs +490 -0
  74. package/node_modules/projectstore/scripts/reconcile.mjs +422 -0
  75. package/node_modules/projectstore/scripts/statusline-launcher.mjs +141 -0
  76. package/node_modules/projectstore/scripts/statusline.mjs +253 -0
  77. package/node_modules/projectstore/scripts/story-section.mjs +209 -0
  78. package/node_modules/projectstore/scripts/surfaces.mjs +421 -0
  79. package/node_modules/projectstore/scripts/tokens.mjs +449 -0
  80. package/node_modules/projectstore/scripts/touch-session.mjs +336 -0
  81. package/node_modules/projectstore/scripts/version-guard.mjs +255 -0
  82. package/node_modules/projectstore/scripts/worktree.mjs +109 -0
  83. package/node_modules/projectstore/skills/projectstore-decision-detector/SKILL.md +40 -0
  84. package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +38 -0
  85. package/node_modules/projectstore/skills/projectstore-story-completion/SKILL.md +50 -0
  86. package/node_modules/projectstore/skills/projectstore-vault-communication/SKILL.md +96 -0
  87. package/node_modules/projectstore/templates/claude-md-block.md.tmpl +26 -0
  88. package/node_modules/projectstore/templates/de/adr.md.tmpl +67 -0
  89. package/node_modules/projectstore/templates/de/concept.md.tmpl +43 -0
  90. package/node_modules/projectstore/templates/de/epic.md.tmpl +59 -0
  91. package/node_modules/projectstore/templates/de/folder-readme.md.tmpl +14 -0
  92. package/node_modules/projectstore/templates/de/kanban.md.tmpl +36 -0
  93. package/node_modules/projectstore/templates/de/meeting.md.tmpl +38 -0
  94. package/node_modules/projectstore/templates/de/research.md.tmpl +47 -0
  95. package/node_modules/projectstore/templates/de/runbook.md.tmpl +53 -0
  96. package/node_modules/projectstore/templates/de/spec.md.tmpl +64 -0
  97. package/node_modules/projectstore/templates/de/story.md.tmpl +76 -0
  98. package/node_modules/projectstore/templates/de/strings.json +6 -0
  99. package/node_modules/projectstore/templates/en/adr.md.tmpl +67 -0
  100. package/node_modules/projectstore/templates/en/concept.md.tmpl +43 -0
  101. package/node_modules/projectstore/templates/en/epic.md.tmpl +59 -0
  102. package/node_modules/projectstore/templates/en/folder-readme.md.tmpl +14 -0
  103. package/node_modules/projectstore/templates/en/kanban.md.tmpl +36 -0
  104. package/node_modules/projectstore/templates/en/meeting.md.tmpl +38 -0
  105. package/node_modules/projectstore/templates/en/research.md.tmpl +47 -0
  106. package/node_modules/projectstore/templates/en/runbook.md.tmpl +53 -0
  107. package/node_modules/projectstore/templates/en/spec.md.tmpl +64 -0
  108. package/node_modules/projectstore/templates/en/story.md.tmpl +76 -0
  109. package/node_modules/projectstore/templates/en/strings.json +6 -0
  110. package/node_modules/projectstore/templates/es/adr.md.tmpl +67 -0
  111. package/node_modules/projectstore/templates/es/concept.md.tmpl +43 -0
  112. package/node_modules/projectstore/templates/es/epic.md.tmpl +59 -0
  113. package/node_modules/projectstore/templates/es/folder-readme.md.tmpl +14 -0
  114. package/node_modules/projectstore/templates/es/kanban.md.tmpl +36 -0
  115. package/node_modules/projectstore/templates/es/meeting.md.tmpl +38 -0
  116. package/node_modules/projectstore/templates/es/research.md.tmpl +47 -0
  117. package/node_modules/projectstore/templates/es/runbook.md.tmpl +53 -0
  118. package/node_modules/projectstore/templates/es/spec.md.tmpl +64 -0
  119. package/node_modules/projectstore/templates/es/story.md.tmpl +76 -0
  120. package/node_modules/projectstore/templates/es/strings.json +6 -0
  121. package/node_modules/projectstore/templates/fr/adr.md.tmpl +67 -0
  122. package/node_modules/projectstore/templates/fr/concept.md.tmpl +43 -0
  123. package/node_modules/projectstore/templates/fr/epic.md.tmpl +59 -0
  124. package/node_modules/projectstore/templates/fr/folder-readme.md.tmpl +14 -0
  125. package/node_modules/projectstore/templates/fr/kanban.md.tmpl +36 -0
  126. package/node_modules/projectstore/templates/fr/meeting.md.tmpl +38 -0
  127. package/node_modules/projectstore/templates/fr/research.md.tmpl +47 -0
  128. package/node_modules/projectstore/templates/fr/runbook.md.tmpl +53 -0
  129. package/node_modules/projectstore/templates/fr/spec.md.tmpl +64 -0
  130. package/node_modules/projectstore/templates/fr/story.md.tmpl +76 -0
  131. package/node_modules/projectstore/templates/fr/strings.json +6 -0
  132. package/node_modules/projectstore/templates/ru/adr.md.tmpl +67 -0
  133. package/node_modules/projectstore/templates/ru/concept.md.tmpl +43 -0
  134. package/node_modules/projectstore/templates/ru/epic.md.tmpl +59 -0
  135. package/node_modules/projectstore/templates/ru/folder-readme.md.tmpl +14 -0
  136. package/node_modules/projectstore/templates/ru/kanban.md.tmpl +36 -0
  137. package/node_modules/projectstore/templates/ru/meeting.md.tmpl +38 -0
  138. package/node_modules/projectstore/templates/ru/research.md.tmpl +47 -0
  139. package/node_modules/projectstore/templates/ru/runbook.md.tmpl +53 -0
  140. package/node_modules/projectstore/templates/ru/spec.md.tmpl +64 -0
  141. package/node_modules/projectstore/templates/ru/story.md.tmpl +76 -0
  142. package/node_modules/projectstore/templates/ru/strings.json +6 -0
  143. package/node_modules/projectstore/templates/zh/adr.md.tmpl +67 -0
  144. package/node_modules/projectstore/templates/zh/concept.md.tmpl +43 -0
  145. package/node_modules/projectstore/templates/zh/epic.md.tmpl +59 -0
  146. package/node_modules/projectstore/templates/zh/folder-readme.md.tmpl +14 -0
  147. package/node_modules/projectstore/templates/zh/kanban.md.tmpl +36 -0
  148. package/node_modules/projectstore/templates/zh/meeting.md.tmpl +38 -0
  149. package/node_modules/projectstore/templates/zh/research.md.tmpl +47 -0
  150. package/node_modules/projectstore/templates/zh/runbook.md.tmpl +53 -0
  151. package/node_modules/projectstore/templates/zh/spec.md.tmpl +64 -0
  152. package/node_modules/projectstore/templates/zh/story.md.tmpl +76 -0
  153. package/node_modules/projectstore/templates/zh/strings.json +6 -0
  154. package/package.json +36 -14
  155. package/plugin.json +53 -0
  156. package/skills/projectstore-adr/SKILL.md +76 -0
  157. package/skills/projectstore-agents/SKILL.md +50 -0
  158. package/skills/projectstore-archaeologist/SKILL.md +109 -0
  159. package/skills/projectstore-bind/SKILL.md +44 -0
  160. package/skills/projectstore-clerk/SKILL.md +126 -0
  161. package/skills/projectstore-codemap/SKILL.md +69 -0
  162. package/skills/projectstore-concept/SKILL.md +36 -0
  163. package/skills/projectstore-critic/SKILL.md +127 -0
  164. package/skills/projectstore-decision-detector/SKILL.md +59 -0
  165. package/skills/projectstore-doctor/SKILL.md +33 -0
  166. package/skills/projectstore-epic/SKILL.md +59 -0
  167. package/skills/projectstore-graph/SKILL.md +75 -0
  168. package/skills/projectstore-kanban/SKILL.md +60 -0
  169. package/skills/projectstore-librarian/SKILL.md +114 -0
  170. package/skills/projectstore-meeting/SKILL.md +36 -0
  171. package/skills/projectstore-peer-reviewer/SKILL.md +57 -0
  172. package/skills/projectstore-planner/SKILL.md +113 -0
  173. package/skills/projectstore-reconcile/SKILL.md +92 -0
  174. package/skills/projectstore-research/SKILL.md +36 -0
  175. package/skills/projectstore-review/SKILL.md +108 -0
  176. package/skills/projectstore-reviewer/SKILL.md +131 -0
  177. package/skills/projectstore-runbook/SKILL.md +36 -0
  178. package/skills/projectstore-scaffold/SKILL.md +42 -0
  179. package/skills/projectstore-search/SKILL.md +41 -0
  180. package/skills/projectstore-spec/SKILL.md +110 -0
  181. package/skills/projectstore-status/SKILL.md +47 -0
  182. package/skills/projectstore-statusline/SKILL.md +29 -0
  183. package/skills/projectstore-story/SKILL.md +132 -0
  184. package/skills/projectstore-story-completion/SKILL.md +69 -0
  185. package/skills/projectstore-vault-communication/SKILL.md +115 -0
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env node
2
+ // projectstore — SessionStart hook.
3
+ // 1. Reads .projectstore/projectstore.json from the project root. If absent or
4
+ // auto_inject=false, silently no-ops.
5
+ // 2. Registers this session in <vault>/.projectstore/sessions/<id>.json,
6
+ // keyed by Claude's own session_id from hook stdin. Cleans stale
7
+ // entries (>24h). Detects other active sessions (mtime < 30min) and
8
+ // appends a warning so the agent knows it is not alone on this vault.
9
+ // 3. Injects a NAVIGATION SKELETON — the layout's folders, what each is for,
10
+ // what is in flight, and the order to descend in. Not a copy of the vault:
11
+ // it used to inject every folder README, which on a real vault exceeded the
12
+ // 10,000-character hook cap and was written to a file the agent then had to
13
+ // open. Bounded and O(1) in vault size by construction.
14
+
15
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
16
+ import { join, dirname } from "node:path";
17
+ import {
18
+ readConfig,
19
+ gatherVaultFacts,
20
+ renderVaultSkeleton,
21
+ writeSession,
22
+ readActiveSessions,
23
+ cleanupStaleSessions,
24
+ readStdinJson,
25
+ adoptHookInput,
26
+ projectRoot,
27
+ layoutPaths,
28
+ pickExisting,
29
+ activeHarnessId,
30
+ ensureRuntimeDir,
31
+ syncStatusLine,
32
+ cleanupStaleSessionState,
33
+ armReminder,
34
+ truncEnd,
35
+ truncFront,
36
+ PATH_CELL,
37
+ ERROR_CELL,
38
+ TITLE_CELL,
39
+ } from "../scripts/lib.mjs";
40
+ import { runStartupChecks } from "../scripts/doctor.mjs";
41
+ import { resolveBinding, bindingOfferText } from "../scripts/worktree.mjs";
42
+
43
+ // The marker lives under the harness's state directory; a legacy marker at
44
+ // .claude/.projectstore-welcomed still counts while the window is open.
45
+ function welcomedMarkerPath(proj) {
46
+ const p = layoutPaths(proj);
47
+ return pickExisting(p.welcomed(activeHarnessId()), p.legacy.welcomed);
48
+ }
49
+ function welcomedMarkerWritePath(proj) {
50
+ return layoutPaths(proj).welcomed(activeHarnessId());
51
+ }
52
+
53
+ // One-time orientation packet shown when projectstore first loads in a project.
54
+ // Idempotent via a marker file at <project>/.claude/.projectstore-welcomed.
55
+ // The welcome fires once per project, and a project can already be bound when
56
+ // it does — a fresh install into a project someone bound first is the ordinary
57
+ // case, not a corner (measured 2026-09-06: the first line a new user read told
58
+ // them to bind a project whose vault the install preview had just named). So
59
+ // the one instruction it carries branches on the binding; everything else is
60
+ // the same message.
61
+ function buildWelcome(cfg = null) {
62
+ const start = cfg && cfg.vault_path
63
+ ? `**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 \`/projectstore:adr\` and \`/projectstore:epic\` from the conversation, and you approve every write. If the vault has no folders yet, \`/projectstore:scaffold\` lays them out.`
64
+ : "**To start using it**: run `/projectstore:bind <vault-path>` and point it at an Obsidian vault (or any folder). After that, the agent will pick up commands like `/projectstore:adr` and `/projectstore:epic` from the conversation; you only approve the writes.";
65
+ return [
66
+ "# 👋 projectstore is loaded for the first time in this project",
67
+ "",
68
+ "**What it does**: turns the conversation's decisions into a structured Obsidian-friendly markdown vault — ADRs, epics, stories, runbooks, research. Agent-maintained, you approve every write.",
69
+ "",
70
+ start,
71
+ "",
72
+ "**About future updates**: Claude Code does NOT auto-update third-party marketplaces by default. To get notified of new releases (v0.7+):",
73
+ "1. Open `/plugin` → **Marketplaces** tab.",
74
+ "2. Find **SmartAndPoint**.",
75
+ "3. Toggle **auto-update** on.",
76
+ "",
77
+ "Without it, you'd run `/plugin marketplace update SmartAndPoint` manually. See https://github.com/SmartAndPoint/ProjectStore#updates for details.",
78
+ "",
79
+ "_This message appears once per project._",
80
+ "",
81
+ "_If projectstore helps you ship, a [GitHub star](https://github.com/SmartAndPoint/ProjectStore) helps others discover it. No pressure._",
82
+ "",
83
+ ].join("\n");
84
+ }
85
+
86
+ function showWelcomeOnce(proj, cfg = null) {
87
+ if (existsSync(welcomedMarkerPath(proj))) return "";
88
+ const text = buildWelcome(cfg);
89
+ try {
90
+ const marker = welcomedMarkerWritePath(proj);
91
+ ensureRuntimeDir(proj); // .projectstore/.gitignore ignores state/; the marker is not a session file
92
+ mkdirSync(dirname(marker), { recursive: true });
93
+ writeFileSync(marker, new Date().toISOString() + "\n", "utf8");
94
+ } catch {}
95
+ return text;
96
+ }
97
+
98
+ // Writes the payload and ends the process — after the flush, never before.
99
+ //
100
+ // The gather races its reads against a timer, so when the timer wins there are
101
+ // reads still outstanding, and an evicted file could hold the event loop open
102
+ // long past the budget the user is actually waiting on. Exiting here caps the
103
+ // hook's wall time at that budget. The callback is the whole safety of it:
104
+ // process.exit does not flush pending pipe writes, so exiting on the line after
105
+ // a write is how a payload gets truncated.
106
+ function emit(additionalContext, systemMessage) {
107
+ const out = {
108
+ hookSpecificOutput: {
109
+ hookEventName: "SessionStart",
110
+ additionalContext,
111
+ },
112
+ };
113
+ if (systemMessage) out.systemMessage = systemMessage;
114
+ process.stdout.write(JSON.stringify(out) + "\n", () => process.exit(0));
115
+ }
116
+
117
+ // Contract 3 — capped at 5 like the in-flight list, and for the same reason.
118
+ // The warning costs ~138 characters per sibling on top of a 748-character
119
+ // frame, so an uncapped list breaches the 10,000 composed cap at roughly 32
120
+ // concurrent sessions. That bound is empirical, and an empirical bound is what
121
+ // contract 1 exists to forbid; the cap makes it structural instead.
122
+ const SIBLING_CAP = 5;
123
+
124
+ function buildOthersWarning(others) {
125
+ const lines = [
126
+ "",
127
+ "---",
128
+ "",
129
+ `## ⚠️ Multi-session warning — ${others.length} other projectstore session(s) active on this vault`,
130
+ "",
131
+ "Another Claude Code session is currently working on the same vault.",
132
+ "Active session(s):",
133
+ "",
134
+ ];
135
+ for (const s of others.slice(0, SIBLING_CAP)) {
136
+ lines.push(
137
+ `- project: \`${truncFront(String(s.project_root ?? ""), PATH_CELL)}\`` +
138
+ // Free text from a session file this process never wrote, rendered five
139
+ // times over. The last unbounded term in the composed value: `last_active`
140
+ // is a real Date, the layout fields are plugin-bundled, counts are numbers.
141
+ ` — started ${truncEnd(String(s.started_at ?? ""), TITLE_CELL)},` +
142
+ ` last activity ${s.last_active.toISOString()}`,
143
+ );
144
+ }
145
+ if (others.length > SIBLING_CAP) {
146
+ lines.push(`- …and ${others.length - SIBLING_CAP} more — run \`/projectstore:status\``);
147
+ }
148
+ lines.push(
149
+ "",
150
+ "**Before creating new ADRs / epics / stories / research:**",
151
+ "1. Run `/projectstore:search <topic-keywords>` to check for in-flight artifacts on the same topic.",
152
+ "2. Run `/projectstore:status` to see what artifacts have been touched recently.",
153
+ "3. After creation, the plugin re-checks file existence right before write — collisions are detected, but topic / number reservation across sessions is on you and the other agent to coordinate.",
154
+ "",
155
+ );
156
+ return lines.join("\n");
157
+ }
158
+
159
+ async function main() {
160
+ // The payload before the project. readConfig() resolves through
161
+ // projectRoot(), so anything read above this line answers for whatever
162
+ // directory the process started in — silently, on any harness that exports
163
+ // no project-dir variable. Read once: fd 0 is empty on a second read, and
164
+ // `sid` below is this object's, not a re-read.
165
+ const input = adoptHookInput(readStdinJson());
166
+ const sid = input?.session_id || null;
167
+
168
+ const cfg = readConfig();
169
+ const proj = projectRoot();
170
+ // Asked only on the unbound path. A bound project must not spend a git
171
+ // subprocess at every session start on a question it has already answered;
172
+ // an unbound one that is not a worktree pays a single ~10 ms probe.
173
+ let binding = null;
174
+ if (!cfg) {
175
+ try { binding = resolveBinding(proj); } catch {}
176
+ }
177
+ const welcome = showWelcomeOnce(proj, cfg);
178
+ // The person's channel carries one instruction, and it has to be the right
179
+ // one: a bound project is told what it is bound to, not to bind again.
180
+ const welcomeSystemMessage = welcome
181
+ ? (cfg && cfg.vault_path
182
+ ? `👋 projectstore: first-run welcome shown. Bound to ${cfg.vault_path}. See /plugin → Marketplaces to enable auto-update.`
183
+ : "👋 projectstore: first-run welcome shown. Start with /projectstore:bind <vault-path>. See /plugin → Marketplaces to enable auto-update.")
184
+ : null;
185
+
186
+ if (!cfg) {
187
+ // The offer goes ahead of the welcome, not after it: a fresh worktree has no
188
+ // welcome marker either, so the first-run welcome fires here too — and its
189
+ // advice, to bind and point at a vault, is the wrong move for a checkout
190
+ // whose parent is already bound. Ordering is the cheap fix; the small
191
+ // redundancy is accepted rather than papered over with copy that will rot.
192
+ const offer = binding && binding.state === "inheritable" ? bindingOfferText(binding) : "";
193
+ const offerSystemMessage = offer
194
+ ? "projectstore: this worktree is unbound — /projectstore:bind --inherit adopts the binding of the checkout it was forked from."
195
+ : null;
196
+ const body = offer + (welcome || "");
197
+ if (body) {
198
+ // The person's channel carries ONE instruction. Joining both would put
199
+ // "bind --inherit" and "bind <vault-path>" in one line with no ordering
200
+ // cue — not redundancy but a contradiction, in the channel that gets read
201
+ // fastest. The body still carries the welcome for the agent.
202
+ return emit(body, offerSystemMessage || welcomeSystemMessage);
203
+ }
204
+ process.exit(0);
205
+ }
206
+
207
+ // Opt-in status line: keep settings.local.json pointed at this plugin
208
+ // version's statusline.mjs (self-heals on update). Best-effort; a settings
209
+ // write must never break session-context injection.
210
+ try { syncStatusLine(cfg, proj); } catch {}
211
+ // Statusline-feature housekeeping, like syncStatusLine — must run even when
212
+ // auto_inject=false (touch-session writes pointers regardless of it).
213
+ try { cleanupStaleSessionState(proj); } catch {}
214
+
215
+ // Arm BEFORE the auto_inject gate. The entry reminder's markers must be
216
+ // re-armed after a compaction whether or not this session injects context —
217
+ // an auto_inject=false session still writes code, and its reminder was
218
+ // discarded with the conversation just the same.
219
+ //
220
+ // `input` and `sid` are read at the top of main() now, because the project
221
+ // resolves from that payload; this block only consumes them.
222
+ //
223
+ // `compact` and `clear` are the two sources where the session id survives but
224
+ // the conversation does not, so a reminder already delivered is gone from
225
+ // context while its marker persists on disk. Arming lets it fire once more;
226
+ // the cap of two is enforced by the election, not here.
227
+ if (sid && (input?.source === "compact" || input?.source === "clear")) {
228
+ try { armReminder(proj, sid); } catch {}
229
+ }
230
+
231
+ if (cfg.auto_inject === false) {
232
+ if (welcome) return emit(welcome, welcomeSystemMessage);
233
+ process.exit(0);
234
+ }
235
+
236
+ // Contract 23, first half — the gather (and with it the activity read) runs
237
+ // BEFORE registration, so the continuity section sees the log exactly as the
238
+ // previous conversation left it. The exemption below is the other half: it
239
+ // protects the NEXT compaction, this ordering protects this one.
240
+ let facts = null;
241
+ let gatherError = null;
242
+ try {
243
+ facts = await gatherVaultFacts(cfg, { sessionId: sid, source: input?.source });
244
+ } catch (e) {
245
+ gatherError = e;
246
+ }
247
+
248
+ let warning = "";
249
+ // A bound vault that has vanished must not be silently recreated: ensureSessionsDir's
250
+ // recursive mkdir would manufacture it, and from the NEXT start the skeleton would
251
+ // assert eight rows of zeros and "nothing in progress" about a vault nobody
252
+ // scaffolded. The not-found shape has to survive more than one run to be worth
253
+ // anything (contract 17).
254
+ if (sid && !facts?.vaultMissing) {
255
+ try {
256
+ cleanupStaleSessions(cfg.vault_path, 24, sid);
257
+ writeSession(cfg.vault_path, sid, proj);
258
+ const others = readActiveSessions(cfg.vault_path, sid);
259
+ if (others.length > 0) warning = buildOthersWarning(others);
260
+ } catch (e) {
261
+ // Contract 3 — a raw `e.message` is free text and therefore unbounded;
262
+ // node's own filesystem errors already carry two full paths. Assigned
263
+ // here rather than appended: registration failure REPLACES the sibling
264
+ // warning, so the two cannot compound.
265
+ warning = `\n\n## projectstore: session registration failed\n\n${truncEnd(String(e.message), ERROR_CELL)}\n`;
266
+ }
267
+ }
268
+
269
+ // Cheap install-only doctor subset (ADR-005): one line, only when N > 0;
270
+ // aborted past its budget rather than reporting a false "clean".
271
+ let doctorMsg = null;
272
+ let offers = [];
273
+ try {
274
+ const r = runStartupChecks(cfg, proj);
275
+ if (r.skipped) {
276
+ doctorMsg = "projectstore doctor: startup checks skipped — run /projectstore:doctor";
277
+ } else if (r.count > 0) {
278
+ doctorMsg = `projectstore doctor: ${r.count} install issue(s) — run /projectstore:doctor`;
279
+ }
280
+ // Offers (doctor's OFFER_CHECKS): one-time steps a user should see once,
281
+ // e.g. the re-stamp after a plugin update — not issues, not silent.
282
+ offers = (r.offers || []).map((m) => `projectstore: ${m}`);
283
+ } catch {}
284
+ const systemMessage =
285
+ [welcomeSystemMessage, doctorMsg, ...offers].filter(Boolean).join(" · ") || null;
286
+
287
+ if (gatherError) {
288
+ emit(
289
+ welcome +
290
+ `# projectstore: vault load failed\n\n${truncEnd(String(gatherError.message), ERROR_CELL)}\n\nFix \`.projectstore/projectstore.json\` or run \`/projectstore:bind <path>\` again.`,
291
+ systemMessage,
292
+ );
293
+ return;
294
+ }
295
+ emit(welcome + renderVaultSkeleton(facts) + warning, systemMessage);
296
+ }
297
+
298
+ // A hook must never break session startup (contract 17): an unhandled rejection
299
+ // in the async path would exit non-zero and surface as a hook failure to the
300
+ // user, which is a worse outcome than a session with no orientation.
301
+ main().catch(() => process.exit(0));
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // projectstore — session-stop.mjs
3
+ //
4
+ // The entry reminder's fallback carrier (spec contract 14).
5
+ //
6
+ // The PostToolUse carrier reaches the main agent on its next own tool call.
7
+ // Under a delegation-heavy configuration there may not be one: the main agent
8
+ // hands the writing to subagents — whose calls count toward the score but are
9
+ // never the audience — and then answers the user. Stop fires exactly there.
10
+ //
11
+ // Two things about Stop that shape this file:
12
+ //
13
+ // * `additionalContext` on Stop CONTINUES the turn. So this hook must run the
14
+ // same create-then-emit election as the PostToolUse branch, not merely read
15
+ // the markers: a guard that only checks would emit, the turn would continue,
16
+ // the model would finish again, Stop would fire again — forever. Creating
17
+ // the marker before emitting is what makes the second firing impossible.
18
+ // * `stop_hook_active` is set while a Stop hook is already driving the turn.
19
+ // Honouring it is belt and braces; the election is the load-bearing guard.
20
+ //
21
+ // Not SubagentStop: it fires once per subagent and its continuation belongs to
22
+ // the subagent, which is the actor that cannot open a story.
23
+ //
24
+ // Requires Claude Code 2.1.163+, where additionalContext on Stop is honoured.
25
+
26
+ import {
27
+ readConfig,
28
+ readStdinJson,
29
+ adoptHookInput,
30
+ projectRoot,
31
+ entryScore,
32
+ readOpenStoryCache,
33
+ resolveOpenStory,
34
+ writeOpenStoryCache,
35
+ mayRemind,
36
+ electEmitter,
37
+ appendEntryLog,
38
+ entryReminderText,
39
+ ENTRY_THRESHOLD,
40
+ } from "../scripts/lib.mjs";
41
+
42
+ async function main() {
43
+ // The payload before the project: readConfig() resolves through
44
+ // projectRoot(), and on a harness that exports no project-dir variable the
45
+ // payload's cwd is the only answer better than this process's cwd. Read once
46
+ // — fd 0 is empty on a second read.
47
+ const input = adoptHookInput(readStdinJson());
48
+
49
+ const cfg = readConfig();
50
+ if (!cfg || cfg.guard === "off") return;
51
+
52
+ const sid = input?.session_id;
53
+ if (!sid) return;
54
+ if (input.stop_hook_active) return;
55
+ // Same audience rule as the tool-call carrier.
56
+ if (input.agent_id || input.agent_type) return;
57
+
58
+ const proj = projectRoot();
59
+ const score = entryScore(proj, sid);
60
+ if (score < ENTRY_THRESHOLD) return;
61
+ if (!mayRemind(proj, sid)) return;
62
+
63
+ let verdict = readOpenStoryCache(proj, sid);
64
+ if (verdict === null) {
65
+ verdict = await resolveOpenStory(cfg.vault_path);
66
+ writeOpenStoryCache(proj, sid, verdict);
67
+ }
68
+ // Nothing written to stdout yet, so exiting here cannot truncate anything.
69
+ if (verdict === "unknown") process.exit(0);
70
+ if (verdict !== false) return;
71
+
72
+ if (!electEmitter(proj, sid)) return;
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");
80
+ }
81
+
82
+ main().catch(() => {
83
+ // A hook must never break the user's turn.
84
+ });
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "projectstore",
3
+ "version": "0.28.1",
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
+ "keywords": [
6
+ "project-management",
7
+ "adr",
8
+ "epics",
9
+ "stories",
10
+ "kanban",
11
+ "obsidian",
12
+ "markdown",
13
+ "knowledge-base",
14
+ "engineering-process",
15
+ "claude-code",
16
+ "codex",
17
+ "agentic",
18
+ "ai-agents"
19
+ ],
20
+ "homepage": "https://github.com/SmartAndPoint/ProjectStore#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/SmartAndPoint/ProjectStore/issues"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/SmartAndPoint/ProjectStore.git"
27
+ },
28
+ "license": "MIT",
29
+ "author": {
30
+ "name": "Evgenii Konev",
31
+ "email": "ekonev@smartandpoint.com",
32
+ "url": "https://github.com/SmartAndPoint"
33
+ },
34
+ "type": "module",
35
+ "engines": {
36
+ "node": ">=20.0.0"
37
+ },
38
+ "bin": {
39
+ "projectstore": "bin/projectstore.mjs"
40
+ },
41
+ "files": [
42
+ "bin/",
43
+ ".claude-plugin/",
44
+ "agents/",
45
+ "commands/",
46
+ "harnesses/",
47
+ "hooks/",
48
+ "scripts/",
49
+ "skills/",
50
+ "scaffold/",
51
+ "templates/",
52
+ "docs/",
53
+ "AGENTS.md",
54
+ ".mcp.json"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "test": "node --test tests/*.test.mjs",
61
+ "guard": "node scripts/version-guard.mjs",
62
+ "packlist": "node scripts/version-guard.mjs --write-packlist",
63
+ "adapters": "node scripts/build-adapters.mjs --write",
64
+ "adapters:check": "node scripts/build-adapters.mjs --check",
65
+ "shells": "node packaging/shells.mjs --write",
66
+ "shells:check": "node packaging/shells.mjs --check",
67
+ "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
+ }
70
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "_description": "Per-kind structural checklists used by /projectstore:review. Each kind defines `default_review` (whether the projectstore-peer-reviewer skill auto-suggests review after creation) and an `items` array fed verbatim to the critic agent.",
3
+ "adr": {
4
+ "default_review": true,
5
+ "items": [
6
+ "Decision section is unambiguous — a stranger could implement it from this file alone, with no other context.",
7
+ "At least one named Alternative is present with an explicit rejection reason ('Rejected because: ...').",
8
+ "Consequences include at least one Negative / trade-off stated honestly. Reject sales framing.",
9
+ "All hand-wavy referents ('we', 'users', 'the system') are resolved to concrete actors or components.",
10
+ "No marketing language: 'blazingly', 'robust', 'best-in-class', 'magnificent', 'rock-solid', etc.",
11
+ "Context explains why the decision is needed NOW (vs. a year ago or in 6 months).",
12
+ "References cite at least one external doc / prior ADR / RFC if relevant.",
13
+ "Status, Date, Authors metadata filled (not template placeholders).",
14
+ "No TODOs, no '...', no empty bullets in the final body."
15
+ ]
16
+ },
17
+ "research": {
18
+ "default_review": true,
19
+ "items": [
20
+ "Question is sharp — answerable by yes/no/comparison, not 'tell me about X'.",
21
+ "TL;DR is a single paragraph that gives the conclusion before the body.",
22
+ "Method describes how findings were gathered (docs read, benchmarks run, experts asked) — not just 'I researched'.",
23
+ "Comparison table has populated Pros AND Cons cells; empty cells flagged.",
24
+ "Conclusion explicitly ties back to the original Question. Recommendation is actionable.",
25
+ "References are linkable (URL / file path), not 'see [some doc]'.",
26
+ "No unstated benchmarks ('it's faster' without numbers).",
27
+ "Findings are evidence-based, not opinion. Where opinion appears, it's labelled."
28
+ ]
29
+ },
30
+ "spec": {
31
+ "default_review": true,
32
+ "items": [
33
+ "How we solve references at least one ADR by id; the references resolve.",
34
+ "Behavioral contracts are numbered and individually testable — each states an observable outcome, not an implementation step.",
35
+ "Acceptance items are checkboxes; acceptance is explicitly ADDITIVE to the covered stories' own criteria, not a replacement.",
36
+ "Acceptance items that belong to specific stories carry a '— stories: <id>' attribution; items meant for all covered stories are unattributed on purpose.",
37
+ "frontmatter stories: uses <epic-id>/<story-id> path form, inline flow list.",
38
+ "Mandatory sections present (How we solve / Behavioral contracts / Acceptance); Modules-on-disk and Testing may be omitted only for research spikes.",
39
+ "For a spike spec: contracts read as questions-to-answer plus exit criteria.",
40
+ "No TODOs, no '...', no empty bullets in the final body."
41
+ ]
42
+ },
43
+ "epic": {
44
+ "default_review": true,
45
+ "items": [
46
+ "Goal is measurable — success can be verified, not just described.",
47
+ "Context explains why this epic exists and which downstream pieces depend on it.",
48
+ "Stories table has at least 2 entries; first story description is concrete enough to start.",
49
+ "Expected Results contains testable outcomes, not aspirations.",
50
+ "Dependencies list external blockers (people, systems, decisions), not just sibling stories.",
51
+ "Open Questions are real unknowns, not rhetorical filler.",
52
+ "Priority assigned (p0 / p1 / p2), not left as default if epic is non-trivial."
53
+ ]
54
+ },
55
+ "story": {
56
+ "default_review": false,
57
+ "items": [
58
+ "Description states the user/actor and the value of the change.",
59
+ "Decomposition checklist has items granular enough to finish in a session each.",
60
+ "Acceptance Criteria are testable (someone could write tests from these).",
61
+ "Technical Notes flag any non-obvious risks or constraints."
62
+ ]
63
+ },
64
+ "concept": {
65
+ "default_review": false,
66
+ "items": [
67
+ "Definition fits in one paragraph; a stranger leaves knowing what the concept IS.",
68
+ "Examples ground the concept (at least one concrete case)."
69
+ ]
70
+ },
71
+ "meeting": {
72
+ "default_review": false,
73
+ "items": [
74
+ "Attendees listed.",
75
+ "Decisions section captures what was actually decided (not just discussed).",
76
+ "Action items have an owner."
77
+ ]
78
+ },
79
+ "runbook": {
80
+ "default_review": false,
81
+ "items": [
82
+ "Purpose states WHEN to run this runbook (trigger conditions).",
83
+ "Prerequisites are listable checks, not prose.",
84
+ "Verification step exists and is testable.",
85
+ "Rollback step exists for any irreversible action."
86
+ ]
87
+ }
88
+ }