@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
|
@@ -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`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 📐 [{xxx-slug}] {title}
|
|
2
|
+
|
|
3
|
+
> **Status**: In-Progress
|
|
4
|
+
> **Track**: Fast-Track (Task-Isolated Living Spec Mode - Feature)
|
|
5
|
+
> **Category**: Feature
|
|
6
|
+
> **Source**: `devflow/build-plan.md: Feature {n}` & `devflow/discoveries/{DISC-ID}/discovery.md`
|
|
7
|
+
> **Branch**: `feature/{xxx-slug}`
|
|
8
|
+
> **Started Date**: {YYYY-MM-DD}
|
|
9
|
+
> **Delivered Date**: TBD
|
|
10
|
+
> **Owner**: DevFlow Core Framework Team & AI
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Specification & Scope
|
|
15
|
+
|
|
16
|
+
### 1.1 Problem Statement
|
|
17
|
+
{คำอธิบายปัญหาและที่มาของความจำเป็นในการพัฒนาฟีเจอร์นี้}
|
|
18
|
+
|
|
19
|
+
### 1.2 In-Scope
|
|
20
|
+
1. {ขอบเขตการทำงานข้อที่ 1}
|
|
21
|
+
2. {ขอบเขตการทำงานข้อที่ 2}
|
|
22
|
+
|
|
23
|
+
### 1.3 Out-of-Scope
|
|
24
|
+
- {สิ่งที่อยู่นอกเหนือขอบเขตหรือไม่ทำในรอบนี้}
|
|
25
|
+
|
|
26
|
+
### 1.4 Acceptance Criteria (เกณฑ์การยอมรับ)
|
|
27
|
+
- [ ] **AC-1**: {เกณฑ์การตรวจรับข้อที่ 1}
|
|
28
|
+
- [ ] **AC-2**: {เกณฑ์การตรวจรับข้อที่ 2}
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Plan & Test Strategy
|
|
33
|
+
|
|
34
|
+
### 2.1 Files Modified / Created
|
|
35
|
+
- `{file-path}` [NEW | MODIFY | DELETE]
|
|
36
|
+
|
|
37
|
+
### 2.2 Quality Gates & Sensitivity Check
|
|
38
|
+
- **Quality Gate Policy (`independentReview`)**: `manual` | `always` | `when-sensitive`
|
|
39
|
+
- **UI Evidence / Browser Tests**: {Not applicable | Playwright / BrowserOS Neo}
|
|
40
|
+
- **Review Strategy**: One feature-level review packet at completion
|
|
41
|
+
|
|
42
|
+
### 2.3 Test Decision: Required (TDD) | Optional
|
|
43
|
+
- **Rationale**: {เหตุผลความจำเป็นในการเขียน Unit Tests / TDD}
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 3. Implementation Checklist (Strict TDD)
|
|
48
|
+
|
|
49
|
+
- [ ] **Task 1: {หัวข้องานที่ 1}**
|
|
50
|
+
- [ ] 1.1 `[TDD-Red]`: {เขียน Test เคสล้มเหลว}
|
|
51
|
+
- [ ] 1.2 `[TDD-Green]`: {เขียนโค้ดขั้นต่ำเพื่อให้ Test ผ่าน}
|
|
52
|
+
- [ ] 1.3 `[TDD-Refactor]`: {Refactor และตรวจให้ 100% Tests Green}
|
|
53
|
+
|
|
54
|
+
- [ ] **Task 2: {หัวข้องานที่ 2}**
|
|
55
|
+
- [ ] 2.1 `[TDD-Red]`: ...
|
|
56
|
+
- [ ] 2.2 `[TDD-Green]`: ...
|
|
57
|
+
- [ ] 2.3 `[TDD-Refactor]`: ...
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 4. Verification Evidence Matrix
|
|
62
|
+
|
|
63
|
+
### ⚖️ Axis 1: Standards, Architecture & Quality Gate
|
|
64
|
+
- **Type Safety & Build Integrity**: TBD
|
|
65
|
+
- **Automated Test Matrix**: TBD
|
|
66
|
+
- **Static Contract Verification**: TBD
|
|
67
|
+
- **Package Smoke Test**: TBD
|
|
68
|
+
- **Findings Ledger**: ตรวจสอบ `findings.md` สะอาด 100%
|
|
69
|
+
|
|
70
|
+
### 🎯 Axis 2: Spec Fidelity & Behavioral Acceptance Gate
|
|
71
|
+
- [ ] **AC-1**: {หลักฐานการผ่านเกณฑ์ข้อที่ 1}
|
|
72
|
+
- [ ] **AC-2**: {หลักฐานการผ่านเกณฑ์ข้อที่ 2}
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 5. Delivery Verification & Independent Receipt
|
|
77
|
+
|
|
78
|
+
- **Delivery Date**: TBD
|
|
79
|
+
- **Verification Verdict**: TBD
|
|
80
|
+
- **Framework Tests**: TBD
|
|
81
|
+
- **Static Contract**: TBD
|
|
82
|
+
- **Package Smoke Test**: TBD
|
|
@@ -6,6 +6,8 @@ argument-hint: "[{run-id, number, or name}]"
|
|
|
6
6
|
|
|
7
7
|
# implement - build the target spec, one reviewed step at a time
|
|
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`.
|
|
@@ -81,41 +83,7 @@ instead of creating a new one.
|
|
|
81
83
|
|
|
82
84
|
### Type: Rollback safeguard
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
commit `git revert`. Completed feature commits also contain Blueprint history and
|
|
86
|
-
plan bookkeeping, while `devflow/context/{xxx-slug}/spec.md` now contains the active rollback
|
|
87
|
-
spec. Reversing the whole commit would damage that state.
|
|
88
|
-
|
|
89
|
-
Before the first rollback build step:
|
|
90
|
-
|
|
91
|
-
1. Re-resolve the target archive's introducing commit and confirm it matches the
|
|
92
|
-
full Target commit SHA recorded in the approved spec.
|
|
93
|
-
2. Confirm the target is an ancestor of `HEAD`, has the recorded single parent,
|
|
94
|
-
and the only dirty path before applying the patch is the approved rollback
|
|
95
|
-
spec. Stop on drift.
|
|
96
|
-
3. Preview the target's product diff while excluding `.agents/**`, `.claude/**`,
|
|
97
|
-
`devflow/**`, `AGENTS.md`, `CLAUDE.md`, and
|
|
98
|
-
`prototypes/**`. Confirm the preview is non-empty and matches the Product
|
|
99
|
-
paths in the spec.
|
|
100
|
-
4. Apply that product diff in reverse with three-way conflict detection and
|
|
101
|
-
stage it. Substitute the two approved full SHAs before running:
|
|
102
|
-
|
|
103
|
-
git diff --binary <target-parent> <target-commit> -- . \
|
|
104
|
-
':(exclude).agents/**' \
|
|
105
|
-
':(exclude).claude/**' ':(exclude)devflow/**' \
|
|
106
|
-
':(exclude)AGENTS.md' ':(exclude)CLAUDE.md' \
|
|
107
|
-
':(exclude)prototypes/**' |
|
|
108
|
-
git apply --reverse --3way --index
|
|
109
|
-
|
|
110
|
-
Never omit the protected pathspec exclusions for convenience.
|
|
111
|
-
5. Show both `git diff --cached` and `git status`. Confirm no protected path is
|
|
112
|
-
staged or modified before presenting the step for review.
|
|
113
|
-
|
|
114
|
-
If the reverse patch conflicts, stop and report the exact paths and later commit
|
|
115
|
-
that appears involved. Do not auto-resolve, discard, stash, reset, or switch to a
|
|
116
|
-
broad checkout. Ask whether to resolve only the conflict allowed by the approved
|
|
117
|
-
spec or abandon the attempt. A cascade into another completed feature needs a
|
|
118
|
-
new rollback plan.
|
|
86
|
+
When implementing a rollback task, follow the exact safety procedure in `reference/rollback-implementation.md`.
|
|
119
87
|
|
|
120
88
|
## Step 2 - build one step, review, iterate, checkpoint (Strict TDD)
|
|
121
89
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Rollback implementation safeguard
|
|
2
|
+
|
|
3
|
+
Use this only when `stage.md` / `spec.md` indicates a rollback task.
|
|
4
|
+
|
|
5
|
+
Do not hand-delete the old feature and do not run a whole-commit `git revert`.
|
|
6
|
+
The completed commit also contains DevFlow history and plan bookkeeping.
|
|
7
|
+
|
|
8
|
+
Before the first rollback step:
|
|
9
|
+
|
|
10
|
+
1. Read `Target commit` and `Target parent`. Stop unless both values match
|
|
11
|
+
`^[0-9a-f]{40}$`.
|
|
12
|
+
2. Resolve the archive's introducing commit and verify it has exactly one parent.
|
|
13
|
+
Stop on a merge target. Confirm the resolved commit exactly equals `Target
|
|
14
|
+
commit` and the resolved parent exactly equals `Target parent`.
|
|
15
|
+
3. Confirm the target is an ancestor of `HEAD` and the approved rollback spec is
|
|
16
|
+
the only dirty path. Stop on drift.
|
|
17
|
+
4. Preview the target's product diff while excluding `.agents/**`,
|
|
18
|
+
`.claude/**`, `devflow/**`, `AGENTS.md`, `CLAUDE.md`, and `prototypes/**`.
|
|
19
|
+
Confirm it is non-empty and matches the Product paths in the spec.
|
|
20
|
+
5. Apply only the resolved product diff in reverse with three-way conflict
|
|
21
|
+
detection. Use only the resolved full SHA values:
|
|
22
|
+
|
|
23
|
+
git diff --binary <target-parent> <target-commit> -- . \
|
|
24
|
+
':(exclude).agents/**' \
|
|
25
|
+
':(exclude).claude/**' ':(exclude)devflow/**' \
|
|
26
|
+
':(exclude)AGENTS.md' ':(exclude)CLAUDE.md' \
|
|
27
|
+
':(exclude)prototypes/**' |
|
|
28
|
+
git apply --reverse --3way --index
|
|
29
|
+
|
|
30
|
+
6. Show the staged diff and status. Stop if any protected path is staged or
|
|
31
|
+
modified.
|
|
32
|
+
|
|
33
|
+
If the reverse patch conflicts, report the exact paths and later commit involved.
|
|
34
|
+
Do not auto-resolve, discard, stash, reset, or broaden the rollback. Ask whether
|
|
35
|
+
to resolve only the approved conflict or abandon the attempt.
|