@namewta/speculo 0.7.2 → 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 (53) hide show
  1. package/dist/src/migrations.js +604 -23
  2. package/dist/src/migrations.js.map +1 -1
  3. package/package.json +1 -1
  4. package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +171 -455
  5. package/template/canonical/canonical-specdev-goal-plan.md +686 -1117
  6. package/template/canonical/canonical-specdev-grill-with-docs.md +172 -456
  7. package/template/canonical/canonical-specdev-spec.md +199 -493
  8. package/template/canonical/canonical-specdev-tickets.md +378 -597
  9. package/template/canonical/canonical-specdev-wayfinder.md +170 -454
  10. package/template/skills/migrate-runtime-state/SKILL.md +6 -6
  11. package/template/skills/migrate-runtime-state/references/migration-contract.md +9 -3
  12. package/template/skills/migrate-runtime-state/scripts/migrate-runtime-state.mjs +322 -33
  13. package/template/workflows/specdev/I-implement/I-implement.md +97 -143
  14. package/template/workflows/specdev/I-implement/evidence-template.md +60 -48
  15. package/template/workflows/specdev/I-implement/execution-preflight.md +29 -21
  16. package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +12 -12
  17. package/template/workflows/specdev/I-init-setup/I-init-setup.md +4 -5
  18. package/template/workflows/specdev/I-init-setup/change-status-template.json +14 -1
  19. package/template/workflows/specdev/I-init-setup/config-template.json +3 -5
  20. package/template/workflows/specdev/I-init-setup/status-template.json +1 -1
  21. package/template/workflows/specdev/INDEX.md +11 -8
  22. package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +76 -102
  23. package/template/workflows/specdev/P-goal-plan/completion-control.md +26 -44
  24. package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +40 -37
  25. package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +34 -0
  26. package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +31 -46
  27. package/template/workflows/specdev/P-goal-plan/planning-modes.md +42 -76
  28. package/template/workflows/specdev/T-tickets/T-tickets.md +6 -3
  29. package/template/workflows/specdev/T-tickets/ticket-readiness.md +5 -3
  30. package/template/workflows/specdev/T-tickets/ticket-template.md +8 -1
  31. package/template/workflows/specdev/T-tickets/tickets-map-template.md +5 -4
  32. package/template/workflows/specdev/_state/status.json +1 -1
  33. package/template/workflows/specdev/common/README.md +2 -2
  34. package/template/workflows/specdev/common/rules/change-completion.md +17 -20
  35. package/template/workflows/specdev/common/rules/deviation-control.md +1 -1
  36. package/template/workflows/specdev/common/rules/evidence-and-verification.md +27 -37
  37. package/template/workflows/specdev/common/rules/path-ownership.md +21 -23
  38. package/template/workflows/specdev/common/rules/readiness-and-depth.md +1 -1
  39. package/template/workflows/specdev/common/schemas/change-status.schema.json +136 -373
  40. package/template/workflows/specdev/common/schemas/config.schema.json +9 -11
  41. package/template/workflows/specdev/common/schemas/goal-plan.schema.json +24 -16
  42. package/template/workflows/specdev/common/schemas/status.schema.json +7 -63
  43. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +42 -21
  44. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +36 -21
  45. package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +46 -18
  46. package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +34 -31
  47. package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +10 -23
  48. package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +15 -25
  49. package/template/workflows/specdev/common/tools/validate-specdev.mjs +386 -209
  50. package/template/workflows/specdev/I-implement/delegated-evidence-template.md +0 -12
  51. package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +0 -35
  52. package/template/workflows/specdev/P-goal-plan/delegated-execution.md +0 -59
  53. package/template/workflows/specdev/P-goal-plan/workspace-execution-template.md +0 -24
@@ -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,14 +59,11 @@ 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
  ]);
