@osovv/vv-opencode 1.3.8 → 1.4.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +66 -14
  3. package/dist/commands/completion.js +10 -2
  4. package/dist/commands/completion.js.map +1 -1
  5. package/dist/commands/patch-provider.d.ts +148 -1
  6. package/dist/commands/patch-provider.js +151 -5
  7. package/dist/commands/patch-provider.js.map +1 -1
  8. package/dist/lib/orchestration.d.ts +2 -2
  9. package/dist/lib/orchestration.js +31 -2
  10. package/dist/lib/orchestration.js.map +1 -1
  11. package/dist/lib/spec-lint.d.ts +71 -1
  12. package/dist/lib/spec-lint.js +566 -8
  13. package/dist/lib/spec-lint.js.map +1 -1
  14. package/dist/lib/vvoc-config.d.ts +3 -3
  15. package/dist/lib/vvoc-preset-registry.d.ts +42 -30
  16. package/dist/lib/vvoc-preset-registry.js +41 -31
  17. package/dist/lib/vvoc-preset-registry.js.map +1 -1
  18. package/dist/plugins/hashline-edit/index.js.map +1 -1
  19. package/dist/plugins/workflow/checkpoint-io.d.ts +33 -0
  20. package/dist/plugins/workflow/checkpoint-io.js +224 -0
  21. package/dist/plugins/workflow/checkpoint-io.js.map +1 -0
  22. package/dist/plugins/workflow/checkpoints.d.ts +182 -0
  23. package/dist/plugins/workflow/checkpoints.js +1067 -0
  24. package/dist/plugins/workflow/checkpoints.js.map +1 -0
  25. package/dist/plugins/workflow/delegated.d.ts +182 -0
  26. package/dist/plugins/workflow/delegated.js +751 -0
  27. package/dist/plugins/workflow/delegated.js.map +1 -0
  28. package/dist/plugins/workflow/index.js +685 -23
  29. package/dist/plugins/workflow/index.js.map +1 -1
  30. package/dist/plugins/workflow/persistence.d.ts +32 -3
  31. package/dist/plugins/workflow/persistence.js +516 -70
  32. package/dist/plugins/workflow/persistence.js.map +1 -1
  33. package/dist/plugins/workflow/repair.js +3 -1
  34. package/dist/plugins/workflow/repair.js.map +1 -1
  35. package/dist/plugins/workflow/snapshots.d.ts +59 -0
  36. package/dist/plugins/workflow/snapshots.js +261 -0
  37. package/dist/plugins/workflow/snapshots.js.map +1 -0
  38. package/dist/plugins/workflow/state.d.ts +20 -2
  39. package/dist/plugins/workflow/state.js +127 -12
  40. package/dist/plugins/workflow/state.js.map +1 -1
  41. package/dist/plugins/workflow/tooling.d.ts +45 -0
  42. package/dist/plugins/workflow/tooling.js +344 -12
  43. package/dist/plugins/workflow/tooling.js.map +1 -1
  44. package/dist/plugins/workflow/transitions.js +2 -2
  45. package/dist/plugins/workflow/transitions.js.map +1 -1
  46. package/dist/tui/context/analyze.js +3 -1
  47. package/dist/tui/context/analyze.js.map +1 -1
  48. package/package.json +1 -1
  49. package/schemas/vvoc/v3.json +5 -3
  50. package/templates/agents/vv-code-reviewer.md +3 -1
  51. package/templates/agents/vv-implementer.md +5 -2
  52. package/templates/agents/vv-spec-reviewer.md +3 -0
  53. package/templates/skills/vv-execute/SKILL.md +62 -15
  54. package/templates/skills/vv-plan/SKILL.md +13 -3
  55. package/templates/skills/vv-plan/references/plan-template.xml +45 -0
  56. package/templates/skills/vv-review/SKILL.md +1 -0
@@ -1,10 +1,10 @@
1
1
  // FILE: src/lib/spec-lint.ts
2
- // VERSION: 1.0.0
2
+ // VERSION: 1.1.0
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Pure strict linter for .vvoc spec-package XML artifacts (spec.xml, plan.xml, design-context.xml) enforcing the element-name identity format.
5
- // SCOPE: Strict well-formedness validation over the htmlparser2 xmlMode event stream, template-contract checks per artifact kind, component/task/wave identity rules, reference integrity, lifecycle-aware severity, plan-subset-of-spec cross-file checking, and package layout checks. No filesystem access — callers pass artifact contents.
4
+ // PURPOSE: Pure strict linter for .vvoc spec-package XML artifacts (spec.xml, plan.xml, design-context.xml) enforcing the element-name identity format and delegated execution/checkpoint contracts.
5
+ // SCOPE: Strict well-formedness validation over the htmlparser2 xmlMode event stream, template-contract checks per artifact kind, component/task/wave/checkpoint identity rules, reference integrity, lifecycle-aware severity, plan-subset-of-spec cross-file checking, delegated execution and review-checkpoint validation with typed extraction, and package layout checks. No filesystem access — callers pass artifact contents.
6
6
  // DEPENDS: [htmlparser2]
