@devflow-core/dsh-devflow 0.5.0 → 0.6.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/assets/commands/devflow.toml +1 -1
- package/assets/presets/devflow-2/agent.cordis.yml +1 -1
- package/assets/scripts/devflow-plan.js +488 -15
- package/assets/skills/devflow-brainstorm/SKILL.md +2 -0
- package/assets/skills/devflow-build/SKILL.md +6 -4
- package/assets/skills/devflow-build/references/build-methods.md +4 -0
- package/assets/skills/devflow-core/SKILL.md +4 -1
- package/assets/skills/devflow-core/references/core-methods.md +5 -4
- package/assets/skills/devflow-cut/SKILL.md +10 -1
- package/assets/skills/devflow-cut/references/cut-methods.md +3 -0
- package/assets/skills/devflow-docs-followup/SKILL.md +1 -1
- package/assets/skills/devflow-learn/SKILL.md +13 -2
- package/assets/skills/devflow-plan/SKILL.md +41 -65
- package/assets/skills/devflow-plan/references/plan-methods.md +28 -50
- package/assets/skills/devflow-prove/SKILL.md +4 -2
- package/assets/skills/devflow-prove/references/flow-self-test.md +13 -11
- package/assets/skills/devflow-spec/SKILL.md +1 -0
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ Use Fast for pure Q&A, fact lookup, verification, or an already approved trivial
|
|
|
12
12
|
Use Design-lite when the risk gate skips Brainstorm for a small unapproved change to an existing feature with all low-risk conditions holding. Design-lite states goal, acceptance, and exclusions, records Depth C, and selects Cut then Build. It does not wait for Brainstorm. Not for new requirements.
|
|
13
13
|
If the boundary is unclear after Brainstorm, have Core select the smallest compatible route instead of guessing.
|
|
14
14
|
Run Sense first by reading or citing relevant project facts.
|
|
15
|
-
At Sense, probe `.copilot/LEARNING_INDEX.md` and `docs/project-knowledge
|
|
15
|
+
At Sense, match the task keywords against `docs/features/INDEX.md` and `docs/plans/INDEX.md` when present and open only the matched row's entry file or ledger section (an index over 40 rows is filtered with `node scripts/devflow-plan.js --index --query 关键词`); do not bulk-read capability documents. Then probe `.copilot/LEARNING_INDEX.md` (global preference cards first) and `docs/project-knowledge/`, reading only matched learning cards or navigation-selected knowledge documents and applying known developer preferences; missing locations are non-blocking and do not create storage.
|
|
16
16
|
The direct success map is Skip-Brainstorm Design-lite: Cut -> Build -> Prove, A: Brainstorm -> Spec -> Cut -> Plan -> Build -> Prove, B: Brainstorm -> Cut -> Plan -> Build -> Prove, C: Brainstorm -> Cut -> Build -> Prove. An approved A Spec directly starts Cut; A/B `CUT_PASS` directly starts Plan; C `CUT_PASS` and a skip-path `CUT_PASS` directly start Build; an approved A/B Plan directly starts Build; a completed Build directly starts Prove.
|
|
17
17
|
Before implementation, run Cut with Required Gates: Reuse, Native, Overbuild, Diff, and Scope checks. Core keeps non-unique selection: `CUT_REDUCE`/`CUT_REUSE` STOP for user confirmation, then return facts to Core; `CUT_BLOCKED`, Plan scope drift, `BUILD_BLOCKED`, Proof `FAIL`/`BLOCKED`, changed intent, and PUA recovery return facts so Core chooses the next owner.
|
|
18
18
|
For bug fixes, include Root-Cause Check: searched callers/references; shared vs narrow fix; reason.
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
- id: persona
|
|
67
67
|
name: '@deepseek-ai/dsh-persona'
|
|
68
68
|
config:
|
|
69
|
-
|
|
69
|
+
prefix: |-
|
|
70
70
|
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
|
71
71
|
|
|
72
72
|
You run DevFlow. Before any development work — creating features, building components, adding functionality, modifying behavior, or defining an unapproved problem-directed change — load the `devflow-core` skill with the skill tool and read its `core-methods.md` reference, then load only the selected lifecycle owner skill. The route table and hard boundaries live in that skill and in AGENTS.md; do not invent lifecycle details. Pure Q&A, lookup, verification, and investigation-only reports need no lifecycle.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
2
3
|
|
|
3
4
|
const requiredGlobalFields = ["Goal", "Architecture", "Tech Stack", "Source", "Spec coverage", "External Skills"];
|
|
4
5
|
const requiredTaskFields = ["Task", "Task type", "Files", "Interfaces", "Steps", "Acceptance", "Verify", "Comments", "Not doing"];
|
|
@@ -38,10 +39,32 @@ const worklistItemPattern = /^\s*-\s*\[ \]\s+(.+)$/gim;
|
|
|
38
39
|
const worklistDetailNames = ["Anchors", "Verify", "Done when"];
|
|
39
40
|
const maximumWorklistItems = 12;
|
|
40
41
|
|
|
42
|
+
// v2 契约(2026-09-10 可用性重构):计划只保留排序与验收字段;实现改法归 Build。
|
|
43
|
+
// 判定:含 `## Progress` 且不含 `Prewalk` 视为 v2;否则走 legacy 分支,旧计划无需迁移。
|
|
44
|
+
const v2GlobalFields = ["Goal", "Not doing", "Cut"];
|
|
45
|
+
const v2TaskFields = ["Task", "Files", "Change", "Acceptance", "Verify", "Not doing"];
|
|
46
|
+
const v2AllFields = [...v2GlobalFields, ...v2TaskFields];
|
|
47
|
+
const v2FieldPatterns = Object.fromEntries(
|
|
48
|
+
v2AllFields.map((field) => [field, new RegExp(`^(?:\\*\\*)?${field}(?:\\*\\*)?\\s*:`, "im")])
|
|
49
|
+
);
|
|
50
|
+
const progressHeadingPattern = /^##\s+Progress\s*$/im;
|
|
51
|
+
const progressRowPattern = /^\s*\|\s*(\d+)\s*\|\s*([^|]*)\|\s*(todo|doing|done)\s*\|\s*([^|]*)\|/gim;
|
|
52
|
+
const indexStatuses = ["active", "planned", "legacy", "retired"];
|
|
53
|
+
const indexPaths = { plans: "docs/plans/INDEX.md", features: "docs/features/INDEX.md" };
|
|
54
|
+
// 需求台账:一条需求从确认到落地的唯一记录;opt-out 表示用户显式跳过文档(仍必须有验证证据与原因)。
|
|
55
|
+
const requirementPath = "docs/requirements.md";
|
|
56
|
+
const requirementStatuses = ["open", "designed", "planned", "built", "landed", "opt-out", "dropped"];
|
|
57
|
+
const terminalRequirementStatuses = ["landed", "opt-out", "dropped"];
|
|
58
|
+
const promotionConfidence = 0.7;
|
|
59
|
+
|
|
41
60
|
/** Print the checker command contract and default plan landing. */
|
|
42
61
|
function usage() {
|
|
43
|
-
console.log("Usage: node scripts/devflow-plan.js [plan-file] [--self-test] [--json]");
|
|
62
|
+
console.log("Usage: node scripts/devflow-plan.js [plan-file] [--index] [--self-test] [--json]");
|
|
44
63
|
console.log("Checks whether a DevFlow Plan Pack has an executable header, task contracts, and plan landing.");
|
|
64
|
+
console.log("v2 plans carry a slim header, per-task Files/Change/Acceptance/Verify/Not doing, and a ## Progress table; legacy plans keep the old contract.");
|
|
65
|
+
console.log("--index checks docs/plans/INDEX.md and docs/features/INDEX.md against the filesystem.");
|
|
66
|
+
console.log("--index --query <keyword> prints only matching index rows for progressive loading; an empty result still exits 0.");
|
|
67
|
+
console.log("--loop prints a read-only loop report: requirement status counts, plan landing rate, feature rows, and promotion candidates.");
|
|
45
68
|
console.log("Default plan landing is docs/plans/YYYY-MM-DD-<short-kebab-name>.md unless the project documents another plan path.");
|
|
46
69
|
console.log("--json prints a single-line machine-readable summary; optional Status header values: " + validStatuses.join(" | "));
|
|
47
70
|
}
|
|
@@ -311,8 +334,369 @@ function checkTask(task, fileStructure) {
|
|
|
311
334
|
};
|
|
312
335
|
}
|
|
313
336
|
|
|
314
|
-
/**
|
|
337
|
+
/** v2 计划判定:新格式带 Progress 表且不含 Prewalk;旧格式继续走 legacy 校验。 */
|
|
338
|
+
function detectV2(body) {
|
|
339
|
+
return progressHeadingPattern.test(body) && !/^\s*Prewalk\s*:\s*$/im.test(body);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Extract one v2 field block; ends at the next v1 or v2 field line. */
|
|
343
|
+
function v2FieldBlock(body, field) {
|
|
344
|
+
const lines = body.split(/\r?\n/);
|
|
345
|
+
const start = lines.findIndex((line) => v2FieldPatterns[field].test(line));
|
|
346
|
+
if (start < 0) return "";
|
|
347
|
+
|
|
348
|
+
const value = lines[start].replace(v2FieldPatterns[field], "").trim();
|
|
349
|
+
const end = lines.findIndex(
|
|
350
|
+
(line, index) =>
|
|
351
|
+
index > start && (v2AllFields.some((name) => v2FieldPatterns[name].test(line)) || /^#{1,6}\s+\S/.test(line))
|
|
352
|
+
);
|
|
353
|
+
return [value, ...lines.slice(start + 1, end < 0 ? lines.length : end)].join("\n").trim();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Split a v2 plan at Task fields; the Progress table is not part of any task body. */
|
|
357
|
+
function splitTasksV2(body) {
|
|
358
|
+
const lines = body.split(/\r?\n/);
|
|
359
|
+
const progressIndex = lines.findIndex((line) => progressHeadingPattern.test(line));
|
|
360
|
+
const limit = progressIndex < 0 ? lines.length : progressIndex;
|
|
361
|
+
const taskStarts = [];
|
|
362
|
+
lines.forEach((line, index) => {
|
|
363
|
+
if (index < limit && v2FieldPatterns.Task.test(line)) taskStarts.push(index);
|
|
364
|
+
});
|
|
365
|
+
return taskStarts.map((start, index) => {
|
|
366
|
+
const end = taskStarts[index + 1] ?? limit;
|
|
367
|
+
return { number: index + 1, body: lines.slice(start, end).join("\n") };
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Validate one v2 task: files, change intent, acceptance, proof, and exclusion. */
|
|
372
|
+
function checkTaskV2(task) {
|
|
373
|
+
const missing = v2TaskFields.filter((field) => !v2FieldPatterns[field].test(task.body));
|
|
374
|
+
const files = v2FieldBlock(task.body, "Files");
|
|
375
|
+
const change = v2FieldBlock(task.body, "Change");
|
|
376
|
+
const verify = v2FieldBlock(task.body, "Verify");
|
|
377
|
+
const notDoing = v2FieldBlock(task.body, "Not doing");
|
|
378
|
+
const fileEntries = parseFileEntries(files);
|
|
379
|
+
const invalidFiles = fileEntries.filter(({ match }) => !match).map(({ line }) => line);
|
|
380
|
+
const unlocatedCodeFiles = findUnlocatedCodeFiles(fileEntries);
|
|
381
|
+
const unresolved = findMatches(task.body, unresolvedPatterns);
|
|
382
|
+
const vague = findMatches([change, v2FieldBlock(task.body, "Acceptance"), verify].join("\n"), vaguePatterns);
|
|
383
|
+
// v2 不强制精确改法;只要求 Change 说出可执行意图,具体实现归 Build。
|
|
384
|
+
const missingChange = !implementationVerbPattern.test(change) || genericMechanicsPattern.test(change);
|
|
385
|
+
const incompleteVerification = !hasVerificationExpectation(verify);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
number: task.number,
|
|
389
|
+
missing,
|
|
390
|
+
unresolved,
|
|
391
|
+
vague,
|
|
392
|
+
invalidFiles,
|
|
393
|
+
unlocatedCodeFiles,
|
|
394
|
+
missingChange,
|
|
395
|
+
incompleteVerification,
|
|
396
|
+
missingNotDoing: !notDoing,
|
|
397
|
+
ok:
|
|
398
|
+
missing.length === 0 &&
|
|
399
|
+
unresolved.length === 0 &&
|
|
400
|
+
vague.length === 0 &&
|
|
401
|
+
invalidFiles.length === 0 &&
|
|
402
|
+
unlocatedCodeFiles.length === 0 &&
|
|
403
|
+
!missingChange &&
|
|
404
|
+
!incompleteVerification &&
|
|
405
|
+
Boolean(notDoing)
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Validate the v2 plan contract: slim header, task rows, Progress table, and Cut subtraction. */
|
|
410
|
+
function checkPlanV2(body) {
|
|
411
|
+
const tasks = splitTasksV2(body);
|
|
412
|
+
const taskResults = tasks.map(checkTaskV2);
|
|
413
|
+
const missingGlobal = v2GlobalFields.filter((field) => !v2FieldPatterns[field].test(body));
|
|
414
|
+
const cutBlock = v2FieldBlock(body, "Cut");
|
|
415
|
+
const missingRejected = !/Rejected\s*:\s*\S/im.test(cutBlock);
|
|
416
|
+
const statusMatch = body.match(statusPattern);
|
|
417
|
+
const status = statusMatch ? statusMatch[1].trim() : "legacy";
|
|
418
|
+
const invalidStatus = statusMatch ? !validStatuses.includes(status) : false;
|
|
419
|
+
const progress = [...body.matchAll(progressRowPattern)].map((match) => ({
|
|
420
|
+
number: Number(match[1]),
|
|
421
|
+
state: match[3],
|
|
422
|
+
evidence: match[4].trim()
|
|
423
|
+
}));
|
|
424
|
+
const missingEvidence = progress.filter((row) => row.state === "done" && (!row.evidence || row.evidence === "-"));
|
|
425
|
+
const progressMismatch = progress.length !== tasks.length;
|
|
426
|
+
const doneStatusNeedsAllDone = status === "done" && progress.some((row) => row.state !== "done");
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
v2: true,
|
|
430
|
+
status,
|
|
431
|
+
invalidStatus,
|
|
432
|
+
missingGlobal,
|
|
433
|
+
cut: { missingRejected },
|
|
434
|
+
tasks: taskResults,
|
|
435
|
+
progress: { count: progress.length, mismatch: progressMismatch, missingEvidence },
|
|
436
|
+
doneStatusNeedsAllDone,
|
|
437
|
+
ok:
|
|
438
|
+
missingGlobal.length === 0 &&
|
|
439
|
+
!missingRejected &&
|
|
440
|
+
!invalidStatus &&
|
|
441
|
+
tasks.length > 0 &&
|
|
442
|
+
taskResults.every((task) => task.ok) &&
|
|
443
|
+
!progressMismatch &&
|
|
444
|
+
missingEvidence.length === 0 &&
|
|
445
|
+
!doneStatusNeedsAllDone
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Read every markdown table in a file as header cells plus data rows, skipping separator rows. */
|
|
450
|
+
function readMarkdownTables(filePath) {
|
|
451
|
+
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
|
452
|
+
const tables = [];
|
|
453
|
+
let current = [];
|
|
454
|
+
for (const line of lines) {
|
|
455
|
+
if (/^\s*\|/.test(line)) {
|
|
456
|
+
current.push(line.split("|").slice(1, -1).map((cell) => cell.trim()));
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (current.length) {
|
|
460
|
+
tables.push(current);
|
|
461
|
+
current = [];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (current.length) tables.push(current);
|
|
465
|
+
return tables
|
|
466
|
+
.map((cells) => cells.filter((row) => !row.every((cell) => /^:?-{2,}:?$/.test(cell))))
|
|
467
|
+
.filter((cells) => cells.length > 1)
|
|
468
|
+
.map((cells) => ({ header: cells[0], rows: cells.slice(1) }));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Read the first markdown table into its header cells and data rows. */
|
|
472
|
+
function readMarkdownTable(filePath) {
|
|
473
|
+
return readMarkdownTables(filePath)[0] || { header: [], rows: [] };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Resolve named column positions from the table header so a new column cannot shift the checks. */
|
|
477
|
+
function columnIndexes(header, names) {
|
|
478
|
+
return Object.fromEntries(names.map((name) => [name, header.findIndex((cell) => cell.includes(name))]));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Read one index file's data rows, returning an empty list when the index does not exist yet. */
|
|
482
|
+
function readIndexRows(root, kind) {
|
|
483
|
+
const filePath = path.join(root, ...indexPaths[kind].split("/"));
|
|
484
|
+
return fs.existsSync(filePath) ? readMarkdownTable(filePath).rows : [];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Return index rows whose text contains the query, for progressive index loading. */
|
|
488
|
+
function matchIndexRows(rows, query) {
|
|
489
|
+
const needle = String(query || "").trim().toLowerCase();
|
|
490
|
+
if (!needle) return [];
|
|
491
|
+
return rows.filter((cells) => cells.join(" ").toLowerCase().includes(needle));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Read the value after a flag, or an empty string when it is absent or looks like another flag. */
|
|
495
|
+
function argValue(args, flag) {
|
|
496
|
+
const index = args.indexOf(flag);
|
|
497
|
+
return index >= 0 && args[index + 1] && !args[index + 1].startsWith("-") ? args[index + 1] : "";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Check docs/plans/INDEX.md and docs/features/INDEX.md against the filesystem. */
|
|
501
|
+
function checkIndexes(root) {
|
|
502
|
+
const problems = [];
|
|
503
|
+
const plansDir = path.join(root, "docs", "plans");
|
|
504
|
+
const featuresDir = path.join(root, "docs", "features");
|
|
505
|
+
const plansIndex = path.join(root, ...indexPaths.plans.split("/"));
|
|
506
|
+
const featuresIndex = path.join(root, ...indexPaths.features.split("/"));
|
|
507
|
+
const planFiles = fs.existsSync(plansDir)
|
|
508
|
+
? fs.readdirSync(plansDir).filter((name) => name.endsWith(".md") && name !== "INDEX.md")
|
|
509
|
+
: [];
|
|
510
|
+
const planRows = readIndexRows(root, "plans");
|
|
511
|
+
const featureRows = readIndexRows(root, "features");
|
|
512
|
+
const plansTable = fs.existsSync(plansIndex) ? readMarkdownTable(plansIndex) : { header: [] };
|
|
513
|
+
const featuresTable = fs.existsSync(featuresIndex) ? readMarkdownTable(featuresIndex) : { header: [] };
|
|
514
|
+
const planCols = columnIndexes(plansTable.header, ["计划", "Status", "落地证据", "功能条目"]);
|
|
515
|
+
const featureCols = columnIndexes(featuresTable.header, ["功能", "Status", "关键文件", "来源计划"]);
|
|
516
|
+
|
|
517
|
+
if (planFiles.length > 0 && !fs.existsSync(plansIndex)) {
|
|
518
|
+
problems.push("docs/plans/INDEX.md missing while plan files exist");
|
|
519
|
+
}
|
|
520
|
+
for (const file of planFiles) {
|
|
521
|
+
const rows = planRows.filter((cells) => cells.some((cell) => cell.includes(file)));
|
|
522
|
+
if (rows.length === 0) problems.push(`plan not listed in docs/plans/INDEX.md: ${file}`);
|
|
523
|
+
if (rows.length > 1) problems.push(`plan listed more than once in docs/plans/INDEX.md: ${file}`);
|
|
524
|
+
}
|
|
525
|
+
for (const cells of planRows) {
|
|
526
|
+
const link = cells.find((cell) => cell.includes(".md"));
|
|
527
|
+
const file = link ? (link.match(/([^()/]+\.md)/) || [])[1] : null;
|
|
528
|
+
if (!file) continue;
|
|
529
|
+
if (!planFiles.includes(file)) problems.push(`docs/plans/INDEX.md lists unknown plan: ${file}`);
|
|
530
|
+
const filePath = path.join(plansDir, file);
|
|
531
|
+
if (!fs.existsSync(filePath)) continue;
|
|
532
|
+
const body = fs.readFileSync(filePath, "utf8");
|
|
533
|
+
const statusMatch = body.match(statusPattern);
|
|
534
|
+
const expected = statusMatch ? statusMatch[1].trim() : "legacy";
|
|
535
|
+
const rowStatus = (cells[planCols.Status] || "").trim();
|
|
536
|
+
if (rowStatus && rowStatus !== expected) {
|
|
537
|
+
problems.push(`status mismatch for ${file}: index ${rowStatus} vs file ${expected}`);
|
|
538
|
+
}
|
|
539
|
+
const evidence = (cells[planCols["落地证据"]] || "").trim();
|
|
540
|
+
if (rowStatus === "done" && (!evidence || evidence === "-")) {
|
|
541
|
+
problems.push(`done plan needs landing evidence in docs/plans/INDEX.md: ${file}`);
|
|
542
|
+
}
|
|
543
|
+
const featureCell = (cells[planCols["功能条目"]] || "").trim();
|
|
544
|
+
if (featureCell && featureCell !== "-" && !featureRows.some((row) => (row[featureCols["功能"]] || "").includes(featureCell))) {
|
|
545
|
+
problems.push(`feature entry not found in docs/features/INDEX.md: ${featureCell}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const cells of featureRows) {
|
|
549
|
+
const status = (cells[featureCols.Status] || "").trim();
|
|
550
|
+
if (status && !indexStatuses.includes(status)) {
|
|
551
|
+
problems.push(`invalid feature status in docs/features/INDEX.md: ${status}`);
|
|
552
|
+
}
|
|
553
|
+
const files = (cells[featureCols["关键文件"]] || "").split(",").map((value) => value.replaceAll("`", "").trim()).filter((value) => value && value !== "-");
|
|
554
|
+
for (const rel of files) {
|
|
555
|
+
if (!fs.existsSync(path.join(root, rel))) problems.push(`feature file missing: ${rel}`);
|
|
556
|
+
}
|
|
557
|
+
const plan = (cells[featureCols["来源计划"]] || "").replaceAll("`", "").trim();
|
|
558
|
+
if (plan && plan !== "-" && !fs.existsSync(path.join(root, "docs", "plans", plan))) {
|
|
559
|
+
problems.push(`feature source plan missing: ${plan}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const requirementTable = readRequirementTable(root);
|
|
563
|
+
problems.push(
|
|
564
|
+
...checkRequirementTable(requirementTable.header, requirementTable.rows, (rel) => fs.existsSync(path.join(root, rel)))
|
|
565
|
+
);
|
|
566
|
+
return problems;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Run the plan and feature index consistency check, or answer a single query. */
|
|
570
|
+
function runIndexCheck(json, query) {
|
|
571
|
+
const root = path.resolve(__dirname, "..");
|
|
572
|
+
if (query) {
|
|
573
|
+
const featureMatches = matchIndexRows(readIndexRows(root, "features"), query);
|
|
574
|
+
const planMatches = matchIndexRows(readIndexRows(root, "plans"), query);
|
|
575
|
+
if (json) {
|
|
576
|
+
console.log(JSON.stringify({ checker: "plan-index-query", query, features: featureMatches, plans: planMatches, judgment: "PASS" }));
|
|
577
|
+
} else {
|
|
578
|
+
console.log(`DevFlow index query: ${query}`);
|
|
579
|
+
for (const cells of featureMatches) console.log(`功能行: ${cells.join(" | ")}`);
|
|
580
|
+
for (const cells of planMatches) console.log(`计划行: ${cells.join(" | ")}`);
|
|
581
|
+
console.log(`Matches: ${featureMatches.length + planMatches.length}`);
|
|
582
|
+
}
|
|
583
|
+
return 0;
|
|
584
|
+
}
|
|
585
|
+
const problems = checkIndexes(root);
|
|
586
|
+
if (json) {
|
|
587
|
+
console.log(JSON.stringify({ checker: "plan-index", problems, judgment: problems.length === 0 ? "PASS" : "FAIL" }));
|
|
588
|
+
} else {
|
|
589
|
+
console.log("DevFlow plan and feature index report");
|
|
590
|
+
console.log(`Index files: ${indexPaths.plans} | ${indexPaths.features}`);
|
|
591
|
+
if (problems.length === 0) console.log("Problems: none");
|
|
592
|
+
for (const problem of problems) console.log(`Problem: ${problem}`);
|
|
593
|
+
console.log(`Judgment: ${problems.length === 0 ? "PASS" : "FAIL"}`);
|
|
594
|
+
}
|
|
595
|
+
return problems.length === 0 ? 0 : 1;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Validate the requirement ledger table: status whitelist, terminal evidence, opt-out reason, artifact paths. */
|
|
599
|
+
function checkRequirementTable(header, rows, exists) {
|
|
600
|
+
const problems = [];
|
|
601
|
+
const cols = columnIndexes(header, ["需求", "日期", "落地物", "状态", "证据或跳过"]);
|
|
602
|
+
if (cols["状态"] < 0 || cols["证据或跳过"] < 0) {
|
|
603
|
+
problems.push("docs/requirements.md header must contain 状态 and 证据或跳过 columns");
|
|
604
|
+
return problems;
|
|
605
|
+
}
|
|
606
|
+
for (const cells of rows) {
|
|
607
|
+
const requirement = (cells[cols["需求"]] || "").trim();
|
|
608
|
+
const date = cols["日期"] >= 0 ? (cells[cols["日期"]] || "").trim() : "";
|
|
609
|
+
const status = (cells[cols["状态"]] || "").trim();
|
|
610
|
+
const evidence = (cells[cols["证据或跳过"]] || "").trim();
|
|
611
|
+
if (!requirement) problems.push("requirement row missing 需求 text");
|
|
612
|
+
if (!date) problems.push(`requirement row missing 日期: ${requirement}`);
|
|
613
|
+
if (!requirementStatuses.includes(status)) problems.push(`invalid requirement status: ${status}`);
|
|
614
|
+
if (terminalRequirementStatuses.includes(status) && (!evidence || evidence === "-")) {
|
|
615
|
+
problems.push(`terminal requirement needs evidence: ${requirement}`);
|
|
616
|
+
}
|
|
617
|
+
if (status === "opt-out" && !/原因|reason|skip/i.test(evidence)) {
|
|
618
|
+
problems.push(`opt-out needs a recorded reason: ${requirement}`);
|
|
619
|
+
}
|
|
620
|
+
const artifacts = (cells[cols["落地物"]] || "")
|
|
621
|
+
.split(",")
|
|
622
|
+
.map((value) => value.replaceAll("`", "").trim())
|
|
623
|
+
.filter((value) => value && value !== "-");
|
|
624
|
+
for (const rel of artifacts) {
|
|
625
|
+
if (!exists(rel)) problems.push(`requirement artifact missing: ${rel}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return problems;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** Read the requirement ledger table, selecting the table that carries the ledger columns. */
|
|
632
|
+
function readRequirementTable(root) {
|
|
633
|
+
const filePath = path.join(root, ...requirementPath.split("/"));
|
|
634
|
+
if (!fs.existsSync(filePath)) return { header: [], rows: [] };
|
|
635
|
+
const tables = readMarkdownTables(filePath);
|
|
636
|
+
const ledger = tables.find(
|
|
637
|
+
(table) => table.header.some((cell) => cell.includes("状态")) && table.header.some((cell) => cell.includes("证据或跳过"))
|
|
638
|
+
);
|
|
639
|
+
return ledger || tables[0] || { header: [], rows: [] };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Count requirement statuses and promotion candidates for the read-only loop report. */
|
|
643
|
+
function summarizeLoop(root) {
|
|
644
|
+
const { header, rows } = readRequirementTable(root);
|
|
645
|
+
const cols = columnIndexes(header, ["状态", "需求"]);
|
|
646
|
+
const counts = Object.fromEntries(requirementStatuses.map((status) => [status, 0]));
|
|
647
|
+
for (const cells of rows) {
|
|
648
|
+
const status = cols["状态"] >= 0 ? (cells[cols["状态"]] || "").trim() : "";
|
|
649
|
+
if (Object.prototype.hasOwnProperty.call(counts, status)) counts[status] += 1;
|
|
650
|
+
}
|
|
651
|
+
const planRows = readIndexRows(root, "plans");
|
|
652
|
+
const plansWithStatus = planRows.filter((cells) => validStatuses.includes((cells[2] || "").trim()));
|
|
653
|
+
const plansDone = plansWithStatus.filter((cells) => (cells[2] || "").trim() === "done");
|
|
654
|
+
const learningIndex = path.join(root, ".copilot", "LEARNING_INDEX.md");
|
|
655
|
+
const learningTable = fs.existsSync(learningIndex) ? readMarkdownTable(learningIndex) : { header: [], rows: [] };
|
|
656
|
+
const cardCols = columnIndexes(learningTable.header, ["Card", "Confidence"]);
|
|
657
|
+
const candidates = learningTable.rows
|
|
658
|
+
.map((cells) => ({
|
|
659
|
+
name: ((cells[cardCols.Card] || "").match(/\[([^\]]+)\]/) || [])[1] || "",
|
|
660
|
+
confidence: Number((cells[cardCols.Confidence] || "").trim())
|
|
661
|
+
}))
|
|
662
|
+
.filter((card) => card.name && Number.isFinite(card.confidence) && card.confidence >= promotionConfidence);
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
requirements: { total: rows.length, counts },
|
|
666
|
+
plans: { withStatus: plansWithStatus.length, done: plansDone.length },
|
|
667
|
+
features: readIndexRows(root, "features").length,
|
|
668
|
+
candidates
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Run the read-only loop report: requirement counts, plan landing rate, feature rows, promotion candidates. */
|
|
673
|
+
function runLoopReport(json) {
|
|
674
|
+
const summary = summarizeLoop(path.resolve(__dirname, ".."));
|
|
675
|
+
const counts = summary.requirements.counts;
|
|
676
|
+
const landingRate = summary.plans.withStatus
|
|
677
|
+
? Math.round((summary.plans.done / summary.plans.withStatus) * 100)
|
|
678
|
+
: 0;
|
|
679
|
+
if (json) {
|
|
680
|
+
console.log(JSON.stringify({ checker: "devflow-loop", ...summary, landingRate, judgment: "PASS" }));
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
console.log("DevFlow loop report");
|
|
684
|
+
console.log(`Requirements: total ${summary.requirements.total} | ${requirementStatuses.map((status) => `${status} ${counts[status]}`).join(" | ")}`);
|
|
685
|
+
console.log(`Plan landing: ${summary.plans.done}/${summary.plans.withStatus} with status (${landingRate}%)`);
|
|
686
|
+
console.log(`Feature rows: ${summary.features}`);
|
|
687
|
+
console.log(`Promotion candidates (confidence >= ${promotionConfidence}): ${summary.candidates.length}`);
|
|
688
|
+
for (const card of summary.candidates) console.log(` - ${card.name} (${card.confidence})`);
|
|
689
|
+
console.log("Judgment: PASS");
|
|
690
|
+
return 0;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Dispatch the v2 and legacy plan contracts so old plans keep validating. */
|
|
315
694
|
function checkPlan(body) {
|
|
695
|
+
return detectV2(body) ? checkPlanV2(body) : checkPlanLegacy(body);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** Validate global plan headers and all independently scoped task contracts. */
|
|
699
|
+
function checkPlanLegacy(body) {
|
|
316
700
|
const fileStructure = checkFileStructure(body);
|
|
317
701
|
const tasks = splitTasks(body);
|
|
318
702
|
const requiresFileStructure = tasks.some((task) => fieldBlock(task.body, "Task type") === "Code change");
|
|
@@ -366,12 +750,21 @@ function report(body, filePath, json) {
|
|
|
366
750
|
console.log(
|
|
367
751
|
JSON.stringify({
|
|
368
752
|
checker: "plan",
|
|
753
|
+
format: result.v2 ? "v2" : "legacy",
|
|
369
754
|
landing: landing.message,
|
|
370
755
|
status: result.status,
|
|
371
756
|
invalidStatus: result.invalidStatus,
|
|
372
757
|
missingGlobal: result.missingGlobal,
|
|
373
|
-
globalUnresolved: result.globalUnresolved,
|
|
374
|
-
fileStructure: result.fileStructure.ok ? "ok" : "missing or invalid",
|
|
758
|
+
globalUnresolved: result.globalUnresolved || [],
|
|
759
|
+
fileStructure: result.v2 ? "v2" : result.fileStructure.ok ? "ok" : "missing or invalid",
|
|
760
|
+
cut: result.v2 ? { missingRejected: result.cut.missingRejected } : null,
|
|
761
|
+
progress: result.v2
|
|
762
|
+
? {
|
|
763
|
+
count: result.progress.count,
|
|
764
|
+
mismatch: result.progress.mismatch,
|
|
765
|
+
missingEvidence: result.progress.missingEvidence.length
|
|
766
|
+
}
|
|
767
|
+
: null,
|
|
375
768
|
tasks: result.tasks.map((task) => ({ number: task.number, ok: task.ok })),
|
|
376
769
|
judgment
|
|
377
770
|
})
|
|
@@ -381,22 +774,43 @@ function report(body, filePath, json) {
|
|
|
381
774
|
|
|
382
775
|
console.log("DevFlow plan pack report");
|
|
383
776
|
console.log(landing.message);
|
|
777
|
+
console.log(`Format: ${result.v2 ? "v2" : "legacy"}`);
|
|
384
778
|
console.log(`Status: ${result.invalidStatus ? "invalid" : result.status}`);
|
|
385
|
-
for (const field of
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}`
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
779
|
+
for (const field of result.v2 ? v2GlobalFields : requiredGlobalFields) {
|
|
780
|
+
console.log(`${field}: ${result.missingGlobal.includes(field) ? "missing" : "ok"}`);
|
|
781
|
+
}
|
|
782
|
+
if (result.v2) {
|
|
783
|
+
console.log(`Cut Rejected: ${result.cut.missingRejected ? "missing" : "ok"}`);
|
|
784
|
+
console.log(`Progress rows: ${result.progress.count} (tasks ${result.tasks.length})${result.progress.mismatch ? " mismatch" : ""}`);
|
|
785
|
+
for (const row of result.progress.missingEvidence) console.log(`Progress row ${row.number} is done without evidence`);
|
|
786
|
+
if (result.doneStatusNeedsAllDone) console.log("Status done requires every Progress row done");
|
|
787
|
+
} else {
|
|
788
|
+
console.log(`Global unresolved markers: ${result.globalUnresolved.join(", ") || "none"}`);
|
|
789
|
+
console.log(
|
|
790
|
+
`File Structure: ${
|
|
791
|
+
!result.requiresFileStructure ? "documentation-only exception" : result.fileStructure.ok ? "ok" : "missing or invalid"
|
|
792
|
+
}`
|
|
793
|
+
);
|
|
794
|
+
if (result.requiresFileStructure && !result.fileStructure.ok) {
|
|
795
|
+
console.log(`File Structure invalid rows: ${result.fileStructure.invalidRows.join("; ") || "none"}`);
|
|
796
|
+
}
|
|
394
797
|
}
|
|
395
798
|
console.log(`Tasks: ${result.tasks.length}`);
|
|
396
799
|
if (result.tasks.length === 0) console.log("Missing: at least one Task field");
|
|
397
800
|
|
|
398
801
|
for (const task of result.tasks) {
|
|
399
|
-
const issues =
|
|
802
|
+
const issues = result.v2
|
|
803
|
+
? [
|
|
804
|
+
...task.missing.map((field) => `missing ${field}`),
|
|
805
|
+
...task.unresolved.map((match) => `unresolved ${match}`),
|
|
806
|
+
...task.vague.map((match) => `vague ${match}`),
|
|
807
|
+
...task.invalidFiles.map((line) => `unclassified file ${line}`),
|
|
808
|
+
...task.unlocatedCodeFiles.map((line) => `missing file symbol/anchor ${line}`),
|
|
809
|
+
...(task.missingChange ? ["Change needs an executable intent verb"] : []),
|
|
810
|
+
...(task.missingNotDoing ? ["missing Not doing exclusion"] : []),
|
|
811
|
+
...(task.incompleteVerification ? ["Verify needs command/scenario and expected result"] : [])
|
|
812
|
+
]
|
|
813
|
+
: [
|
|
400
814
|
...task.missing.map((field) => `missing ${field}`),
|
|
401
815
|
...task.unresolved.map((match) => `unresolved ${match}`),
|
|
402
816
|
...task.vague.map((match) => `vague ${match}`),
|
|
@@ -621,8 +1035,61 @@ function selfTest() {
|
|
|
621
1035
|
if (!checkPlanLanding("docs/plans/2026-07-14-add-plan-scanner.md").ok) throw new Error("Self-test expected docs/plans landing to pass");
|
|
622
1036
|
if (checkPlanLanding("docs/features/add-plan-scanner.md").ok) throw new Error("Self-test expected docs/features plan landing to fail");
|
|
623
1037
|
|
|
1038
|
+
const validV2Plan = [
|
|
1039
|
+
"Status: approved",
|
|
1040
|
+
"Goal: validate the slim v2 plan contract",
|
|
1041
|
+
"Not doing: architecture review",
|
|
1042
|
+
"Cut: 做 checker | 不做 迁移旧计划 | 复用 既有 self-test | 验证 node scripts/devflow-plan.js | Rejected: 新增独立脚本 — 复用既有 checker",
|
|
1043
|
+
"## Tasks",
|
|
1044
|
+
"",
|
|
1045
|
+
"Task: Add v2 validation",
|
|
1046
|
+
"Files:",
|
|
1047
|
+
"- Modify: scripts/devflow-plan.js | symbol: `checkPlanV2` | validate the slim contract",
|
|
1048
|
+
"Change: add v2 field validation and dispatch",
|
|
1049
|
+
"Acceptance: v2 plans pass and legacy plans keep passing",
|
|
1050
|
+
"Verify: run `node scripts/devflow-plan.js --self-test` expect exit 0",
|
|
1051
|
+
"Not doing: changing legacy validation",
|
|
1052
|
+
"",
|
|
1053
|
+
"## Progress",
|
|
1054
|
+
"",
|
|
1055
|
+
"| # | Task | Status | Evidence |",
|
|
1056
|
+
"|---|---|---|---|",
|
|
1057
|
+
"| 1 | Add v2 validation | todo | - |"
|
|
1058
|
+
].join("\n");
|
|
1059
|
+
if (!detectV2(validV2Plan)) throw new Error("Self-test expected v2 detection to pass");
|
|
1060
|
+
if (!checkPlan(validV2Plan).ok) throw new Error("Self-test expected valid v2 plan to pass");
|
|
1061
|
+
if (checkPlan(validV2Plan.replace(/Rejected:[^\n]*/, "Rejected:")).ok) throw new Error("Self-test expected missing Rejected to fail");
|
|
1062
|
+
if (checkPlan(validV2Plan.replace("| 1 | Add v2 validation | todo | - |", "| 1 | Add v2 validation | done | - |")).ok) {
|
|
1063
|
+
throw new Error("Self-test expected a done Progress row without evidence to fail");
|
|
1064
|
+
}
|
|
1065
|
+
if (checkPlan(validV2Plan.replace("| 1 | Add v2 validation | todo | - |", "")).ok) {
|
|
1066
|
+
throw new Error("Self-test expected a Progress/task count mismatch to fail");
|
|
1067
|
+
}
|
|
1068
|
+
if (typeof checkIndexes !== "function") throw new Error("Self-test expected the index checker to exist");
|
|
1069
|
+
if (matchIndexRows([["功能A", "计划相关", "x"]], "计划").length !== 1) throw new Error("Self-test expected index query to match a row");
|
|
1070
|
+
if (matchIndexRows([["功能A", "计划相关", "x"]], "不存在").length !== 0) throw new Error("Self-test expected index query to miss");
|
|
1071
|
+
if (matchIndexRows([["功能A"]], "").length !== 0) throw new Error("Self-test expected an empty index query to match nothing");
|
|
1072
|
+
if (argValue(["--index", "--query", "计划"], "--query") !== "计划") throw new Error("Self-test expected --query to read its value");
|
|
1073
|
+
if (argValue(["--index", "--query", "--json"], "--query") !== "") throw new Error("Self-test expected --query to reject a flag as its value");
|
|
1074
|
+
|
|
1075
|
+
const requirementHeader = ["日期", "需求", "来源", "深度", "落地物", "状态", "证据或跳过", "更新日"];
|
|
1076
|
+
const requirementRows = [
|
|
1077
|
+
["2026-09-10", "已落地需求", "用户请求", "A", "docs/specs/x.md", "landed", "npm test 通过", "2026-09-10"],
|
|
1078
|
+
["2026-09-10", "跳过文档需求", "用户请求", "C", "-", "opt-out", "原因:用户显式跳过", "2026-09-10"]
|
|
1079
|
+
];
|
|
1080
|
+
const requirementProblems = checkRequirementTable(requirementHeader, requirementRows, () => true);
|
|
1081
|
+
if (requirementProblems.length !== 0) throw new Error(`Self-test expected a valid requirement table to pass: ${requirementProblems.join("; ")}`);
|
|
1082
|
+
const badStatus = checkRequirementTable(requirementHeader, [["2026-09-10", "状态非法", "用户请求", "A", "-", "shipped", "证据", "2026-09-10"]], () => true);
|
|
1083
|
+
if (badStatus.length === 0) throw new Error("Self-test expected an invalid requirement status to fail");
|
|
1084
|
+
const missingEvidence = checkRequirementTable(requirementHeader, [["2026-09-10", "终态无证据", "用户请求", "A", "-", "landed", "-", "2026-09-10"]], () => true);
|
|
1085
|
+
if (missingEvidence.length === 0) throw new Error("Self-test expected a terminal requirement without evidence to fail");
|
|
1086
|
+
const missingReason = checkRequirementTable(requirementHeader, [["2026-09-10", "跳过无原因", "用户请求", "C", "-", "opt-out", "验证通过", "2026-09-10"]], () => true);
|
|
1087
|
+
if (missingReason.length === 0) throw new Error("Self-test expected an opt-out without a reason to fail");
|
|
1088
|
+
const missingArtifact = checkRequirementTable(requirementHeader, [["2026-09-10", "落地物不存在", "用户请求", "A", "docs/specs/nope.md", "planned", "-", "2026-09-10"]], () => false);
|
|
1089
|
+
if (missingArtifact.length === 0) throw new Error("Self-test expected a missing requirement artifact to fail");
|
|
1090
|
+
|
|
624
1091
|
console.log("DevFlow plan self-test passed");
|
|
625
|
-
console.log("Checked code-level fields, precise file locations,
|
|
1092
|
+
console.log("Checked v2 and legacy plan contracts, Progress evidence, Cut Rejected, code-level fields, precise file locations, verification expectations, documentation-only exception, external-skill declaration, and plan landing guidance");
|
|
626
1093
|
}
|
|
627
1094
|
|
|
628
1095
|
const args = process.argv.slice(2);
|
|
@@ -634,5 +1101,11 @@ if (args.includes("--self-test")) {
|
|
|
634
1101
|
selfTest();
|
|
635
1102
|
process.exit(0);
|
|
636
1103
|
}
|
|
1104
|
+
if (args.includes("--loop")) {
|
|
1105
|
+
process.exit(runLoopReport(args.includes("--json")));
|
|
1106
|
+
}
|
|
1107
|
+
if (args.includes("--index")) {
|
|
1108
|
+
process.exit(runIndexCheck(args.includes("--json"), argValue(args, "--query")));
|
|
1109
|
+
}
|
|
637
1110
|
const targetArg = args.find((arg) => !arg.startsWith("-"));
|
|
638
1111
|
process.exitCode = report(readInput(args), targetArg, args.includes("--json"));
|
|
@@ -122,6 +122,8 @@ Confirmed request:
|
|
|
122
122
|
|
|
123
123
|
The summary records the agreed request and the exploration findings only. It must not contain an implementation plan, solution-space design, lifecycle route, or handoff instruction. It is the factual basis that downstream skills — starting with `devflow-spec` — build on, so record findings faithfully rather than trimming them away.
|
|
124
124
|
|
|
125
|
+
On `Status: clarified`, create or update the requirement row in `docs/requirements.md` with status `open`; depth is recorded when the lifecycle path is chosen. Pure Q&A, lookup, and read-only verification create no row.
|
|
126
|
+
|
|
125
127
|
## A/B/C Gate
|
|
126
128
|
|
|
127
129
|
After the fixed summary, present these choices and wait for one explicit user selection:
|
|
@@ -19,7 +19,7 @@ When no plan file exists, the approved design and Cut Decision form the Build Co
|
|
|
19
19
|
|
|
20
20
|
Load `skills/devflow-build/references/build-methods.md` after this section and before implementation slices. It owns the detailed minimal-change and slice discipline.
|
|
21
21
|
|
|
22
|
-
There is no broad pre-edit plan review. The executor reads
|
|
22
|
+
There is no broad pre-edit plan review. The executor reads the current task's `Files`, `Change`, `Acceptance`, `Verify`, and `Not doing`, and may read the task's named anchors plus one directly changed neighbor to choose the smallest implementation. Build owns how inside the task boundary; it must not broadly rediscover the repository, redesign outside the task boundary, silently repair the plan, or expand scope. An anchor mismatch or verification failure must stop and return `BUILD_BLOCKED` with facts to `devflow-core`: observed mismatch, affected anchor, and smallest replan decision.
|
|
23
23
|
|
|
24
24
|
Every skill declared in `External Skills` (Cut Decision or plan header) must actually be loaded through the platform's skill mechanism, or the reason it does not apply recorded; loading alone is not completion — Build requires the specialist's returned result, not-applicable, or failure facts. A specialist result implying structure outside the approved scope returns scope-drift facts to `devflow-core`, not silent adoption. Skill loading is not a pre-edit view and remains mandatory.
|
|
25
25
|
|
|
@@ -44,9 +44,11 @@ If work touches more than one file or one logical step, create Implementation Sl
|
|
|
44
44
|
|
|
45
45
|
When saving a plan file, use `docs/plans/YYYY-MM-DD-<short-kebab-name>.md`, resolved from the current target project's root, unless that project already documents another plan/spec path. Do not save implementation plans under `docs/features/`; that directory is for feature ledgers.
|
|
46
46
|
|
|
47
|
-
For multi-step work, tasks must cite the approved source, be small and verifiable, and follow the
|
|
47
|
+
For multi-step work, tasks must cite the approved source, be small and verifiable, and follow the six-field contract (`Task` / `Files` / `Change` / `Acceptance` / `Verify` / `Not doing`) in `skills/devflow-plan/SKILL.md`. Legacy plans with `Change mechanics` and `Prewalk` stay executable under their own contract.
|
|
48
48
|
|
|
49
|
-
No unresolved markers.
|
|
49
|
+
No unresolved markers. `Change` states the executable intent and boundary; add exact mechanics only when the change crosses a module contract, is irreversible, or touches security or data boundaries. Otherwise Build chooses the smallest implementation and records it as Progress evidence. The verification step keeps its trigger/input, expected result, and command or manual scenario. A task whose files are all documentation paths is documentation-only. No "add tests" without naming the behavior, no "handle edge cases" without naming the edge case, no "similar to Task N" shortcuts.
|
|
50
|
+
|
|
51
|
+
Close each task by writing back its `## Progress` row: `doing` when starting, `done` with the command and key result when its `Verify` passes. A `done` row without evidence fails the checker. On a legacy plan without a Progress table, report the same evidence in the completion message. When every task is `done`, advance the requirement row in `docs/requirements.md` to `built`.
|
|
50
52
|
|
|
51
53
|
Before Build, run `node scripts/devflow-plan.js <plan-file>` when a plan is saved to a file. If not found at `scripts/devflow-plan.js` (project-level), try `~/.codex/scripts/devflow-plan.js` or `~/.claude/scripts/devflow-plan.js` (user-level). Do NOT look under `skills/scripts/`. See `core-methods.md` Script Path Resolution.
|
|
52
54
|
|
|
@@ -191,7 +193,7 @@ If any file has no goal link, remove that change.
|
|
|
191
193
|
| "We'll verify everything at the end." | Verify slices when focused checks exist. |
|
|
192
194
|
| "Docs changes do not need proof." | Docs/rules/skills need validation just like code. |
|
|
193
195
|
| "The issue only mentions one caller." | Check sibling callers before choosing the fix location. |
|
|
194
|
-
| "The plan is approved, so I just execute." |
|
|
196
|
+
| "The plan is approved, so I just execute." | The plan fixes the boundary and proof; Build still chooses the smallest implementation inside that boundary. A real edit or verification failure returns `BUILD_BLOCKED` to Core. |
|
|
195
197
|
| "I'll infer the missing step." | Guessing past a gap is forbidden; unclear instructions return `BUILD_BLOCKED` facts. |
|
|
196
198
|
| "The code is self-explanatory." | That does not waive a comment required by the approved contract, project convention, or a non-obvious boundary. |
|
|
197
199
|
| "Comments will get stale." | Keep a required comment accurate or remove a stale one; a stale explanation is not a reason to skip a needed decision record. |
|