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.
- package/.codex-plugin/plugin.json +48 -0
- package/README.md +15 -7
- package/bin/projectstore-codex.mjs +88 -0
- package/hooks/hooks.json +59 -0
- package/node_modules/projectstore/.claude-plugin/marketplace.json +40 -0
- package/node_modules/projectstore/.claude-plugin/plugin.json +23 -0
- package/node_modules/projectstore/.mcp.json +14 -0
- package/node_modules/projectstore/AGENTS.md +26 -0
- package/node_modules/projectstore/LICENSE +21 -0
- package/node_modules/projectstore/README.md +284 -0
- package/node_modules/projectstore/agents/archaeologist.md +76 -0
- package/node_modules/projectstore/agents/clerk.md +93 -0
- package/node_modules/projectstore/agents/critic.md +94 -0
- package/node_modules/projectstore/agents/librarian.md +81 -0
- package/node_modules/projectstore/agents/planner.md +80 -0
- package/node_modules/projectstore/agents/reviewer.md +98 -0
- package/node_modules/projectstore/bin/projectstore.mjs +7 -0
- package/node_modules/projectstore/commands/adr.md +57 -0
- package/node_modules/projectstore/commands/agents.md +180 -0
- package/node_modules/projectstore/commands/bind.md +128 -0
- package/node_modules/projectstore/commands/codemap.md +50 -0
- package/node_modules/projectstore/commands/concept.md +17 -0
- package/node_modules/projectstore/commands/doctor.md +166 -0
- package/node_modules/projectstore/commands/epic.md +40 -0
- package/node_modules/projectstore/commands/graph.md +56 -0
- package/node_modules/projectstore/commands/kanban.md +40 -0
- package/node_modules/projectstore/commands/meeting.md +17 -0
- package/node_modules/projectstore/commands/reconcile.md +73 -0
- package/node_modules/projectstore/commands/research.md +17 -0
- package/node_modules/projectstore/commands/review.md +89 -0
- package/node_modules/projectstore/commands/runbook.md +17 -0
- package/node_modules/projectstore/commands/scaffold.md +23 -0
- package/node_modules/projectstore/commands/search.md +22 -0
- package/node_modules/projectstore/commands/spec.md +91 -0
- package/node_modules/projectstore/commands/status.md +27 -0
- package/node_modules/projectstore/commands/statusline.md +46 -0
- package/node_modules/projectstore/commands/story.md +113 -0
- package/node_modules/projectstore/docs/extending.md +172 -0
- package/node_modules/projectstore/docs/getting-started.md +133 -0
- package/node_modules/projectstore/docs/harnesses.md +163 -0
- package/node_modules/projectstore/docs/how-it-works.md +263 -0
- package/node_modules/projectstore/docs/images/loop-light.svg +94 -0
- package/node_modules/projectstore/docs/images/loop.svg +93 -0
- package/node_modules/projectstore/docs/images/statusline-hud.png +0 -0
- package/node_modules/projectstore/docs/images/team-light.svg +79 -0
- package/node_modules/projectstore/docs/images/team.svg +79 -0
- package/node_modules/projectstore/harnesses/claude-code.json +483 -0
- package/node_modules/projectstore/harnesses/codex.json +332 -0
- package/node_modules/projectstore/hooks/hooks.json +59 -0
- package/node_modules/projectstore/hooks/pre-compact.mjs +121 -0
- package/node_modules/projectstore/hooks/session-rules.mjs +63 -0
- package/node_modules/projectstore/hooks/session-start.mjs +301 -0
- package/node_modules/projectstore/hooks/session-stop.mjs +84 -0
- package/node_modules/projectstore/package.json +70 -0
- package/node_modules/projectstore/scaffold/checklists.json +88 -0
- package/node_modules/projectstore/scaffold/headings.json +171 -0
- package/node_modules/projectstore/scaffold/layouts/engineering.json +85 -0
- package/node_modules/projectstore/scripts/binding.mjs +165 -0
- package/node_modules/projectstore/scripts/build-adapters.mjs +264 -0
- package/node_modules/projectstore/scripts/cli.mjs +595 -0
- package/node_modules/projectstore/scripts/codemap.mjs +99 -0
- package/node_modules/projectstore/scripts/diff-refs.mjs +127 -0
- package/node_modules/projectstore/scripts/doctor.mjs +2127 -0
- package/node_modules/projectstore/scripts/draft.mjs +261 -0
- package/node_modules/projectstore/scripts/graph.mjs +219 -0
- package/node_modules/projectstore/scripts/harness.mjs +608 -0
- package/node_modules/projectstore/scripts/install-harness.mjs +1387 -0
- package/node_modules/projectstore/scripts/kanban.mjs +174 -0
- package/node_modules/projectstore/scripts/lib.mjs +3085 -0
- package/node_modules/projectstore/scripts/mcp.mjs +391 -0
- package/node_modules/projectstore/scripts/portable-registration.mjs +198 -0
- package/node_modules/projectstore/scripts/provenance.mjs +375 -0
- package/node_modules/projectstore/scripts/query.mjs +490 -0
- package/node_modules/projectstore/scripts/reconcile.mjs +422 -0
- package/node_modules/projectstore/scripts/statusline-launcher.mjs +141 -0
- package/node_modules/projectstore/scripts/statusline.mjs +253 -0
- package/node_modules/projectstore/scripts/story-section.mjs +209 -0
- package/node_modules/projectstore/scripts/surfaces.mjs +421 -0
- package/node_modules/projectstore/scripts/tokens.mjs +449 -0
- package/node_modules/projectstore/scripts/touch-session.mjs +336 -0
- package/node_modules/projectstore/scripts/version-guard.mjs +255 -0
- package/node_modules/projectstore/scripts/worktree.mjs +109 -0
- package/node_modules/projectstore/skills/projectstore-decision-detector/SKILL.md +40 -0
- package/node_modules/projectstore/skills/projectstore-peer-reviewer/SKILL.md +38 -0
- package/node_modules/projectstore/skills/projectstore-story-completion/SKILL.md +50 -0
- package/node_modules/projectstore/skills/projectstore-vault-communication/SKILL.md +96 -0
- package/node_modules/projectstore/templates/claude-md-block.md.tmpl +26 -0
- package/node_modules/projectstore/templates/de/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/de/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/de/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/de/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/de/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/de/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/de/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/de/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/de/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/de/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/de/strings.json +6 -0
- package/node_modules/projectstore/templates/en/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/en/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/en/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/en/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/en/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/en/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/en/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/en/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/en/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/en/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/en/strings.json +6 -0
- package/node_modules/projectstore/templates/es/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/es/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/es/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/es/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/es/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/es/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/es/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/es/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/es/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/es/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/es/strings.json +6 -0
- package/node_modules/projectstore/templates/fr/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/fr/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/fr/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/fr/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/fr/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/fr/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/fr/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/fr/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/fr/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/fr/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/fr/strings.json +6 -0
- package/node_modules/projectstore/templates/ru/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/ru/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/ru/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/ru/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/ru/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/ru/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/ru/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/ru/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/ru/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/ru/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/ru/strings.json +6 -0
- package/node_modules/projectstore/templates/zh/adr.md.tmpl +67 -0
- package/node_modules/projectstore/templates/zh/concept.md.tmpl +43 -0
- package/node_modules/projectstore/templates/zh/epic.md.tmpl +59 -0
- package/node_modules/projectstore/templates/zh/folder-readme.md.tmpl +14 -0
- package/node_modules/projectstore/templates/zh/kanban.md.tmpl +36 -0
- package/node_modules/projectstore/templates/zh/meeting.md.tmpl +38 -0
- package/node_modules/projectstore/templates/zh/research.md.tmpl +47 -0
- package/node_modules/projectstore/templates/zh/runbook.md.tmpl +53 -0
- package/node_modules/projectstore/templates/zh/spec.md.tmpl +64 -0
- package/node_modules/projectstore/templates/zh/story.md.tmpl +76 -0
- package/node_modules/projectstore/templates/zh/strings.json +6 -0
- package/package.json +36 -14
- package/plugin.json +53 -0
- package/skills/projectstore-adr/SKILL.md +76 -0
- package/skills/projectstore-agents/SKILL.md +50 -0
- package/skills/projectstore-archaeologist/SKILL.md +109 -0
- package/skills/projectstore-bind/SKILL.md +44 -0
- package/skills/projectstore-clerk/SKILL.md +126 -0
- package/skills/projectstore-codemap/SKILL.md +69 -0
- package/skills/projectstore-concept/SKILL.md +36 -0
- package/skills/projectstore-critic/SKILL.md +127 -0
- package/skills/projectstore-decision-detector/SKILL.md +59 -0
- package/skills/projectstore-doctor/SKILL.md +33 -0
- package/skills/projectstore-epic/SKILL.md +59 -0
- package/skills/projectstore-graph/SKILL.md +75 -0
- package/skills/projectstore-kanban/SKILL.md +60 -0
- package/skills/projectstore-librarian/SKILL.md +114 -0
- package/skills/projectstore-meeting/SKILL.md +36 -0
- package/skills/projectstore-peer-reviewer/SKILL.md +57 -0
- package/skills/projectstore-planner/SKILL.md +113 -0
- package/skills/projectstore-reconcile/SKILL.md +92 -0
- package/skills/projectstore-research/SKILL.md +36 -0
- package/skills/projectstore-review/SKILL.md +108 -0
- package/skills/projectstore-reviewer/SKILL.md +131 -0
- package/skills/projectstore-runbook/SKILL.md +36 -0
- package/skills/projectstore-scaffold/SKILL.md +42 -0
- package/skills/projectstore-search/SKILL.md +41 -0
- package/skills/projectstore-spec/SKILL.md +110 -0
- package/skills/projectstore-status/SKILL.md +47 -0
- package/skills/projectstore-statusline/SKILL.md +29 -0
- package/skills/projectstore-story/SKILL.md +132 -0
- package/skills/projectstore-story-completion/SKILL.md +69 -0
- package/skills/projectstore-vault-communication/SKILL.md +115 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — statusline.mjs (v2, ADR-006)
|
|
3
|
+
//
|
|
4
|
+
// Renders the Claude Code status line for a projectstore-bound project.
|
|
5
|
+
// statusLine is NOT a plugin-declarable capability, so it lives in settings
|
|
6
|
+
// with an absolute path: the SessionStart hook points .claude/settings.local.json
|
|
7
|
+
// at the generated .projectstore/state/claude-code/statusline.mjs launcher (when
|
|
8
|
+
// projectstore.json → statusline.enabled=true), and the launcher imports THIS
|
|
9
|
+
// file from whichever plugin version is installed at render time.
|
|
10
|
+
//
|
|
11
|
+
// COMPOSING, not clobbering: the statusLine slot is single. This script
|
|
12
|
+
// DELEGATES to the base statusLine command from the project's
|
|
13
|
+
// .claude/settings.json, else the user's settings.json, prints it verbatim, and adds
|
|
14
|
+
// ONE projectstore line above it (position configurable). With no base
|
|
15
|
+
// command it renders a standalone line: [PS#v] <model> · <dir> · ⎇ <branch> · 📚 …
|
|
16
|
+
//
|
|
17
|
+
// The 📚 segment is resolved PER SESSION with zero cross-session and zero
|
|
18
|
+
// vault reads (ADR-006):
|
|
19
|
+
// 1. this session's pointer file
|
|
20
|
+
// (<project>/.projectstore/state/sessions/<session_id>.json — written by
|
|
21
|
+
// the PreToolUse hook with denormalized titles), else
|
|
22
|
+
// 2. an explicit localized cold-start line ("no epic or story in this
|
|
23
|
+
// session yet"). Never blank while enabled; a pointer that exists but
|
|
24
|
+
// fails to parse renders an error-marked string, never the cheerful
|
|
25
|
+
// cold-start line. The renderer drops a .last-render.json breadcrumb so
|
|
26
|
+
// doctor can detect session_id divergence between hook and statusLine.
|
|
27
|
+
//
|
|
28
|
+
// Everything is best-effort; on any error emit what we have and exit 0.
|
|
29
|
+
|
|
30
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
31
|
+
import { join, basename, resolve } from "node:path";
|
|
32
|
+
import { spawnSync } from "node:child_process";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
import {
|
|
35
|
+
readConfigAt,
|
|
36
|
+
pluginRoot,
|
|
37
|
+
sessionStatePath,
|
|
38
|
+
stateDir,
|
|
39
|
+
ensureStateDir,
|
|
40
|
+
statusLineIsOurs,
|
|
41
|
+
claudeHome,
|
|
42
|
+
writeFileAtomic,
|
|
43
|
+
} from "./lib.mjs";
|
|
44
|
+
|
|
45
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
46
|
+
const SEP = " · ";
|
|
47
|
+
const BRANCH = "⎇ ";
|
|
48
|
+
const BOOK = "📚 ";
|
|
49
|
+
const ARROW = " › ";
|
|
50
|
+
|
|
51
|
+
// Claude Code cancels an in-flight statusLine by closing our stdout pipe when
|
|
52
|
+
// a newer update arrives; guard the pipe so we honour the never-crash contract.
|
|
53
|
+
process.stdout.on("error", () => process.exit(0));
|
|
54
|
+
|
|
55
|
+
function readRawStdin() {
|
|
56
|
+
try { return readFileSync(0, "utf8"); } catch { return ""; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const FALLBACK_STRINGS = {
|
|
60
|
+
statusline_no_work: "No epic or story in this session yet",
|
|
61
|
+
statusline_state_error: "⚠ session state unreadable",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function loadStrings(lang) {
|
|
65
|
+
const read = (l) => {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(readFileSync(join(pluginRoot(), "templates", l, "strings.json"), "utf8"));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
...FALLBACK_STRINGS,
|
|
74
|
+
...(read("en") || {}),
|
|
75
|
+
...(lang && lang !== "en" ? read(lang) || {} : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pluginVersion() {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(readFileSync(join(pluginRoot(), ".claude-plugin", "plugin.json"), "utf8")).version;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Current git branch by reading .git/HEAD directly — no process spawn.
|
|
88
|
+
function gitBranch(projectDir) {
|
|
89
|
+
try {
|
|
90
|
+
const dotgit = join(projectDir, ".git");
|
|
91
|
+
let gitDir;
|
|
92
|
+
const st = readFileSync(dotgit, "utf8");
|
|
93
|
+
const m = st.trim().match(/^gitdir:\s*(.+)$/);
|
|
94
|
+
gitDir = m ? resolve(projectDir, m[1].trim()) : dotgit;
|
|
95
|
+
const head = readFileSync(join(gitDir, "HEAD"), "utf8").trim();
|
|
96
|
+
const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
97
|
+
if (ref) return ref[1];
|
|
98
|
+
if (/^[0-9a-f]{7,40}$/i.test(head)) return head.slice(0, 7);
|
|
99
|
+
return null;
|
|
100
|
+
} catch {
|
|
101
|
+
try {
|
|
102
|
+
const head = readFileSync(join(projectDir, ".git", "HEAD"), "utf8").trim();
|
|
103
|
+
const ref = head.match(/^ref:\s*refs\/heads\/(.+)$/);
|
|
104
|
+
return ref ? ref[1] : null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The 📚 segment — strictly this-session (ADR-006). Returns a non-empty string
|
|
112
|
+
// whenever the feature is enabled; distinguishes "no work yet" from "state
|
|
113
|
+
// unreadable" so failures are visible, never masked.
|
|
114
|
+
function bookSegment(cfg, proj, input, strings) {
|
|
115
|
+
const sid = input.session_id;
|
|
116
|
+
// Breadcrumb for doctor's divergence check — best-effort, never fatal.
|
|
117
|
+
try {
|
|
118
|
+
ensureStateDir(proj);
|
|
119
|
+
// sweep=false: this is the hottest path in the plugin (every HUD frame),
|
|
120
|
+
// and the state dir holds one file per session — no readdir here.
|
|
121
|
+
writeFileAtomic(
|
|
122
|
+
join(stateDir(proj), ".last-render.json"),
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
session_id: sid || null,
|
|
125
|
+
at: new Date().toISOString(),
|
|
126
|
+
// Which plugin actually rendered this line — doctor compares it with
|
|
127
|
+
// the installed version to explain a stale badge instead of leaving
|
|
128
|
+
// the user to wonder why an update did not show up.
|
|
129
|
+
version: pluginVersion(),
|
|
130
|
+
root: pluginRoot(),
|
|
131
|
+
}) + "\n",
|
|
132
|
+
{ sweep: false },
|
|
133
|
+
);
|
|
134
|
+
} catch {}
|
|
135
|
+
|
|
136
|
+
if (!sid) return BOOK + strings.statusline_no_work;
|
|
137
|
+
const p = sessionStatePath(proj, sid);
|
|
138
|
+
if (!existsSync(p)) return BOOK + strings.statusline_no_work;
|
|
139
|
+
|
|
140
|
+
let state;
|
|
141
|
+
try {
|
|
142
|
+
state = JSON.parse(readFileSync(p, "utf8"));
|
|
143
|
+
} catch {
|
|
144
|
+
return BOOK + strings.statusline_state_error;
|
|
145
|
+
}
|
|
146
|
+
if (!state || !state.active_epic) return BOOK + strings.statusline_no_work;
|
|
147
|
+
|
|
148
|
+
let seg = BOOK + (state.epic_title || state.active_epic);
|
|
149
|
+
if (state.active_story) {
|
|
150
|
+
seg += ARROW + (state.story_title || state.active_story);
|
|
151
|
+
if (state.story_status) seg += ` (${state.story_status})`;
|
|
152
|
+
}
|
|
153
|
+
return seg;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// The base statusLine command we compose with — highest-precedence entry BELOW
|
|
157
|
+
// our own local one: project settings.json, else user settings.json.
|
|
158
|
+
function discoverBaseCommand(projectDir) {
|
|
159
|
+
const candidates = [
|
|
160
|
+
join(projectDir, ".claude", "settings.json"),
|
|
161
|
+
join(claudeHome(), "settings.json"), // config-dir-aware: else their HUD vanishes
|
|
162
|
+
];
|
|
163
|
+
for (const p of candidates) {
|
|
164
|
+
try {
|
|
165
|
+
const cmd = JSON.parse(readFileSync(p, "utf8"))?.statusLine?.command;
|
|
166
|
+
// Never compose over ourselves: any of our own wirings (plugin script or
|
|
167
|
+
// generated launcher) would recurse.
|
|
168
|
+
if (typeof cmd === "string" && cmd.trim() && !cmd.includes(SELF) && !statusLineIsOurs(cmd)) {
|
|
169
|
+
return cmd;
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function runBase(cmd, rawStdin, env) {
|
|
177
|
+
try {
|
|
178
|
+
const r = spawnSync(cmd, {
|
|
179
|
+
shell: true,
|
|
180
|
+
input: rawStdin,
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
timeout: 2000,
|
|
183
|
+
maxBuffer: 1 << 20,
|
|
184
|
+
env,
|
|
185
|
+
});
|
|
186
|
+
if (r.status !== 0) return null;
|
|
187
|
+
const out = (r.stdout || "").replace(/\s+$/, "");
|
|
188
|
+
return out || null;
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function main() {
|
|
195
|
+
const raw = readRawStdin();
|
|
196
|
+
let input = {};
|
|
197
|
+
try { input = JSON.parse(raw) || {}; } catch { input = {}; }
|
|
198
|
+
|
|
199
|
+
// The status line gets no project variable from the harness; the project is
|
|
200
|
+
// on stdin, and it deliberately beats any exported variable. Read the config
|
|
201
|
+
// for that directory directly — never by mutating process.env, which the
|
|
202
|
+
// composed base command would then inherit.
|
|
203
|
+
const projectDir =
|
|
204
|
+
(input.workspace && input.workspace.project_dir) || input.cwd || process.cwd();
|
|
205
|
+
|
|
206
|
+
let cfg = null;
|
|
207
|
+
try { cfg = readConfigAt(projectDir); } catch { cfg = null; }
|
|
208
|
+
|
|
209
|
+
const strings = loadStrings(cfg && cfg.language);
|
|
210
|
+
let book = null;
|
|
211
|
+
try { book = cfg && cfg.vault_path ? bookSegment(cfg, projectDir, input, strings) : null; } catch { book = null; }
|
|
212
|
+
|
|
213
|
+
const showVersion = !(cfg && cfg.statusline && cfg.statusline.show_version === false);
|
|
214
|
+
const ver = showVersion ? pluginVersion() : null;
|
|
215
|
+
const badge = ver ? `[PS#${ver}] ` : "";
|
|
216
|
+
|
|
217
|
+
const baseCmd = discoverBaseCommand(projectDir);
|
|
218
|
+
const baseOut = baseCmd ? runBase(baseCmd, raw, process.env) : null;
|
|
219
|
+
|
|
220
|
+
// "Never blank" must be total: even if the resolver threw (book === null on a
|
|
221
|
+
// bound project), fall back to the cold-start line rather than vanishing.
|
|
222
|
+
const bookOut =
|
|
223
|
+
cfg && cfg.vault_path ? book || BOOK + strings.statusline_no_work : null;
|
|
224
|
+
|
|
225
|
+
const lines = [];
|
|
226
|
+
if (baseOut) {
|
|
227
|
+
// Compose: keep the base HUD intact; our line = badge + 📚 segment.
|
|
228
|
+
const position = (cfg && cfg.statusline && cfg.statusline.position) || "above";
|
|
229
|
+
const ours = bookOut ? badge + bookOut : null;
|
|
230
|
+
if (ours && position === "above") lines.push(ours);
|
|
231
|
+
lines.push(baseOut);
|
|
232
|
+
if (ours && position !== "above") lines.push(ours);
|
|
233
|
+
} else {
|
|
234
|
+
// Standalone: badge leads the whole line; the book value is its 📚 segment.
|
|
235
|
+
const parts = [];
|
|
236
|
+
const model = input.model && input.model.display_name;
|
|
237
|
+
if (model) parts.push(model);
|
|
238
|
+
if (projectDir) parts.push(basename(projectDir));
|
|
239
|
+
const branch = gitBranch(projectDir);
|
|
240
|
+
if (branch) parts.push(BRANCH + branch);
|
|
241
|
+
if (bookOut) parts.push(bookOut);
|
|
242
|
+
lines.push(badge + parts.join(SEP));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
main();
|
|
250
|
+
} catch {
|
|
251
|
+
// A status line must never crash. Emit nothing rather than an error.
|
|
252
|
+
process.stdout.write("\n");
|
|
253
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// projectstore — story-section.mjs (PS-SPEC story-007)
|
|
3
|
+
// Computes the story lifecycle-gate mutations for /projectstore:story.
|
|
4
|
+
// --check <baseline> adds a deterministic drift verdict (volatile fields
|
|
5
|
+
// excluded) to the JSON as `check: {baseline, match, drift}` — exit 0 either
|
|
6
|
+
// way; drift is a computed fact, not a failure.
|
|
7
|
+
// plan|close. Pure compute, models reconcile.mjs rebuildIndex: reads the
|
|
8
|
+
// original, splices the managed pieces, returns {path, changed, content,
|
|
9
|
+
// notes} on stdout — the COMMAND writes after approval, never this script.
|
|
10
|
+
//
|
|
11
|
+
// node story-section.mjs plan <story-path>
|
|
12
|
+
// node story-section.mjs close <story-path>
|
|
13
|
+
//
|
|
14
|
+
// plan — ensures a `## Implementation Plan` section exists (inserted with a
|
|
15
|
+
// placeholder; the agent fills it in the preview before approval),
|
|
16
|
+
// status → in-progress (never downgrades review/done), stamps
|
|
17
|
+
// started_at (once) and plan_updated_at (every run), bumps updated.
|
|
18
|
+
// close — ensures a `## Final Summary` section exists, status → done,
|
|
19
|
+
// stamps closed_at (once), bumps updated.
|
|
20
|
+
//
|
|
21
|
+
// Timestamps are full ISO-8601 UTC (lib.mjs nowIso) — they anchor the legacy
|
|
22
|
+
// predicate (closed_at vs spec_policy_since) and diff-refs' git --since.
|
|
23
|
+
// Timestamps are written UNCONDITIONALLY of lifecycle_gates: the gates key
|
|
24
|
+
// controls checks and prompts, never the data that other predicates need.
|
|
25
|
+
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
+
import { resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import {
|
|
30
|
+
readConfig,
|
|
31
|
+
parseFrontmatter,
|
|
32
|
+
headingLineRe,
|
|
33
|
+
heading,
|
|
34
|
+
footerDateRe,
|
|
35
|
+
nowIso,
|
|
36
|
+
today,
|
|
37
|
+
} from "./lib.mjs";
|
|
38
|
+
|
|
39
|
+
function die(msg) {
|
|
40
|
+
process.stderr.write(`projectstore/story-section: ${msg}\n`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Replace `key: ...` inside the frontmatter block, or insert the line before
|
|
45
|
+
// the closing --- when the key is absent. Returns the whole file text.
|
|
46
|
+
function setFm(text, key, value) {
|
|
47
|
+
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
48
|
+
if (!m) die("story has no frontmatter block");
|
|
49
|
+
const fmLines = m[1].split("\n");
|
|
50
|
+
const idx = fmLines.findIndex((l) => l.startsWith(`${key}:`));
|
|
51
|
+
const line = `${key}: ${value}`;
|
|
52
|
+
if (idx >= 0) fmLines[idx] = line;
|
|
53
|
+
else fmLines.push(line);
|
|
54
|
+
return text.slice(0, m.index) + `---\n${fmLines.join("\n")}\n---` + text.slice(m.index + m[0].length);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// End index of section `id` (position right before the next `## ` heading
|
|
58
|
+
// after it), or null when the section is absent.
|
|
59
|
+
function sectionEnd(text, id) {
|
|
60
|
+
const m = text.match(headingLineRe(id));
|
|
61
|
+
if (!m) return null;
|
|
62
|
+
const rest = text.slice(m.index + m[0].length);
|
|
63
|
+
const next = rest.search(/^## /m);
|
|
64
|
+
return next === -1 ? text.length : m.index + m[0].length + next;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function insertSection(text, id, lang, placeholder, anchors) {
|
|
68
|
+
if (headingLineRe(id).test(text)) return { text, inserted: false };
|
|
69
|
+
const block = `## ${heading(id, lang)}\n\n${placeholder}\n\n`;
|
|
70
|
+
for (const anchor of anchors) {
|
|
71
|
+
const end = sectionEnd(text, anchor);
|
|
72
|
+
if (end !== null) {
|
|
73
|
+
return { text: text.slice(0, end) + block + text.slice(end), inserted: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Fallback: before the trailing `---` footer, else EOF.
|
|
77
|
+
const footer = text.lastIndexOf("\n---\n");
|
|
78
|
+
const at = footer > 0 ? footer + 1 : text.length;
|
|
79
|
+
return { text: text.slice(0, at) + block + text.slice(at), inserted: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The volatile frontmatter fields — the ones a re-run stamps fresh even when
|
|
83
|
+
// nothing else changed. --check must ignore exactly these, or a comparison
|
|
84
|
+
// against a moments-old baseline reports drift on every single run. started_at
|
|
85
|
+
// belongs here by the same mechanism as the rest: a `plan` re-run against a
|
|
86
|
+
// still-null field stamps a fresh nowIso() each time.
|
|
87
|
+
export const VOLATILE_FM = ["updated", "started_at", "closed_at", "plan_updated_at"];
|
|
88
|
+
|
|
89
|
+
// Both sides of a --check comparison, with the volatile fields and the footer
|
|
90
|
+
// date neutralised. setFm INSERTS an absent key at the END of the frontmatter,
|
|
91
|
+
// which is what makes a null closed_at compare equal to a fresh stamp. A
|
|
92
|
+
// hand-DELETED mid-frontmatter volatile key therefore reads as DRIFT (the
|
|
93
|
+
// insert lands at a different position) — erring safe. The masking that IS
|
|
94
|
+
// accepted: a hand-REWRITTEN volatile value (e.g. a bogus closed_at on a done
|
|
95
|
+
// story) compares equal, and closed_at anchors the legacy predicate and
|
|
96
|
+
// diff-refs --since — --check cannot guard those two consumers.
|
|
97
|
+
function checkNormalize(text) {
|
|
98
|
+
let t = text;
|
|
99
|
+
for (const k of VOLATILE_FM) t = setFm(t, k, '"«volatile»"');
|
|
100
|
+
return t.replace(footerDateRe(), (_m, prefix, suffix) => `${prefix}«volatile»${suffix}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function main() {
|
|
104
|
+
const argv = process.argv.slice(2);
|
|
105
|
+
let checkPath = null;
|
|
106
|
+
const positional = [];
|
|
107
|
+
for (let i = 0; i < argv.length; i++) {
|
|
108
|
+
const a = argv[i];
|
|
109
|
+
if (a === "--check") checkPath = argv[++i];
|
|
110
|
+
else if (a.startsWith("--check=")) checkPath = a.slice("--check=".length);
|
|
111
|
+
else positional.push(a);
|
|
112
|
+
}
|
|
113
|
+
if (checkPath === undefined || checkPath === "") die("--check requires a value");
|
|
114
|
+
const [mode, storyPath] = positional;
|
|
115
|
+
if (!["plan", "close"].includes(mode) || !storyPath) {
|
|
116
|
+
die("usage: story-section.mjs <plan|close> <story-path> [--check <baseline-file>]");
|
|
117
|
+
}
|
|
118
|
+
const cfg = readConfig();
|
|
119
|
+
if (!cfg) die("No projectstore config. Run /projectstore:bind first.");
|
|
120
|
+
const lang = cfg.language || "en";
|
|
121
|
+
const abs = resolve(storyPath);
|
|
122
|
+
if (!existsSync(abs)) die(`story not found: ${abs}`);
|
|
123
|
+
|
|
124
|
+
const original = readFileSync(abs, "utf8");
|
|
125
|
+
const fm = parseFrontmatter(original).data;
|
|
126
|
+
if (fm.type && fm.type !== "story") die(`not a story (type: ${fm.type})`);
|
|
127
|
+
|
|
128
|
+
let text = original;
|
|
129
|
+
const notes = [];
|
|
130
|
+
const now = nowIso();
|
|
131
|
+
const status = String(fm.status || "").toLowerCase();
|
|
132
|
+
|
|
133
|
+
if (mode === "plan") {
|
|
134
|
+
const r = insertSection(text, "implementation_plan", lang,
|
|
135
|
+
"<!-- Route through the covering spec's contracts: which contracts, in what order, which files. Filled at the gate. -->",
|
|
136
|
+
["decomposition", "description"]);
|
|
137
|
+
text = r.text;
|
|
138
|
+
if (r.inserted) notes.push("inserted Implementation Plan section");
|
|
139
|
+
if (!["in-progress", "in_progress", "review", "done"].includes(status)) {
|
|
140
|
+
text = setFm(text, "status", "in-progress");
|
|
141
|
+
notes.push(`status: ${fm.status || "planned"} → in-progress`);
|
|
142
|
+
}
|
|
143
|
+
if (!fm.started_at || fm.started_at === "null") {
|
|
144
|
+
text = setFm(text, "started_at", JSON.stringify(now));
|
|
145
|
+
notes.push("stamped started_at");
|
|
146
|
+
}
|
|
147
|
+
text = setFm(text, "plan_updated_at", JSON.stringify(now));
|
|
148
|
+
notes.push("stamped plan_updated_at");
|
|
149
|
+
} else {
|
|
150
|
+
const r = insertSection(text, "final_summary", lang,
|
|
151
|
+
"<!-- What changed, why, tests executed, risks and follow-ups. Filled at the gate. -->",
|
|
152
|
+
["acceptance", "decomposition", "description"]);
|
|
153
|
+
text = r.text;
|
|
154
|
+
if (r.inserted) notes.push("inserted Final Summary section");
|
|
155
|
+
if (status !== "done") {
|
|
156
|
+
text = setFm(text, "status", "done");
|
|
157
|
+
notes.push(`status: ${fm.status || "?"} → done`);
|
|
158
|
+
}
|
|
159
|
+
if (!fm.closed_at || fm.closed_at === "null") {
|
|
160
|
+
text = setFm(text, "closed_at", JSON.stringify(now));
|
|
161
|
+
notes.push("stamped closed_at");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
text = setFm(text, "updated", today());
|
|
165
|
+
// Keep the body footer in step with frontmatter `updated:`. Registry-driven, so
|
|
166
|
+
// every bundled language is covered; the file's own label and punctuation are
|
|
167
|
+
// preserved and only the date is rewritten.
|
|
168
|
+
text = text.replace(footerDateRe(), (_m, prefix, suffix) => `${prefix}${today()}${suffix}`);
|
|
169
|
+
|
|
170
|
+
let check = null;
|
|
171
|
+
if (checkPath !== null) {
|
|
172
|
+
if (!existsSync(resolve(checkPath))) die(`--check baseline not found: ${checkPath}`);
|
|
173
|
+
const baseline = readFileSync(resolve(checkPath), "utf8").replace(/\r\n/g, "\n");
|
|
174
|
+
if (!/^---\n[\s\S]*?\n---/.test(baseline)) die(`--check baseline has no frontmatter block: ${checkPath}`);
|
|
175
|
+
const a = checkNormalize(text.replace(/\r\n/g, "\n"));
|
|
176
|
+
const b = checkNormalize(baseline);
|
|
177
|
+
const drift = [];
|
|
178
|
+
if (a !== b) {
|
|
179
|
+
// Trim the common prefix and suffix first: a single inserted line
|
|
180
|
+
// otherwise shifts every later pairing and the report drowns in
|
|
181
|
+
// artifacts of the shift instead of naming the change. Line numbers are
|
|
182
|
+
// post-normalization (volatile keys may have been inserted).
|
|
183
|
+
const al = a.split("\n"), bl = b.split("\n");
|
|
184
|
+
let lo = 0;
|
|
185
|
+
while (lo < al.length && lo < bl.length && al[lo] === bl[lo]) lo++;
|
|
186
|
+
let hiA = al.length - 1, hiB = bl.length - 1;
|
|
187
|
+
while (hiA > lo && hiB > lo && al[hiA] === bl[hiB]) { hiA--; hiB--; }
|
|
188
|
+
for (let i = 0; i <= Math.max(hiA, hiB) - lo && drift.length < 5; i++) {
|
|
189
|
+
const x = al[lo + i], y = bl[lo + i];
|
|
190
|
+
if (lo + i > hiA && lo + i > hiB) break;
|
|
191
|
+
drift.push(`normalized line ${lo + i + 1}: ${JSON.stringify((x ?? "«absent»").slice(0, 120))} vs ${JSON.stringify((y ?? "«absent»").slice(0, 120))}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
check = { baseline: resolve(checkPath), match: a === b, drift };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.stdout.write(JSON.stringify({
|
|
198
|
+
path: abs,
|
|
199
|
+
mode,
|
|
200
|
+
changed: text !== original,
|
|
201
|
+
check,
|
|
202
|
+
notes,
|
|
203
|
+
content: text,
|
|
204
|
+
}, null, 2) + "\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
208
|
+
main();
|
|
209
|
+
}
|