@jakkrichm/create-nexus-devflow 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/create-nexus-devflow.d.ts +2 -0
  3. package/dist/bin/create-nexus-devflow.js +63 -2
  4. package/dist/bin/create-nexus-devflow.js.map +1 -1
  5. package/dist/lib/project-config.d.ts +3 -1
  6. package/dist/lib/project-config.js +3 -1
  7. package/dist/lib/project-config.js.map +1 -1
  8. package/dist/lib/skill-manager.d.ts +5 -0
  9. package/dist/lib/skill-manager.js +30 -0
  10. package/dist/lib/skill-manager.js.map +1 -1
  11. package/dist/lib/update.d.ts +4 -2
  12. package/dist/lib/update.js +12 -6
  13. package/dist/lib/update.js.map +1 -1
  14. package/dist/scripts/prepare-template.js +11 -0
  15. package/dist/scripts/prepare-template.js.map +1 -1
  16. package/package.json +1 -1
  17. package/template/.agents/skills/analyze/SKILL.md +113 -0
  18. package/template/.agents/skills/doctor/SKILL.md +52 -49
  19. package/template/.agents/skills/doctor/scripts/run-state.mjs +513 -0
  20. package/template/.agents/skills/feature/SKILL.md +2 -0
  21. package/template/.agents/skills/feature/reference/feature-spec-template.md +82 -0
  22. package/template/.agents/skills/implement/SKILL.md +3 -35
  23. package/template/.agents/skills/implement/reference/rollback-implementation.md +35 -0
  24. package/template/.agents/skills/report-html/SKILL.md +25 -5
  25. package/template/.agents/skills/status/SKILL.md +17 -20
  26. package/template/.claude/skills/analyze/SKILL.md +113 -0
  27. package/template/.claude/skills/doctor/SKILL.md +52 -49
  28. package/template/.claude/skills/doctor/scripts/run-state.mjs +513 -0
  29. package/template/.claude/skills/feature/SKILL.md +2 -0
  30. package/template/.claude/skills/feature/reference/feature-spec-template.md +82 -0
  31. package/template/.claude/skills/implement/SKILL.md +3 -35
  32. package/template/.claude/skills/implement/reference/rollback-implementation.md +35 -0
  33. package/template/.claude/skills/report-html/SKILL.md +25 -5
  34. package/template/.claude/skills/status/SKILL.md +17 -20
  35. package/template/AGENTS.md +19 -11
  36. package/template/devflow/build-plan.md +31 -0
  37. package/template/devflow/reference/studio.html +0 -504
@@ -1,20 +1,23 @@
1
1
  ---
2
2
  name: doctor
3
- description: "[devflow] Run a read-only DevFlow health and context check covering setup, adapters, commands, visibility, plans, overview freshness, configuration, and workflow drift. Use for /doctor, setup problems, context overhead, or health checks."
3
+ description: "[devflow] Run a DevFlow health and context check covering setup, adapters, commands, visibility, plans, overview freshness, configuration, dashboard state, and workflow drift. May offer to reset malformed generated dashboard state after approval. Use for /doctor, setup problems, context overhead, or health checks."
4
4
  ---
5
5
 
6
- # doctor - Blueprint health check
6
+ # doctor - DevFlow health check
7
+
8
+ **Context reuse:** Reuse any required file already loaded in project instructions or the current session. Read it again only if absent, changed, or exact current bytes or line references are needed.
7
9
 
8
10
  Where this sits in the workflow:
9
11
 
10
12
  any time -> [doctor] -> reads setup + plans + workflow state + git
11
- (read-only) prints health, warnings, and repair order
13
+ (diagnostic) prints health, warnings, and repair order
12
14
 
13
- This skill answers one question: *is this Blueprint project ready to use?* It is
15
+ This skill answers one question: *is this DevFlow project ready to use?* It is
14
16
  the diagnostic pass for setup drift, incomplete onboarding, missing files,
