@dotdotgod/cli 0.1.42 → 0.1.43

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
4
4
  "description": "CLI for dotdotgod validation, project memory snapshots, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
@@ -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': ['Workstream Handoff', 'Todo Contract'],
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 = {
@@ -50,6 +57,9 @@ export const PLAN_BLOCKER_LABELS = new Map([
50
57
  ['malformed-checklist-item', 'Malformed construction checklist item'],
51
58
  ['open-checklist-item', 'Construction checklist item is open'],
52
59
  ['placeholder-checklist-item', 'Construction checklist item only contains placeholder content'],
60
+ ['missing-workstream-split-decision', 'Workstream Handoff must include an explicit split decision'],
61
+ ['incomplete-workstream-split', 'Split workstream handoff must include actionable workstream contracts'],
62
+ ['missing-workstream-no-split-rationale', 'No-split workstream handoff must include rationale'],
53
63
  ]);
54
64
 
55
65
  export function buildPlanValidationNextStagePrompt(planPath, stage, stagePath) {
@@ -57,7 +67,7 @@ export function buildPlanValidationNextStagePrompt(planPath, stage, stagePath) {
57
67
  `Continue Plan Mode stage advancement for ${planPath}.`,
58
68
  `Current stage: ${stage}.`,
59
69
  `Make ${planPath} satisfy the current stage's durable README requirements first. Use ${stagePath} only as an optional checkpoint/workflow note for this stage; do not make numbered checkpoint creation the goal and do not create later checkpoint files yet.`,
60
- stage === '05-workstream-handoff' ? 'For this stage, ensure the durable README or split UPPER_SNAKE_CASE workstream plan files expose numbered `Plan:` sections so Pi can extract todos during execution. For small plans, explicitly state that no split is needed.' : undefined,
70
+ stage === '05-workstream-handoff' ? 'For this stage, 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
71
  `After completing this stage, stop and rerun \`dotdotgod plan validate ${planPath} --stage ${stage} --json\`.`,
62
72
  ].filter(Boolean).join('\n\n');
63
73
  }
@@ -273,6 +273,82 @@ function hasUnresolvedAssumption(content) {
273
273
  });
274
274
  }
275
275
 
276
+ function workstreamSplitDecision(content) {
277
+ const match = /\bsplit\s+decision\s*:\s*(yes|no)\b/i.exec(content);
278
+ return match?.[1].toLowerCase();
279
+ }
280
+
281
+ function hasNoSplitRationale(content) {
282
+ return /\b(?:rationale|reason|because)\b/i.test(content);
283
+ }
284
+
285
+ function hasMultipleWorkstreams(content) {
286
+ const normalized = removeCodeBlocks(content).toLowerCase();
287
+ const explicitIds = new Set([...normalized.matchAll(/\b(?:workstream|ws)\s*[-:#]?\s*([a-z0-9]+)\b/g)].map((match) => match[1]));
288
+ if (explicitIds.size >= 2) return true;
289
+ const workstreamMentions = normalized.match(/\bworkstreams?\b/g) ?? [];
290
+ return workstreamMentions.length >= 2 && /\b(?:multiple|parallel|split)\b/.test(normalized);
291
+ }
292
+
293
+ function hasActionableWorkstreamFields(content) {
294
+ const normalized = content.toLowerCase();
295
+ const fieldGroups = [
296
+ ['purpose', 'objective'],
297
+ ['required context', 'read'],
298
+ ['allowed edit'],
299
+ ['forbidden edit', 'do not edit', 'do-not edit'],
300
+ ['task'],
301
+ ['validation', 'verify'],
302
+ ['handoff output', 'output'],
303
+ ['dependenc'],
304
+ ];
305
+ return fieldGroups.every((group) => group.some((term) => normalized.includes(term)));
306
+ }
307
+
308
+ function hasActionableIntegrationFields(content) {
309
+ const normalized = content.toLowerCase();
310
+ return ['sequence', 'integrat'].every((term) => normalized.includes(term)) && ['validation', 'verify', 'handoff'].some((term) => normalized.includes(term));
311
+ }
312
+
313
+ function validateWorkstreamHandoffContract(root, files, stageName, blockers) {
314
+ if (stageName !== '05-workstream-handoff') return true;
315
+ let valid = true;
316
+ const handoff = findSection(files, 'Workstream Handoff');
317
+ const todo = findSection(files, 'Todo Contract');
318
+ const relHandoff = projectRelative(root, handoff?.file ?? files[0]);
319
+ const decision = workstreamSplitDecision(handoff?.content ?? '');
320
+ if (!decision) {
321
+ addBlocker(blockers, 'missing-workstream-split-decision', 'Workstream Handoff must include `Split decision: yes` or `Split decision: no`.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
322
+ return false;
323
+ }
324
+ if (decision === 'no') {
325
+ if (!hasNoSplitRationale(handoff.content)) {
326
+ valid = false;
327
+ addBlocker(blockers, 'missing-workstream-no-split-rationale', 'Workstream Handoff with `Split decision: no` must include a rationale.', { path: relHandoff, stage: stageName, section: 'Workstream Handoff' });
328
+ }
329
+ if (!todo || !contentHasValue(todo.content) || isPlaceholderContent(todo.content)) valid = false;
330
+ return valid;
331
+ }
332
+
333
+ const map = findSection(files, 'Workstream Map');
334
+ const workstreams = findSection(files, 'Workstreams');
335
+ const integration = findSection(files, 'Integration Sequence');
336
+ const splitContent = [map?.content, workstreams?.content, integration?.content].filter(Boolean).join('\n');
337
+ if (!hasMultipleWorkstreams(splitContent)) {
338
+ valid = false;
339
+ 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' });
340
+ }
341
+ if (!workstreams || !hasActionableWorkstreamFields(workstreams.content)) {
342
+ valid = false;
343
+ 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' });
344
+ }
345
+ if (!integration || !hasActionableIntegrationFields(integration.content)) {
346
+ valid = false;
347
+ 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' });
348
+ }
349
+ return valid;
350
+ }
351
+
276
352
  function constructionChecklistHeader(stageName) {
277
353
  const match = /^(\d{2})-/.exec(stageName);
278
354
  return `Stage ${match?.[1] ?? stageName} Construction Checklist`;
@@ -465,6 +541,7 @@ export function validatePlanArtifact(planPath, options = {}) {
465
541
  addBlocker(blockers, 'unresolved-assumption', 'Assumptions contain unresolved items.', { path: relFound, stage: stageName, section: header });
466
542
  }
467
543
  }
544
+ if (!validateWorkstreamHandoffContract(root, files, stageName, blockers)) stageValid = false;
468
545
  if (!validateConstructionChecklist(root, files, stageName, blockers)) stageValid = false;
469
546
  if (stageValid) summary.stages.valid += 1;
470
547
  }