@namewta/speculo 0.7.1 → 0.7.3

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 (57) hide show
  1. package/README.md +2 -1
  2. package/dist/src/migrations.js +604 -23
  3. package/dist/src/migrations.js.map +1 -1
  4. package/package.json +1 -1
  5. package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +171 -277
  6. package/template/canonical/canonical-specdev-goal-plan.md +714 -833
  7. package/template/canonical/canonical-specdev-grill-with-docs.md +172 -278
  8. package/template/canonical/canonical-specdev-spec.md +199 -315
  9. package/template/canonical/canonical-specdev-tickets.md +405 -398
  10. package/template/canonical/canonical-specdev-wayfinder.md +170 -276
  11. package/template/skills/migrate-runtime-state/SKILL.md +6 -6
  12. package/template/skills/migrate-runtime-state/references/migration-contract.md +9 -3
  13. package/template/skills/migrate-runtime-state/scripts/migrate-runtime-state.mjs +322 -33
  14. package/template/skills/optimize-codex-config/SKILL.md +81 -0
  15. package/template/skills/optimize-codex-config/references/configuration-contract.md +103 -0
  16. package/template/skills/optimize-codex-config/references/troubleshooting.md +79 -0
  17. package/template/skills/optimize-codex-config/scripts/audit-codex-config.mjs +747 -0
  18. package/template/workflows/specdev/I-implement/I-implement.md +97 -142
  19. package/template/workflows/specdev/I-implement/evidence-template.md +60 -48
  20. package/template/workflows/specdev/I-implement/execution-preflight.md +29 -19
  21. package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +12 -11
  22. package/template/workflows/specdev/I-init-setup/I-init-setup.md +4 -5
  23. package/template/workflows/specdev/I-init-setup/change-status-template.json +14 -1
  24. package/template/workflows/specdev/I-init-setup/config-template.json +3 -5
  25. package/template/workflows/specdev/I-init-setup/status-template.json +1 -1
  26. package/template/workflows/specdev/INDEX.md +12 -8
  27. package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +76 -93
  28. package/template/workflows/specdev/P-goal-plan/completion-control.md +26 -44
  29. package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +43 -31
  30. package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +34 -0
  31. package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +31 -42
  32. package/template/workflows/specdev/P-goal-plan/planning-modes.md +42 -61
  33. package/template/workflows/specdev/T-tickets/T-tickets.md +6 -3
  34. package/template/workflows/specdev/T-tickets/ticket-readiness.md +5 -3
  35. package/template/workflows/specdev/T-tickets/ticket-template.md +8 -1
  36. package/template/workflows/specdev/T-tickets/tickets-map-template.md +5 -4
  37. package/template/workflows/specdev/_state/status.json +1 -1
  38. package/template/workflows/specdev/common/README.md +2 -2
  39. package/template/workflows/specdev/common/rules/change-completion.md +17 -19
  40. package/template/workflows/specdev/common/rules/deviation-control.md +1 -1
  41. package/template/workflows/specdev/common/rules/evidence-and-verification.md +27 -37
  42. package/template/workflows/specdev/common/rules/path-ownership.md +21 -23
  43. package/template/workflows/specdev/common/rules/readiness-and-depth.md +1 -1
  44. package/template/workflows/specdev/common/schemas/change-status.schema.json +136 -195
  45. package/template/workflows/specdev/common/schemas/config.schema.json +9 -11
  46. package/template/workflows/specdev/common/schemas/goal-plan.schema.json +24 -6
  47. package/template/workflows/specdev/common/schemas/status.schema.json +7 -63
  48. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +42 -21
  49. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +48 -18
  50. package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +47 -12
  51. package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +34 -30
  52. package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +10 -23
  53. package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +14 -25
  54. package/template/workflows/specdev/common/tools/validate-specdev.mjs +507 -117
  55. package/template/workflows/specdev/I-implement/delegated-evidence-template.md +0 -11
  56. package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +0 -33
  57. package/template/workflows/specdev/P-goal-plan/delegated-execution.md +0 -53