15
- placeholder plans, stale generated context, Blueprint visibility, and confusing
17
+ placeholder plans, stale generated context, DevFlow visibility, and confusing
16
18
  workflow state. It never changes anything: no edits, no commits, no installs, no
17
- builds, no branch changes.
19
+ builds, no branch changes. Its only repair is an approved reset of a malformed
20
+ generated `devflow/.state/run.json` file.
18
21
 
19
22
  Use `/status` when the user mainly wants progress and the next build action. Use
20
23
  `/doctor` when the user wants to know whether the workflow itself is healthy.
@@ -66,6 +69,10 @@ Gather these, then summarize. Do not dump file contents.
66
69
  logical adapters: Codex, Claude Code, GitHub Copilot, Antigravity, and OpenCode.
67
70
  - Confirm at least one compatible skill tree exists. Codex, Antigravity, and
68
71
  GitHub Copilot use `.agents/skills/`. Claude Code uses `.claude/skills/`.
72
+ - Confirm each installed adapter tree contains
73
+ `doctor/scripts/run-state.mjs`. This managed helper validates and atomically
74
+ writes dashboard activity. A missing helper needs a DevFlow update before
75
+ tracked commands can record activity safely.
69
76
  - If both skill trees are present, say that is healthy when the selected
70
77
  tools require both. Compare their skill folder names and warn about missing
71
78
  skills on either side.
@@ -105,39 +112,41 @@ Gather these, then summarize. Do not dump file contents.
105
112
  - A missing `Verify` command or GitHub workflow is informational. It means the
106
113
  optional automatic-check setup was not selected, not that DevFlow is
107
114
  unhealthy.
108
- 4. **Ignore rules**
109
- - Check obvious ignore patterns for the detected stack. For Node or Astro,
110
- look for `node_modules`, `.env`, `dist`, and framework cache folders such as
111
- `.astro` or `.next` when relevant.
112
- - Detect local-only DevFlow mode if `.gitignore` ignores `.agents/`,
113
- `.claude/`, `devflow/`, or `CLAUDE.md`. Report it as a visibility choice,
114
- not a failure, when the local files exist.
115
- - In local-only mode, check whether tracked `AGENTS.md` still describes the
116
- Blueprint workflow, lists hidden adapter paths, or exposes the core skill
117
- list. If so, warn that `/onboard` should make `AGENTS.md` public-safe.
118
- - If local-only mode is active but those paths are already tracked by git,
119
- warn that `.gitignore` does not hide tracked files and the user must approve
120
- any `git rm --cached` cleanup separately.
121
- - Keep this conservative. If uncertain, report "review" instead of failure.
122
- 5. **Planning readiness**
123
- - Check whether `devflow/project-plan.md` and `devflow/build-plan.md` look
124
- filled in or still template-like. Treat obvious TODO, TBD, example-only text,
125
- or empty required sections as not ready.
126
- - Check whether `devflow/build-plan.md` is a numbered checkbox list. Raw
127
- bullets are allowed as a first draft, but they should be normalized by
128
- `/overview` before the build loop starts.
129
- - Count checked and unchecked leaf items in `devflow/build-plan.md`.
130
- 6. **Overview freshness**
131
- - Check whether `devflow/context/project-overview.md` exists and looks
132
- generated from the current plans.
115
+ 4. **Visibility and context loading**
116
+ - Confirm whether DevFlow files are public in git or listed in `.gitignore`
117
+ as local-only.
118
+ - When Claude Code is installed, report its startup-context shape. Confirm
119
+ `CLAUDE.md` imports `AGENTS.md` and lets skills load planning context on demand.
120
+ 5. **Project and build plans**
121
+ - Read `devflow/project-plan.md` and `devflow/build-plan.md`.
122
+ - Report total features in the build plan, how many are checked off (`- [x]`),
123
+ and which feature is next.
124
+ - Flag placeholder text (e.g. `[Your project name]`, `Feature 1 description`).
125
+ 6. **Project overview freshness**
126
+ - Read `devflow/context/project-overview.md` if it exists.
133
127
  - Report its byte size. At or above 20,000 bytes, call it oversized and say
