@namewta/speculo 0.8.6 → 0.8.8

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 (98) hide show
  1. package/dist/src/structured.js +8 -4
  2. package/dist/src/structured.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template/canonical/canonical-specdev-goal-plan.md +67 -20
  5. package/template/canonical/canonical-specdev-grill-with-docs.md +17 -9
  6. package/template/canonical/canonical-specdev-orchestrate-implementation.md +2784 -0
  7. package/template/canonical/canonical-specdev-spec.md +19 -9
  8. package/template/canonical/canonical-specdev-tickets.md +58 -16
  9. package/template/canonical/canonical-specdev-wayfinder.md +11 -11
  10. package/template/workflows/specdev/A-archive-and-consolidate/A-archive-and-consolidate.md +1 -1
  11. package/template/workflows/specdev/I-implement/I-implement.md +10 -5
  12. package/template/workflows/specdev/I-implement/execution-preflight.md +2 -0
  13. package/template/workflows/specdev/I-init-setup/I-init-setup.md +1 -1
  14. package/template/workflows/specdev/I-init-setup/config-template.json +2 -2
  15. package/template/workflows/specdev/O-orchestrate-implementation/O-orchestrate-implementation.md +129 -0
  16. package/template/workflows/specdev/O-orchestrate-implementation/conflict-and-drift.md +18 -0
  17. package/template/workflows/specdev/O-orchestrate-implementation/execution-loop.md +30 -0
  18. package/template/workflows/specdev/O-orchestrate-implementation/implementation-evidence-template.md +39 -0
  19. package/template/workflows/specdev/O-orchestrate-implementation/implementation-map-template.md +50 -0
  20. package/template/workflows/specdev/O-orchestrate-implementation/implementation-plan-template.md +61 -0
  21. package/template/workflows/specdev/O-orchestrate-implementation/input-readiness.md +25 -0
  22. package/template/workflows/specdev/O-orchestrate-implementation/super-dag.md +27 -0
  23. package/template/workflows/specdev/P-goal-plan/planning-modes.md +1 -1
  24. package/template/workflows/specdev/P-prototype/P-prototype.md +30 -26
  25. package/template/workflows/specdev/P-prototype/design-library/INDEX.md +43 -0
  26. package/template/workflows/specdev/P-prototype/design-library/color-and-theme.md +65 -0
  27. package/template/workflows/specdev/P-prototype/design-library/foundations.md +66 -0
  28. package/template/workflows/specdev/P-prototype/design-library/interaction-patterns.md +61 -0
  29. package/template/workflows/specdev/P-prototype/design-library/product-pattern-index.md +73 -0
  30. package/template/workflows/specdev/P-prototype/design-library/research-provenance.md +54 -0
  31. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/METHODOLOGY.md +90 -0
  32. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/README.md +31 -0
  33. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/extended-projects.json +241 -0
  34. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/data/projects.json +253 -0
  35. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/USAGE.md +40 -0
  36. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/reference/design-tokens.css +145 -0
  37. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/report/OPEN_SOURCE_UI_RESEARCH_2026.md +519 -0
  38. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/PROJECT_TEMPLATE.md +29 -0
  39. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/README.md +9 -0
  40. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/claude-code-modern-clients.md +407 -0
  41. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/creative-ai-communication.md +574 -0
  42. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/data-dev-tools.md +432 -0
  43. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/personal-multiplatform-apps.md +185 -0
  44. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/research/productivity-collaboration.md +398 -0
  45. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/01-dense-ide.html +68 -0
  46. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/02-monochrome-console.html +18 -0
  47. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/03-soft-personal-ai.html +17 -0
  48. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/04-responsive-web.html +14 -0
  49. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/05-mobile-supervisor.html +16 -0
  50. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/06-cross-platform-workspace.html +17 -0
  51. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/07-local-first-content.html +9 -0
  52. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/08-media-first.html +17 -0
  53. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/README.md +49 -0
  54. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/ATTRIBUTION.md +9 -0
  55. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/landscape.jpg +0 -0
  56. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/lucide.js +20494 -0
  57. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/mountain.jpg +0 -0
  58. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/assets/workspace.jpg +0 -0
  59. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/index.html +70 -0
  60. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package-lock.json +78 -0
  61. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/package.json +12 -0
  62. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/screenshots/index-desktop.png +0 -0
  63. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/scripts/gallery.js +134 -0
  64. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/base.css +795 -0
  65. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/styles/pages.css +533 -0
  66. package/template/workflows/specdev/P-prototype/design-library/research-snapshot/ui-gallery/tests/gallery.spec.js +56 -0
  67. package/template/workflows/specdev/P-prototype/design-library/responsive-and-platforms.md +53 -0
  68. package/template/workflows/specdev/P-prototype/design-library/style-index.md +32 -0
  69. package/template/workflows/specdev/P-prototype/design-package.schema.json +70 -0
  70. package/template/workflows/specdev/P-prototype/design-system-template.md +366 -0
  71. package/template/workflows/specdev/P-prototype/detect-existing-style.md +53 -0
  72. package/template/workflows/specdev/P-prototype/generate-design-package.md +50 -0
  73. package/template/workflows/specdev/P-prototype/style-selection-protocol.md +41 -0
  74. package/template/workflows/specdev/P-prototype/tools/materialize-prototype.mjs +126 -0
  75. package/template/workflows/specdev/P-prototype/tools/validate-design-package.mjs +155 -0
  76. package/template/workflows/specdev/README.md +28 -15
  77. package/template/workflows/specdev/T-triage/T-triage.md +1 -1
  78. package/template/workflows/specdev/W-wayfinder/W-wayfinder.md +2 -2
  79. package/template/workflows/specdev/W-wayfinder/local-tracker-contract.md +3 -4
  80. package/template/workflows/specdev/common/README.md +6 -3
  81. package/template/workflows/specdev/common/rules/artifact-contract.md +9 -3
  82. package/template/workflows/specdev/common/rules/change-completion.md +3 -0
  83. package/template/workflows/specdev/common/rules/deviation-control.md +2 -0
  84. package/template/workflows/specdev/common/rules/evidence-and-verification.md +2 -0
  85. package/template/workflows/specdev/common/rules/parent-implementation-orchestration.md +27 -0
  86. package/template/workflows/specdev/common/rules/path-ownership.md +3 -1
  87. package/template/workflows/specdev/common/schemas/config.schema.json +4 -4
  88. package/template/workflows/specdev/common/schemas/implementation-map.schema.json +40 -0
  89. package/template/workflows/specdev/common/schemas/implementation-plan.schema.json +44 -0
  90. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +4 -4
  91. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +1 -3
  92. package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +5 -3
  93. package/template/workflows/specdev/common/tools/README.md +2 -2
  94. package/template/workflows/specdev/common/tools/validate-specdev.mjs +572 -30
  95. package/template/workflows/specdev/P-prototype/logic-prototype.md +0 -24
  96. package/template/workflows/specdev/P-prototype/prototype-record-template.md +0 -46
  97. package/template/workflows/specdev/P-prototype/ui-prototype.md +0 -21
  98. package/template/workflows/specdev/common/schemas/prototype-record.schema.json +0 -24
