@dotdotgod/cli 0.1.42 → 0.1.44
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/package.json +1 -1
- package/src/commands/plan-stage-contract.mjs +16 -4
- package/src/commands/plan.mjs +115 -18
- package/src/validate/run.mjs +1 -0
package/package.json
CHANGED
|
@@ -17,7 +17,14 @@ export const PLAN_REQUIRED_HEADERS = {
|
|
|
17
17
|
'02-context-load': ['Memory Reads', 'Impact Candidates', 'Related Files'],
|
|
18
18
|
'03-discovery': ['Findings', 'Risks', 'Open Questions'],
|
|
19
19
|
'04-plan': ['Milestones', 'Atomic Tasks', 'Validation Plan', 'Resume Point'],
|
|
20
|
-
'05-workstream-handoff': [
|
|
20
|
+
'05-workstream-handoff': [
|
|
21
|
+
'Workstream Handoff',
|
|
22
|
+
'Workstream Map',
|
|
23
|
+
'Shared Context',
|
|
24
|
+
'Workstreams',
|
|
25
|
+
'Integration Sequence',
|
|
26
|
+
'Todo Contract',
|
|
27
|
+
],
|
|
21
28
|
};
|
|
22
29
|
|
|
23
30
|
export const PLAN_CONSTRUCTION_CHECKLISTS = {
|
|
@@ -34,8 +41,9 @@ export const PLAN_BLOCKER_LABELS = new Map([
|
|
|
34
41
|
['invalid-task-slug', 'Plan task directory must be kebab-case'],
|
|
35
42
|
['empty-plan', 'Plan README is empty'],
|
|
36
43
|
['missing-stage', 'Missing required stage directory'],
|
|
37
|
-
['missing-internal-stage', 'Missing internal stage file'],
|
|
38
44
|
['missing-stage-readme', 'Missing stage README.md'],
|
|
45
|
+
['missing-internal-stage', 'Missing internal stage checkpoint'],
|
|
46
|
+
['open-internal-stage', 'Internal stage checkpoint is not completed'],
|
|
39
47
|
['invalid-markdown-name', 'Invalid Markdown file name'],
|
|
40
48
|
['missing-section', 'Missing required section'],
|
|
41
49
|
['empty-section', 'Required section has no content'],
|
|
@@ -50,14 +58,18 @@ export const PLAN_BLOCKER_LABELS = new Map([
|
|
|
50
58
|
['malformed-checklist-item', 'Malformed construction checklist item'],
|
|
51
59
|
['open-checklist-item', 'Construction checklist item is open'],
|
|
52
60
|
['placeholder-checklist-item', 'Construction checklist item only contains placeholder content'],
|
|
61
|
+
['missing-workstream-split-decision', 'Workstream Handoff must include an explicit split decision'],
|
|
62
|
+
['incomplete-workstream-split', 'Split workstream handoff must include actionable workstream contracts'],
|
|
63
|
+
['missing-workstream-no-split-rationale', 'No-split workstream handoff must include rationale'],
|
|
53
64
|
]);
|
|
54
65
|
|
|
55
66
|
export function buildPlanValidationNextStagePrompt(planPath, stage, stagePath) {
|
|
56
67
|
return [
|
|
57
68
|
`Continue Plan Mode stage advancement for ${planPath}.`,
|
|
58
69
|
`Current stage: ${stage}.`,
|
|
59
|
-
`
|
|
60
|
-
|
|
70
|
+
`Update the durable plan for human readers, then make ${stagePath} the machine-readable checkpoint that satisfies the current stage validation contract. Do not create later checkpoint files yet.`,
|
|
71
|
+
`In ${stagePath}, set Status: completed and include the current stage required sections plus \`## Stage ${stage.slice(0, 2)} Construction Checklist\` rows in the canonical \`- [x] Category: evidence\` form.`,
|
|
72
|
+
stage === '05-workstream-handoff' ? 'For this stage checkpoint, record `Split decision: yes` or `Split decision: no` in `## Workstream Handoff`. If splitting, make `## Workstream Map`, `## Workstreams`, and `## Integration Sequence` describe multiple actionable workstreams with required context, allowed/forbidden edits, tasks, validation, dependencies, and handoff output. If not splitting, record the no-split rationale and keep `## Todo Contract` actionable so Pi can extract execution todos.' : undefined,
|
|
61
73
|
`After completing this stage, stop and rerun \`dotdotgod plan validate ${planPath} --stage ${stage} --json\`.`,
|
|
62
74
|
].filter(Boolean).join('\n\n');
|
|
63
75
|
}
|
package/src/commands/plan.mjs
CHANGED
|
@@ -201,17 +201,37 @@ function readInternalStageState(taskDir, stageName) {
|
|
|
201
201
|
|
|
202
202
|
function validateInternalStageState(root, taskDir, stageName, blockers) {
|
|
203
203
|
const state = readInternalStageState(taskDir, stageName);
|
|
204
|
-
|
|
204
|
+
const expectedPath = internalStageFile(taskDir, stageName);
|
|
205
|
+
const rel = projectRelative(root, state?.file ?? expectedPath);
|
|
206
|
+
if (!state) {
|
|
207
|
+
addBlocker(blockers, 'missing-internal-stage', `Missing internal stage checkpoint: ${rel}`, { path: rel, stage: stageName });
|
|
208
|
+
return { state: undefined, valid: false };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let valid = true;
|
|
205
212
|
const stage = contentMatch(state.content, /^Stage:\s*(\S+)\s*$/m);
|
|
206
213
|
const status = contentMatch(state.content, /^Status:\s*(created|blocked|completed)\s*$/m);
|
|
207
214
|
const updated = contentMatch(state.content, /^Updated:\s*(\S+)\s*$/m);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!stage)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
if (stage && stage !== stageName) {
|
|
216
|
+
valid = false;
|
|
217
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file has wrong Stage field: ${rel}`, { path: rel, stage: stageName });
|
|
218
|
+
}
|
|
219
|
+
if (!stage) {
|
|
220
|
+
valid = false;
|
|
221
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing Stage: ${rel}`, { path: rel, stage: stageName });
|
|
222
|
+
}
|
|
223
|
+
if (!status) {
|
|
224
|
+
valid = false;
|
|
225
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing valid Status: ${rel}`, { path: rel, stage: stageName });
|
|
226
|
+
} else if (status !== 'completed') {
|
|
227
|
+
valid = false;
|
|
228
|
+
addBlocker(blockers, 'open-internal-stage', `Internal stage checkpoint is not completed: ${rel}`, { path: rel, stage: stageName });
|
|
229
|
+
}
|
|
230
|
+
if (!updated) {
|
|
231
|
+
valid = false;
|
|
232
|
+
addBlocker(blockers, 'missing-internal-stage', `Internal stage file is missing Updated: ${rel}`, { path: rel, stage: stageName });
|
|
233
|
+
}
|
|
234
|
+
return { state, valid };
|
|
215
235
|
}
|
|
216
236
|
|
|
217
237
|
function contentMatch(content, pattern) {
|
|
@@ -273,6 +293,82 @@ function hasUnresolvedAssumption(content) {
|
|
|
273
293
|
});
|
|
274
294
|
}
|
|
275
295
|
|
|
296
|
+
function workstreamSplitDecision(content) {
|
|
297
|
+
const match = /\bsplit\s+decision\s*:\s*(yes|no)\b/i.exec(content);
|
|
298
|
+
return match?.[1].toLowerCase();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function hasNoSplitRationale(content) {
|
|
302
|
+
return /\b(?:rationale|reason|because)\b/i.test(content);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function hasMultipleWorkstreams(content) {
|
|
306
|
+
const normalized = removeCodeBlocks(content).toLowerCase();
|
|
307
|
+
const explicitIds = new Set([...normalized.matchAll(/\b(?:workstream|ws)\s*[-:#]?\s*([a-z0-9]+)\b/g)].map((match) => match[1]));
|
|
308
|
+
if (explicitIds.size >= 2) return true;
|
|
309
|
+
const workstreamMentions = normalized.match(/\bworkstreams?\b/g) ?? [];
|
|
310
|
+
return workstreamMentions.length >= 2 && /\b(?:multiple|parallel|split)\b/.test(normalized);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasActionableWorkstreamFields(content) {
|
|
314
|
+
const normalized = content.toLowerCase();
|
|
315
|
+
const fieldGroups = [
|
|
316
|
+
['purpose', 'objective'],
|
|
317
|
+
['required context', 'read'],
|
|
318
|
+
['allowed edit'],
|
|
319
|
+
['forbidden edit', 'do not edit', 'do-not edit'],
|
|
320
|
+
['task'],
|
|
321
|
+
['validation', 'verify'],
|
|
322
|
+
['handoff output', 'output'],
|
|
323
|
+
['dependenc'],
|
|
324
|
+
];
|
|
325
|
+
return fieldGroups.every((group) => group.some((term) => normalized.includes(term)));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function hasActionableIntegrationFields(content) {
|
|
329
|
+
const normalized = content.toLowerCase();
|
|
330
|
+
return ['sequence', 'integrat'].every((term) => normalized.includes(term)) && ['validation', 'verify', 'handoff'].some((term) => normalized.includes(term));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
|
|
334
|
+
if (stageName !== '05-workstream-handoff') return true;
|
|
335
|
+
let valid = true;
|
|
336
|
+
const handoff = findSection(files, 'Workstream Handoff');
|
|
337
|
+
const todo = findSection(files, 'Todo Contract');
|
|
338
|
+
const relHandoff = projectRelative(root, handoff?.file ?? files[0]);
|
|
339
|
+
const decision = workstreamSplitDecision(handoff?.content ?? '');
|
|
340
|
+
if (!decision) {
|
|
341
|
+
addBlocker(blockers, 'missing-workstream-split-decision', 'Workstream Handoff must include `Split decision: yes` or `Split decision: no`.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
if (decision === 'no') {
|
|
345
|
+
if (!hasNoSplitRationale(handoff.content)) {
|
|
346
|
+
valid = false;
|
|
347
|
+
addBlocker(blockers, 'missing-workstream-no-split-rationale', 'Workstream Handoff with `Split decision: no` must include a rationale.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
|
|
348
|
+
}
|
|
349
|
+
if (!todo || !contentHasValue(todo.content) || isPlaceholderContent(todo.content)) valid = false;
|
|
350
|
+
return valid;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const map = findSection(files, 'Workstream Map');
|
|
354
|
+
const workstreams = findSection(files, 'Workstreams');
|
|
355
|
+
const integration = findSection(files, 'Integration Sequence');
|
|
356
|
+
const splitContent = [map?.content, workstreams?.content, integration?.content].filter(Boolean).join('\n');
|
|
357
|
+
if (!hasMultipleWorkstreams(splitContent)) {
|
|
358
|
+
valid = false;
|
|
359
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Split Stage 05 handoff must indicate multiple workstreams in Workstream Map, Workstreams, or Integration Sequence.', { path: projectRelative(root, map?.file ?? workstreams?.file ?? integration?.file ?? handoff.file), stage: stageName, section: 'Workstream Map' });
|
|
360
|
+
}
|
|
361
|
+
if (!workstreams || !hasActionableWorkstreamFields(workstreams.content)) {
|
|
362
|
+
valid = false;
|
|
363
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Workstreams must mention purpose/objective, required context/read, allowed edits, forbidden edits/do-not-edit, tasks, validation, handoff output, and dependencies.', { path: projectRelative(root, workstreams?.file ?? handoff.file), stage: stageName, section: 'Workstreams' });
|
|
364
|
+
}
|
|
365
|
+
if (!integration || !hasActionableIntegrationFields(integration.content)) {
|
|
366
|
+
valid = false;
|
|
367
|
+
addBlocker(blockers, 'incomplete-workstream-split', 'Integration Sequence must describe actionable integration sequence and validation/handoff expectations.', { path: projectRelative(root, integration?.file ?? handoff.file), stage: stageName, section: 'Integration Sequence' });
|
|
368
|
+
}
|
|
369
|
+
return valid;
|
|
370
|
+
}
|
|
371
|
+
|
|
276
372
|
function constructionChecklistHeader(stageName) {
|
|
277
373
|
const match = /^(\d{2})-/.exec(stageName);
|
|
278
374
|
return `Stage ${match?.[1] ?? stageName} Construction Checklist`;
|
|
@@ -416,28 +512,28 @@ export function validatePlanArtifact(planPath, options = {}) {
|
|
|
416
512
|
const durableFiles = discoverDurablePlanMarkdownFiles(taskDir);
|
|
417
513
|
if (durableFiles.length > 1) summary.splitFiles.detected = true;
|
|
418
514
|
for (const stageName of stagesToValidate) {
|
|
419
|
-
validateInternalStageState(root, taskDir, stageName, blockers);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
515
|
+
const internal = validateInternalStageState(root, taskDir, stageName, blockers);
|
|
516
|
+
summary.stages.present += internal.state ? 1 : 0;
|
|
517
|
+
let stageValid = internal.valid;
|
|
518
|
+
const files = internal.state ? [internal.state.file] : [];
|
|
519
|
+
const fallbackPath = projectRelative(root, internalStageFile(taskDir, stageName));
|
|
424
520
|
for (const header of PLAN_REQUIRED_HEADERS[stageName]) {
|
|
425
|
-
const found = findSection(files, header);
|
|
521
|
+
const found = files.length > 0 ? findSection(files, header) : undefined;
|
|
426
522
|
if (!found) {
|
|
427
523
|
stageValid = false;
|
|
428
|
-
addBlocker(blockers, 'missing-section', `Missing required section: ## ${header}`, { path:
|
|
524
|
+
addBlocker(blockers, 'missing-section', `Missing required section in internal checkpoint: ## ${header}`, { path: fallbackPath, stage: stageName, section: header });
|
|
429
525
|
continue;
|
|
430
526
|
}
|
|
431
527
|
summary.requiredSections.present += 1;
|
|
432
528
|
const relFound = projectRelative(root, found.file);
|
|
433
529
|
if (!contentHasValue(found.content)) {
|
|
434
530
|
stageValid = false;
|
|
435
|
-
addBlocker(blockers, 'empty-section', `Required section has no content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
531
|
+
addBlocker(blockers, 'empty-section', `Required checkpoint section has no content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
436
532
|
continue;
|
|
437
533
|
}
|
|
438
534
|
if (isPlaceholderContent(found.content)) {
|
|
439
535
|
stageValid = false;
|
|
440
|
-
addBlocker(blockers, 'placeholder-section', `Required section only contains placeholder content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
536
|
+
addBlocker(blockers, 'placeholder-section', `Required checkpoint section only contains placeholder content: ## ${header}`, { path: relFound, stage: stageName, section: header });
|
|
441
537
|
continue;
|
|
442
538
|
}
|
|
443
539
|
summary.requiredSections.valid += 1;
|
|
@@ -465,7 +561,8 @@ export function validatePlanArtifact(planPath, options = {}) {
|
|
|
465
561
|
addBlocker(blockers, 'unresolved-assumption', 'Assumptions contain unresolved items.', { path: relFound, stage: stageName, section: header });
|
|
466
562
|
}
|
|
467
563
|
}
|
|
468
|
-
if (!
|
|
564
|
+
if (files.length > 0 && !validateWorkstreamHandoffContract(root, files, stageName, blockers)) stageValid = false;
|
|
565
|
+
if (files.length > 0 && !validateConstructionChecklist(root, files, stageName, blockers)) stageValid = false;
|
|
469
566
|
if (stageValid) summary.stages.valid += 1;
|
|
470
567
|
}
|
|
471
568
|
|
package/src/validate/run.mjs
CHANGED
|
@@ -35,6 +35,7 @@ export function runValidate(argv) {
|
|
|
35
35
|
if (path.includes('/node_modules') || path === 'node_modules') return true;
|
|
36
36
|
if (path.includes('/.git') || path === '.git') return true;
|
|
37
37
|
if (path === CACHE_DIR || path.startsWith(`${CACHE_DIR}/`)) return true;
|
|
38
|
+
if (/^docs\/(?:plan|archive\/plan)\/[a-z0-9]+(?:-[a-z0-9]+)*\/\.dotdotgod-plan(?:\/|$)/.test(path)) return true;
|
|
38
39
|
if (!options.includeLocalMemory && (path === 'docs/plan' || path.startsWith('docs/plan/') || path === 'docs/archive' || path.startsWith('docs/archive/'))) return true;
|
|
39
40
|
return false;
|
|
40
41
|
};
|