@@ -18,10 +18,14 @@ import {
18
18
  import { dirname, basename, extname, join, relative, resolve, sep } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
 
21
- const SCHEMA_VERSION = 3;
22
- const GLOBAL_STATUS_SCHEMA_VERSION = 4;
21
+ const DOMAIN_SCHEMA_VERSION = 3;
22
+ const CONFIG_SCHEMA_VERSION = 4;
23
+ const GOAL_PLAN_SCHEMA_VERSION = 5;
24
+ const CHANGE_STATUS_SCHEMA_VERSION = 5;
25
+ const GLOBAL_STATUS_SCHEMA_VERSION = 5;
23
26
  const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
24
27
  const STATE_PREFIX = "{roots.state}/specdev/";
28
+ const STATE_ROOT_PREFIX = "{roots.state}/";
25
29
  const SKILLS_PREFIX = "{roots.skills}/";
26
30
  const COMMANDS_PREFIX = "{roots.commands}/";
27
31
 
@@ -55,12 +59,24 @@ const VALID_TICKET_STATUS = new Set([
55
59
  const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
56
60
  const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
57
61
  const VALID_PLAN_MODES = new Set([
58
- "coordination",
59
62
  "migration",
60
63
  "high-assurance",
61
64
  "reference-conformance",
62
65
  "release-coordination",
63
66
  ]);
67
+ const VALID_WORKTREE_STATUS = new Set([
68
+ "planned",
69
+ "active",
70
+ "review",
71
+ "integrating",
72
+ "integrated",
73
+ "removed",
74
+ "blocked",
75
+ ]);
76
+ const VALID_INTEGRATION_STATUS = new Set(["pending", "candidate", "passed", "failed", "stale"]);
77
+ const VALID_INTEGRATION_METHOD = new Set([null, "fast-forward", "merge-commit"]);
78
+ const VALID_INTEGRATION_VERIFICATION = new Set(["pending", "passed", "failed"]);
79
+ const VALID_E2E_STATUS = new Set(["not-required", "pending", "passed", "failed"]);
64
80
  const VALID_DESIGN_TREE_STATUS = new Set(["active", "consensus", "blocked"]);
65
81
  const VALID_DESIGN_NODE_STATUS = new Set(["open", "answered", "deferred", "rejected"]);
66
82
  const VALID_WAYFINDER_LABEL = new Set([
@@ -127,30 +143,16 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
127
143
  "## 5. Constraints, Risk and Recovery",
128
144
  "## 6. Progress and Decisions",
129
145
  ]);
130
- const DELEGATED_GOAL_PLAN_TRIGGERS = [
131
- "## Delegated Execution Addendum",
132
- "### Delivery Contract",
133
- "### Per-Ticket Dispatch Packets",
134
- "### Candidate Delivery Return and Lead Integration",
135
- "Lead / Provider",
136
- "Lead:",
137
- "Lead:",
138
- "Worker:",
139
- "Worker:",
140
- "Subagent",
141
- "subagent",
142
- "execution_model",
143
- "execution model: direct",
144
- "max_correction_rounds",
145
- ];
146
- const REQUIRED_DELEGATED_GOAL_PLAN_MARKERS = [
147
- "## Delegated Execution Addendum",
148
- "### Delivery Contract",
149
- "### Per-Ticket Dispatch Packets",
150
- "### Candidate Delivery Return and Lead Integration",
151
- "native-subagent / external-web-subagent",
152
- "Lead / Provider",
153
- "#### Dispatch:",
146
+ const REQUIRED_LEAD_GOAL_PLAN_MARKERS = [
147
+ "### Lead Orchestration",
148
+ "Implementation subagents",
149
+ "Read-only agents",
150
+ "execution-time dynamic",
151
+ "### Ticket Workspace and Candidate Integration",
152
+ "source worktree 不运行 E2E",
153
+ "### Authorization Matrix",
154
+ "Implementation commit",
155
+ "Local candidate integration and parent update",
154
156
  ];
155
157
  const STATE_ARTIFACT_BASENAMES = new Set([
156
158
  "spec.md",
@@ -171,6 +173,10 @@ const STATE_ARTIFACT_BASENAMES = new Set([
171
173
  ]);
172
174
  const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
173
175
  "source-issue.md",
176
+ "delegated-execution.md",
177
+ "delegated-execution-template.md",
178
+ "workspace-execution-template.md",
179
+ "delegated-evidence-template.md",
174
180
  "input-validation.md",
175
181
  "vision-sections.md",
176
182
  "execution-sections.md",
@@ -391,6 +397,19 @@ function validatePathValue(value, { allowProject = true } = {}) {
391
397
  }
392
398
  return null;
393
399
  }
400
+ if (value.startsWith(STATE_ROOT_PREFIX)) {
401
+ const suffix = value.slice(STATE_ROOT_PREFIX.length);
402
+ if (
403
+ !suffix ||
404
+ suffix.startsWith("/") ||
405
+ suffix.startsWith("./") ||
406
+ suffix.startsWith("../") ||
407
+ suffix.includes("/../")
408
+ ) {
409
+ return "state-root Path is not canonical";
410
+ }
411
+ return null;
412
+ }
394
413
  for (const [prefix, name] of [[SKILLS_PREFIX, "skills"], [COMMANDS_PREFIX, "commands"]]) {
395
414
  if (value.startsWith(prefix)) {
396
415
  const suffix = value.slice(prefix.length);
@@ -592,7 +611,7 @@ function validateGlobalStatusAssets(root) {
592
611
  !Array.isArray(data.active) ||
593
612
  !Array.isArray(data.archived)
594
613
  ) {
595
- errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status v4 seed`);
614
+ errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status v5 seed`);
596
615
  }
597
616
  }
598
617
 
@@ -603,11 +622,11 @@ function validateGlobalStatusAssets(root) {
603
622
  }
604
623
  const schema = JSON.parse(readText(schemaPath));
605
624
  if (
606
- schema.$id !== "urn:speculo:specdev:status:v4" ||
625
+ schema.$id !== "urn:speculo:specdev:status:v5" ||
607
626
  schema.properties?.schema_version?.const !== GLOBAL_STATUS_SCHEMA_VERSION ||
608
627
  schema.additionalProperties !== false
609
628
  ) {
610
- errors.push("status.schema.json must define the strict SpecDev global status v4 contract");
629
+ errors.push("status.schema.json must define the strict SpecDev global status v5 index contract");
611
630
  }
612
631
  for (const removed of ["work_history", "completed", "result"]) {
613
632
  if (JSON.stringify(schema).includes(`\"${removed}\"`)) {
@@ -617,6 +636,88 @@ function validateGlobalStatusAssets(root) {
617
636
  return errors;
618
637
  }
619
638
 
639
+ function validateExecutionContractAssets(root) {
640
+ const errors = [];
641
+ const configTemplatePath = join(root, "I-init-setup", "config-template.json");
642
+ const changeTemplatePath = join(root, "I-init-setup", "change-status-template.json");
643
+ const configSchemaPath = join(root, "common", "schemas", "config.schema.json");
644
+ const goalPlanSchemaPath = join(root, "common", "schemas", "goal-plan.schema.json");
645
+ const changeSchemaPath = join(root, "common", "schemas", "change-status.schema.json");
646
+
647
+ for (const path of [
648
+ configTemplatePath,
649
+ changeTemplatePath,
650
+ configSchemaPath,
651
+ goalPlanSchemaPath,
652
+ changeSchemaPath,
653
+ ]) {
654
+ if (!isFile(path)) errors.push(`missing execution contract asset ${toPosix(relative(root, path))}`);
655
+ }
656
+ if (errors.length) return errors;
657
+
658
+ const configTemplate = JSON.parse(readText(configTemplatePath));
659
+ if (
660
+ configTemplate.schema_version !== CONFIG_SCHEMA_VERSION ||
661
+ !Number.isInteger(configTemplate.execution?.max_implementation_agents) ||
662
+ configTemplate.execution.max_implementation_agents < 1 ||
663
+ configTemplate.execution.max_implementation_agents > 3
664
+ ) {
665
+ errors.push("config-template.json must define SpecDev config v4 with max_implementation_agents from 1 to 3");
666
+ }
667
+ for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
668
+ if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
669
+ errors.push(`config-template.json still contains obsolete execution field ${obsolete}`);
670
+ }
671
+ }
672
+
673
+ const configSchema = JSON.parse(readText(configSchemaPath));
674
+ const limit = configSchema.properties?.execution?.properties?.max_implementation_agents;
675
+ if (
676
+ configSchema.$id !== "urn:speculo:specdev:config:v4" ||
677
+ configSchema.properties?.schema_version?.const !== CONFIG_SCHEMA_VERSION ||
678
+ limit?.minimum !== 1 ||
679
+ limit?.maximum !== 3 ||
680
+ configSchema.additionalProperties !== false
681
+ ) {
682
+ errors.push("config.schema.json must define the strict SpecDev config v4 execution contract");
683
+ }
684
+
685
+ const goalPlanSchema = JSON.parse(readText(goalPlanSchemaPath));
686
+ if (
687
+ goalPlanSchema.$id !== "urn:speculo:specdev:goal-plan:v5" ||
688
+ goalPlanSchema.properties?.schema_version?.const !== GOAL_PLAN_SCHEMA_VERSION ||
689
+ goalPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
690
+ goalPlanSchema.properties?.implementation_agent_limit?.maximum !== 3 ||
691
+ goalPlanSchema.properties?.ticket_workspace_policy?.const !== "required" ||
692
+ goalPlanSchema.properties?.integration_gate?.const !== "candidate-merge" ||
693
+ goalPlanSchema.additionalProperties !== false
694
+ ) {
695
+ errors.push("goal-plan.schema.json must define the strict Lead-directed Goal Plan v5 contract");
696
+ }
697
+
698
+ const changeTemplate = JSON.parse(readText(changeTemplatePath));
699
+ if (
700
+ changeTemplate.schema_version !== CHANGE_STATUS_SCHEMA_VERSION ||
701
+ changeTemplate.artifact !== "change-status" ||
702
+ !Array.isArray(changeTemplate.worktrees)
703
+ ) {
704
+ errors.push("change-status-template.json must define the SpecDev change-status v4 seed");
705
+ }
706
+
707
+ const changeSchema = JSON.parse(readText(changeSchemaPath));
708
+ const worktreeRequired = new Set(changeSchema.$defs?.worktree?.required ?? []);
709
+ const integrationRequired = new Set(changeSchema.$defs?.integration?.required ?? []);
710
+ if (
711
+ changeSchema.$id !== "urn:speculo:specdev:change-status:v5" ||
712
+ !["implementation_owner", "integration_owner", "source_checkpoint", "integration"].every((key) => worktreeRequired.has(key)) ||
713
+ !["parent_ref", "candidate_sha", "candidate_tree_sha", "candidate_branch", "candidate_workspace_ref", "full_suite", "e2e", "promotion_status"].every((key) => integrationRequired.has(key)) ||
714
+ changeSchema.additionalProperties !== false
715
+ ) {
716
+ errors.push("change-status.schema.json must define the strict Ticket candidate-integration v5 contract");
717
+ }
718
+ return errors;
719
+ }
720
+
620
721
  function capabilityChecks(root) {
621
722
  const checks = new Map([
622
723
  [
@@ -659,14 +760,14 @@ function capabilityChecks(root) {
659
760
  join(root, "P-goal-plan", "P-goal-plan.md"),
660
761
  [
661
762
  "Outcome",
662
- "Authority",
763
+ "权威来源",
663
764
  "DAG",
664
765
  "Gate",
665
766
  "Wave",
666
- "普通 Goal Plan",
667
- "委派 Goal Plan",
668
- "每次向用户询问",
669
- "delegated-execution.md",
767
+ "lead-directed",
768
+ "implementation_agent_limit",
769
+ "candidate-merge",
770
+ "lead-orchestration.md",
670
771
  "Definition of Done",
671
772
  ],
672
773
  ],
@@ -675,7 +776,7 @@ function capabilityChecks(root) {
675
776
  "implement",
676
777
  [
677
778
  join(root, "I-implement", "I-implement.md"),
678
- ["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "delegated execution", "subagent-delivery", "提交"],
779
+ ["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "parent-candidate", "subagent-delivery", "实现 commit"],
679
780
  ],
680
781
  ],
681
782
  [
@@ -767,45 +868,30 @@ function capabilityChecks(root) {
767
868
  errors.push("Wayfinder map template must query open Tickets instead of caching them");
768
869
  }
769
870
 
770
- const ordinaryGoalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
771
- if (isFile(ordinaryGoalPlanTemplate)) {
772
- const text = readText(ordinaryGoalPlanTemplate);
773
- for (const forbidden of [
774
- "Lead",
775
- "Subagent",
776
- "subagent",
777
- "Provider",
778
- "Delivery Contract",
779
- "Dispatch Packet",
780
- "Worker",
781
- "max_correction_rounds",
782
- "execution_model",
871
+ const goalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
872
+ if (!isFile(goalPlanTemplate)) {
873
+ errors.push("missing Goal Plan template");
874
+ } else {
875
+ const text = readText(goalPlanTemplate);
876
+ for (const required of [
877
+ "schema_version: 5",
878
+ "orchestration: lead-directed",
879
+ "implementation_agent_limit: 3",
880
+ "ticket_workspace_policy: required",
881
+ "integration_gate: candidate-merge",
882
+ ...REQUIRED_LEAD_GOAL_PLAN_MARKERS,
783
883
  ]) {
784
- if (text.includes(forbidden)) {
785
- errors.push(`ordinary Goal Plan template contains delegated marker ${forbidden}`);
786
- }
884
+ if (!text.includes(required)) errors.push(`Goal Plan template lost marker ${required}`);
787
885
  }
788
886
  }
789
-
790
- const delegatedTemplate = join(root, "P-goal-plan", "delegated-execution-template.md");
791
- if (!isFile(delegatedTemplate)) {
792
- errors.push("missing delegated Goal Plan template");
793
- } else {
794
- const text = readText(delegatedTemplate);
795
- const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !text.includes(marker));
796
- if (missing.length) errors.push(`delegated Goal Plan template lost markers: ${JSON.stringify(missing)}`);
797
- }
798
-
799
- const sampleCore = [...REQUIRED_GOAL_PLAN_SECTIONS].join("\n");
800
- const partialErrors = [];
801
- validateDelegatedGoalPlan(`${sampleCore}\n## Delegated Execution Addendum\n### Delivery Contract`, "partial delegated self-check", partialErrors);
802
- if (!partialErrors.length) errors.push("partial delegated Goal Plan self-check did not fail");
803
- const strayRoleErrors = [];
804
- validateDelegatedGoalPlan(`${sampleCore}\nLead: owner`, "stray delegated role self-check", strayRoleErrors);
805
- if (!strayRoleErrors.length) errors.push("stray delegated role self-check did not fail");
806
- const completeErrors = [];
807
- validateDelegatedGoalPlan(`${sampleCore}\n${REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.join("\n")}`, "complete delegated self-check", completeErrors);
808
- if (completeErrors.length) errors.push(...completeErrors);
887
+ for (const obsolete of [
888
+ "P-goal-plan/delegated-execution.md",
889
+ "P-goal-plan/delegated-execution-template.md",
890
+ "P-goal-plan/workspace-execution-template.md",
891
+ "I-implement/delegated-evidence-template.md",
892
+ ]) {
893
+ if (isFile(join(root, obsolete))) errors.push(`obsolete topology asset must be removed: ${obsolete}`);
894
+ }
809
895
  return errors;
810
896
  }
811
897
 
@@ -875,6 +961,7 @@ function selfCheck(root) {
875
961
 
876
962
  errors.push(...validateJsonFiles(root));
877
963
  errors.push(...validateGlobalStatusAssets(root));
964
+ errors.push(...validateExecutionContractAssets(root));
878
965
  const references = validateDocumentReferences(root);
879
966
  errors.push(...references.errors);
880
967
  warnings.push(...references.warnings);
@@ -887,6 +974,10 @@ function selfCheck(root) {
887
974
  "P-goal-plan/governance-sections.md",
888
975
  "P-goal-plan/lead-orchestration-protocol.md",
889
976
  "P-goal-plan/quick-reference-table.md",
977
+ "P-goal-plan/delegated-execution.md",
978
+ "P-goal-plan/delegated-execution-template.md",
979
+ "P-goal-plan/workspace-execution-template.md",
980
+ "I-implement/delegated-evidence-template.md",
890
981
  "schemas",
891
982
  "tools",
892
983
  "skills",
@@ -1112,8 +1203,8 @@ function validateSpec(path, errors, warnings) {
1112
1203
  }
1113
1204
  const { meta, body } = parseFrontmatter(path);
1114
1205
  const label = basename(path);
1115
- if (meta.artifact !== "spec" || meta.schema_version !== SCHEMA_VERSION) {
1116
- errors.push(`${label}: artifact/schema_version must be spec/${SCHEMA_VERSION}`);
1206
+ if (meta.artifact !== "spec" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
1207
+ errors.push(`${label}: artifact/schema_version must be spec/${DOMAIN_SCHEMA_VERSION}`);
1117
1208
  }
1118
1209
  const sources = requireList(meta, "sources", label, errors);
1119
1210
  if (!sources.length) errors.push(`${label}: sources must contain at least one source`);
@@ -1141,8 +1232,8 @@ function validateMap(path, errors) {
1141
1232
  return null;
1142
1233
  }
1143
1234
  const { meta, body } = parseFrontmatter(path);
1144
- if (meta.artifact !== "tickets-map" || meta.schema_version !== SCHEMA_VERSION) {
1145
- errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${SCHEMA_VERSION}`);
1235
+ if (meta.artifact !== "tickets-map" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
1236
+ errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${DOMAIN_SCHEMA_VERSION}`);
1146
1237
  }
1147
1238
  for (const heading of [
1148
1239
  "## 2. 执行清单",
@@ -1155,35 +1246,73 @@ function validateMap(path, errors) {
1155
1246
  return { path, meta, body };
1156
1247
  }
1157
1248
 
1158
- function validateDelegatedGoalPlan(body, label, errors) {
1159
- if (!DELEGATED_GOAL_PLAN_TRIGGERS.some((marker) => body.includes(marker))) return;
1160
- const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
1161
- if (missing.length) {
1162
- errors.push(`${label}: incomplete delegated execution addendum; missing ${JSON.stringify(missing)}`);
1163
- }
1164
- }
1165
-
1166
1249
  function validateGoalPlan(path, errors) {
1167
1250
  if (!isFile(path)) return null;
1168
1251
  const { meta, body } = parseFrontmatter(path);
1169
- if (meta.artifact !== "goal-plan" || meta.schema_version !== SCHEMA_VERSION) {
1170
- errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/${SCHEMA_VERSION}`);
1252
+ const required = [
1253
+ "schema_version",
1254
+ "artifact",
1255
+ "change",
1256
+ "status",
1257
+ "modes",
1258
+ "orchestration",
1259
+ "lead",
1260
+ "implementation_agent_limit",
1261
+ "ticket_workspace_policy",
1262
+ "integration_gate",
1263
+ "ready_for_execution",
1264
+ ];
1265
+ const missing = required.filter((key) => !(key in meta));
1266
+ if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
1267
+ const unexpected = Object.keys(meta).filter((key) => !required.includes(key));
1268
+ if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
1269
+ if (meta.artifact !== "goal-plan" || meta.schema_version !== GOAL_PLAN_SCHEMA_VERSION) {
1270
+ errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/${GOAL_PLAN_SCHEMA_VERSION}`);
1171
1271
  }
1172
1272
  const modes = requireList(meta, "modes", basename(path), errors);
1173
1273
  const invalidModes = modes.filter((mode) => !VALID_PLAN_MODES.has(mode));
1174
1274
  if (invalidModes.length) {
1175
1275
  errors.push(`${basename(path)}: invalid modes ${JSON.stringify(invalidModes)}`);
1176
1276
  }
1177
- validateDelegatedGoalPlan(body, basename(path), errors);
1277
+ if (meta.orchestration !== "lead-directed") {
1278
+ errors.push(`${basename(path)}: orchestration must be lead-directed`);
1279
+ }
1280
+ if (typeof meta.lead !== "string" || !meta.lead.trim()) {
1281
+ errors.push(`${basename(path)}: lead must be a non-empty recoverable locator`);
1282
+ }
1283
+ if (!Number.isInteger(meta.implementation_agent_limit) || meta.implementation_agent_limit < 1 || meta.implementation_agent_limit > 3) {
1284
+ errors.push(`${basename(path)}: implementation_agent_limit must be an integer from 1 to 3`);
1285
+ }
1286
+ if (meta.ticket_workspace_policy !== "required") {
1287
+ errors.push(`${basename(path)}: ticket_workspace_policy must be required`);
1288
+ }
1289
+ if (meta.integration_gate !== "candidate-merge") {
1290
+ errors.push(`${basename(path)}: integration_gate must be candidate-merge`);
1291
+ }
1292
+ for (const obsolete of ["coordination_mode", "workspace_strategy", "terminal_action"]) {
1293
+ if (obsolete in meta) errors.push(`${basename(path)}: obsolete Goal Plan field ${obsolete} is not allowed`);
1294
+ }
1295
+ for (const obsolete of ["## Delegated Execution Addendum", "## Isolated Workspace Addendum"]) {
1296
+ if (body.includes(obsolete)) errors.push(`${basename(path)}: obsolete Goal Plan addendum '${obsolete}' is not allowed`);
1297
+ }
1298
+ const validReadyStates = new Set(["ready", "in_progress"]);
1299
+ const validNotReadyStates = new Set(["draft", "blocked", "completed"]);
1300
+ if (
1301
+ (meta.ready_for_execution === true && !validReadyStates.has(meta.status)) ||
1302
+ (meta.ready_for_execution === false && !validNotReadyStates.has(meta.status))
1303
+ ) {
1304
+ errors.push(`${basename(path)}: ready_for_execution must match status`);
1305
+ }
1178
1306
  if (meta.ready_for_execution === true) {
1179
- if (!new Set(["ready", "in_progress", "completed"]).has(meta.status)) {
1180
- errors.push(`${basename(path)}: ready_for_execution conflicts with status`);
1181
- }
1182
1307
  for (const heading of REQUIRED_GOAL_PLAN_SECTIONS) {
1183
1308
  if (!body.includes(heading)) {
1184
1309
  errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
1185
1310
  }
1186
1311
  }
1312
+ const missingLeadMarkers = REQUIRED_LEAD_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
1313
+ if (missingLeadMarkers.length) {
1314
+ errors.push(`${basename(path)}: ready Goal Plan missing Lead/candidate markers ${JSON.stringify(missingLeadMarkers)}`);
1315
+ }
1187
1316
  const assumptions = sectionBody(body, "## Assumptions");
1188
1317
  if (assumptions && assumptions.includes("高影响") && !assumptions.includes("必须为 `false`")) {
1189
1318
  errors.push(`${basename(path)}: high-impact assumptions cannot remain in a ready Goal Plan`);
@@ -1369,8 +1498,8 @@ function validateTicket(path, errors) {
1369
1498
  errors.push(`${basename(path)}: missing frontmatter keys ${JSON.stringify(missing.sort())}`);
1370
1499
  return null;
1371
1500
  }
1372
- if (meta.artifact !== "ticket" || meta.schema_version !== SCHEMA_VERSION) {
1373
- errors.push(`${basename(path)}: artifact/schema_version must be ticket/${SCHEMA_VERSION}`);
1501
+ if (meta.artifact !== "ticket" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
1502
+ errors.push(`${basename(path)}: artifact/schema_version must be ticket/${DOMAIN_SCHEMA_VERSION}`);
1374
1503
  }
1375
1504
  const ticketId = String(meta.id);
1376
1505
  if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
@@ -1423,13 +1552,7 @@ function validateTicket(path, errors) {
1423
1552
  if (unresolved && !/^无[。.]?$/.test(unresolved.trim())) {
1424
1553
  errors.push(`${basename(path)}: Ready Ticket has unresolved questions`);
1425
1554
  }
1426
- if (
1427
- !lists.writable_paths.length &&
1428
- !body.includes("无代码变更") &&
1429
- !body.includes("仅文档")
1430
- ) {
1431
- errors.push(`${basename(path)}: Ready Ticket has no writable_paths and no no-code explanation`);
1432
- }
1555
+ if (!lists.writable_paths.length) errors.push(`${basename(path)}: Ready implementation Ticket requires writable_paths`);
1433
1556
  if (new Set(["standard", "deep"]).has(meta.planning_depth)) {
1434
1557
  for (const heading of ["## 5. 实现契约", "## 6. 执行路线"]) {
1435
1558
  if (!body.includes(heading)) {
@@ -1444,6 +1567,9 @@ function validateTicket(path, errors) {
1444
1567
  if (!verification || !verification.includes("|")) {
1445
1568
  errors.push(`${basename(path)}: Ready Ticket has no usable verification matrix`);
1446
1569
  }
1570
+ if (!verification.includes("E2E disposition") || !verification.includes("parent-candidate")) {
1571
+ errors.push(`${basename(path)}: Ready Ticket must define E2E disposition and parent-candidate owner/environment`);
1572
+ }
1447
1573
  const acceptance = sectionBody(body, "## 10. 验收标准").toLowerCase();
1448
1574
  if (!acceptance.includes("- [ ]") && !acceptance.includes("- [x]")) {
1449
1575
  errors.push(`${basename(path)}: Ready Ticket has no acceptance checklist`);
@@ -1470,6 +1596,10 @@ function validateChangeStatus(path, expectedChange, errors) {
1470
1596
  "change",
1471
1597
  "change_status",
1472
1598
  "current_work",
1599
+ "works_run",
1600
+ "claimed_investigations",
1601
+ "execution_authorization",
1602
+ "leadership",
1473
1603
  "created_at",
1474
1604
  "updated_at",
1475
1605
  "completed_at",
@@ -1477,11 +1607,14 @@ function validateChangeStatus(path, expectedChange, errors) {
1477
1607
  "archive_path",
1478
1608
  "blockers",
1479
1609
  "deviations",
1610
+ "worktrees",
1480
1611
  ];
1481
1612
  const missing = required.filter((key) => !(key in data));
1482
1613
  if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
1483
- if (data.schema_version !== SCHEMA_VERSION || data.artifact !== "change-status") {
1484
- errors.push(`${basename(path)}: artifact/schema_version must be change-status/${SCHEMA_VERSION}`);
1614
+ const unexpected = Object.keys(data).filter((key) => !required.includes(key));
1615
+ if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
1616
+ if (data.schema_version !== CHANGE_STATUS_SCHEMA_VERSION || data.artifact !== "change-status") {
1617
+ errors.push(`${basename(path)}: artifact/schema_version must be change-status/${CHANGE_STATUS_SCHEMA_VERSION}`);
1485
1618
  }
1486
1619
  if (data.change !== expectedChange) {
1487
1620
  errors.push(`${basename(path)}: change must equal directory name ${expectedChange}`);
@@ -1489,17 +1622,45 @@ function validateChangeStatus(path, expectedChange, errors) {
1489
1622
  if (!new Set(["active", "blocked", "completed", "archived"]).has(data.change_status)) {
1490
1623
  errors.push(`${basename(path)}: invalid change_status ${data.change_status}`);
1491
1624
  }
1492
- if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations)) {
1493
- errors.push(`${basename(path)}: blockers and deviations must be arrays`);
1494
- }
1495
- if (data.worktrees !== undefined && !Array.isArray(data.worktrees)) {
1496
- errors.push(`${basename(path)}: worktrees must be an array when present`);
1625
+ if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations) || !Array.isArray(data.worktrees)) {
1626
+ errors.push(`${basename(path)}: blockers, deviations, and worktrees must be arrays`);
1497
1627
  }
1628
+ const seenWorktreeTickets = new Set();
1498
1629
  for (const worktree of Array.isArray(data.worktrees) ? data.worktrees : []) {
1499
1630
  if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
1500
1631
  errors.push(`${basename(path)}: invalid worktree entry`);
1501
1632
  continue;
1502
1633
  }
1634
+ const requiredWorktreeKeys = [
1635
+ "ticket_id",
1636
+ "owner",
1637
+ "implementation_owner",
1638
+ "integration_owner",
1639
+ "provider",
1640
+ "base_sha",
1641
+ "parent_branch",
1642
+ "branch",
1643
+ "workspace_ref",
1644
+ "source_checkpoint",
1645
+ "integration",
1646
+ "status",
1647
+ "updated_at",
1648
+ ];
1649
+ const missingWorktreeKeys = requiredWorktreeKeys.filter((key) => !(key in worktree));
1650
+ if (missingWorktreeKeys.length) {
1651
+ errors.push(`${basename(path)}: worktree integration contract missing ${JSON.stringify(missingWorktreeKeys)}`);
1652
+ continue;
1653
+ }
1654
+ const unexpectedWorktreeKeys = Object.keys(worktree).filter((key) => !requiredWorktreeKeys.includes(key));
1655
+ if (unexpectedWorktreeKeys.length) {
1656
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has unexpected keys ${JSON.stringify(unexpectedWorktreeKeys.sort())}`);
1657
+ }
1658
+ if (!/^T-[0-9]{2,}$/.test(String(worktree.ticket_id))) {
1659
+ errors.push(`${basename(path)}: invalid worktree ticket_id ${worktree.ticket_id}`);
1660
+ } else if (seenWorktreeTickets.has(worktree.ticket_id)) {
1661
+ errors.push(`${basename(path)}: duplicate worktree for ${worktree.ticket_id}`);
1662
+ }
1663
+ seenWorktreeTickets.add(worktree.ticket_id);
1503
1664
  const ref = worktree.workspace_ref;
1504
1665
  if (
1505
1666
  typeof ref !== "string" ||
@@ -1509,10 +1670,209 @@ function validateChangeStatus(path, expectedChange, errors) {
1509
1670
  errors.push(`${basename(path)}: workspace_ref must be a portable non-absolute locator`);
1510
1671
  continue;
1511
1672
  }
1512
- if (worktree.provider === "git") {
1513
- const expectedRef = `specdev-worktree/${worktree.ticket_id}`;
1514
- if (ref !== expectedRef) {
1515
- errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
1673
+ if (!VALID_WORKTREE_STATUS.has(worktree.status)) {
1674
+ errors.push(`${basename(path)}: invalid worktree status ${worktree.status}`);
1675
+ }
1676
+ if (worktree.provider !== "git") {
1677
+ errors.push(`${basename(path)}: Ticket worktree ${worktree.ticket_id} provider must be git`);
1678
+ }
1679
+ const expectedRef = `specdev-worktree/${expectedChange}/${worktree.ticket_id}`;
1680
+ if (ref !== expectedRef) {
1681
+ errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
1682
+ }
1683
+
1684
+ if ("terminal_action" in worktree) {
1685
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} obsolete terminal_action is not allowed`);
1686
+ }
1687
+ const requiredIntegrationKeys = [
1688
+ "owner",
1689
+ "implementation_owner",
1690
+ "integration_owner",
1691
+ "parent_branch",
1692
+ "source_checkpoint",
1693
+ "integration",
1694
+ ];
1695
+ const missingIntegrationKeys = requiredIntegrationKeys.filter((key) => !(key in worktree));
1696
+ if (missingIntegrationKeys.length) {
1697
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
1698
+ continue;
1699
+ }
1700
+ for (const key of ["owner", "implementation_owner", "integration_owner", "base_sha", "parent_branch", "branch", "updated_at"]) {
1701
+ if (typeof worktree[key] !== "string" || !worktree[key].trim()) {
1702
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${key} is required`);
1703
+ }
1704
+ }
1705
+ if (typeof worktree.parent_branch !== "string" || !worktree.parent_branch.trim()) {
1706
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch is required`);
1707
+ } else if (worktree.parent_branch === worktree.branch) {
1708
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch must differ from branch`);
1709
+ }
1710
+
1711
+ const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(worktree.status);
1712
+ if (sourceRequired && (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())) {
1713
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_checkpoint`);
1714
+ } else if (
1715
+ worktree.source_checkpoint !== null &&
1716
+ (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())
1717
+ ) {
1718
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_checkpoint must be null or non-empty`);
1719
+ }
1720
+
1721
+ const integration = worktree.integration;
1722
+ if (!integration || typeof integration !== "object" || Array.isArray(integration)) {
1723
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration must be an object`);
1724
+ continue;
1725
+ }
1726
+ const requiredResultKeys = [
1727
+ "status",
1728
+ "parent_ref",
1729
+ "parent_before_sha",
1730
+ "source_sha",
1731
+ "candidate_sha",
1732
+ "candidate_tree_sha",
1733
+ "candidate_branch",
1734
+ "candidate_workspace_ref",
1735
+ "result_sha",
1736
+ "method",
1737
+ "conflict_paths",
1738
+ "verification",
1739
+ "full_suite",
1740
+ "e2e",
1741
+ "evidence",
1742
+ "attempts",
1743
+ "promotion_status",
1744
+ ];
1745
+ const missingResultKeys = requiredResultKeys.filter((key) => !(key in integration));
1746
+ if (missingResultKeys.length) {
1747
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration missing ${JSON.stringify(missingResultKeys)}`);
1748
+ }
1749
+ const unexpectedResultKeys = Object.keys(integration).filter((key) => !requiredResultKeys.includes(key));
1750
+ if (unexpectedResultKeys.length) {
1751
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration has unexpected keys ${JSON.stringify(unexpectedResultKeys.sort())}`);
1752
+ }
1753
+ if (!VALID_INTEGRATION_STATUS.has(integration.status)) {
1754
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration status ${integration.status}`);
1755
+ }
1756
+ if (!VALID_INTEGRATION_METHOD.has(integration.method)) {
1757
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration method ${integration.method}`);
1758
+ }
1759
+ if (!VALID_INTEGRATION_VERIFICATION.has(integration.verification)) {
1760
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration verification ${integration.verification}`);
1761
+ }
1762
+ if (!Array.isArray(integration.conflict_paths)) {
1763
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} conflict_paths must be an array`);
1764
+ }
1765
+ if (!Number.isInteger(integration.attempts) || integration.attempts < 0) {
1766
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} attempts must be a non-negative integer`);
1767
+ }
1768
+ if (
1769
+ typeof integration.evidence !== "string" ||
1770
+ !/^<Path>\{roots\.state\}\/specdev\/changes\/[^<]+\/evidence\/T-[0-9]{2,}\.md<\/Path>$/.test(integration.evidence)
1771
+ ) {
1772
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration evidence must be a rooted Ticket Evidence path`);
1773
+ }
1774
+ if (!new Set(["pending", "applying", "applied", "failed", "stale"]).has(integration.promotion_status)) {
1775
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid promotion_status ${integration.promotion_status}`);
1776
+ }
1777
+ for (const [name, suite] of [["full_suite", integration.full_suite], ["e2e", integration.e2e]]) {
1778
+ if (!suite || typeof suite !== "object" || Array.isArray(suite) || typeof suite.required !== "boolean" || !VALID_E2E_STATUS.has(suite.status)) {
1779
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} contract is invalid`);
1780
+ continue;
1781
+ }
1782
+ const unexpectedSuiteKeys = Object.keys(suite).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
1783
+ if (unexpectedSuiteKeys.length) errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} has unexpected keys ${JSON.stringify(unexpectedSuiteKeys.sort())}`);
1784
+ if (suite.required === false && (suite.status !== "not-required" || typeof suite.reason !== "string" || !suite.reason.trim())) {
1785
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required ${name} needs not-required status and reason`);
1786
+ }
1787
+ if (suite.required === true && suite.status === "not-required") {
1788
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required ${name} cannot be not-required`);
1789
+ }
1790
+ }
1791
+ const e2e = integration.e2e;
1792
+ if (
1793
+ !e2e ||
1794
+ typeof e2e !== "object" ||
1795
+ Array.isArray(e2e) ||
1796
+ typeof e2e.required !== "boolean" ||
1797
+ !VALID_E2E_STATUS.has(e2e.status)
1798
+ ) {
1799
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e contract is invalid`);
1800
+ } else {
1801
+ const unexpectedE2eKeys = Object.keys(e2e).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
1802
+ if (unexpectedE2eKeys.length) {
1803
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e has unexpected keys ${JSON.stringify(unexpectedE2eKeys.sort())}`);
1804
+ }
1805
+ if (e2e.required === false && e2e.status !== "not-required") {
1806
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required E2E must use status=not-required`);
1807
+ }
1808
+ if (e2e.required === true && e2e.status === "not-required") {
1809
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required E2E cannot be not-required`);
1810
+ }
1811
+ if (
1812
+ e2e.required === true &&
1813
+ e2e.status === "passed" &&
1814
+ (typeof e2e.evidence !== "string" || !e2e.evidence.trim())
1815
+ ) {
1816
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} passed required E2E needs evidence`);
1817
+ }
1818
+ }
1819
+ const integrationLifecycleActive = new Set(["integrating", "integrated", "removed"]).has(worktree.status);
1820
+ if (integrationLifecycleActive) {
1821
+ if (typeof integration.parent_before_sha !== "string" || !integration.parent_before_sha.trim()) {
1822
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires parent_before_sha`);
1823
+ }
1824
+ if (typeof integration.source_sha !== "string" || !integration.source_sha.trim()) {
1825
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_sha`);
1826
+ } else if (integration.source_sha !== worktree.source_checkpoint) {
1827
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_sha must equal source_checkpoint`);
1828
+ }
1829
+ if (typeof integration.candidate_sha !== "string" || !integration.candidate_sha.trim()) {
1830
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires candidate_sha`);
1831
+ }
1832
+ const expectedCandidateBranch = `speculo/integration/${expectedChange}/${worktree.ticket_id}`;
1833
+ if (integration.candidate_branch !== expectedCandidateBranch) {
1834
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_branch must equal ${expectedCandidateBranch}`);
1835
+ }
1836
+ const expectedCandidateRef = `specdev-worktree/.integration/${expectedChange}/${worktree.ticket_id}`;
1837
+ if (integration.candidate_workspace_ref !== expectedCandidateRef) {
1838
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_workspace_ref must equal ${expectedCandidateRef}`);
1839
+ }
1840
+ if (!new Set(["fast-forward", "merge-commit"]).has(integration.method)) {
1841
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires an integration method`);
1842
+ }
1843
+ if (!Number.isInteger(integration.attempts) || integration.attempts < 1) {
1844
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires attempts >= 1`);
1845
+ }
1846
+ }
1847
+ if (worktree.status === "integrating" && integration.status !== "candidate") {
1848
+ errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=candidate`);
1849
+ }
1850
+ if (new Set(["integrated", "removed"]).has(worktree.status)) {
1851
+ if (
1852
+ integration.status !== "passed" ||
1853
+ integration.verification !== "passed" ||
1854
+ !new Set(["fast-forward", "merge-commit"]).has(integration.method) ||
1855
+ typeof integration.result_sha !== "string" ||
1856
+ !integration.result_sha.trim() ||
1857
+ integration.result_sha !== integration.candidate_sha ||
1858
+ !e2e ||
1859
+ !new Set(["not-required", "passed"]).has(e2e.status)
1860
+ ) {
1861
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} requires passed candidate/result/E2E state`);
1862
+ }
1863
+ if (integration.method === "fast-forward") {
1864
+ if (integration.candidate_sha !== worktree.source_checkpoint) {
1865
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward candidate/result must equal source_checkpoint`);
1866
+ }
1867
+ if (Array.isArray(integration.conflict_paths) && integration.conflict_paths.length > 0) {
1868
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
1869
+ }
1870
+ } else if (
1871
+ integration.method === "merge-commit" &&
1872
+ typeof integration.candidate_sha === "string" &&
1873
+ (integration.candidate_sha === worktree.source_checkpoint || integration.candidate_sha === integration.parent_before_sha)
1874
+ ) {
1875
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} merge-commit candidate must be a distinct checkpoint`);
1516
1876
  }
1517
1877
  }
1518
1878
  }
@@ -1624,6 +1984,25 @@ function validateChange(change, stage = null) {
1624
1984
  }
1625
1985
  }
1626
1986
 
1987
+ if (stage === "implement" && ticketMode && changeStatus) {
1988
+ const worktreesByTicket = new Map(
1989
+ (Array.isArray(changeStatus.worktrees) ? changeStatus.worktrees : [])
1990
+ .filter((entry) => entry && typeof entry === "object")
1991
+ .map((entry) => [String(entry.ticket_id), entry]),
1992
+ );
1993
+ for (const [ticketId, artifact] of tickets) {
1994
+ if (new Set(["draft", "cancelled"]).has(artifact.meta.status)) continue;
1995
+ if (!worktreesByTicket.has(ticketId)) {
1996
+ errors.push(`${ticketId}: Implement stage requires one Ticket source worktree record`);
1997
+ }
1998
+ }
1999
+ for (const ticketId of worktreesByTicket.keys()) {
2000
+ if (!tickets.has(ticketId)) {
2001
+ errors.push(`${ticketId}: worktree record has no matching Ticket`);
2002
+ }
2003
+ }
2004
+ }
2005
+
1627
2006
  if (spec) {
1628
2007
  const declaredContracts = new Set(spec.body.match(/\bAC-\d+\b/g) ?? []);
1629
2008
  const coveredContracts = new Set(
@@ -1710,18 +2089,29 @@ function validateChange(change, stage = null) {
1710
2089
  }
1711
2090
  const text = readText(evidence);
1712
2091
  for (const heading of [
1713
- "## 2. 修改范围",
1714
- "## 3. 验收与合同映射",
1715
- "## 4. 验证执行",
1716
- "## 5. 双轴审查",
1717
- "## 6. 路径所有权审计",
1718
- "## 7. 偏差与决策",
1719
- "## 8. 残余风险与后续",
2092
+ "## 2. Lead Dispatch And Candidate Return",
2093
+ "## 3. 修改范围与路径所有权",
2094
+ "## 4. 验收与合同映射",
2095
+ "## 5. Source-worktree 验证",
2096
+ "## 6. 双轴审查",
2097
+ "## 7. Parent-candidate 集成验证",
2098
+ "## 8. 偏差与决策",
2099
+ "## 9. 残余风险与交付定位",
1720
2100
  ]) {
1721
2101
  if (!text.includes(heading)) {
1722
2102
  errors.push(`${toPosix(relative(change, evidence))}: missing '${heading}'`);
1723
2103
  }
1724
2104
  }
2105
+ const sourceVerification = sectionBody(text, "## 5. Source-worktree 验证");
2106
+ if (/E2E[^\n]*(?:\bpass(?:ed)?\b|通过)/i.test(sourceVerification) && !/不得|禁止|not-run/i.test(sourceVerification)) {
2107
+ errors.push(`${toPosix(relative(change, evidence))}: E2E pass cannot come from source-worktree`);
2108
+ }
2109
+ const worktree = Array.isArray(changeStatus?.worktrees)
2110
+ ? changeStatus.worktrees.find((entry) => entry?.ticket_id === ticketId)
2111
+ : null;
2112
+ if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
2113
+ errors.push(`${ticketId}: status is done but no integrated or removed Ticket worktree exists`);
2114
+ }
1725
2115
  }
1726
2116
 
1727
2117
  if (goalPlan?.meta.ready_for_execution === true) {