@@ -22,6 +22,8 @@ import { fileURLToPath } from "node:url";
22
22
  const DOMAIN_SCHEMA_VERSION = 3;
23
23
  const CONFIG_SCHEMA_VERSION = 5;
24
24
  const GOAL_PLAN_SCHEMA_VERSION = 6;
25
+ const IMPLEMENTATION_MAP_SCHEMA_VERSION = 1;
26
+ const IMPLEMENTATION_PLAN_SCHEMA_VERSION = 1;
25
27
  const CHANGE_STATUS_SCHEMA_VERSION = 6;
26
28
  const GLOBAL_STATUS_SCHEMA_VERSION = 5;
27
29
  const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
@@ -29,6 +31,7 @@ const STATE_PREFIX = "{roots.state}/specdev/";
29
31
  const STATE_ROOT_PREFIX = "{roots.state}/";
30
32
  const SKILLS_PREFIX = "{roots.skills}/";
31
33
  const COMMANDS_PREFIX = "{roots.commands}/";
34
+ const CHANGE_NAME = /^[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
32
35
 
33
36
  const EXPECTED_WORKS = new Set([
34
37
  "A-archive-and-consolidate",
@@ -37,6 +40,7 @@ const EXPECTED_WORKS = new Set([
37
40
  "G-grill-with-docs",
38
41
  "I-implement",
39
42
  "I-init-setup",
43
+ "O-orchestrate-implementation",
40
44
  "P-goal-plan",
41
45
  "P-prototype",
42
46
  "R-review-architecture",
@@ -104,6 +108,7 @@ const VALID_STAGES = new Set([
104
108
  "review",
105
109
  "prototype",
106
110
  "wayfinder",
111
+ "orchestrate-implementation",
107
112
  "complete",
108
113
  ]);
109
114
  const REQUIRED_TICKET_KEYS = new Set([
@@ -156,6 +161,8 @@ const STATE_ARTIFACT_BASENAMES = new Set([
156
161
  "spec.md",
157
162
  "tickets-map.md",
158
163
  "goal-plan.md",
164
+ "implementation-map.md",
165
+ "implementation-plan.md",
159
166
  "ADR.md",
160
167
  "CONTEXT.md",
161
168
  "LOG.md",
@@ -168,6 +175,7 @@ const STATE_ARTIFACT_BASENAMES = new Set([
168
175
  "architecture-review.html",
169
176
  "wayfinder-map.md",
170
177
  "design-tree.json",
178
+ "implementation-orchestration.md",
171
179
  ]);
172
180
  const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
173
181
  "source-issue.md",
@@ -465,10 +473,21 @@ function validateDocumentReferences(root) {
465
473
  const errors = [];
466
474
  const warnings = [];
467
475
  const entries = walk(root);
468
- const knownFiles = new Set(entries.filter(isFile).map((path) => basename(path)));
469
- const knownDirectories = new Set(entries.filter(isDirectory).map((path) => basename(path)));
476
+ const frozenResearchRoot = join(
477
+ root,
478
+ "P-prototype",
479
+ "design-library",
480
+ "research-snapshot",
481
+ );
482
+ const governedEntries = entries.filter(
483
+ (path) => path !== frozenResearchRoot && !path.startsWith(`${frozenResearchRoot}${sep}`),
484
+ );
485
+ const knownFiles = new Set(governedEntries.filter(isFile).map((path) => basename(path)));
486
+ const knownDirectories = new Set(governedEntries.filter(isDirectory).map((path) => basename(path)));
470
487
 
471
- for (const path of entries.filter(
488
+ // The pinned research snapshot preserves its upstream relative links and HTML
489
+ // asset paths byte-for-byte. Global template link checks still validate it.
490
+ for (const path of governedEntries.filter(
472
491
  (item) => isFile(item) && [".md", ".html"].includes(extname(item).toLowerCase()),
473
492
  )) {
474
493
  const label = toPosix(relative(root, path));
@@ -663,6 +682,8 @@ function validateExecutionContractAssets(root) {
663
682
  const configSchemaPath = join(root, "common", "schemas", "config.schema.json");
664
683
  const goalPlanSchemaPath = join(root, "common", "schemas", "goal-plan.schema.json");
665
684
  const changeSchemaPath = join(root, "common", "schemas", "change-status.schema.json");
685
+ const implementationMapSchemaPath = join(root, "common", "schemas", "implementation-map.schema.json");
686
+ const implementationPlanSchemaPath = join(root, "common", "schemas", "implementation-plan.schema.json");
666
687
 
667
688
  for (const path of [
668
689
  configTemplatePath,
@@ -670,6 +691,8 @@ function validateExecutionContractAssets(root) {
670
691
  configSchemaPath,
671
692
  goalPlanSchemaPath,
672
693
  changeSchemaPath,
694
+ implementationMapSchemaPath,
695
+ implementationPlanSchemaPath,
673
696
  ]) {
674
697
  if (!isFile(path)) errors.push(`missing execution contract asset ${toPosix(relative(root, path))}`);
675
698
  }
@@ -682,12 +705,13 @@ function validateExecutionContractAssets(root) {
682
705
  configTemplate.execution.max_implementation_agents < 1 ||
683
706
  !Number.isInteger(configTemplate.execution?.max_integration_attempts) ||
684
707
  configTemplate.execution.max_integration_attempts < 1 ||
685
- !Number.isInteger(configTemplate.planning?.ui_prototype_default_variants) ||
686
- !Number.isInteger(configTemplate.planning?.ui_prototype_max_variants) ||
687
- configTemplate.planning.ui_prototype_default_variants < 1 ||
688
- configTemplate.planning.ui_prototype_max_variants < configTemplate.planning.ui_prototype_default_variants
708
+ !Number.isInteger(configTemplate.planning?.ui_design_default_candidates) ||
709
+ !Number.isInteger(configTemplate.planning?.ui_design_max_candidates) ||
710
+ configTemplate.planning.ui_design_default_candidates < 2 ||
711
+ configTemplate.planning.ui_design_max_candidates > 4 ||
712
+ configTemplate.planning.ui_design_max_candidates < configTemplate.planning.ui_design_default_candidates
689
713
  ) {
690
- errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid prototype variant bounds");
714
+ errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid UI design candidate bounds");
691
715
  }
692
716
  for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
693
717
  if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
@@ -741,6 +765,29 @@ function validateExecutionContractAssets(root) {
741
765
  ) {
742
766
  errors.push("change-status.schema.json must define the strict Ticket integration v6 contract");
743
767
  }
768
+
769
+ const implementationMapSchema = JSON.parse(readText(implementationMapSchemaPath));
770
+ if (
771
+ implementationMapSchema.$id !== "urn:speculo:specdev:implementation-map:v1" ||
772
+ implementationMapSchema.properties?.schema_version?.const !== IMPLEMENTATION_MAP_SCHEMA_VERSION ||
773
+ implementationMapSchema.properties?.members?.minItems !== 2 ||
774
+ implementationMapSchema.properties?.tasks?.minItems !== 1 ||
775
+ implementationMapSchema.additionalProperties !== false
776
+ ) {
777
+ errors.push("implementation-map.schema.json must define the strict parent implementation graph contract v1");
778
+ }
779
+ const implementationPlanSchema = JSON.parse(readText(implementationPlanSchemaPath));
780
+ if (
781
+ implementationPlanSchema.$id !== "urn:speculo:specdev:implementation-plan:v1" ||
782
+ implementationPlanSchema.properties?.schema_version?.const !== IMPLEMENTATION_PLAN_SCHEMA_VERSION ||
783
+ implementationPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
784
+ implementationPlanSchema.properties?.implementation_agent_limit?.minimum !== 1 ||
785
+ implementationPlanSchema.properties?.integration_attempt_limit?.minimum !== 1 ||
786
+ !["current", "required"].every((value) => implementationPlanSchema.properties?.ticket_workspace_policy?.enum?.includes(value)) ||
787
+ implementationPlanSchema.additionalProperties !== false
788
+ ) {
789
+ errors.push("implementation-plan.schema.json must define the strict parent implementation projection contract v1");
790
+ }
744
791
  return errors;
745
792
  }
746
793
 
@@ -798,6 +845,13 @@ function capabilityChecks(root) {
798
845
  ],
799
846
  ],
800
847
  ],
848
+ [
849
+ "orchestrate-implementation",
850
+ [
851
+ join(root, "O-orchestrate-implementation", "O-orchestrate-implementation.md"),
852
+ ["Ready Spec", "Ready Tickets", "Implementation Map", "Implementation Plan", "lead-directed", "implementation_agent_limit", "serialization", "I-implement"],
853
+ ],
854
+ ],
801
855
  [
802
856
  "implement",
803
857
  [
@@ -816,7 +870,7 @@ function capabilityChecks(root) {
816
870
  "prototype",
817
871
  [
818
872
  join(root, "P-prototype", "P-prototype.md"),
819
- ["一个问题", "Logic", "UI", "临时 branch/worktree", "promotion target", "main"],
873
+ ["设计定向", "风格", "design-system.md", "comparison", "HTML/CSS/JS", "design-library/INDEX.md"],
820
874
  ],
821
875
  ],
822
876
  [
@@ -885,6 +939,13 @@ function capabilityChecks(root) {
885
939
  "W-wayfinder/local-tracker-contract.md",
886
940
  "W-wayfinder/solution-comment-template.md",
887
941
  "R-review-architecture/architecture-report-contract.md",
942
+ "common/rules/parent-implementation-orchestration.md",
943
+ "common/schemas/implementation-map.schema.json",
944
+ "common/schemas/implementation-plan.schema.json",
945
+ "O-orchestrate-implementation/input-readiness.md",
946
+ "O-orchestrate-implementation/implementation-map-template.md",
947
+ "O-orchestrate-implementation/implementation-plan-template.md",
948
+ "O-orchestrate-implementation/implementation-evidence-template.md",
888
949
  ]) {
889
950
  if (!isFile(join(root, required))) errors.push(`missing architecture/wayfinding contract ${required}`);
890
951
  }
@@ -1196,31 +1257,78 @@ function validateReviews(change, required, errors) {
1196
1257
 
1197
1258
  function validatePrototypes(change, required, errors) {
1198
1259
  const root = join(change, "prototypes");
1199
- const paths = isDirectory(root)
1200
- ? walk(root).filter((path) => isFile(path) && basename(path) === "record.md").sort()
1201
- : [];
1202
- if (required && !paths.length) errors.push("prototype stage requires a prototypes/<id>/record.md artifact");
1203
- for (const path of paths) {
1204
- const { meta, body } = parseFrontmatter(path);
1205
- const label = toPosix(relative(change, path));
1206
- if (meta.schema_version !== 1 || meta.artifact !== "prototype-record") {
1207
- errors.push(`${label}: artifact/schema_version must be prototype-record/1`);
1260
+ if (!isDirectory(root)) {
1261
+ if (required) errors.push("prototype stage requires a prototypes/UI-NNN/design-system.md artifact");
1262
+ return [];
1263
+ }
1264
+
1265
+ const validator = resolve(
1266
+ dirname(fileURLToPath(import.meta.url)),
1267
+ "..",
1268
+ "..",
1269
+ "P-prototype",
1270
+ "tools",
1271
+ "validate-design-package.mjs",
1272
+ );
1273
+ const paths = [];
1274
+ let readyCount = 0;
1275
+
1276
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
1277
+ const entryPath = join(root, entry.name);
1278
+ const label = toPosix(relative(change, entryPath));
1279
+ if (entry.isSymbolicLink()) {
1280
+ errors.push(`${label}: prototype design directories and files must not be symlinks`);
1281
+ continue;
1208
1282
  }
1209
- if (meta.change !== basename(change)) errors.push(`${label}: change must equal directory name`);
1210
- if (!/^PROTO-\d{3,}$/.test(String(meta.prototype_id ?? ""))) errors.push(`${label}: invalid prototype_id`);
1211
- if (!new Set(["logic", "ui"]).has(meta.branch)) errors.push(`${label}: invalid prototype branch`);
1212
- if (!new Set(["active", "answered", "blocked", "discarded"]).has(meta.status)) errors.push(`${label}: invalid prototype status`);
1213
- if (typeof meta.workspace_ref !== "string" || ABSOLUTE_MACHINE_PATH_RE.test(meta.workspace_ref)) {
1214
- errors.push(`${label}: workspace_ref must be a portable locator`);
1283
+ if (!entry.isDirectory()) {
1284
+ errors.push(`${label}: prototypes may only contain UI-NNN design directories`);
1285
+ continue;
1215
1286
  }
1216
- if (meta.status === "answered" && (!String(meta.winner ?? "").trim() || !String(meta.promotion_target ?? "").trim())) {
1217
- errors.push(`${label}: answered prototype requires winner and promotion_target`);
1287
+ if (!/^UI-\d{3,}$/.test(entry.name)) {
1288
+ errors.push(`${label}: prototype design directory must use UI-NNN`);
1289
+ continue;
1218
1290
  }
1219
- if (!new Set(["pending", "clean", "registered"]).has(meta.cleanup_status)) errors.push(`${label}: invalid cleanup_status`);
1220
- for (const heading of ["## Question and Assumption", "## Run and Assets", "## Evaluation", "## Promotion and Cleanup"]) {
1221
- if (!body.includes(heading)) errors.push(`${label}: missing '${heading}'`);
1291
+
1292
+ const designPath = join(entryPath, "design-system.md");
1293
+ if (!isFile(designPath)) {
1294
+ errors.push(`${label}: missing design-system.md`);
1295
+ continue;
1296
+ }
1297
+ paths.push(designPath);
1298
+ const { meta } = parseFrontmatter(designPath);
1299
+ if (meta.status === "ready") readyCount += 1;
1300
+ try {
1301
+ execFileSync(process.execPath, [validator, designPath], {
1302
+ encoding: "utf8",
1303
+ stdio: ["ignore", "pipe", "pipe"],
1304
+ });
1305
+ } catch (error) {
1306
+ const details = String(error.stderr || error.stdout || error.message)
1307
+ .trim()
1308
+ .split(/\r?\n/)
1309
+ .filter(Boolean);
1310
+ if (!details.length) errors.push(`${label}: design package validation failed`);
1311
+ for (const detail of details) errors.push(`${label}: ${detail}`);
1312
+ }
1313
+ }
1314
+
1315
+ const obsoleteRecords = walk(root).filter(
1316
+ (path) => isFile(path) && basename(path) === "record.md",
1317
+ );
1318
+ for (const path of obsoleteRecords) {
1319
+ errors.push(`${toPosix(relative(change, path))}: obsolete prototype record is forbidden; use UI-NNN/design-system.md`);
1320
+ }
1321
+ const discoveredDesigns = walk(root).filter(
1322
+ (path) => isFile(path) && basename(path) === "design-system.md",
1323
+ );
1324
+ for (const path of discoveredDesigns) {
1325
+ if (!paths.includes(path)) {
1326
+ errors.push(`${toPosix(relative(change, path))}: design-system.md must be directly under prototypes/UI-NNN`);
1222
1327
  }
1223
1328
  }
1329
+ if (required && readyCount === 0) {
1330
+ errors.push("prototype stage requires at least one ready UI design package");
1331
+ }
1224
1332
  return paths;
1225
1333
  }
1226
1334
 
@@ -2058,6 +2166,438 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
2058
2166
  }
2059
2167
  }
2060
2168
 
2169
+ function validateParentImplementation(change, parentStatus, stage, errors, warnings) {
2170
+ const required = stage === "orchestrate-implementation";
2171
+ const mapPath = join(change, "implementation-map.md");
2172
+ const planPath = join(change, "implementation-plan.md");
2173
+ if (!required && !isFile(mapPath) && !isFile(planPath)) return null;
2174
+ if (!isFile(mapPath)) errors.push("orchestrate-implementation stage requires implementation-map.md");
2175
+ if (!isFile(planPath)) errors.push("orchestrate-implementation stage requires implementation-plan.md");
2176
+ if (!isFile(mapPath) || !isFile(planPath)) return null;
2177
+
2178
+ const parentName = basename(change);
2179
+ const map = parseFrontmatter(mapPath);
2180
+ const plan = parseFrontmatter(planPath);
2181
+ const mapKeys = ["schema_version", "artifact", "change", "status", "revision", "members", "tasks", "dependencies", "serializations"];
2182
+ const planKeys = ["schema_version", "artifact", "change", "status", "source_map_revision", "orchestration", "lead", "implementation_agent_limit", "integration_attempt_limit", "ticket_workspace_policy", "integration_gate", "ready_for_execution"];
2183
+ for (const [label, meta, expected] of [
2184
+ ["implementation-map.md", map.meta, mapKeys],
2185
+ ["implementation-plan.md", plan.meta, planKeys],
2186
+ ]) {
2187
+ const missing = expected.filter((key) => !(key in meta));
2188
+ const unexpected = Object.keys(meta).filter((key) => !expected.includes(key));
2189
+ if (missing.length) errors.push(`${label}: missing keys ${JSON.stringify(missing)}`);
2190
+ if (unexpected.length) errors.push(`${label}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
2191
+ }
2192
+ if (map.meta.schema_version !== IMPLEMENTATION_MAP_SCHEMA_VERSION || map.meta.artifact !== "implementation-map") {
2193
+ errors.push("implementation-map.md: artifact/schema_version must be implementation-map/1");
2194
+ }
2195
+ if (plan.meta.schema_version !== IMPLEMENTATION_PLAN_SCHEMA_VERSION || plan.meta.artifact !== "implementation-plan") {
2196
+ errors.push("implementation-plan.md: artifact/schema_version must be implementation-plan/1");
2197
+ }
2198
+ if (map.meta.change !== parentName || plan.meta.change !== parentName) {
2199
+ errors.push("parent implementation artifacts must name their containing change");
2200
+ }
2201
+ const artifactStatuses = new Set(["ready", "in_progress", "blocked", "completed"]);
2202
+ if (!artifactStatuses.has(map.meta.status)) errors.push(`implementation-map.md: invalid status ${map.meta.status}`);
2203
+ if (!artifactStatuses.has(plan.meta.status)) errors.push(`implementation-plan.md: invalid status ${plan.meta.status}`);
2204
+ if (!Number.isInteger(map.meta.revision) || map.meta.revision < 1) {
2205
+ errors.push("implementation-map.md: revision must be a positive integer");
2206
+ }
2207
+ if (plan.meta.source_map_revision !== map.meta.revision) {
2208
+ errors.push("implementation-plan.md: source_map_revision must equal Implementation Map revision");
2209
+ }
2210
+ if (plan.meta.orchestration !== "lead-directed" || typeof plan.meta.lead !== "string" || !plan.meta.lead.trim()) {
2211
+ errors.push("implementation-plan.md: lead-directed orchestration requires a recoverable Lead");
2212
+ }
2213
+ for (const key of ["implementation_agent_limit", "integration_attempt_limit"]) {
2214
+ if (!Number.isInteger(plan.meta[key]) || plan.meta[key] < 1) {
2215
+ errors.push(`implementation-plan.md: ${key} must be a positive integer`);
2216
+ }
2217
+ }
2218
+ const validStrategy =
2219
+ (plan.meta.ticket_workspace_policy === "current" && plan.meta.integration_gate === "direct-parent") ||
2220
+ (plan.meta.ticket_workspace_policy === "required" && plan.meta.integration_gate === "candidate-merge");
2221
+ if (!validStrategy) {
2222
+ errors.push("implementation-plan.md: workspace/integration strategy must be current/direct-parent or required/candidate-merge");
2223
+ }
2224
+ const readyStatuses = new Set(["ready", "in_progress"]);
2225
+ if (
2226
+ typeof plan.meta.ready_for_execution !== "boolean" ||
2227
+ (plan.meta.ready_for_execution === true) !== readyStatuses.has(plan.meta.status)
2228
+ ) {
2229
+ errors.push("implementation-plan.md: ready_for_execution must match status");
2230
+ }
2231
+ for (const heading of [
2232
+ "## 1. Members and Source Authority",
2233
+ "## 2. Composite Ticket Inventory",
2234
+ "## 3. Implementation Super-DAG",
2235
+ "## 4. Conflict and Serialization",
2236
+ "## 5. Contract and Path Coverage",
2237
+ "## 6. Revision Log",
2238
+ ]) {
2239
+ if (!map.body.includes(heading)) errors.push(`implementation-map.md: missing '${heading}'`);
2240
+ }
2241
+ for (const heading of [
2242
+ "## 1. Outcome and Authority",
2243
+ "## 2. Ready Frontier and Waves",
2244
+ "## 3. Workspace and Dispatch Contract",
2245
+ "## 4. Repository Integration Queue",
2246
+ "## 5. Gates and Aggregate Verification",
2247
+ "## 6. Conflict, Drift and Recovery",
2248
+ "## 7. Progress and Decisions",
2249
+ ]) {
2250
+ if (!plan.body.includes(heading)) errors.push(`implementation-plan.md: missing '${heading}'`);
2251
+ }
2252
+
2253
+ const members = requireList(map.meta, "members", "implementation-map.md", errors).map(String);
2254
+ const tasks = requireList(map.meta, "tasks", "implementation-map.md", errors).map(String);
2255
+ const dependencies = requireList(map.meta, "dependencies", "implementation-map.md", errors).map(String);
2256
+ const serializations = requireList(map.meta, "serializations", "implementation-map.md", errors).map(String);
2257
+ if (members.length < 2) errors.push("implementation-map.md: parent implementation requires at least two members");
2258
+ if (!tasks.length) errors.push("implementation-map.md: tasks must contain the child Ticket inventory");
2259
+ for (const [key, values] of [["members", members], ["tasks", tasks], ["dependencies", dependencies], ["serializations", serializations]]) {
2260
+ if (new Set(values).size !== values.length) errors.push(`implementation-map.md: ${key} must be unique`);
2261
+ }
2262
+ if (members.includes(parentName)) errors.push("implementation-map.md: parent change cannot include itself");
2263
+
2264
+ const changesRoot = dirname(change);
2265
+ const memberSet = new Set(members);
2266
+ const memberStatuses = new Map();
2267
+ const ticketByTask = new Map();
2268
+ const expectedTasks = new Set();
2269
+ const expectedInternalEdges = new Set();
2270
+ let activeImplementations = 0;
2271
+ let activeCurrentWriters = 0;
2272
+ const integratingByRef = new Map();
2273
+
2274
+ for (const member of members) {
2275
+ if (!CHANGE_NAME.test(member)) {
2276
+ errors.push(`implementation-map.md: invalid member change name ${member}`);
2277
+ continue;
2278
+ }
2279
+ const memberRoot = join(changesRoot, member);
2280
+ if (!isDirectory(memberRoot)) {
2281
+ errors.push(`implementation-map.md: member change does not exist: ${member}`);
2282
+ continue;
2283
+ }
2284
+ if (isFile(join(memberRoot, "implementation-map.md"))) {
2285
+ errors.push(`implementation-map.md: nested parent implementation is not supported in v1: ${member}`);
2286
+ }
2287
+
2288
+ const memberErrors = [];
2289
+ const memberWarnings = [];
2290
+ const status = validateChangeStatus(join(memberRoot, ".status.json"), member, memberErrors);
2291
+ if (status) {
2292
+ memberStatuses.set(member, status);
2293
+ if (status.change_status === "archived") memberErrors.push("archived change cannot be an implementation member");
2294
+ if (status.current_work !== null && status.current_work !== "specdev/implement") {
2295
+ memberErrors.push(`current_work=${status.current_work} conflicts with parent implementation ownership`);
2296
+ }
2297
+ for (const worktree of Array.isArray(status.worktrees) ? status.worktrees : []) {
2298
+ if (worktree?.status === "active") {
2299
+ activeImplementations += 1;
2300
+ if (worktree.workspace_ref === "current") activeCurrentWriters += 1;
2301
+ if (plan.meta.ticket_workspace_policy === "current" && worktree.workspace_ref !== "current") {
2302
+ memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent current policy`);
2303
+ }
2304
+ if (plan.meta.ticket_workspace_policy === "required" && worktree.workspace_ref === "current") {
2305
+ memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent required policy`);
2306
+ }
2307
+ }
2308
+ if (worktree?.status === "integrating") {
2309
+ const key = String(worktree.parent_branch ?? "<unknown-ref>");
2310
+ integratingByRef.set(key, (integratingByRef.get(key) ?? 0) + 1);
2311
+ }
2312
+ if (Number.isInteger(worktree?.integration?.attempts) && worktree.integration.attempts > plan.meta.integration_attempt_limit) {
2313
+ memberErrors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration.attempts} exceed parent limit ${plan.meta.integration_attempt_limit}`);
2314
+ }
2315
+ }
2316
+ }
2317
+
2318
+ const specPath = join(memberRoot, "spec.md");
2319
+ const childSpec = isFile(specPath) ? validateSpec(specPath, memberErrors, memberWarnings) : null;
2320
+ if (!childSpec) memberErrors.push("Ready Spec is required before parent creation");
2321
+ else {
2322
+ if (childSpec.meta.change !== member) memberErrors.push("spec.md: change must equal member directory name");
2323
+ if (childSpec.meta.status !== "ready" || childSpec.meta.ready_for_tickets !== true) {
2324
+ memberErrors.push("Spec must have status=ready and ready_for_tickets=true");
2325
+ }
2326
+ }
2327
+
2328
+ const childMapPath = join(memberRoot, "tickets-map.md");
2329
+ const childMap = isFile(childMapPath) ? validateMap(childMapPath, memberErrors) : null;
2330
+ if (!childMap) memberErrors.push("Ready Tickets Map is required before parent creation");
2331
+ else {
2332
+ if (childMap.meta.change !== member) memberErrors.push("tickets-map.md: change must equal member directory name");
2333
+ if (!new Set(["ready", "in_progress", "completed"]).has(childMap.meta.status)) {
2334
+ memberErrors.push("Tickets Map status must be ready, in_progress, or completed");
2335
+ }
2336
+ }
2337
+
2338
+ const ticketRoot = join(memberRoot, "ticket");
2339
+ const ticketFiles = isDirectory(ticketRoot)
2340
+ ? readdirSync(ticketRoot).filter((name) => name.endsWith(".md")).sort()
2341
+ : [];
2342
+ if (!ticketFiles.length) memberErrors.push("Ready Tickets are required before parent creation");
2343
+ const childTickets = new Map();
2344
+ for (const name of ticketFiles) {
2345
+ const artifact = validateTicket(join(ticketRoot, name), memberErrors);
2346
+ if (!artifact) continue;
2347
+ const ticketId = String(artifact.meta.id);
2348
+ if (artifact.meta.change !== member) memberErrors.push(`${name}: change must equal member directory name`);
2349
+ if (childTickets.has(ticketId)) memberErrors.push(`duplicate Ticket id ${ticketId}`);
2350
+ childTickets.set(ticketId, artifact);
2351
+ const numericId = ticketId.replace(/^T-/, "");
2352
+ if (!name.startsWith(`${numericId}-`)) memberErrors.push(`${name}: filename prefix must match ${ticketId}`);
2353
+ if (childMap && !childMap.body.includes(ticketId)) memberErrors.push(`${name}: Ticket id is absent from Tickets Map`);
2354
+ if (childSpec) {
2355
+ for (const contractId of artifact.meta.contract_ids ?? []) {
2356
+ if (!childSpec.body.includes(String(contractId))) memberErrors.push(`${name}: contract ${contractId} not found in Spec`);
2357
+ }
2358
+ }
2359
+
2360
+ const taskId = `${member}::${ticketId}`;
2361
+ expectedTasks.add(taskId);
2362
+ ticketByTask.set(taskId, { artifact, member, memberRoot, status });
2363
+ const terminal = new Set(["done", "cancelled"]).has(artifact.meta.status);
2364
+ if (!terminal) {
2365
+ if (map.meta.status === "ready" && (artifact.meta.status !== "ready" || artifact.meta.ready !== true)) {
2366
+ memberErrors.push(`${ticketId}: parent creation requires status=ready and ready=true`);
2367
+ } else if (new Set(["ready", "in_progress", "review"]).has(artifact.meta.status) && artifact.meta.ready !== true) {
2368
+ memberErrors.push(`${ticketId}: executable Ticket must keep ready=true`);
2369
+ } else if (new Set(["blocked", "deviated"]).has(artifact.meta.status) && map.meta.status !== "blocked" && plan.meta.status !== "blocked") {
2370
+ memberErrors.push(`${ticketId}: blocked/deviated Ticket requires blocked parent artifacts`);
2371
+ } else if (artifact.meta.status === "draft") {
2372
+ memberErrors.push(`${ticketId}: draft Ticket cannot belong to a parent implementation`);
2373
+ }
2374
+ }
2375
+ }
2376
+
2377
+ for (const [ticketId, artifact] of childTickets) {
2378
+ for (const dependency of (artifact.meta.blocked_by ?? []).map(String)) {
2379
+ if (!childTickets.has(dependency)) memberErrors.push(`${ticketId}: blocked_by references missing ${dependency}`);
2380
+ expectedInternalEdges.add(`${member}::${ticketId} <- ${member}::${dependency}`);
2381
+ }
2382
+ }
2383
+ const childGraph = new Map([...childTickets].map(([ticketId, artifact]) => [ticketId, (artifact.meta.blocked_by ?? []).map(String)]));
2384
+ const childCycle = findCycle(childGraph);
2385
+ if (childCycle) memberErrors.push(`Ticket dependency cycle: ${childCycle.join(" -> ")}`);
2386
+
2387
+ if (childSpec) {
2388
+ const declared = new Set(childSpec.body.match(/\bAC-\d+\b/g) ?? []);
2389
+ const covered = new Set([...childTickets.values()].flatMap((artifact) => (artifact.meta.contract_ids ?? []).map(String)));
2390
+ const uncovered = [...declared].filter((id) => !covered.has(id) && !(childMap && new RegExp(`${escapeRegExp(id)}.*\\bdeferred\\b`, "i").test(childMap.body)));
2391
+ if (uncovered.length) memberErrors.push(`Spec acceptance contracts are not covered by Tickets: ${JSON.stringify(uncovered.sort())}`);
2392
+ }
2393
+ if (status?.change_status === "completed") {
2394
+ const unfinished = [...childTickets].filter(([, artifact]) => !new Set(["done", "cancelled"]).has(artifact.meta.status)).map(([id]) => id);
2395
+ if (unfinished.length) memberErrors.push(`completed member has unfinished Tickets: ${JSON.stringify(unfinished)}`);
2396
+ }
2397
+
2398
+ const goalPath = join(memberRoot, "goal-plan.md");
2399
+ if (isFile(goalPath)) {
2400
+ const goal = validateGoalPlan(goalPath, memberErrors);
2401
+ if (goal && (
2402
+ goal.meta.ticket_workspace_policy !== plan.meta.ticket_workspace_policy ||
2403
+ goal.meta.integration_gate !== plan.meta.integration_gate
2404
+ )) {
2405
+ memberErrors.push("Goal Plan workspace/integration strategy conflicts with parent Implementation Plan");
2406
+ }
2407
+ }
2408
+ errors.push(...memberErrors.map((message) => `${member}: ${message}`));
2409
+ warnings.push(...memberWarnings.map((message) => `${member}: ${message}`));
2410
+ }
2411
+
2412
+ const taskSet = new Set(tasks);
2413
+ for (const task of tasks) {
2414
+ if (!/^\d{4}-\d{2}-\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*::T-\d{2,}$/.test(task)) {
2415
+ errors.push(`implementation-map.md: invalid composite task '${task}'`);
2416
+ }
2417
+ }
2418
+ const missingTasks = [...expectedTasks].filter((task) => !taskSet.has(task)).sort();
2419
+ const extraTasks = tasks.filter((task) => !expectedTasks.has(task)).sort();
2420
+ if (missingTasks.length || extraTasks.length) {
2421
+ errors.push(`implementation-map.md: tasks must exactly match child Tickets; missing=${JSON.stringify(missingTasks)} extra=${JSON.stringify(extraTasks)}`);
2422
+ }
2423
+
2424
+ const graph = new Map(tasks.map((task) => [task, []]));
2425
+ const dependencySet = new Set(dependencies);
2426
+ for (const edge of dependencies) {
2427
+ const match = /^([^ ]+::T-\d{2,}) <- ([^ ]+::T-\d{2,})$/.exec(edge);
2428
+ if (!match) {
2429
+ errors.push(`implementation-map.md: invalid dependency '${edge}'`);
2430
+ continue;
2431
+ }
2432
+ const [, dependent, prerequisite] = match;
2433
+ if (!taskSet.has(dependent) || !taskSet.has(prerequisite)) {
2434
+ errors.push(`implementation-map.md: dependency endpoints must be composite tasks: ${edge}`);
2435
+ } else if (dependent === prerequisite) {
2436
+ errors.push(`implementation-map.md: dependency cannot be self-referential: ${edge}`);
2437
+ } else {
2438
+ graph.get(dependent).push(prerequisite);
2439
+ const dependentMember = dependent.split("::")[0];
2440
+ const prerequisiteMember = prerequisite.split("::")[0];
2441
+ if (dependentMember === prerequisiteMember && !expectedInternalEdges.has(edge)) {
2442
+ errors.push(`implementation-map.md: same-member dependency must come from child blocked_by: ${edge}`);
2443
+ }
2444
+ }
2445
+ }
2446
+ const missingInternalEdges = [...expectedInternalEdges].filter((edge) => !dependencySet.has(edge)).sort();
2447
+ if (missingInternalEdges.length) {
2448
+ errors.push(`implementation-map.md: missing child Ticket dependencies ${JSON.stringify(missingInternalEdges)}`);
2449
+ }
2450
+ const cycle = findCycle(graph);
2451
+ if (cycle) errors.push(`implementation super-DAG cycle: ${cycle.join(" -> ")}`);
2452
+
2453
+ const serializationPairs = new Set();
2454
+ for (const edge of serializations) {
2455
+ const match = /^([^ ]+::T-\d{2,}) <> ([^ ]+::T-\d{2,})$/.exec(edge);
2456
+ if (!match) {
2457
+ errors.push(`implementation-map.md: invalid serialization '${edge}'`);
2458
+ continue;
2459
+ }
2460
+ const [, left, right] = match;
2461
+ if (!taskSet.has(left) || !taskSet.has(right)) {
2462
+ errors.push(`implementation-map.md: serialization endpoints must be composite tasks: ${edge}`);
2463
+ continue;
2464
+ }
2465
+ if (left === right) {
2466
+ errors.push(`implementation-map.md: serialization cannot be self-referential: ${edge}`);
2467
+ continue;
2468
+ }
2469
+ const key = [left, right].sort().join("\0");
2470
+ if (serializationPairs.has(key)) errors.push(`implementation-map.md: duplicate unordered serialization pair: ${edge}`);
2471
+ serializationPairs.add(key);
2472
+ }
2473
+
2474
+ const unfinishedTasks = [...ticketByTask]
2475
+ .filter(([, value]) => !new Set(["done", "cancelled"]).has(value.artifact.meta.status))
2476
+ .map(([task]) => task);
2477
+ for (let index = 0; index < unfinishedTasks.length; index += 1) {
2478
+ const leftId = unfinishedTasks[index];
2479
+ const left = ticketByTask.get(leftId).artifact;
2480
+ for (const rightId of unfinishedTasks.slice(index + 1)) {
2481
+ if (leftId.split("::")[0] === rightId.split("::")[0]) continue;
2482
+ if (
2483
+ transitivelyDepends(graph, leftId, rightId) ||
2484
+ transitivelyDepends(graph, rightId, leftId) ||
2485
+ serializationPairs.has([leftId, rightId].sort().join("\0"))
2486
+ ) continue;
2487
+ const right = ticketByTask.get(rightId).artifact;
2488
+ const overlaps = [];
2489
+ for (const leftPath of left.meta.writable_paths ?? []) {
2490
+ for (const rightPath of right.meta.writable_paths ?? []) {
2491
+ if (pathsOverlap(String(leftPath), String(rightPath))) overlaps.push([String(leftPath), String(rightPath)]);
2492
+ }
2493
+ }
2494
+ if (overlaps.length) {
2495
+ errors.push(`composite tasks ${leftId}/${rightId} have writable overlap without dependency or serialization: ${JSON.stringify(overlaps.slice(0, 3))}`);
2496
+ }
2497
+ }
2498
+ }
2499
+
2500
+ for (const entry of readdirSync(changesRoot, { withFileTypes: true })) {
2501
+ if (!entry.isDirectory() || entry.name === parentName) continue;
2502
+ const otherMapPath = join(changesRoot, entry.name, "implementation-map.md");
2503
+ if (!isFile(otherMapPath)) continue;
2504
+ const otherMap = parseFrontmatter(otherMapPath).meta;
2505
+ if (!Array.isArray(otherMap.members)) continue;
2506
+ let otherStatus = null;
2507
+ try {
2508
+ otherStatus = JSON.parse(readText(join(changesRoot, entry.name, ".status.json"))).change_status;
2509
+ } catch {
2510
+ // Invalid competing parents still own their members conservatively.
2511
+ }
2512
+ if (new Set(["completed", "archived"]).has(otherStatus)) continue;
2513
+ const overlap = otherMap.members.map(String).filter((member) => memberSet.has(member));
2514
+ if (overlap.length) errors.push(`member changes already belong to unfinished parent implementation ${entry.name}: ${JSON.stringify(overlap)}`);
2515
+ }
2516
+
2517
+ const config = findSpecdevConfig(change);
2518
+ const configuredAgents = positiveConfigLimit(config, "max_implementation_agents", 0);
2519
+ const configuredAttempts = positiveConfigLimit(config, "max_integration_attempts", 0);
2520
+ if (!config || config.schema_version !== CONFIG_SCHEMA_VERSION || configuredAgents === 0 || configuredAttempts === 0) {
2521
+ errors.push("implementation-plan.md: SpecDev config v5 with positive execution limits is required");
2522
+ } else {
2523
+ if (plan.meta.implementation_agent_limit > configuredAgents) {
2524
+ errors.push(`implementation-plan.md: implementation_agent_limit ${plan.meta.implementation_agent_limit} exceeds config max_implementation_agents ${configuredAgents}`);
2525
+ }
2526
+ if (plan.meta.integration_attempt_limit > configuredAttempts) {
2527
+ errors.push(`implementation-plan.md: integration_attempt_limit ${plan.meta.integration_attempt_limit} exceeds config max_integration_attempts ${configuredAttempts}`);
2528
+ }
2529
+ }
2530
+ if (activeImplementations > plan.meta.implementation_agent_limit) {
2531
+ errors.push(`parent implementation agent limit exceeded: ${activeImplementations} active for limit ${plan.meta.implementation_agent_limit}`);
2532
+ }
2533
+ if (plan.meta.ticket_workspace_policy === "current" && activeCurrentWriters > 1) {
2534
+ errors.push(`parent current policy permits only one active implementation writer; found ${activeCurrentWriters}`);
2535
+ }
2536
+ for (const [ref, count] of integratingByRef) {
2537
+ if (count > 1) errors.push(`repository/ref integration must be serialized for ${ref}; found ${count}`);
2538
+ }
2539
+
2540
+ if (required && parentStatus && new Set(["active", "blocked"]).has(parentStatus.change_status) && parentStatus.current_work !== "specdev/orchestrate-implementation") {
2541
+ errors.push("parent active/blocked status must keep current_work=specdev/orchestrate-implementation");
2542
+ }
2543
+ if (parentStatus?.change_status === "completed") {
2544
+ const incomplete = members.filter((member) => memberStatuses.get(member)?.change_status !== "completed");
2545
+ if (incomplete.length) errors.push(`parent implementation is completed while members remain incomplete: ${JSON.stringify(incomplete)}`);
2546
+ const unfinished = [...ticketByTask].filter(([, value]) => !new Set(["done", "cancelled"]).has(value.artifact.meta.status)).map(([task]) => task);
2547
+ if (unfinished.length) errors.push(`completed parent implementation has unfinished composite tasks: ${JSON.stringify(unfinished)}`);
2548
+ if (map.meta.status !== "completed" || plan.meta.status !== "completed") {
2549
+ errors.push("completed parent requires completed Implementation Map and Implementation Plan");
2550
+ }
2551
+ if (
2552
+ parentStatus.current_work !== null ||
2553
+ !Array.isArray(parentStatus.blockers) || parentStatus.blockers.length > 0 ||
2554
+ !Array.isArray(parentStatus.deviations) || parentStatus.deviations.length > 0
2555
+ ) {
2556
+ errors.push("completed parent requires null current_work and no blockers or deviations");
2557
+ }
2558
+ const activeMemberWork = [...memberStatuses]
2559
+ .flatMap(([member, status]) => (status.worktrees ?? []).map((worktree) => ({ member, worktree })))
2560
+ .filter(({ worktree }) => new Set(["planned", "active", "review", "integrating", "blocked"]).has(worktree?.status) || worktree?.integration?.status === "candidate")
2561
+ .map(({ member, worktree }) => `${member}::${worktree.ticket_id}`);
2562
+ if (activeMemberWork.length) {
2563
+ errors.push(`completed parent has active member worktrees or candidates: ${JSON.stringify(activeMemberWork)}`);
2564
+ }
2565
+ for (const [task, value] of ticketByTask) {
2566
+ if (value.artifact.meta.status !== "done") continue;
2567
+ if (!isFile(join(value.memberRoot, "evidence", `${value.artifact.meta.id}.md`))) {
2568
+ errors.push(`${task}: completed parent requires child Ticket Evidence`);
2569
+ }
2570
+ const worktree = value.status?.worktrees?.find((entry) => entry?.ticket_id === value.artifact.meta.id);
2571
+ if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
2572
+ errors.push(`${task}: completed parent requires integrated or removed child workspace record`);
2573
+ }
2574
+ }
2575
+ const evidencePath = join(change, "evidence", "implementation-orchestration.md");
2576
+ if (!isFile(evidencePath)) {
2577
+ errors.push("completed parent requires evidence/implementation-orchestration.md");
2578
+ } else {
2579
+ const evidence = readText(evidencePath);
2580
+ for (const heading of [
2581
+ "## 1. Parent Plan and Final Revision",
2582
+ "## 2. Member and Ticket Completion",
2583
+ "## 3. Dependency and Serialization Audit",
2584
+ "## 4. Repository Integration Audit",
2585
+ "## 5. Aggregate Verification",
2586
+ "## 6. Contract, Drift and Deviation Audit",
2587
+ "## 7. Residual Risk and Boundary",
2588
+ ]) {
2589
+ if (!evidence.includes(heading)) errors.push(`evidence/implementation-orchestration.md: missing '${heading}'`);
2590
+ }
2591
+ }
2592
+ } else if (map.meta.status === "completed" || plan.meta.status === "completed") {
2593
+ errors.push("completed Implementation Map/Plan requires completed parent change status");
2594
+ }
2595
+ if (plan.meta.ready_for_execution === true && !new Set(["ready", "in_progress"]).has(map.meta.status)) {
2596
+ errors.push("ready Implementation Plan requires a ready or in_progress Implementation Map");
2597
+ }
2598
+ return { map, plan, members, tasks, memberStatuses, ticketByTask };
2599
+ }
2600
+
2061
2601
  function validateChange(change, stage = null, repoRoot = null) {
2062
2602
  const errors = [];
2063
2603
  const warnings = [];
@@ -2066,6 +2606,7 @@ function validateChange(change, stage = null, repoRoot = null) {
2066
2606
  }
2067
2607
 
2068
2608
  const changeStatus = validateChangeStatus(join(change, ".status.json"), basename(change), errors);
2609
+ validateParentImplementation(change, changeStatus, stage, errors, warnings);
2069
2610
  if (isFile(join(change, "source-issue.md"))) {
2070
2611
  errors.push("obsolete source-issue.md is forbidden; use source.md without compatibility fallback");
2071
2612
  }
@@ -2085,7 +2626,7 @@ function validateChange(change, stage = null, repoRoot = null) {
2085
2626
  validateReviews(change, stage === "review", errors);
2086
2627
  validatePrototypes(change, stage === "prototype", errors);
2087
2628
 
2088
- const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage);
2629
+ const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage) && !isFile(join(change, "implementation-map.md"));
2089
2630
  const specPath = join(change, "spec.md");
2090
2631
  const spec = isFile(specPath) || specRequired
2091
2632
  ? validateSpec(specPath, errors, warnings)
@@ -2341,6 +2882,7 @@ function validateChange(change, stage = null, repoRoot = null) {
2341
2882
  if (
2342
2883
  stage === "complete" &&
2343
2884
  !ticketFiles.length &&
2885
+ !isFile(join(change, "implementation-map.md")) &&
2344
2886
  !isFile(join(change, "evidence", "direct-spec.md"))
2345
2887
  ) {
2346
2888
  errors.push("complete stage without Tickets requires evidence/direct-spec.md");