134
128
  `/feature` should stop until `/overview` regenerates a compact
135
129
  consolidation.
136
130
  - If either planning file appears newer than the overview by filesystem time,
137
131
  call the overview possibly stale and suggest `/overview` before feature work.
138
132
  7. **Current workflow state**
139
- - Scan `devflow/context/{xxx-slug}/` for active task directories and specs.
140
- - If a spec is active, report checked and unchecked implementation steps.
133
+ - Inspect `devflow/.state/run.json` when it exists. Missing means no recorded
134
+ activity and is healthy. Require a regular non-symbolic-link JSON file that
135
+ matches dashboard schema version 1 from `AGENTS.md`.
136
+ - If the path is a symbolic link or not a regular file, do not read, replace,
137
+ or remove it. Report the exact path for manual review.
138
+ - If the regular file is invalid JSON or does not match the schema, report it
139
+ as malformed generated state. Explain that resetting it removes only the
140
+ dashboard's last-command record, not project work, and that the next tracked
141
+ DevFlow command recreates it.
142
+ - Offer this exact repair question: `Reset the malformed dashboard state now?`
143
+ On approval, use the installed dashboard activity helper's `reset` action.
144
+ It confirms the exact path is a regular non-symbolic-link file and removes
145
+ only `devflow/.state/run.json`. Verify the file is absent and report the
146
+ dashboard state as reset. Never remove `devflow/.state/`, its manifest,
147
+ backups, or any project file. Without approval, leave it unchanged and
148
+ include the reset in `Repair order:`.
149
+ - If a spec is active in `devflow/context/{xxx-slug}/`, report checked and unchecked implementation steps.
141
150
  - If no active task directory exists in `devflow/context/` but git has source or workflow
142
151
  changes, warn that work is happening without an active spec.
143
152
  - Flag active spec on `main`, all spec steps checked but no completion, or a
@@ -168,28 +177,20 @@ steps, in order. Keep it short and practical.
168
177
 
169
178
  Choose the repair order in this priority:
170
179
 
171
- - Required Blueprint files missing -> overlay the Blueprint again, or use
172
- `/adopt` for a brownfield app.
180
+ - Required DevFlow files missing -> overlay DevFlow again, or use `/adopt` for a brownfield app.
173
181
  - No git repo -> initialize git before using the build loop.
174
182
  - No tool adapter -> restore `.agents/skills/` or `.claude/skills/` for the
175
183
  selected tool. OpenCode can use either compatible tree.
184
+ - Installed adapter is missing `doctor/scripts/run-state.mjs` -> update
185
+ DevFlow before relying on dashboard activity.
176
186
  - Onboarding incomplete -> run `/onboard`.
177
- - Root README is still the Blueprint workflow doc -> run `/onboard` to replace
187
+ - Root README is still the DevFlow workflow doc -> run `/onboard` to replace
178
188
  it with a project README before publishing.
179
- - Local-only visibility selected but ignored Blueprint files are missing ->
180
- reinstall or restore the Blueprint files locally.
181
- - Local-only visibility selected but Blueprint paths are tracked -> ask whether
182
- to untrack them with `git rm --cached` while keeping local files.
183
- - Local-only visibility selected but `AGENTS.md` still exposes the workflow ->
184
- run `/onboard` to make `AGENTS.md` a lightweight public project guide.
185
- - A documented `Verify` command, project script, and GitHub workflow disagree ->
186
- run `/ci` to review and align them. Missing optional CI alone does not need
187
- repair.
188
- - Commands or ignore rules need review -> update the files or run `/onboard` if
189
- this is an early project.
190
189
  - Plans are placeholders -> fill `devflow/project-plan.md` and
191
190
  `devflow/build-plan.md`.
192
191
  - Overview missing or stale -> run `/overview`.