7
- // LINKS: [M-SPEC-LINT, M-PLUGIN-SPEC-GUARD, M-CLI-COMMANDS]
7
+ // LINKS: [M-SPEC-LINT, M-PLUGIN-SPEC-GUARD, M-CLI-COMMANDS, M-WORKFLOW-CHECKPOINTS]
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
10
10
  // END_MODULE_CONTRACT
@@ -17,6 +17,14 @@
17
17
  // SpecLintVerdict - Per-artifact lint result with kind, ok flag, and findings.
18
18
  // SpecLintArtifactInput - One artifact to lint identified by a file label and raw content.
19
19
  // SpecLintOptions - Options for lint runs (skipCrossFile for single-file contexts).
20
+ // DelegatedReviewer - Canonical delegated checkpoint reviewer roles (spec, code).
21
+ // DelegatedTaskDefinition - Typed delegated obligation for one declared plan task.
22
+ // DelegatedCheckpointDefinition - Typed delegated obligation for one declared review checkpoint.
23
+ // DelegatedPlanDefinition - Typed obligations extracted from a delegated execution section.
24
+ // DelegatedPlanExtractionResult - Success payload or error list returned by delegated extraction.
25
+ // DeclaredScopePathResult - Normalized workspace-relative path or a rejection reason.
26
+ // normalizeDeclaredScopePath - Text-level canonical normalization for declared scope file paths.
27
+ // extractDelegatedPlanDefinition - Strictly extract and validate typed delegated obligations from plan content.
20
28
  // parseSpecXml - Strict xmlMode parse producing a positioned element tree or well-formedness findings.
21
29
  // lintSpecArtifacts - Lint a set of artifacts together, applying cross-file rules between plans and specs.
22
30
  // detectSpecArtifactKind - Map a root element name onto an artifact kind.
@@ -24,19 +32,23 @@
24
32
  // END_MODULE_MAP
25
33
  //
26
34
  // START_CHANGE_SUMMARY
27
- // LAST_CHANGE: [C-SPEC-IDENTITY-LINT - Initial engine: strict stack validation, template contracts, identity rules, references, lifecycle severity, cross-file subset check, layout checks.]
35
+ // LAST_CHANGE: [C-DELEGATED-WORKFLOW-ASTRA-PRESETS - Added the delegated execution section, review-checkpoint vocabulary, scope-path normalization, typed extractDelegatedPlanDefinition, and bumped LINT_VERSION to 2.]
28
36
  // END_CHANGE_SUMMARY
29
37
  import { Tokenizer } from "htmlparser2";
30
38
  // START_BLOCK_PUBLIC_TYPES
31
- export const LINT_VERSION = 1;
39
+ export const LINT_VERSION = 2;
32
40
  const IDENTITY_PATTERNS = {
33
41
  component: /^COMPONENT-[A-Z0-9]+(-[A-Z0-9]+)*$/,
34
42
  task: /^TASK-T-\d{3,}$/,
35
43
  wave: /^WAVE-\d+$/,
44
+ checkpoint: /^CHECKPOINT-R-\d{3,}$/,
36
45
  };
37
46
  const TASK_ID_REF = /^T-\d{3,}$/;
38
47
  const DOC_STATUSES = new Set(["draft", "approved", "applied"]);
39
48
  const TASK_STATUSES = new Set(["pending", "in_progress", "done", "skipped"]);
49
+ const EXECUTION_MODES = new Set(["inline", "classic", "delegated"]);
50
+ const CHECKPOINT_KINDS = new Set(["milestone", "final"]);
51
+ const DELEGATED_REVIEWERS = new Set(["spec", "code"]);
40
52
  const RESERVED_PACKAGE_SLUGS = new Set(["draft", "archive", "template", "plan", "spec", "vvoc"]);
41
53
  const PACKAGE_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z0-9]([a-z0-9_-]*[a-z0-9_])?$/;
42
54
  // END_BLOCK_PARSER_TYPES
@@ -271,10 +283,23 @@ const SPEC_CONTRACT = {
271
283
  };
272
284
  const COMPONENT_CHILDREN = ["name", "responsibility", "depends_on"];