64
- const VALID_COORDINATION_MODES = new Set(["single-session", "lead-team"]);
65
- const VALID_WORKSPACE_STRATEGIES = new Set(["current", "worktree", "mixed"]);
66
67
  const VALID_WORKTREE_STATUS = new Set([
67
68
  "planned",
68
69
  "active",
@@ -72,10 +73,10 @@ const VALID_WORKTREE_STATUS = new Set([
72
73
  "removed",
73
74
  "blocked",
74
75
  ]);
75
- const VALID_TERMINAL_ACTION = new Set(["integrate", "retain"]);
76
- const VALID_INTEGRATION_STATUS = new Set(["pending", "running", "passed", "blocked"]);
76
+ const VALID_INTEGRATION_STATUS = new Set(["pending", "candidate", "passed", "failed", "stale"]);
77
77
  const VALID_INTEGRATION_METHOD = new Set([null, "fast-forward", "merge-commit"]);
78
78
  const VALID_INTEGRATION_VERIFICATION = new Set(["pending", "passed", "failed"]);
79
+ const VALID_E2E_STATUS = new Set(["not-required", "pending", "passed", "failed"]);
79
80
  const VALID_DESIGN_TREE_STATUS = new Set(["active", "consensus", "blocked"]);
80
81
  const VALID_DESIGN_NODE_STATUS = new Set(["open", "answered", "deferred", "rejected"]);
81
82
  const VALID_WAYFINDER_LABEL = new Set([
@@ -142,39 +143,16 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
142
143
  "## 5. Constraints, Risk and Recovery",
143
144
  "## 6. Progress and Decisions",
144
145
  ]);
145
- const DELEGATED_GOAL_PLAN_TRIGGERS = [
146
- "## Delegated Execution Addendum",
147
- "### Delivery Contract",
148
- "### Per-Ticket Dispatch Packets",
149
- "### Candidate Delivery Return and Lead Acceptance",
150
- "Lead / Provider",
151
- "Lead:",
152
- "Lead:",
153
- "Worker:",
154
- "Worker:",
155
- "Subagent",
156
- "subagent",
157
- "execution_model",
158
- "execution model: direct",
159
- "max_correction_rounds",
160
- ];
161
- const REQUIRED_DELEGATED_GOAL_PLAN_MARKERS = [
162
- "## Delegated Execution Addendum",
163
- "### Delivery Contract",
164
- "### Per-Ticket Dispatch Packets",
165
- "### Candidate Delivery Return and Lead Acceptance",
166
- "native-subagent / external-web-subagent",
167
- "Lead / Provider",
168
- "#### Dispatch:",
169
- ];
170
- const REQUIRED_WORKSPACE_GOAL_PLAN_MARKERS = [
171
- "## Isolated Workspace Addendum",
172
- "### Workspace Decision",
173
- "### Per-Ticket Workspace Allocation",
174
- "### Local Integration Authorization",
175
- "Integration owner",
176
- "Parent branch",
177
- "Terminal action",
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",
178
156
  ];
179
157
  const STATE_ARTIFACT_BASENAMES = new Set([
180
158
  "spec.md",
@@ -195,6 +173,10 @@ const STATE_ARTIFACT_BASENAMES = new Set([
195
173
  ]);
196
174
  const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
197
175
  "source-issue.md",
176
+ "delegated-execution.md",
177
+ "delegated-execution-template.md",
178
+ "workspace-execution-template.md",
179
+ "delegated-evidence-template.md",
198
180
  "input-validation.md",
199
181
  "vision-sections.md",
200
182
  "execution-sections.md",
@@ -415,6 +397,19 @@ function validatePathValue(value, { allowProject = true } = {}) {
415
397
  }
416
398
  return null;
417
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
+ }
418
413
  for (const [prefix, name] of [[SKILLS_PREFIX, "skills"], [COMMANDS_PREFIX, "commands"]]) {
419
414
  if (value.startsWith(prefix)) {
420
415
  const suffix = value.slice(prefix.length);
@@ -616,7 +611,7 @@ function validateGlobalStatusAssets(root) {
616
611
  !Array.isArray(data.active) ||
617
612
  !Array.isArray(data.archived)
618
613
  ) {
619
- 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`);
620
615
  }
621
616
  }
622
617
 
@@ -627,11 +622,11 @@ function validateGlobalStatusAssets(root) {
627
622
  }
628
623
  const schema = JSON.parse(readText(schemaPath));
629
624
  if (
630
- schema.$id !== "urn:speculo:specdev:status:v4" ||
625
+ schema.$id !== "urn:speculo:specdev:status:v5" ||
631
626
  schema.properties?.schema_version?.const !== GLOBAL_STATUS_SCHEMA_VERSION ||
632
627
  schema.additionalProperties !== false
633
628
  ) {
634
- 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");
635
630
  }
636
631
  for (const removed of ["work_history", "completed", "result"]) {
637
632
  if (JSON.stringify(schema).includes(`\"${removed}\"`)) {
@@ -641,6 +636,88 @@ function validateGlobalStatusAssets(root) {
641
636
  return errors;
642
637
  }
643
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
+
644
721
  function capabilityChecks(root) {
645
722
  const checks = new Map([
646
723
  [
@@ -683,15 +760,14 @@ function capabilityChecks(root) {
683
760
  join(root, "P-goal-plan", "P-goal-plan.md"),
684
761
  [
685
762
  "Outcome",
686
- "Authority",
763
+ "权威来源",
687
764
  "DAG",
688
765
  "Gate",
689
766
  "Wave",
690
- "coordination_mode",
691
- "workspace_strategy",
692
- "single-session",
693
- "lead-team",
694
- "delegated-execution.md",
767
+ "lead-directed",
768
+ "implementation_agent_limit",
769
+ "candidate-merge",
770
+ "lead-orchestration.md",
695
771
  "Definition of Done",
696
772
  ],
697
773
  ],
@@ -700,7 +776,7 @@ function capabilityChecks(root) {
700
776
  "implement",
701
777
  [
702
778
  join(root, "I-implement", "I-implement.md"),
703
- ["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "delegated execution", "subagent-delivery", "提交"],
779
+ ["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "parent-candidate", "subagent-delivery", "实现 commit"],
704
780
  ],
705
781
  ],
706
782
  [
@@ -792,64 +868,30 @@ function capabilityChecks(root) {
792
868
  errors.push("Wayfinder map template must query open Tickets instead of caching them");
793
869
  }
794
870
 
795
- const ordinaryGoalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
796
- if (isFile(ordinaryGoalPlanTemplate)) {
797
- const text = readText(ordinaryGoalPlanTemplate);
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);
798
876
  for (const required of [
799
- "coordination_mode: single-session",
800
- "workspace_strategy: current",
801
- "### Execution Topology",
802
- ]) {
803
- if (!text.includes(required)) errors.push(`ordinary Goal Plan template lost marker ${required}`);
804
- }
805
- for (const forbidden of [
806
- "Lead",
807
- "Subagent",
808
- "subagent",
809
- "Provider",
810
- "Delivery Contract",
811
- "Dispatch Packet",
812
- "Worker",
813
- "max_correction_rounds",
814
- "execution_model",
815
- "worktree",
816
- "Ticket branch",
817
- "merge commit",
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,
818
883
  ]) {
819
- if (text.includes(forbidden)) {
820
- errors.push(`ordinary Goal Plan template contains delegated marker ${forbidden}`);
821
- }
884
+ if (!text.includes(required)) errors.push(`Goal Plan template lost marker ${required}`);
822
885
  }
823
886
  }
824
-
825
- const workspaceTemplate = join(root, "P-goal-plan", "workspace-execution-template.md");
826
- if (!isFile(workspaceTemplate)) {
827
- errors.push("missing isolated workspace Goal Plan template");
828
- } else {
829
- const text = readText(workspaceTemplate);
830
- const missing = REQUIRED_WORKSPACE_GOAL_PLAN_MARKERS.filter((marker) => !text.includes(marker));
831
- if (missing.length) errors.push(`isolated workspace Goal Plan template lost markers: ${JSON.stringify(missing)}`);
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}`);
832
894
  }
833
-
834
- const delegatedTemplate = join(root, "P-goal-plan", "delegated-execution-template.md");
835
- if (!isFile(delegatedTemplate)) {
836
- errors.push("missing delegated Goal Plan template");
837
- } else {
838
- const text = readText(delegatedTemplate);
839
- const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !text.includes(marker));
840
- if (missing.length) errors.push(`delegated Goal Plan template lost markers: ${JSON.stringify(missing)}`);
841
- }
842
-
843
- const sampleCore = [...REQUIRED_GOAL_PLAN_SECTIONS].join("\n");
844
- const partialErrors = [];
845
- validateDelegatedGoalPlan(`${sampleCore}\n## Delegated Execution Addendum\n### Delivery Contract`, "partial delegated self-check", partialErrors);
846
- if (!partialErrors.length) errors.push("partial delegated Goal Plan self-check did not fail");
847
- const strayRoleErrors = [];
848
- validateDelegatedGoalPlan(`${sampleCore}\nLead: owner`, "stray delegated role self-check", strayRoleErrors);
849
- if (!strayRoleErrors.length) errors.push("stray delegated role self-check did not fail");
850
- const completeErrors = [];
851
- validateDelegatedGoalPlan(`${sampleCore}\n${REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.join("\n")}`, "complete delegated self-check", completeErrors);
852
- if (completeErrors.length) errors.push(...completeErrors);
853
895
  return errors;
854
896
  }
855
897
 
@@ -919,6 +961,7 @@ function selfCheck(root) {
919
961
 
920
962
  errors.push(...validateJsonFiles(root));
921
963
  errors.push(...validateGlobalStatusAssets(root));
964
+ errors.push(...validateExecutionContractAssets(root));
922
965
  const references = validateDocumentReferences(root);
923
966
  errors.push(...references.errors);
924
967
  warnings.push(...references.warnings);
@@ -931,6 +974,10 @@ function selfCheck(root) {
931
974
  "P-goal-plan/governance-sections.md",
932
975
  "P-goal-plan/lead-orchestration-protocol.md",
933
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",
934
981
  "schemas",
935
982
  "tools",
936
983
  "skills",
@@ -1156,8 +1203,8 @@ function validateSpec(path, errors, warnings) {
1156
1203
  }
1157
1204
  const { meta, body } = parseFrontmatter(path);
1158
1205
  const label = basename(path);
1159
- if (meta.artifact !== "spec" || meta.schema_version !== SCHEMA_VERSION) {
1160
- 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}`);
1161
1208
  }
1162
1209
  const sources = requireList(meta, "sources", label, errors);
1163
1210
  if (!sources.length) errors.push(`${label}: sources must contain at least one source`);
@@ -1185,8 +1232,8 @@ function validateMap(path, errors) {
1185
1232
  return null;
1186
1233
  }
1187
1234
  const { meta, body } = parseFrontmatter(path);
1188
- if (meta.artifact !== "tickets-map" || meta.schema_version !== SCHEMA_VERSION) {
1189
- 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}`);
1190
1237
  }
1191
1238
  for (const heading of [
1192
1239
  "## 2. 执行清单",
@@ -1199,71 +1246,73 @@ function validateMap(path, errors) {
1199
1246
  return { path, meta, body };
1200
1247
  }
1201
1248
 
1202
- function validateDelegatedGoalPlan(body, label, errors) {
1203
- if (!DELEGATED_GOAL_PLAN_TRIGGERS.some((marker) => body.includes(marker))) return;
1204
- const missing = REQUIRED_DELEGATED_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
1205
- if (missing.length) {
1206
- errors.push(`${label}: incomplete delegated execution addendum; missing ${JSON.stringify(missing)}`);
1207
- }
1208
- }
1209
-
1210
- function validateWorkspaceGoalPlan(body, label, errors, required = false) {
1211
- const hasWorkspaceAddendum = body.includes("## Isolated Workspace Addendum");
1212
- if (!required && !hasWorkspaceAddendum) return;
1213
- const missing = REQUIRED_WORKSPACE_GOAL_PLAN_MARKERS.filter((marker) => !body.includes(marker));
1214
- if (missing.length) {
1215
- errors.push(`${label}: incomplete isolated workspace addendum; missing ${JSON.stringify(missing)}`);
1216
- }
1217
- }
1218
-
1219
1249
  function validateGoalPlan(path, errors) {
1220
1250
  if (!isFile(path)) return null;
1221
1251
  const { meta, body } = parseFrontmatter(path);
1222
- if (meta.artifact !== "goal-plan" || meta.schema_version !== SCHEMA_VERSION) {
1223
- 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}`);
1224
1271
  }
1225
1272
  const modes = requireList(meta, "modes", basename(path), errors);
1226
1273
  const invalidModes = modes.filter((mode) => !VALID_PLAN_MODES.has(mode));
1227
1274
  if (invalidModes.length) {
1228
1275
  errors.push(`${basename(path)}: invalid modes ${JSON.stringify(invalidModes)}`);
1229
1276
  }
1230
- const coordinationMode = meta.coordination_mode;
1231
- const workspaceStrategy = meta.workspace_strategy;
1232
- if ((coordinationMode === undefined) !== (workspaceStrategy === undefined)) {
1233
- errors.push(`${basename(path)}: coordination_mode and workspace_strategy must appear together`);
1277
+ if (meta.orchestration !== "lead-directed") {
1278
+ errors.push(`${basename(path)}: orchestration must be lead-directed`);
1234
1279
  }
1235
- if (coordinationMode !== undefined && !VALID_COORDINATION_MODES.has(coordinationMode)) {
1236
- errors.push(`${basename(path)}: invalid coordination_mode ${coordinationMode}`);
1280
+ if (typeof meta.lead !== "string" || !meta.lead.trim()) {
1281
+ errors.push(`${basename(path)}: lead must be a non-empty recoverable locator`);
1237
1282
  }
1238
- if (workspaceStrategy !== undefined && !VALID_WORKSPACE_STRATEGIES.has(workspaceStrategy)) {
1239
- errors.push(`${basename(path)}: invalid workspace_strategy ${workspaceStrategy}`);
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`);
1240
1285
  }
1241
-
1242
- const hasDelegatedMarkers = DELEGATED_GOAL_PLAN_TRIGGERS.some((marker) => body.includes(marker));
1243
- if (coordinationMode === "lead-team" && !hasDelegatedMarkers) {
1244
- errors.push(`${basename(path)}: lead-team requires a complete delegated execution addendum`);
1245
- } else if (coordinationMode === "single-session" && hasDelegatedMarkers) {
1246
- errors.push(`${basename(path)}: single-session cannot contain delegated execution markers`);
1286
+ if (meta.ticket_workspace_policy !== "required") {
1287
+ errors.push(`${basename(path)}: ticket_workspace_policy must be required`);
1247
1288
  }
1248
- validateDelegatedGoalPlan(body, basename(path), errors);
1249
-
1250
- const hasWorkspaceAddendum = body.includes("## Isolated Workspace Addendum");
1251
- if (new Set(["worktree", "mixed"]).has(workspaceStrategy)) {
1252
- validateWorkspaceGoalPlan(body, basename(path), errors, true);
1253
- } else if (workspaceStrategy === "current" && hasWorkspaceAddendum) {
1254
- errors.push(`${basename(path)}: current workspace_strategy cannot contain an isolated workspace addendum`);
1255
- } else if (workspaceStrategy === undefined) {
1256
- validateWorkspaceGoalPlan(body, basename(path), errors);
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`);
1257
1305
  }
1258
1306
  if (meta.ready_for_execution === true) {
1259
- if (!new Set(["ready", "in_progress", "completed"]).has(meta.status)) {
1260
- errors.push(`${basename(path)}: ready_for_execution conflicts with status`);
1261
- }
1262
1307
  for (const heading of REQUIRED_GOAL_PLAN_SECTIONS) {
1263
1308
  if (!body.includes(heading)) {
1264
1309
  errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
1265
1310
  }
1266
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
+ }
1267
1316
  const assumptions = sectionBody(body, "## Assumptions");
1268
1317
  if (assumptions && assumptions.includes("高影响") && !assumptions.includes("必须为 `false`")) {
1269
1318
  errors.push(`${basename(path)}: high-impact assumptions cannot remain in a ready Goal Plan`);
@@ -1449,8 +1498,8 @@ function validateTicket(path, errors) {
1449
1498
  errors.push(`${basename(path)}: missing frontmatter keys ${JSON.stringify(missing.sort())}`);
1450
1499
  return null;
1451
1500
  }
1452
- if (meta.artifact !== "ticket" || meta.schema_version !== SCHEMA_VERSION) {
1453
- 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}`);
1454
1503
  }
1455
1504
  const ticketId = String(meta.id);
1456
1505
  if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
@@ -1503,13 +1552,7 @@ function validateTicket(path, errors) {
1503
1552
  if (unresolved && !/^无[。.]?$/.test(unresolved.trim())) {
1504
1553
  errors.push(`${basename(path)}: Ready Ticket has unresolved questions`);
1505
1554
  }
1506
- if (
1507
- !lists.writable_paths.length &&
1508
- !body.includes("无代码变更") &&
1509
- !body.includes("仅文档")
1510
- ) {
1511
- errors.push(`${basename(path)}: Ready Ticket has no writable_paths and no no-code explanation`);
1512
- }
1555
+ if (!lists.writable_paths.length) errors.push(`${basename(path)}: Ready implementation Ticket requires writable_paths`);
1513
1556
  if (new Set(["standard", "deep"]).has(meta.planning_depth)) {
1514
1557
  for (const heading of ["## 5. 实现契约", "## 6. 执行路线"]) {
1515
1558
  if (!body.includes(heading)) {
@@ -1524,6 +1567,9 @@ function validateTicket(path, errors) {
1524
1567
  if (!verification || !verification.includes("|")) {
1525
1568
  errors.push(`${basename(path)}: Ready Ticket has no usable verification matrix`);
1526
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
+ }
1527
1573
  const acceptance = sectionBody(body, "## 10. 验收标准").toLowerCase();
1528
1574
  if (!acceptance.includes("- [ ]") && !acceptance.includes("- [x]")) {
1529
1575
  errors.push(`${basename(path)}: Ready Ticket has no acceptance checklist`);
@@ -1550,6 +1596,10 @@ function validateChangeStatus(path, expectedChange, errors) {
1550
1596
  "change",
1551
1597
  "change_status",
1552
1598
  "current_work",
1599
+ "works_run",
1600
+ "claimed_investigations",
1601
+ "execution_authorization",
1602
+ "leadership",
1553
1603
  "created_at",
1554
1604
  "updated_at",
1555
1605
  "completed_at",
@@ -1557,11 +1607,14 @@ function validateChangeStatus(path, expectedChange, errors) {
1557
1607
  "archive_path",
1558
1608
  "blockers",
1559
1609
  "deviations",
1610
+ "worktrees",
1560
1611
  ];
1561
1612
  const missing = required.filter((key) => !(key in data));
1562
1613
  if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
1563
- if (data.schema_version !== SCHEMA_VERSION || data.artifact !== "change-status") {
1564
- 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}`);
1565
1618
  }
1566
1619
  if (data.change !== expectedChange) {
1567
1620
  errors.push(`${basename(path)}: change must equal directory name ${expectedChange}`);
@@ -1569,17 +1622,45 @@ function validateChangeStatus(path, expectedChange, errors) {
1569
1622
  if (!new Set(["active", "blocked", "completed", "archived"]).has(data.change_status)) {
1570
1623
  errors.push(`${basename(path)}: invalid change_status ${data.change_status}`);
1571
1624
  }
1572
- if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations)) {
1573
- errors.push(`${basename(path)}: blockers and deviations must be arrays`);
1574
- }
1575
- if (data.worktrees !== undefined && !Array.isArray(data.worktrees)) {
1576
- 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`);
1577
1627
  }
1628
+ const seenWorktreeTickets = new Set();
1578
1629
  for (const worktree of Array.isArray(data.worktrees) ? data.worktrees : []) {
1579
1630
  if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
1580
1631
  errors.push(`${basename(path)}: invalid worktree entry`);
1581
1632
  continue;
1582
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);
1583
1664
  const ref = worktree.workspace_ref;
1584
1665
  if (
1585
1666
  typeof ref !== "string" ||
@@ -1592,21 +1673,20 @@ function validateChangeStatus(path, expectedChange, errors) {
1592
1673
  if (!VALID_WORKTREE_STATUS.has(worktree.status)) {
1593
1674
  errors.push(`${basename(path)}: invalid worktree status ${worktree.status}`);
1594
1675
  }
1595
- if (worktree.provider === "git") {
1596
- const expectedRef = `specdev-worktree/${worktree.ticket_id}`;
1597
- if (ref !== expectedRef) {
1598
- errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
1599
- }
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}`);
1600
1682
  }
1601
1683
 
1602
- const hasIntegrationContract = worktree.terminal_action !== undefined;
1603
- if (!hasIntegrationContract) {
1604
- if (worktree.status === "integrating") {
1605
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integrating requires the complete integration contract`);
1606
- }
1607
- continue;
1684
+ if ("terminal_action" in worktree) {
1685
+ errors.push(`${basename(path)}: worktree ${worktree.ticket_id} obsolete terminal_action is not allowed`);
1608
1686
  }
1609
1687
  const requiredIntegrationKeys = [
1688
+ "owner",
1689
+ "implementation_owner",
1610
1690
  "integration_owner",
1611
1691
  "parent_branch",
1612
1692
  "source_checkpoint",
@@ -1617,11 +1697,10 @@ function validateChangeStatus(path, expectedChange, errors) {
1617
1697
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
1618
1698
  continue;
1619
1699
  }
1620
- if (!VALID_TERMINAL_ACTION.has(worktree.terminal_action)) {
1621
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid terminal_action ${worktree.terminal_action}`);
1622
- }
1623
- if (typeof worktree.integration_owner !== "string" || !worktree.integration_owner.trim()) {
1624
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration_owner is required`);
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
+ }
1625
1704
  }
1626
1705
  if (typeof worktree.parent_branch !== "string" || !worktree.parent_branch.trim()) {
1627
1706
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch is required`);
@@ -1629,7 +1708,7 @@ function validateChangeStatus(path, expectedChange, errors) {
1629
1708
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch must differ from branch`);
1630
1709
  }
1631
1710
 
1632
- const sourceRequired = new Set(["review", "integrating", "integrated"]).has(worktree.status);
1711
+ const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(worktree.status);
1633
1712
  if (sourceRequired && (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())) {
1634
1713
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_checkpoint`);
1635
1714
  } else if (
@@ -1646,19 +1725,31 @@ function validateChangeStatus(path, expectedChange, errors) {
1646
1725
  }
1647
1726
  const requiredResultKeys = [
1648
1727
  "status",
1728
+ "parent_ref",
1649
1729
  "parent_before_sha",
1650
1730
  "source_sha",
1731
+ "candidate_sha",
1732
+ "candidate_tree_sha",
1733
+ "candidate_branch",
1734
+ "candidate_workspace_ref",
1651
1735
  "result_sha",
1652
1736
  "method",
1653
1737
  "conflict_paths",
1654
1738
  "verification",
1739
+ "full_suite",
1740
+ "e2e",
1655
1741
  "evidence",
1656
1742
  "attempts",
1743
+ "promotion_status",
1657
1744
  ];
1658
1745
  const missingResultKeys = requiredResultKeys.filter((key) => !(key in integration));
1659
1746
  if (missingResultKeys.length) {
1660
1747
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration missing ${JSON.stringify(missingResultKeys)}`);
1661
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
+ }
1662
1753
  if (!VALID_INTEGRATION_STATUS.has(integration.status)) {
1663
1754
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration status ${integration.status}`);
1664
1755
  }
@@ -1680,14 +1771,53 @@ function validateChangeStatus(path, expectedChange, errors) {
1680
1771
  ) {
1681
1772
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration evidence must be a rooted Ticket Evidence path`);
1682
1773
  }
1683
- if (worktree.terminal_action === "retain" && new Set(["integrating", "integrated"]).has(worktree.status)) {
1684
- errors.push(`${basename(path)}: retain worktree ${worktree.ticket_id} cannot be ${worktree.status}`);
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}`);
1685
1776
  }
1686
- const integrationLifecycleActive = new Set(["integrating", "integrated"]).has(worktree.status);
1687
- if (integrationLifecycleActive) {
1688
- if (worktree.terminal_action !== "integrate") {
1689
- errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires terminal_action=integrate`);
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`);
1690
1817
  }
1818
+ }
1819
+ const integrationLifecycleActive = new Set(["integrating", "integrated", "removed"]).has(worktree.status);
1820
+ if (integrationLifecycleActive) {
1691
1821
  if (typeof integration.parent_before_sha !== "string" || !integration.parent_before_sha.trim()) {
1692
1822
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires parent_before_sha`);
1693
1823
  }
@@ -1696,36 +1826,53 @@ function validateChangeStatus(path, expectedChange, errors) {
1696
1826
  } else if (integration.source_sha !== worktree.source_checkpoint) {
1697
1827
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_sha must equal source_checkpoint`);
1698
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
+ }
1699
1843
  if (!Number.isInteger(integration.attempts) || integration.attempts < 1) {
1700
1844
  errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires attempts >= 1`);
1701
1845
  }
1702
1846
  }
1703
- if (worktree.status === "integrating" && integration.status !== "running") {
1704
- errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=running`);
1847
+ if (worktree.status === "integrating" && integration.status !== "candidate") {
1848
+ errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=candidate`);
1705
1849
  }
1706
- if (worktree.status === "integrated") {
1850
+ if (new Set(["integrated", "removed"]).has(worktree.status)) {
1707
1851
  if (
1708
1852
  integration.status !== "passed" ||
1709
1853
  integration.verification !== "passed" ||
1710
1854
  !new Set(["fast-forward", "merge-commit"]).has(integration.method) ||
1711
1855
  typeof integration.result_sha !== "string" ||
1712
- !integration.result_sha.trim()
1856
+ !integration.result_sha.trim() ||
1857
+ integration.result_sha !== integration.candidate_sha ||
1858
+ !e2e ||
1859
+ !new Set(["not-required", "passed"]).has(e2e.status)
1713
1860
  ) {
1714
- errors.push(`${basename(path)}: integrated worktree ${worktree.ticket_id} requires a passed result checkpoint`);
1861
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} requires passed candidate/result/E2E state`);
1715
1862
  }
1716
1863
  if (integration.method === "fast-forward") {
1717
- if (integration.result_sha !== worktree.source_checkpoint) {
1718
- errors.push(`${basename(path)}: integrated worktree ${worktree.ticket_id} fast-forward result_sha must equal source_checkpoint`);
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`);
1719
1866
  }
1720
1867
  if (Array.isArray(integration.conflict_paths) && integration.conflict_paths.length > 0) {
1721
- errors.push(`${basename(path)}: integrated worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
1868
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
1722
1869
  }
1723
1870
  } else if (
1724
1871
  integration.method === "merge-commit" &&
1725
- typeof integration.result_sha === "string" &&
1726
- (integration.result_sha === worktree.source_checkpoint || integration.result_sha === integration.parent_before_sha)
1872
+ typeof integration.candidate_sha === "string" &&
1873
+ (integration.candidate_sha === worktree.source_checkpoint || integration.candidate_sha === integration.parent_before_sha)
1727
1874
  ) {
1728
- errors.push(`${basename(path)}: integrated worktree ${worktree.ticket_id} merge-commit result_sha must be a distinct checkpoint`);
1875
+ errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} merge-commit candidate must be a distinct checkpoint`);
1729
1876
  }
1730
1877
  }
1731
1878
  }
@@ -1837,6 +1984,25 @@ function validateChange(change, stage = null) {
1837
1984
  }
1838
1985
  }
1839
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
+
1840
2006
  if (spec) {
1841
2007
  const declaredContracts = new Set(spec.body.match(/\bAC-\d+\b/g) ?? []);
1842
2008
  const coveredContracts = new Set(
@@ -1923,18 +2089,29 @@ function validateChange(change, stage = null) {
1923
2089
  }
1924
2090
  const text = readText(evidence);
1925
2091
  for (const heading of [
1926
- "## 2. 修改范围",
1927
- "## 3. 验收与合同映射",
1928
- "## 4. 验证执行",
1929
- "## 5. 双轴审查",
1930
- "## 6. 路径所有权审计",
1931
- "## 7. 偏差与决策",
1932
- "## 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. 残余风险与交付定位",
1933
2100
  ]) {
1934
2101
  if (!text.includes(heading)) {
1935
2102
  errors.push(`${toPosix(relative(change, evidence))}: missing '${heading}'`);
1936
2103
  }
1937
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
+ }
1938
2115
  }
1939
2116
 
1940
2117
  if (goalPlan?.meta.ready_for_execution === true) {