192
+ - Malformed regular `devflow/.state/run.json` -> offer to reset that exact
193
+ generated file, then rerun `/doctor` or refresh the dashboard.
193
194
  - Active spec has unchecked steps -> run `/status` or `/implement`, depending on
194
195
  whether the user wants orientation or action.
195
196
  - A P0 or P1 finding is `open` -> repair it through `/implement` while a spec
@@ -201,8 +202,10 @@ Choose the repair order in this priority:
201
202
 
202
203
  ## Rules
203
204
 
204
- - **Read-only, always.** This skill never writes files, never commits, never runs
205
- installs, never runs builds or tests, and never switches branches.
205
+ - **Diagnostic by default.** This skill never edits project files, commits, runs
206
+ installs, runs builds or tests, or switches branches. It may remove only a
207
+ malformed regular `devflow/.state/run.json` through the installed helper
208
+ after the user approves the exact reset described above.
206
209
  - **Diagnose, then order repairs.** Do not just list problems. End with the
207
210
  smallest ordered sequence that gets the project back to a healthy state.
208
211
  - **Do not over-police adapters.** Extra adapters are optional clutter, not a
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Standalone ESM because installed projects may use CommonJS.
4
+ import { randomUUID } from "node:crypto";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+
8
+ const ACTIONS = new Set(["start", "update", "finish", "reset"]);
9
+ const BOUNDARIES = new Set(["read-only", "reviewed", "local-only"]);
10
+ const COMMAND_PATTERN = /^[a-z][a-z-]{0,31}$/;
11
+ const RUN_PATH = path.join("devflow", ".state", "run.json");
12
+ const STATUSES = new Set(["running", "blocked", "ready", "completed"]);
13
+ const MAX_LENGTHS = {
14
+ detail: 1000,
15
+ featureId: 80,
16
+ featureTitle: 160,
17
+ progressLabel: 80,
18
+ resumeCommand: 240,
19
+ summary: 240
20
+ };
21
+
22
+ async function main() {
23
+ const options = parseArgs(process.argv.slice(2));
24
+
25
+ if (options.help) {
26
+ printHelp();
27
+ return;
28
+ }
29
+
30
+ const projectRoot = await findProjectRoot(options.target);
31
+
32
+ if (options.action === "reset") {
33
+ await resetState(projectRoot);
34
+ console.log("Dashboard activity reset.");
35
+ return;
36
+ }
37
+
38
+ const previous = options.action === "start"
39
+ ? null
40
+ : await readState(projectRoot);
41
+ const state = buildState(options, previous, new Date().toISOString());
42
+ await writeState(projectRoot, state);
43
+ console.log(`Recorded /${state.command}: ${state.status} - ${state.summary}`);
44
+ }
45
+
46
+ function parseArgs(args) {
47
+ const options = {
48
+ action: null,
49
+ boundary: null,
50
+ command: null,
51
+ current: null,
52
+ detail: null,
53
+ featureId: null,
54
+ featureTitle: null,
55
+ help: false,
56
+ label: null,
57
+ resumeCommand: null,
58
+ status: null,
59
+ summary: null,
60
+ target: null,
61
+ total: null
62
+ };
63
+
64
+ for (let index = 0; index < args.length; index += 1) {
65
+ const arg = args[index];
66
+
67
+ if (ACTIONS.has(arg)) {
68
+ if (options.action) {
69
+ throw new Error("Choose only one run-state action.");
70
+ }
71
+ options.action = arg;
72
+ continue;
73
+ }
74
+ if (arg === "--help" || arg === "-h") {
75
+ options.help = true;
76
+ continue;
77
+ }
78
+ if (arg === "--command") {
79
+ options.command = readValue(args, ++index, arg);
80
+ continue;
81
+ }
82
+ if (arg === "--summary") {
83
+ options.summary = readValue(args, ++index, arg);
84
+ continue;
85
+ }
86
+ if (arg === "--detail") {
87
+ options.detail = readValue(args, ++index, arg);
88
+ continue;
89
+ }
90
+ if (arg === "--boundary") {
91
+ options.boundary = readValue(args, ++index, arg);
92
+ continue;
93
+ }
94
+ if (arg === "--status") {
95
+ options.status = readValue(args, ++index, arg);
96
+ continue;
97
+ }
98
+ if (arg === "--resume") {
99
+ options.resumeCommand = readValue(args, ++index, arg);
100
+ continue;
101
+ }
102
+ if (arg === "--feature-id") {
103
+ options.featureId = readValue(args, ++index, arg);
104
+ continue;
105
+ }
106
+ if (arg === "--feature-title") {
107
+ options.featureTitle = readValue(args, ++index, arg);
108
+ continue;
109
+ }
110
+ if (arg === "--current") {
111
+ options.current = readInteger(args, ++index, arg);
112
+ continue;
113
+ }
114
+ if (arg === "--total") {
115
+ options.total = readInteger(args, ++index, arg);
116
+ continue;
117
+ }
118
+ if (arg === "--label") {
119
+ options.label = readValue(args, ++index, arg);
120
+ continue;
121
+ }
122
+ if (arg === "--target" || arg === "-t") {
123
+ options.target = readValue(args, ++index, arg);
124
+ continue;
125
+ }
126
+
127
+ throw new Error(`Unknown run-state option: ${arg}`);
128
+ }
129
+
130
+ validateOptions(options);
131
+ return options;
132
+ }
133
+
134
+ function validateOptions(options) {
135
+ if (options.help) {
136
+ return;
137
+ }
138
+ if (!options.action) {
139
+ throw new Error("Choose one run-state action: start, update, finish, or reset.");
140
+ }
141
+
142
+ const progressCount = [options.current, options.total, options.label]
143
+ .filter((value) => value !== null).length;
144
+ if (progressCount !== 0 && progressCount !== 3) {
145
+ throw new Error("Progress requires --current, --total, and --label together.");
146
+ }
147
+ if (options.featureId && !options.featureTitle) {
148
+ throw new Error("--feature-id requires --feature-title.");
149
+ }
150
+
151
+ if (options.action === "reset") {
152
+ const unsupported = [
153
+ options.boundary,
154
+ options.command,
155
+ options.current,
156
+ options.detail,
157
+ options.featureId,
158
+ options.featureTitle,
159
+ options.label,
160
+ options.resumeCommand,
161
+ options.status,
162
+ options.summary,
163
+ options.total
164
+ ].some((value) => value !== null);
165
+ if (unsupported) {
166
+ throw new Error("Reset accepts only --target and --help.");
167
+ }
168
+ return;
169
+ }
170
+
171
+ if (options.action === "start") {
172
+ if (!options.command || !options.summary || !options.boundary) {
173
+ throw new Error("Start requires --command, --summary, and --boundary.");
174
+ }
175
+ if (options.status) {
176
+ throw new Error("Start always records running status.");
177
+ }
178
+ return;
179
+ }
180
+
181
+ if (options.command) {
182
+ throw new Error("Only start accepts --command.");
183
+ }
184
+ if (options.boundary) {
185
+ throw new Error("Only start accepts --boundary.");
186
+ }
187
+ if (options.action === "finish") {
188
+ if (options.status && !["ready", "completed"].includes(options.status)) {
189
+ throw new Error("Finish status must be ready or completed.");
190
+ }
191
+ return;
192
+ }
193
+ if (options.status && !["running", "blocked", "ready"].includes(options.status)) {
194
+ throw new Error("Update status must be running, blocked, or ready.");
195
+ }
196
+
197
+ const hasUpdate = [
198
+ options.current,
199
+ options.detail,
200
+ options.featureTitle,
201
+ options.resumeCommand,
202
+ options.status,
203
+ options.summary
204
+ ].some((value) => value !== null);
205
+ if (!hasUpdate) {
206
+ throw new Error("Update needs at least one changed activity field.");
207
+ }
208
+ }
209
+
210
+ function buildState(options, previous, timestamp) {
211
+ if (options.action === "start") {
212
+ const state = {
213
+ schemaVersion: 1,
214
+ command: requireText(options.command, "command", 32),
215
+ status: "running",
216
+ summary: requireText(options.summary, "summary", MAX_LENGTHS.summary),
217
+ boundary: requireBoundary(options.boundary),
218
+ startedAt: timestamp,
219
+ updatedAt: timestamp
220
+ };
221
+ applyOptionalFields(state, options);
222
+ return validateState(state);
223
+ }
224
+
225
+ const state = {
226
+ ...previous,
227
+ status: options.action === "finish"
228
+ ? options.status || "completed"
229
+ : options.status || previous.status,
230
+ summary: options.summary
231
+ ? requireText(options.summary, "summary", MAX_LENGTHS.summary)
232
+ : previous.summary,
233
+ updatedAt: timestamp
234
+ };
235
+ applyOptionalFields(state, options);
236
+
237
+ if (
238
+ options.action === "finish" ||
239
+ (previous.status === "blocked" && state.status !== "blocked")
240
+ ) {
241
+ if (!options.resumeCommand) {
242
+ delete state.resumeCommand;
243
+ }
244
+ if (!options.detail) {
245
+ delete state.detail;
246
+ }
247
+ }
248
+
249
+ return validateState(state);
250
+ }
251
+
252
+ function applyOptionalFields(state, options) {
253
+ if (options.detail) {
254
+ state.detail = requireText(options.detail, "detail", MAX_LENGTHS.detail);
255
+ }
256
+ if (options.resumeCommand) {
257
+ state.resumeCommand = requireText(
258
+ options.resumeCommand,
259
+ "resume command",
260
+ MAX_LENGTHS.resumeCommand
261
+ );
262
+ }
263
+ if (options.current !== null) {
264
+ state.progress = validateProgress({
265
+ current: options.current,
266
+ total: options.total,
267
+ label: options.label
268
+ });
269
+ }
270
+ if (options.featureTitle) {
271
+ state.feature = {
272
+ id: options.featureId,
273
+ title: requireText(
274
+ options.featureTitle,
275
+ "feature title",
276
+ MAX_LENGTHS.featureTitle
277
+ )
278
+ };
279
+ if (state.feature.id !== null) {
280
+ state.feature.id = requireText(
281
+ state.feature.id,
282
+ "feature ID",
283
+ MAX_LENGTHS.featureId
284
+ );
285
+ }
286
+ }
287
+ }
288
+
289
+ function validateState(value) {
290
+ if (!isRecord(value)) {
291
+ throw new Error("Run state must be an object.");
292
+ }
293
+ if (
294
+ value.schemaVersion !== 1 ||
295
+ typeof value.command !== "string" ||
296
+ !COMMAND_PATTERN.test(value.command) ||
297
+ !STATUSES.has(value.status) ||
298
+ !isBoundedText(value.summary, MAX_LENGTHS.summary) ||
299
+ typeof value.startedAt !== "string" ||
300
+ typeof value.updatedAt !== "string" ||
301
+ Number.isNaN(Date.parse(value.startedAt)) ||
302
+ Number.isNaN(Date.parse(value.updatedAt)) ||
303
+ Date.parse(value.startedAt) > Date.parse(value.updatedAt) ||
304
+ (value.boundary !== undefined && !BOUNDARIES.has(value.boundary)) ||
305
+ (value.detail !== undefined && !isBoundedText(value.detail, MAX_LENGTHS.detail)) ||
306
+ (value.resumeCommand !== undefined &&
307
+ !isBoundedText(value.resumeCommand, MAX_LENGTHS.resumeCommand)) ||
308
+ (value.progress !== undefined && !isValidProgress(value.progress)) ||
309
+ (value.feature !== undefined && !isValidFeature(value.feature))
310
+ ) {
311
+ throw new Error("Run state does not match dashboard schema version 1.");
312
+ }
313
+
314
+ return value;
315
+ }
316
+
317
+ function validateProgress(progress) {
318
+ if (!isValidProgress(progress)) {
319
+ throw new Error(
320
+ "Progress requires integers where 0 <= current <= total and total >= 1."
321
+ );
322
+ }
323
+ return {
324
+ current: progress.current,
325
+ total: progress.total,
326
+ label: progress.label.trim()
327
+ };
328
+ }
329
+
330
+ function isValidProgress(value) {
331
+ return isRecord(value) &&
332
+ Number.isInteger(value.current) &&
333
+ Number.isInteger(value.total) &&
334
+ value.current >= 0 &&
335
+ value.total >= 1 &&
336
+ value.current <= value.total &&
337
+ isBoundedText(value.label, MAX_LENGTHS.progressLabel);
338
+ }
339
+
340
+ function isValidFeature(value) {
341
+ return isRecord(value) &&
342
+ (value.id === null || isBoundedText(value.id, MAX_LENGTHS.featureId)) &&
343
+ isBoundedText(value.title, MAX_LENGTHS.featureTitle);
344
+ }
345
+
346
+ async function findProjectRoot(target) {
347
+ let current = path.resolve(process.cwd(), target || ".");
348
+ const initial = await fs.lstat(current);
349
+ if (initial.isFile()) {
350
+ current = path.dirname(current);
351
+ }
352
+
353
+ while (true) {
354
+ const devflowPath = path.join(current, "devflow");
355
+ try {
356
+ const devflow = await fs.lstat(devflowPath);
357
+ if (devflow.isSymbolicLink() || !devflow.isDirectory()) {
358
+ throw new Error("DevFlow path must be a real directory.");
359
+ }
360
+ const statePath = path.join(devflowPath, ".state");
361
+ const state = await fs.lstat(statePath);
362
+ if (state.isSymbolicLink() || !state.isDirectory()) {
363
+ throw new Error("DevFlow state path must be a real directory.");
364
+ }
365
+ return current;
366
+ } catch (error) {
367
+ if (error?.code !== "ENOENT") {
368
+ throw error;
369
+ }
370
+ }
371
+
372
+ const parent = path.dirname(current);
373
+ if (parent === current) {
374
+ throw new Error("Could not find a DevFlow project with devflow/.state.");
375
+ }
376
+ current = parent;
377
+ }
378
+ }
379
+
380
+ async function readState(projectRoot) {
381
+ const filePath = path.join(projectRoot, RUN_PATH);
382
+ await requireRegularFile(filePath);
383
+
384
+ let parsed;
385
+ try {
386
+ parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
387
+ } catch {
388
+ throw new Error("Existing dashboard state is malformed. Start a new run or use /doctor.");
389
+ }
390
+ return validateState(parsed);
391
+ }
392
+
393
+ async function writeState(projectRoot, state) {
394
+ const filePath = path.join(projectRoot, RUN_PATH);
395
+ await requireRegularOrMissing(filePath);
396
+ const temporaryPath = `${filePath}.${randomUUID()}.tmp`;
397
+
398
+ try {
399
+ await fs.writeFile(
400
+ temporaryPath,
401
+ `${JSON.stringify(validateState(state), null, 2)}\n`,
402
+ { encoding: "utf8", flag: "wx", mode: 0o600 }
403
+ );
404
+ await fs.rename(temporaryPath, filePath);
405
+ validateState(JSON.parse(await fs.readFile(filePath, "utf8")));
406
+ } catch (error) {
407
+ await fs.rm(temporaryPath, { force: true });
408
+ throw error;
409
+ }
410
+ }
411
+
412
+ async function resetState(projectRoot) {
413
+ const filePath = path.join(projectRoot, RUN_PATH);
414
+ try {
415
+ await requireRegularFile(filePath);
416
+ await fs.rm(filePath);
417
+ } catch (error) {
418
+ if (error?.code !== "ENOENT") {
419
+ throw error;
420
+ }
421
+ }
422
+ }
423
+
424
+ async function requireRegularFile(filePath) {
425
+ const stats = await fs.lstat(filePath);
426
+ if (stats.isSymbolicLink() || !stats.isFile()) {
427
+ throw new Error("Dashboard run state must be a regular file.");
428
+ }
429
+ }
430
+
431
+ async function requireRegularOrMissing(filePath) {
432
+ try {
433
+ await requireRegularFile(filePath);
434
+ } catch (error) {
435
+ if (error?.code !== "ENOENT") {
436
+ throw error;
437
+ }
438
+ }
439
+ }
440
+
441
+ function requireBoundary(value) {
442
+ if (!BOUNDARIES.has(value)) {
443
+ throw new Error("Boundary must be read-only, reviewed, or local-only.");
444
+ }
445
+ return value;
446
+ }
447
+
448
+ function requireText(value, label, maxLength) {
449
+ const normalized = value?.trim();
450
+ if (!normalized) {
451
+ throw new Error(`${label} is required.`);
452
+ }
453
+ if (normalized.length > maxLength) {
454
+ throw new Error(`${label} must be ${maxLength} characters or fewer.`);
455
+ }
456
+ return normalized;
457
+ }
458
+
459
+ function isBoundedText(value, maxLength) {
460
+ return typeof value === "string" &&
461
+ value.trim() !== "" &&
462
+ value.length <= maxLength;
463
+ }
464
+
465
+ function isRecord(value) {
466
+ return typeof value === "object" && value !== null && !Array.isArray(value);
467
+ }
468
+
469
+ function readValue(args, index, flag) {
470
+ const value = args[index];
471
+ if (!value) {
472
+ throw new Error(`${flag} needs a value.`);
473
+ }
474
+ return value;
475
+ }
476
+
477
+ function readInteger(args, index, flag) {
478
+ const value = readValue(args, index, flag);
479
+ if (!/^\d+$/.test(value)) {
480
+ throw new Error(`${flag} needs a non-negative integer.`);
481
+ }
482
+ return Number(value);
483
+ }
484
+
485
+ function printHelp() {
486
+ console.log(`DevFlow dashboard activity helper
487
+
488
+ Usage:
489
+ node <helper> start --command feature --summary "Specifying feature 3" --boundary reviewed
490
+ node <helper> update --status blocked --summary "A product decision is required" --resume "/feature 3"
491
+ node <helper> finish --status ready --summary "Feature specification ready"
492
+ node <helper> reset
493
+
494
+ Options:
495
+ --command DevFlow command name, required for start
496
+ --summary Short activity summary
497
+ --detail Concise safe detail
498
+ --boundary read-only, reviewed, or local-only
499
+ --status update: running, blocked, or ready; finish: ready or completed
500
+ --resume Safe recovery command
501
+ --feature-id Build-plan feature ID
502
+ --feature-title Feature, fix, or rollback title
503
+ --current Completed progress count
504
+ --total Total progress count
505
+ --label Progress unit label
506
+ --target, -t Project directory, defaults to the current directory
507
+ --help, -h Show help`);
508
+ }
509
+
510
+ main().catch((error) => {
511
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
512
+ process.exit(1);
513
+ });
@@ -6,6 +6,8 @@ argument-hint: "[{number, name, DISC-id, or IDEA-id}]"
6
6
 
7
7
  # feature - turn a build-plan feature into a buildable spec
8
8
 
9
+ **Context reuse:** Reuse any required file already loaded in project instructions or the current session. Read it again only if absent, changed, or exact current bytes or line references are needed.
10
+
9
11
  **First action:** Before project inspection, preflight, or any other tool call,
10
12
  publish `running` to `devflow/.state/run.json` using the dashboard activity
11
13
  contract in `AGENTS.md`.