@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.
- package/dist/src/migrations.js +604 -23
- package/dist/src/migrations.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +171 -455
- package/template/canonical/canonical-specdev-goal-plan.md +686 -1117
- package/template/canonical/canonical-specdev-grill-with-docs.md +172 -456
- package/template/canonical/canonical-specdev-spec.md +199 -493
- package/template/canonical/canonical-specdev-tickets.md +378 -597
- package/template/canonical/canonical-specdev-wayfinder.md +170 -454
- package/template/skills/migrate-runtime-state/SKILL.md +6 -6
- package/template/skills/migrate-runtime-state/references/migration-contract.md +9 -3
- package/template/skills/migrate-runtime-state/scripts/migrate-runtime-state.mjs +322 -33
- package/template/workflows/specdev/I-implement/I-implement.md +97 -143
- package/template/workflows/specdev/I-implement/evidence-template.md +60 -48
- package/template/workflows/specdev/I-implement/execution-preflight.md +29 -21
- package/template/workflows/specdev/I-implement/merge-conflict-protocol.md +12 -12
- package/template/workflows/specdev/I-init-setup/I-init-setup.md +4 -5
- package/template/workflows/specdev/I-init-setup/change-status-template.json +14 -1
- package/template/workflows/specdev/I-init-setup/config-template.json +3 -5
- package/template/workflows/specdev/I-init-setup/status-template.json +1 -1
- package/template/workflows/specdev/INDEX.md +11 -8
- package/template/workflows/specdev/P-goal-plan/P-goal-plan.md +76 -102
- package/template/workflows/specdev/P-goal-plan/completion-control.md +26 -44
- package/template/workflows/specdev/P-goal-plan/goal-plan-template.md +40 -37
- package/template/workflows/specdev/P-goal-plan/lead-orchestration.md +34 -0
- package/template/workflows/specdev/P-goal-plan/orchestration-protocol.md +31 -46
- package/template/workflows/specdev/P-goal-plan/planning-modes.md +42 -76
- package/template/workflows/specdev/T-tickets/T-tickets.md +6 -3
- package/template/workflows/specdev/T-tickets/ticket-readiness.md +5 -3
- package/template/workflows/specdev/T-tickets/ticket-template.md +8 -1
- package/template/workflows/specdev/T-tickets/tickets-map-template.md +5 -4
- package/template/workflows/specdev/_state/status.json +1 -1
- package/template/workflows/specdev/common/README.md +2 -2
- package/template/workflows/specdev/common/rules/change-completion.md +17 -20
- package/template/workflows/specdev/common/rules/deviation-control.md +1 -1
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +27 -37
- package/template/workflows/specdev/common/rules/path-ownership.md +21 -23
- package/template/workflows/specdev/common/rules/readiness-and-depth.md +1 -1
- package/template/workflows/specdev/common/schemas/change-status.schema.json +136 -373
- package/template/workflows/specdev/common/schemas/config.schema.json +9 -11
- package/template/workflows/specdev/common/schemas/goal-plan.schema.json +24 -16
- package/template/workflows/specdev/common/schemas/status.schema.json +7 -63
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +42 -21
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +36 -21
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +46 -18
- package/template/workflows/specdev/common/skills/subagent-delivery/SKILL.md +34 -31
- package/template/workflows/specdev/common/skills/subagent-delivery/references/external-web-subagent.md +10 -23
- package/template/workflows/specdev/common/skills/subagent-delivery/references/native-subagent.md +15 -25
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +386 -209
- package/template/workflows/specdev/I-implement/delegated-evidence-template.md +0 -12
- package/template/workflows/specdev/P-goal-plan/delegated-execution-template.md +0 -35
- package/template/workflows/specdev/P-goal-plan/delegated-execution.md +0 -59
- 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
|
|
22
|
-
const
|
|
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
|
|
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
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
"
|
|
763
|
+
"权威来源",
|
|
687
764
|
"DAG",
|
|
688
765
|
"Gate",
|
|
689
766
|
"Wave",
|
|
690
|
-
"
|
|
691
|
-
"
|
|
692
|
-
"
|
|
693
|
-
"lead-
|
|
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", "
|
|
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
|
|
796
|
-
if (isFile(
|
|
797
|
-
|
|
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
|
-
"
|
|
800
|
-
"
|
|
801
|
-
"
|
|
802
|
-
|
|
803
|
-
|
|
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(
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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 !==
|
|
1160
|
-
errors.push(`${label}: artifact/schema_version must be spec/${
|
|
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 !==
|
|
1189
|
-
errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${
|
|
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
|
-
|
|
1223
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
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 (
|
|
1236
|
-
errors.push(`${basename(path)}:
|
|
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 (
|
|
1239
|
-
errors.push(`${basename(path)}:
|
|
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
|
-
|
|
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
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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 !==
|
|
1453
|
-
errors.push(`${basename(path)}: artifact/schema_version must be ticket/${
|
|
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
|
-
|
|
1564
|
-
|
|
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
|
|
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
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1603
|
-
|
|
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
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
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 (
|
|
1684
|
-
errors.push(`${basename(path)}:
|
|
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
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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 !== "
|
|
1704
|
-
errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=
|
|
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 (
|
|
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)}:
|
|
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.
|
|
1718
|
-
errors.push(`${basename(path)}:
|
|
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)}:
|
|
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.
|
|
1726
|
-
(integration.
|
|
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)}:
|
|
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) {
|