@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.
- package/README.md +3 -3
- package/dist/bin/create-nexus-devflow.d.ts +2 -0
- package/dist/bin/create-nexus-devflow.js +63 -2
- package/dist/bin/create-nexus-devflow.js.map +1 -1
- package/dist/lib/project-config.d.ts +3 -1
- package/dist/lib/project-config.js +3 -1
- package/dist/lib/project-config.js.map +1 -1
- package/dist/lib/skill-manager.d.ts +5 -0
- package/dist/lib/skill-manager.js +30 -0
- package/dist/lib/skill-manager.js.map +1 -1
- package/dist/lib/update.d.ts +4 -2
- package/dist/lib/update.js +12 -6
- package/dist/lib/update.js.map +1 -1
- package/dist/scripts/prepare-template.js +11 -0
- package/dist/scripts/prepare-template.js.map +1 -1
- package/package.json +1 -1
- package/template/.agents/skills/analyze/SKILL.md +113 -0
- package/template/.agents/skills/doctor/SKILL.md +52 -49
- package/template/.agents/skills/doctor/scripts/run-state.mjs +513 -0
- package/template/.agents/skills/feature/SKILL.md +2 -0
- package/template/.agents/skills/feature/reference/feature-spec-template.md +82 -0
- package/template/.agents/skills/implement/SKILL.md +3 -35
- package/template/.agents/skills/implement/reference/rollback-implementation.md +35 -0
- package/template/.agents/skills/report-html/SKILL.md +25 -5
- package/template/.agents/skills/status/SKILL.md +17 -20
- package/template/.claude/skills/analyze/SKILL.md +113 -0
- package/template/.claude/skills/doctor/SKILL.md +52 -49
- package/template/.claude/skills/doctor/scripts/run-state.mjs +513 -0
- package/template/.claude/skills/feature/SKILL.md +2 -0
- package/template/.claude/skills/feature/reference/feature-spec-template.md +82 -0
- package/template/.claude/skills/implement/SKILL.md +3 -35
- package/template/.claude/skills/implement/reference/rollback-implementation.md +35 -0
- package/template/.claude/skills/report-html/SKILL.md +25 -5
- package/template/.claude/skills/status/SKILL.md +17 -20
- package/template/AGENTS.md +19 -11
- package/template/devflow/build-plan.md +31 -0
- package/template/devflow/reference/studio.html +0 -504
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doctor
|
|
3
|
-
description: "[devflow] Run a
|
|
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 -
|
|
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
|
-
(
|
|
13
|
+
(diagnostic) prints health, warnings, and repair order
|
|
12
14
|
|
|
13
|
-
This skill answers one question: *is this
|
|
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,
|
|
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. **
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
-
|
|
140
|
-
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
205
|
-
installs,
|
|
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`.
|