273
285
  const PLAN_CONTRACT = {
274
- plan: { names: ["spec", "design_context", "created", "status", "meta", "architecture", "tasks"] },
286
+ plan: {
287
+ names: [
288
+ "spec",
289
+ "design_context",
290
+ "created",
291
+ "status",
292
+ "meta",
293
+ "architecture",
294
+ "tasks",
295
+ "execution",
296
+ ],
297
+ },
275
298
  meta: { names: ["summary", "waves", "affected_modules", "complexity"] },
276
299
  architecture: { identity: "component" },
277
300
  tasks: { identity: "wave" },
301
+ execution: { names: ["mode", "review_checkpoints"] },
302
+ review_checkpoints: { identity: "checkpoint" },
278
303
  };
279
304
  const TASK_CHILDREN = [
280
305
  "title",
@@ -285,12 +310,26 @@ const TASK_CHILDREN = [
285
310
  "snippet",
286
311
  "acceptance",
287
312
  "verification",
313
+ "write_scope",
288
314
  ];
289
315
  const TASK_DEPENDS_CHILDREN = ["task_id"];
290
316
  const ACCEPTANCE_CHILDREN = ["criterion"];
291
317
  const VERIFICATION_CHILDREN = ["command"];
292
318
  const WAVE_CHILDREN = ["goal"];
293
319
  const PLAN_FILE_CHILDREN = ["path", "role"];
320
+ const TASK_WRITE_SCOPE_CHILDREN = ["file"];
321
+ const CHECKPOINT_CHILDREN = [
322
+ "kind",
323
+ "after_wave",
324
+ "covers",
325
+ "scope",
326
+ "reviewers",
327
+ "acceptance",
328
+ "verification",
329
+ ];
330
+ const CHECKPOINT_COVERS_CHILDREN = ["task_id"];
331
+ const CHECKPOINT_SCOPE_CHILDREN = ["file"];
332
+ const CHECKPOINT_REVIEWERS_CHILDREN = ["reviewer"];
294
333
  const DESIGN_CONTEXT_CONTRACT = {
295
334
  "design-context": {
296
335
  names: ["decisions", "assumptions", "deferred", "scenarios", "external_constraints"],
@@ -333,7 +372,521 @@ function textOf(node) {
333
372
  function nonEmpty(node) {
334
373
  return textOf(node) !== "";
335
374
  }
336
- // END_BLOCK_TREE_HELPERS
375
+ // END_BLOCK_DELEGATED_TYPES
376
+ // START_BLOCK_SCOPE_PATH_NORMALIZATION
377
+ /**
378
+ * Text-level canonical normalization for declared scope paths. The linter and
379
+ * the runtime snapshot fingerprint share this representation so a plan that
380
+ * lints clean cannot declare paths the runtime normalizer rejects on sight.
381
+ * Filesystem-specific checks (existence, symlinks, regular files) stay in the
382
+ * runtime normalizer; this function is deliberately pure.
383
+ */
384
+ export function normalizeDeclaredScopePath(raw) {
385
+ const trimmed = raw.trim();
386
+ if (trimmed === "")
387
+ return { ok: false, reason: "empty path" };
388
+ if (trimmed.includes("\\"))
389
+ return { ok: false, reason: "backslash separator" };
390
+ if (trimmed.startsWith("/"))
391
+ return { ok: false, reason: "absolute path" };
392
+ if (/^[A-Za-z]:/.test(trimmed))
393
+ return { ok: false, reason: "drive-absolute path" };
394
+ if (trimmed.startsWith("~"))
395
+ return { ok: false, reason: "home-relative path" };
396
+ if (/[*?[\]]/.test(trimmed))
397
+ return { ok: false, reason: "wildcard characters" };
398
+ // eslint-disable-next-line no-control-regex
399
+ if (/[\u0000-\u001f\u007f]/.test(trimmed))
400
+ return { ok: false, reason: "control characters" };
401
+ const segments = trimmed.split("/");
402
+ for (const segment of segments) {
403
+ if (segment === "")
404
+ return { ok: false, reason: "empty path segment" };
405
+ if (segment === "." || segment === "..")
406
+ return { ok: false, reason: "traversal segment" };
407
+ }
408
+ return { ok: true, path: segments.join("/") };
409
+ }
410
+ /**
411
+ * Collect wave order and per-task delegated facts with a standalone walk so
412
+ * lintPlan's main task validation and extractDelegatedPlanDefinition share one
413
+ * interpretation of the declared task/write-scope vocabulary.
414
+ */
415
+ function collectDelegatedTaskFacts(root) {
416
+ const waveOrder = [];
417
+ const tasks = [];
418
+ const duplicateTaskElements = [];
419
+ const seenTasks = new Set();
420
+ const tasksNode = child(root, "tasks");
421
+ if (!tasksNode)
422
+ return { waveOrder, tasks, duplicateTaskElements };
423
+ for (const wave of tasksNode.children) {
424
+ if (!IDENTITY_PATTERNS.wave.test(wave.name))
425
+ continue;
426
+ if (!waveOrder.includes(wave.name))
427
+ waveOrder.push(wave.name);
428
+ for (const task of wave.children) {
429
+ if (!IDENTITY_PATTERNS.task.test(task.name))
430
+ continue;
431
+ if (seenTasks.has(task.name)) {
432
+ duplicateTaskElements.push(task.name);
433
+ continue;
434
+ }
435
+ seenTasks.add(task.name);
436
+ const writeScopeNode = child(task, "write_scope");
437
+ const writeScope = [];
438
+ const writeScopeDuplicates = [];
439
+ if (writeScopeNode) {
440
+ for (const fileNode of writeScopeNode.children) {
441
+ if (fileNode.name !== "file")
442
+ continue;
443
+ const normalized = normalizeDeclaredScopePath(textOf(fileNode));
444
+ if (!normalized.ok)
445
+ continue;
446
+ if (writeScope.includes(normalized.path)) {
447
+ // Duplicate write-scope entries are rejected by the lint rules;
448
+ // here the duplicate is recorded so the rule can point at it.
449
+ if (!writeScopeDuplicates.includes(normalized.path)) {
450
+ writeScopeDuplicates.push(normalized.path);
451
+ }
452
+ continue;
453
+ }
454
+ writeScope.push(normalized.path);
455
+ }
456
+ }
457
+ tasks.push({
458
+ taskElement: task.name,
459
+ taskId: task.name.slice("TASK-".length),
460
+ wave: wave.name,
461
+ line: task.line,
462
+ writeScope,
463
+ writeScopeDeclared: writeScopeNode !== undefined,
464
+ writeScopeDuplicates,
465
+ primaryFile: (() => {
466
+ const normalized = normalizeDeclaredScopePath(textOf(child(task, "file")));
467
+ return normalized.ok ? normalized.path : "";
468
+ })(),
469
+ });
470
+ }
471
+ }
472
+ return { waveOrder, tasks, duplicateTaskElements };
473
+ }
474
+ // END_BLOCK_DELEGATED_FACTS
475
+ // START_BLOCK_DELEGATED_VALIDATION
476
+ /** Emit write-scope obligations for every declared delegated task. */
477
+ function lintDelegatedWriteScopes(facts, completeness, findings, file) {
478
+ for (const task of facts.tasks) {
479
+ if (task.writeScopeDuplicates.length > 0) {
480
+ findings.push({
481
+ severity: "error",
482
+ rule: "execution.scope_path",
483
+ message: `task ${task.taskId} declares write_scope path ${JSON.stringify(task.writeScopeDuplicates[0])} more than once; the runtime normalizer rejects duplicate scope entries`,
484
+ file,
485
+ line: task.line,
486
+ });
487
+ continue;
488
+ }
489
+ if (!task.writeScopeDeclared || task.writeScope.length === 0) {
490
+ findings.push({
491
+ severity: completeness ? "error" : "warning",
492
+ rule: "lifecycle.required",
493
+ message: `task ${task.taskId} declares no write_scope; delegated tasks must list their workspace-relative write scope`,
494
+ file,
495
+ line: task.line,
496
+ });
497
+ continue;
498
+ }
499
+ if (task.primaryFile && !task.writeScope.includes(task.primaryFile)) {
500
+ findings.push({
501
+ severity: "error",
502
+ rule: "execution.write_scope",
503
+ message: `task ${task.taskId} write_scope does not include its primary file ${task.primaryFile}`,
504
+ file,
505
+ line: task.line,
506
+ });
507
+ }
508
+ }
509
+ }
510
+ /**
511
+ * Validate the optional <execution> section. Structural violations of declared
512
+ * content (unknown modes/children, duplicate identities, dangling references,
513
+ * future-wave coverage, multiple or misplaced finals, incomplete declared
514
+ * coverage) are always errors; presence obligations for approved/applied plans
515
+ * escalate from draft warnings.
516
+ */
517
+ function lintExecutionSection(root, file, facts, completeness, findings) {
518
+ const executionNode = child(root, "execution");
519
+ if (!executionNode)
520
+ return;
521
+ checkChildren(executionNode, PLAN_CONTRACT.execution, file, findings);
522
+ const mode = textOf(child(executionNode, "mode"));
523
+ if (!EXECUTION_MODES.has(mode)) {
524
+ findings.push({
525
+ severity: "error",
526
+ rule: "execution.mode",
527
+ message: `execution mode "${mode}" is not one of inline, classic, delegated`,
528
+ file,
529
+ line: child(executionNode, "mode")?.line ?? executionNode.line,
530
+ });
531
+ }
532
+ const reviewCheckpointsNode = child(executionNode, "review_checkpoints");
533
+ if (reviewCheckpointsNode && mode !== "delegated") {
534
+ findings.push({
535
+ severity: "error",
536
+ rule: "execution.checkpoints_mode",
537
+ message: `review_checkpoints are only valid with execution mode delegated, not "${mode}"`,
538
+ file,
539
+ line: reviewCheckpointsNode.line,
540
+ });
541
+ return;
542
+ }
543
+ if (mode !== "delegated")
544
+ return;
545
+ if (!reviewCheckpointsNode) {
546
+ findings.push({
547
+ severity: completeness ? "error" : "warning",
548
+ rule: "lifecycle.required",
549
+ message: `delegated execution declares no review_checkpoints; the plan is ${completeness ? "approved or applied" : "draft"} and must declare its checkpoint obligations`,
550
+ file,
551
+ line: executionNode.line,
552
+ });
553
+ // Task write-scope obligations still apply even without declared checkpoints.
554
+ lintDelegatedWriteScopes(facts, completeness, findings, file);
555
+ return;
556
+ }
557
+ checkChildren(reviewCheckpointsNode, PLAN_CONTRACT.review_checkpoints, file, findings);
558
+ const obligation = (message, line, rule = "lifecycle.required") => {
559
+ findings.push({
560
+ severity: completeness ? "error" : "warning",
561
+ rule,
562
+ message,
563
+ file,
564
+ line,
565
+ });
566
+ };
567
+ const waveIndex = new Map(facts.waveOrder.map((wave, index) => [wave, index]));
568
+ const taskById = new Map(facts.tasks.map((task) => [task.taskId, task]));
569
+ const seenCheckpoints = new Set();
570
+ const validCheckpoints = [];
571
+ for (const checkpoint of reviewCheckpointsNode.children) {
572
+ if (!IDENTITY_PATTERNS.checkpoint.test(checkpoint.name)) {
573
+ findings.push({
574
+ severity: "error",
575
+ rule: "identity.pattern",
576
+ message: `checkpoint element <${checkpoint.name}> does not match the CHECKPOINT-R-NNN pattern`,
577
+ file,
578
+ line: checkpoint.line,
579
+ });
580
+ continue;
581
+ }
582
+ if (seenCheckpoints.has(checkpoint.name)) {
583
+ findings.push({
584
+ severity: "error",
585
+ rule: "identity.duplicate",
586
+ message: `checkpoint ${checkpoint.name} is declared more than once`,
587
+ file,
588
+ line: checkpoint.line,
589
+ });
590
+ continue;
591
+ }
592
+ seenCheckpoints.add(checkpoint.name);
593
+ checkChildren(checkpoint, { names: CHECKPOINT_CHILDREN }, file, findings);
594
+ const kind = textOf(child(checkpoint, "kind"));
595
+ if (!CHECKPOINT_KINDS.has(kind)) {
596
+ findings.push({
597
+ severity: "error",
598
+ rule: "execution.checkpoint_kind",
599
+ message: `checkpoint ${checkpoint.name} kind "${kind}" is not one of milestone, final`,
600
+ file,
601
+ line: child(checkpoint, "kind")?.line ?? checkpoint.line,
602
+ });
603
+ }
604
+ const afterWaveNode = child(checkpoint, "after_wave");
605
+ const afterWave = textOf(afterWaveNode);
606
+ let afterWaveKnown = false;
607
+ if (!IDENTITY_PATTERNS.wave.test(afterWave) || !waveIndex.has(afterWave)) {
608
+ findings.push({
609
+ severity: "error",
610
+ rule: "ref.dangling",
611
+ message: `checkpoint ${checkpoint.name} after_wave "${afterWave}" is not a declared wave in this plan`,
612
+ file,
613
+ line: afterWaveNode?.line ?? checkpoint.line,
614
+ });
615
+ }
616
+ else {
617
+ afterWaveKnown = true;
618
+ }
619
+ const coversNode = child(checkpoint, "covers");
620
+ const covers = [];
621
+ if (coversNode) {
622
+ checkChildren(coversNode, { names: CHECKPOINT_COVERS_CHILDREN }, file, findings);
623
+ for (const taskRef of coversNode.children) {
624
+ if (taskRef.name !== "task_id")
625
+ continue;
626
+ const value = textOf(taskRef);
627
+ if (!TASK_ID_REF.test(value) || !taskById.has(value)) {
628
+ findings.push({
629
+ severity: "error",
630
+ rule: "ref.dangling",
631
+ message: `checkpoint ${checkpoint.name} covers task_id "${value}" which is not a declared task in this plan`,
632
+ file,
633
+ line: taskRef.line,
634
+ });
635
+ continue;
636
+ }
637
+ covers.push(value);
638
+ const coveredTask = taskById.get(value);
639
+ if (kind === "milestone" &&
640
+ afterWaveKnown &&
641
+ coveredTask &&
642
+ (waveIndex.get(coveredTask.wave) ?? -1) > (waveIndex.get(afterWave) ?? -1)) {
643
+ findings.push({
644
+ severity: "error",
645
+ rule: "execution.future_coverage",
646
+ message: `checkpoint ${checkpoint.name} (milestone after ${afterWave}) covers ${value} from a later wave ${coveredTask.wave}`,
647
+ file,
648
+ line: taskRef.line,
649
+ });
650
+ }
651
+ }
652
+ }
653
+ const scopeNode = child(checkpoint, "scope");
654
+ const scope = [];
655
+ if (scopeNode) {
656
+ checkChildren(scopeNode, { names: CHECKPOINT_SCOPE_CHILDREN }, file, findings);
657
+ for (const fileNode of scopeNode.children) {
658
+ if (fileNode.name !== "file")
659
+ continue;
660
+ const normalized = normalizeDeclaredScopePath(textOf(fileNode));
661
+ if (!normalized.ok) {
662
+ findings.push({
663
+ severity: "error",
664
+ rule: "execution.scope_path",
665
+ message: `checkpoint ${checkpoint.name} declares a malformed scope path (${normalized.reason}): ${JSON.stringify(textOf(fileNode))}`,
666
+ file,
667
+ line: fileNode.line,
668
+ });
669
+ continue;
670
+ }
671
+ if (scope.includes(normalized.path)) {
672
+ findings.push({
673
+ severity: "error",
674
+ rule: "execution.scope_path",
675
+ message: `checkpoint ${checkpoint.name} declares scope path ${JSON.stringify(normalized.path)} more than once; the runtime normalizer rejects duplicate scope entries`,
676
+ file,
677
+ line: fileNode.line,
678
+ });
679
+ continue;
680
+ }
681
+ scope.push(normalized.path);
682
+ }
683
+ }
684
+ const reviewersNode = child(checkpoint, "reviewers");
685
+ const reviewers = [];
686
+ if (reviewersNode) {
687
+ checkChildren(reviewersNode, { names: CHECKPOINT_REVIEWERS_CHILDREN }, file, findings);
688
+ for (const reviewerNode of reviewersNode.children) {
689
+ if (reviewerNode.name !== "reviewer")
690
+ continue;
691
+ const value = textOf(reviewerNode);
692
+ if (!DELEGATED_REVIEWERS.has(value)) {
693
+ findings.push({
694
+ severity: "error",
695
+ rule: "execution.reviewer",
696
+ message: `checkpoint ${checkpoint.name} reviewer "${value}" is not one of spec, code`,
697
+ file,
698
+ line: reviewerNode.line,
699
+ });
700
+ continue;
701
+ }
702
+ if (reviewers.includes(value)) {
703
+ findings.push({
704
+ severity: "error",
705
+ rule: "execution.reviewer_duplicate",
706
+ message: `checkpoint ${checkpoint.name} declares reviewer "${value}" more than once`,
707
+ file,
708
+ line: reviewerNode.line,
709
+ });
710
+ continue;
711
+ }
712
+ reviewers.push(value);
713
+ }
714
+ }
715
+ const acceptanceNode = child(checkpoint, "acceptance");
716
+ if (acceptanceNode)
717
+ checkChildren(acceptanceNode, { names: ACCEPTANCE_CHILDREN }, file, findings);
718
+ const verificationNode = child(checkpoint, "verification");
719
+ if (verificationNode)
720
+ checkChildren(verificationNode, { names: VERIFICATION_CHILDREN }, file, findings);
721
+ if (covers.length === 0) {
722
+ obligation(`checkpoint ${checkpoint.name} covers no tasks; delegated checkpoints must list covered task_ids`, coversNode?.line ?? checkpoint.line);
723
+ }
724
+ if (scope.length === 0) {
725
+ obligation(`checkpoint ${checkpoint.name} declares an empty scope; delegated checkpoints must list reviewed files`, scopeNode?.line ?? checkpoint.line);
726
+ }
727
+ if (reviewers.length === 0) {
728
+ obligation(`checkpoint ${checkpoint.name} declares no reviewers; delegated checkpoints require a non-empty spec/code reviewer set`, reviewersNode?.line ?? checkpoint.line);
729
+ }
730
+ if (!acceptanceNode || children(acceptanceNode, "criterion").length === 0) {
731
+ obligation(`checkpoint ${checkpoint.name} has no acceptance criteria`, acceptanceNode?.line ?? checkpoint.line);
732
+ }
733
+ if (!verificationNode || children(verificationNode, "command").length === 0) {
734
+ obligation(`checkpoint ${checkpoint.name} has no verification commands`, verificationNode?.line ?? checkpoint.line);
735
+ }
736
+ validCheckpoints.push({
737
+ node: checkpoint,
738
+ id: checkpoint.name,
739
+ kind,
740
+ afterWave,
741
+ covers,
742
+ scope,
743
+ reviewers,
744
+ });
745
+ }
746
+ // Delegated task write-scope obligations.
747
+ lintDelegatedWriteScopes(facts, completeness, findings, file);
748
+ const finals = validCheckpoints.filter((c) => c.kind === "final");
749
+ if (finals.length > 1) {
750
+ for (const final of finals.slice(1)) {
751
+ findings.push({
752
+ severity: "error",
753
+ rule: "execution.final_count",
754
+ message: `checkpoint ${final.id} is an additional final checkpoint; exactly one final checkpoint is allowed`,
755
+ file,
756
+ line: final.node.line,
757
+ });
758
+ }
759
+ }
760
+ const declaredCheckpointCount = reviewCheckpointsNode.children.filter((c) => IDENTITY_PATTERNS.checkpoint.test(c.name)).length;
761
+ if (finals.length === 0 && declaredCheckpointCount > 0) {
762
+ obligation("delegated execution declares no final checkpoint; exactly one final checkpoint is required", reviewCheckpointsNode.line, "execution.final_count");
763
+ }
764
+ if (finals.length === 1) {
765
+ const finalCheckpoint = finals[0];
766
+ const lastWave = facts.waveOrder[facts.waveOrder.length - 1];
767
+ if (lastWave &&
768
+ finalCheckpoint.afterWave !== lastWave &&
769
+ waveIndex.has(finalCheckpoint.afterWave)) {
770
+ findings.push({
771
+ severity: "error",
772
+ rule: "execution.final_wave",
773
+ message: `final checkpoint ${finalCheckpoint.id} must sit after the last declared wave ${lastWave}, not ${finalCheckpoint.afterWave}`,
774
+ file,
775
+ line: finalCheckpoint.node.line,
776
+ });
777
+ }
778
+ for (const task of facts.tasks) {
779
+ if (!finalCheckpoint.covers.includes(task.taskId)) {
780
+ findings.push({
781
+ severity: "error",
782
+ rule: "execution.final_coverage",
783
+ message: `final checkpoint ${finalCheckpoint.id} does not cover task ${task.taskId}; the final checkpoint must cover every declared task`,
784
+ file,
785
+ line: finalCheckpoint.node.line,
786
+ });
787
+ }
788
+ }
789
+ const declaredWriteScopes = new Set(facts.tasks.flatMap((task) => task.writeScope));
790
+ for (const scopeFile of declaredWriteScopes) {
791
+ if (!finalCheckpoint.scope.includes(scopeFile)) {
792
+ findings.push({
793
+ severity: "error",
794
+ rule: "execution.final_scope",
795
+ message: `final checkpoint ${finalCheckpoint.id} scope does not cover declared task write scope file ${scopeFile}`,
796
+ file,
797
+ line: finalCheckpoint.node.line,
798
+ });
799
+ }
800
+ }
801
+ }
802
+ }
803
+ // END_BLOCK_DELEGATED_VALIDATION
804
+ // START_BLOCK_DELEGATED_EXTRACTION
805
+ /**
806
+ * Strictly extract typed delegated obligations from plan content. Returns
807
+ * ok:false with every validation message when the plan does not declare a
808
+ * structurally complete and valid delegated execution section, so runtime
809
+ * registration and lint share one contract instead of two interpretations.
810
+ */
811
+ export function extractDelegatedPlanDefinition(planContent, file = "plan.xml") {
812
+ const parsed = parseSpecXml(planContent, file);
813
+ const parseErrors = parsed.findings
814
+ .filter((f) => f.severity === "error")
815
+ .map((f) => `${file}: ${f.message}`);
816
+ if (!parsed.root || parseErrors.length > 0) {
817
+ return {
818
+ ok: false,
819
+ errors: parseErrors.length > 0 ? parseErrors : [`${file}: document has no root element`],
820
+ };
821
+ }
822
+ if (parsed.root.name !== "plan") {
823
+ return { ok: false, errors: [`${file}: root element <${parsed.root.name}> is not a plan`] };
824
+ }
825
+ const facts = collectDelegatedTaskFacts(parsed.root);
826
+ if (facts.duplicateTaskElements.length > 0) {
827
+ return {
828
+ ok: false,
829
+ errors: facts.duplicateTaskElements.map((name) => `${file}: task ${name} is declared more than once`),
830
+ };
831
+ }
832
+ const findings = [];
833
+ lintExecutionSection(parsed.root, file, facts, true, findings);
834
+ const errors = findings.filter((f) => f.severity === "error").map((f) => `${file}: ${f.message}`);
835
+ if (errors.length > 0) {
836
+ return { ok: false, errors };
837
+ }
838
+ const executionNode = child(parsed.root, "execution");
839
+ if (!executionNode || textOf(child(executionNode, "mode")) !== "delegated") {
840
+ return { ok: false, errors: [`${file}: plan does not declare execution mode delegated`] };
841
+ }
842
+ const reviewCheckpointsNode = child(executionNode, "review_checkpoints");
843
+ if (!reviewCheckpointsNode) {
844
+ return { ok: false, errors: [`${file}: delegated execution declares no review_checkpoints`] };
845
+ }
846
+ const checkpoints = [];
847
+ for (const checkpoint of reviewCheckpointsNode.children) {
848
+ if (!IDENTITY_PATTERNS.checkpoint.test(checkpoint.name))
849
+ continue;
850
+ const fileTexts = (container) => {
851
+ const node = child(checkpoint, container);
852
+ return node ? children(node, "file").map((entry) => textOf(entry)) : [];
853
+ };
854
+ const childTexts = (container, entry) => {
855
+ const node = child(checkpoint, container);
856
+ return node ? children(node, entry).map((item) => textOf(item)) : [];
857
+ };
858
+ checkpoints.push({
859
+ checkpointId: checkpoint.name,
860
+ kind: textOf(child(checkpoint, "kind")) === "final" ? "final" : "milestone",
861
+ afterWave: textOf(child(checkpoint, "after_wave")),
862
+ covers: childTexts("covers", "task_id"),
863
+ scope: fileTexts("scope").map((raw) => {
864
+ const normalized = normalizeDeclaredScopePath(raw);
865
+ return normalized.ok ? normalized.path : raw;
866
+ }),
867
+ reviewers: childTexts("reviewers", "reviewer").map((value) => {
868
+ return value === "spec" ? "spec" : "code";
869
+ }),
870
+ acceptance: childTexts("acceptance", "criterion"),
871
+ verification: childTexts("verification", "command"),
872
+ });
873
+ }
874
+ return {
875
+ ok: true,
876
+ definition: {
877
+ mode: "delegated",
878
+ waves: facts.waveOrder,
879
+ tasks: facts.tasks.map((task) => ({
880
+ taskId: task.taskId,
881
+ taskElement: task.taskElement,
882
+ wave: task.wave,
883
+ writeScope: [...task.writeScope],
884
+ })),
885
+ checkpoints,
886
+ },
887
+ };
888
+ }
889
+ // END_BLOCK_DELEGATED_EXTRACTION
337
890
  // START_BLOCK_CONTRACT_CHECKS
338
891
  /** Report children that are neither allowed fixed names nor valid identity elements. */
339
892
  function checkChildren(node, rule, file, findings, extraAllowed = []) {
@@ -678,6 +1231,9 @@ function lintPlan(root, file, parseFindings, specByLabel, options) {
678
1231
  const verificationNode = child(task, "verification");
679
1232
  if (verificationNode)
680
1233
  checkChildren(verificationNode, { names: VERIFICATION_CHILDREN }, file, findings);
1234
+ const writeScopeNode = child(task, "write_scope");
1235
+ if (writeScopeNode)
1236
+ checkChildren(writeScopeNode, { names: TASK_WRITE_SCOPE_CHILDREN }, file, findings);
681
1237
  const taskStatusNode = child(task, "status");
682
1238
  const taskStatus = textOf(taskStatusNode);
683
1239
  if (!TASK_STATUSES.has(taskStatus)) {
@@ -803,6 +1359,8 @@ function lintPlan(root, file, parseFindings, specByLabel, options) {
803
1359
  });
804
1360
  }
805
1361
  }
1362
+ // Optional execution section: delegated/classic/inline intent and checkpoints.
1363
+ lintExecutionSection(root, file, collectDelegatedTaskFacts(root), completeness, findings);
806
1364
  // Cross-file: plan components are a subset of spec components.
807
1365
  if (!options.skipCrossFile) {
808
1366
  const specPath = textOf(specPathNodeEarly);