@namewta/speculo 0.7.2 → 0.7.4
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 +755 -23
- package/dist/src/migrations.js.map +1 -1
- package/package.json +1 -1
- package/template/canonical/canonical-specdev-engineering-cognitive-mentor.md +200 -448
- package/template/canonical/canonical-specdev-goal-plan.md +744 -1108
- package/template/canonical/canonical-specdev-grill-with-docs.md +201 -449
- package/template/canonical/canonical-specdev-spec.md +232 -486
- package/template/canonical/canonical-specdev-tickets.md +411 -590
- package/template/canonical/canonical-specdev-wayfinder.md +199 -447
- 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 +404 -33
- package/template/workflows/specdev/I-implement/I-implement.md +98 -143
- package/template/workflows/specdev/I-implement/evidence-template.md +66 -48
- package/template/workflows/specdev/I-implement/execution-preflight.md +31 -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 +7 -6
- 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 +46 -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 +67 -89
- package/template/workflows/specdev/P-prototype/ui-prototype.md +1 -1
- 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 +31 -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 +153 -363
- package/template/workflows/specdev/common/schemas/config.schema.json +17 -13
- package/template/workflows/specdev/common/schemas/goal-plan.schema.json +33 -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/README.md +2 -1
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +591 -219
- 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
|
@@ -15,13 +15,18 @@ import {
|
|
|
15
15
|
readdirSync,
|
|
16
16
|
statSync,
|
|
17
17
|
} from "node:fs";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
18
19
|
import { dirname, basename, extname, join, relative, resolve, sep } from "node:path";
|
|
19
20
|
import { fileURLToPath } from "node:url";
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
const
|
|
22
|
+
const DOMAIN_SCHEMA_VERSION = 3;
|
|
23
|
+
const CONFIG_SCHEMA_VERSION = 5;
|
|
24
|
+
const GOAL_PLAN_SCHEMA_VERSION = 6;
|
|
25
|
+
const CHANGE_STATUS_SCHEMA_VERSION = 6;
|
|
26
|
+
const GLOBAL_STATUS_SCHEMA_VERSION = 5;
|
|
23
27
|
const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
|
|
24
28
|
const STATE_PREFIX = "{roots.state}/specdev/";
|
|
29
|
+
const STATE_ROOT_PREFIX = "{roots.state}/";
|
|
25
30
|
const SKILLS_PREFIX = "{roots.skills}/";
|
|
26
31
|
const COMMANDS_PREFIX = "{roots.commands}/";
|
|
27
32
|
|
|
@@ -55,14 +60,11 @@ const VALID_TICKET_STATUS = new Set([
|
|
|
55
60
|
const VALID_DEPTH = new Set(["lite", "standard", "deep"]);
|
|
56
61
|
const VALID_RISK = new Set(["low", "medium", "high", "critical"]);
|
|
57
62
|
const VALID_PLAN_MODES = new Set([
|
|
58
|
-
"coordination",
|
|
59
63
|
"migration",
|
|
60
64
|
"high-assurance",
|
|
61
65
|
"reference-conformance",
|
|
62
66
|
"release-coordination",
|
|
63
67
|
]);
|
|
64
|
-
const VALID_COORDINATION_MODES = new Set(["single-session", "lead-team"]);
|
|
65
|
-
const VALID_WORKSPACE_STRATEGIES = new Set(["current", "worktree", "mixed"]);
|
|
66
68
|
const VALID_WORKTREE_STATUS = new Set([
|
|
67
69
|
"planned",
|
|
68
70
|
"active",
|
|
@@ -72,10 +74,10 @@ const VALID_WORKTREE_STATUS = new Set([
|
|
|
72
74
|
"removed",
|
|
73
75
|
"blocked",
|
|
74
76
|
]);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const VALID_INTEGRATION_METHOD = new Set([null, "fast-forward", "merge-commit"]);
|
|
77
|
+
const VALID_INTEGRATION_STATUS = new Set(["pending", "candidate", "passed", "failed", "stale"]);
|
|
78
|
+
const VALID_INTEGRATION_METHOD = new Set([null, "direct-parent", "fast-forward", "merge-commit"]);
|
|
78
79
|
const VALID_INTEGRATION_VERIFICATION = new Set(["pending", "passed", "failed"]);
|
|
80
|
+
const VALID_E2E_STATUS = new Set(["not-required", "pending", "passed", "failed"]);
|
|
79
81
|
const VALID_DESIGN_TREE_STATUS = new Set(["active", "consensus", "blocked"]);
|
|
80
82
|
const VALID_DESIGN_NODE_STATUS = new Set(["open", "answered", "deferred", "rejected"]);
|
|
81
83
|
const VALID_WAYFINDER_LABEL = new Set([
|
|
@@ -142,39 +144,14 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
|
|
|
142
144
|
"## 5. Constraints, Risk and Recovery",
|
|
143
145
|
"## 6. Progress and Decisions",
|
|
144
146
|
]);
|
|
145
|
-
const
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"
|
|
152
|
-
"
|
|
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",
|
|
147
|
+
const REQUIRED_LEAD_GOAL_PLAN_MARKERS = [
|
|
148
|
+
"### Lead Orchestration",
|
|
149
|
+
"Implementation subagents",
|
|
150
|
+
"Read-only agents",
|
|
151
|
+
"execution-time dynamic",
|
|
152
|
+
"### Ticket Workspace and Integration",
|
|
153
|
+
"### Authorization Matrix",
|
|
154
|
+
"Implementation commit",
|
|
178
155
|
];
|
|
179
156
|
const STATE_ARTIFACT_BASENAMES = new Set([
|
|
180
157
|
"spec.md",
|
|
@@ -195,6 +172,10 @@ const STATE_ARTIFACT_BASENAMES = new Set([
|
|
|
195
172
|
]);
|
|
196
173
|
const FORBIDDEN_OBSOLETE_BASENAMES = new Set([
|
|
197
174
|
"source-issue.md",
|
|
175
|
+
"delegated-execution.md",
|
|
176
|
+
"delegated-execution-template.md",
|
|
177
|
+
"workspace-execution-template.md",
|
|
178
|
+
"delegated-evidence-template.md",
|
|
198
179
|
"input-validation.md",
|
|
199
180
|
"vision-sections.md",
|
|
200
181
|
"execution-sections.md",
|
|
@@ -263,6 +244,28 @@ function parseScalar(raw) {
|
|
|
263
244
|
return value;
|
|
264
245
|
}
|
|
265
246
|
|
|
247
|
+
function findSpecdevConfig(change) {
|
|
248
|
+
let current = resolve(change);
|
|
249
|
+
while (true) {
|
|
250
|
+
const candidate = join(current, ".speculo", "specdev", "config.json");
|
|
251
|
+
if (isFile(candidate)) {
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(readText(candidate));
|
|
254
|
+
} catch {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const parent = dirname(current);
|
|
259
|
+
if (parent === current) return null;
|
|
260
|
+
current = parent;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function positiveConfigLimit(config, key, fallback) {
|
|
265
|
+
const value = config?.execution?.[key];
|
|
266
|
+
return Number.isInteger(value) && value >= 1 ? value : fallback;
|
|
267
|
+
}
|
|
268
|
+
|
|
266
269
|
function parseFrontmatter(path) {
|
|
267
270
|
const text = readText(path);
|
|
268
271
|
const lines = text.split(/\r?\n/);
|
|
@@ -415,6 +418,19 @@ function validatePathValue(value, { allowProject = true } = {}) {
|
|
|
415
418
|
}
|
|
416
419
|
return null;
|
|
417
420
|
}
|
|
421
|
+
if (value.startsWith(STATE_ROOT_PREFIX)) {
|
|
422
|
+
const suffix = value.slice(STATE_ROOT_PREFIX.length);
|
|
423
|
+
if (
|
|
424
|
+
!suffix ||
|
|
425
|
+
suffix.startsWith("/") ||
|
|
426
|
+
suffix.startsWith("./") ||
|
|
427
|
+
suffix.startsWith("../") ||
|
|
428
|
+
suffix.includes("/../")
|
|
429
|
+
) {
|
|
430
|
+
return "state-root Path is not canonical";
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
418
434
|
for (const [prefix, name] of [[SKILLS_PREFIX, "skills"], [COMMANDS_PREFIX, "commands"]]) {
|
|
419
435
|
if (value.startsWith(prefix)) {
|
|
420
436
|
const suffix = value.slice(prefix.length);
|
|
@@ -616,7 +632,7 @@ function validateGlobalStatusAssets(root) {
|
|
|
616
632
|
!Array.isArray(data.active) ||
|
|
617
633
|
!Array.isArray(data.archived)
|
|
618
634
|
) {
|
|
619
|
-
errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status
|
|
635
|
+
errors.push(`${toPosix(relative(root, path))}: invalid SpecDev global status v5 seed`);
|
|
620
636
|
}
|
|
621
637
|
}
|
|
622
638
|
|
|
@@ -627,11 +643,11 @@ function validateGlobalStatusAssets(root) {
|
|
|
627
643
|
}
|
|
628
644
|
const schema = JSON.parse(readText(schemaPath));
|
|
629
645
|
if (
|
|
630
|
-
schema.$id !== "urn:speculo:specdev:status:
|
|
646
|
+
schema.$id !== "urn:speculo:specdev:status:v5" ||
|
|
631
647
|
schema.properties?.schema_version?.const !== GLOBAL_STATUS_SCHEMA_VERSION ||
|
|
632
648
|
schema.additionalProperties !== false
|
|
633
649
|
) {
|
|
634
|
-
errors.push("status.schema.json must define the strict SpecDev global status
|
|
650
|
+
errors.push("status.schema.json must define the strict SpecDev global status v5 index contract");
|
|
635
651
|
}
|
|
636
652
|
for (const removed of ["work_history", "completed", "result"]) {
|
|
637
653
|
if (JSON.stringify(schema).includes(`\"${removed}\"`)) {
|
|
@@ -641,6 +657,94 @@ function validateGlobalStatusAssets(root) {
|
|
|
641
657
|
return errors;
|
|
642
658
|
}
|
|
643
659
|
|
|
660
|
+
function validateExecutionContractAssets(root) {
|
|
661
|
+
const errors = [];
|
|
662
|
+
const configTemplatePath = join(root, "I-init-setup", "config-template.json");
|
|
663
|
+
const changeTemplatePath = join(root, "I-init-setup", "change-status-template.json");
|
|
664
|
+
const configSchemaPath = join(root, "common", "schemas", "config.schema.json");
|
|
665
|
+
const goalPlanSchemaPath = join(root, "common", "schemas", "goal-plan.schema.json");
|
|
666
|
+
const changeSchemaPath = join(root, "common", "schemas", "change-status.schema.json");
|
|
667
|
+
|
|
668
|
+
for (const path of [
|
|
669
|
+
configTemplatePath,
|
|
670
|
+
changeTemplatePath,
|
|
671
|
+
configSchemaPath,
|
|
672
|
+
goalPlanSchemaPath,
|
|
673
|
+
changeSchemaPath,
|
|
674
|
+
]) {
|
|
675
|
+
if (!isFile(path)) errors.push(`missing execution contract asset ${toPosix(relative(root, path))}`);
|
|
676
|
+
}
|
|
677
|
+
if (errors.length) return errors;
|
|
678
|
+
|
|
679
|
+
const configTemplate = JSON.parse(readText(configTemplatePath));
|
|
680
|
+
if (
|
|
681
|
+
configTemplate.schema_version !== CONFIG_SCHEMA_VERSION ||
|
|
682
|
+
!Number.isInteger(configTemplate.execution?.max_implementation_agents) ||
|
|
683
|
+
configTemplate.execution.max_implementation_agents < 1 ||
|
|
684
|
+
!Number.isInteger(configTemplate.execution?.max_integration_attempts) ||
|
|
685
|
+
configTemplate.execution.max_integration_attempts < 1 ||
|
|
686
|
+
!Number.isInteger(configTemplate.planning?.ui_prototype_default_variants) ||
|
|
687
|
+
!Number.isInteger(configTemplate.planning?.ui_prototype_max_variants) ||
|
|
688
|
+
configTemplate.planning.ui_prototype_default_variants < 1 ||
|
|
689
|
+
configTemplate.planning.ui_prototype_max_variants < configTemplate.planning.ui_prototype_default_variants
|
|
690
|
+
) {
|
|
691
|
+
errors.push("config-template.json must define SpecDev config v5 with positive execution limits and valid prototype variant bounds");
|
|
692
|
+
}
|
|
693
|
+
for (const obsolete of ["auto_commit", "worktree_for_parallel", "max_parallel"]) {
|
|
694
|
+
if (JSON.stringify(configTemplate).includes(`\"${obsolete}\"`)) {
|
|
695
|
+
errors.push(`config-template.json still contains obsolete execution field ${obsolete}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const configSchema = JSON.parse(readText(configSchemaPath));
|
|
700
|
+
const limit = configSchema.properties?.execution?.properties?.max_implementation_agents;
|
|
701
|
+
if (
|
|
702
|
+
configSchema.$id !== "urn:speculo:specdev:config:v5" ||
|
|
703
|
+
configSchema.properties?.schema_version?.const !== CONFIG_SCHEMA_VERSION ||
|
|
704
|
+
limit?.minimum !== 1 ||
|
|
705
|
+
configSchema.properties?.execution?.properties?.max_integration_attempts?.minimum !== 1 ||
|
|
706
|
+
configSchema.additionalProperties !== false
|
|
707
|
+
) {
|
|
708
|
+
errors.push("config.schema.json must define the strict SpecDev config v5 execution contract");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const goalPlanSchema = JSON.parse(readText(goalPlanSchemaPath));
|
|
712
|
+
if (
|
|
713
|
+
goalPlanSchema.$id !== "urn:speculo:specdev:goal-plan:v6" ||
|
|
714
|
+
goalPlanSchema.properties?.schema_version?.const !== GOAL_PLAN_SCHEMA_VERSION ||
|
|
715
|
+
goalPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
|
|
716
|
+
goalPlanSchema.properties?.implementation_agent_limit?.minimum !== 1 ||
|
|
717
|
+
goalPlanSchema.properties?.integration_attempt_limit?.minimum !== 1 ||
|
|
718
|
+
!["current", "required"].every((value) => goalPlanSchema.properties?.ticket_workspace_policy?.enum?.includes(value)) ||
|
|
719
|
+
!["direct-parent", "candidate-merge"].every((value) => goalPlanSchema.properties?.integration_gate?.enum?.includes(value)) ||
|
|
720
|
+
goalPlanSchema.additionalProperties !== false
|
|
721
|
+
) {
|
|
722
|
+
errors.push("goal-plan.schema.json must define the strict Lead-directed Goal Plan v6 workspace strategy contract");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const changeTemplate = JSON.parse(readText(changeTemplatePath));
|
|
726
|
+
if (
|
|
727
|
+
changeTemplate.schema_version !== CHANGE_STATUS_SCHEMA_VERSION ||
|
|
728
|
+
changeTemplate.artifact !== "change-status" ||
|
|
729
|
+
!Array.isArray(changeTemplate.worktrees)
|
|
730
|
+
) {
|
|
731
|
+
errors.push("change-status-template.json must define the SpecDev change-status v6 seed");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const changeSchema = JSON.parse(readText(changeSchemaPath));
|
|
735
|
+
const worktreeRequired = new Set(changeSchema.$defs?.worktree?.required ?? []);
|
|
736
|
+
const integrationRequired = new Set(changeSchema.$defs?.integration?.required ?? []);
|
|
737
|
+
if (
|
|
738
|
+
changeSchema.$id !== "urn:speculo:specdev:change-status:v6" ||
|
|
739
|
+
!["implementation_owner", "integration_owner", "source_checkpoint", "integration"].every((key) => worktreeRequired.has(key)) ||
|
|
740
|
+
!["parent_ref", "candidate_sha", "candidate_tree_sha", "candidate_branch", "candidate_workspace_ref", "full_suite", "e2e", "promotion_status"].every((key) => integrationRequired.has(key)) ||
|
|
741
|
+
changeSchema.additionalProperties !== false
|
|
742
|
+
) {
|
|
743
|
+
errors.push("change-status.schema.json must define the strict Ticket integration v6 contract");
|
|
744
|
+
}
|
|
745
|
+
return errors;
|
|
746
|
+
}
|
|
747
|
+
|
|
644
748
|
function capabilityChecks(root) {
|
|
645
749
|
const checks = new Map([
|
|
646
750
|
[
|
|
@@ -683,15 +787,14 @@ function capabilityChecks(root) {
|
|
|
683
787
|
join(root, "P-goal-plan", "P-goal-plan.md"),
|
|
684
788
|
[
|
|
685
789
|
"Outcome",
|
|
686
|
-
"
|
|
790
|
+
"权威来源",
|
|
687
791
|
"DAG",
|
|
688
792
|
"Gate",
|
|
689
793
|
"Wave",
|
|
690
|
-
"
|
|
691
|
-
"
|
|
692
|
-
"
|
|
693
|
-
"lead-
|
|
694
|
-
"delegated-execution.md",
|
|
794
|
+
"lead-directed",
|
|
795
|
+
"implementation_agent_limit",
|
|
796
|
+
"candidate-merge",
|
|
797
|
+
"lead-orchestration.md",
|
|
695
798
|
"Definition of Done",
|
|
696
799
|
],
|
|
697
800
|
],
|
|
@@ -700,7 +803,7 @@ function capabilityChecks(root) {
|
|
|
700
803
|
"implement",
|
|
701
804
|
[
|
|
702
805
|
join(root, "I-implement", "I-implement.md"),
|
|
703
|
-
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "
|
|
806
|
+
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "parent-candidate", "subagent-delivery", "实现 commit"],
|
|
704
807
|
],
|
|
705
808
|
],
|
|
706
809
|
[
|
|
@@ -792,64 +895,32 @@ function capabilityChecks(root) {
|
|
|
792
895
|
errors.push("Wayfinder map template must query open Tickets instead of caching them");
|
|
793
896
|
}
|
|
794
897
|
|
|
795
|
-
const
|
|
796
|
-
if (isFile(
|
|
797
|
-
|
|
898
|
+
const goalPlanTemplate = join(root, "P-goal-plan", "goal-plan-template.md");
|
|
899
|
+
if (!isFile(goalPlanTemplate)) {
|
|
900
|
+
errors.push("missing Goal Plan template");
|
|
901
|
+
} else {
|
|
902
|
+
const text = readText(goalPlanTemplate);
|
|
798
903
|
for (const required of [
|
|
799
|
-
"
|
|
800
|
-
"
|
|
801
|
-
"
|
|
904
|
+
"schema_version: 6",
|
|
905
|
+
"orchestration: lead-directed",
|
|
906
|
+
"implementation_agent_limit: 3",
|
|
907
|
+
"integration_attempt_limit: 3",
|
|
908
|
+
"ticket_workspace_policy: current",
|
|
909
|
+
"integration_gate: direct-parent",
|
|
910
|
+
...REQUIRED_LEAD_GOAL_PLAN_MARKERS,
|
|
911
|
+
"Local direct-parent verification and parent update",
|
|
802
912
|
]) {
|
|
803
|
-
if (!text.includes(required)) errors.push(`
|
|
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",
|
|
818
|
-
]) {
|
|
819
|
-
if (text.includes(forbidden)) {
|
|
820
|
-
errors.push(`ordinary Goal Plan template contains delegated marker ${forbidden}`);
|
|
821
|
-
}
|
|
913
|
+
if (!text.includes(required)) errors.push(`Goal Plan template lost marker ${required}`);
|
|
822
914
|
}
|
|
823
915
|
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (missing.length) errors.push(`isolated workspace Goal Plan template lost markers: ${JSON.stringify(missing)}`);
|
|
916
|
+
for (const obsolete of [
|
|
917
|
+
"P-goal-plan/delegated-execution.md",
|
|
918
|
+
"P-goal-plan/delegated-execution-template.md",
|
|
919
|
+
"P-goal-plan/workspace-execution-template.md",
|
|
920
|
+
"I-implement/delegated-evidence-template.md",
|
|
921
|
+
]) {
|
|
922
|
+
if (isFile(join(root, obsolete))) errors.push(`obsolete topology asset must be removed: ${obsolete}`);
|
|
832
923
|
}
|
|
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
924
|
return errors;
|
|
854
925
|
}
|
|
855
926
|
|
|
@@ -919,6 +990,7 @@ function selfCheck(root) {
|
|
|
919
990
|
|
|
920
991
|
errors.push(...validateJsonFiles(root));
|
|
921
992
|
errors.push(...validateGlobalStatusAssets(root));
|
|
993
|
+
errors.push(...validateExecutionContractAssets(root));
|
|
922
994
|
const references = validateDocumentReferences(root);
|
|
923
995
|
errors.push(...references.errors);
|
|
924
996
|
warnings.push(...references.warnings);
|
|
@@ -931,6 +1003,10 @@ function selfCheck(root) {
|
|
|
931
1003
|
"P-goal-plan/governance-sections.md",
|
|
932
1004
|
"P-goal-plan/lead-orchestration-protocol.md",
|
|
933
1005
|
"P-goal-plan/quick-reference-table.md",
|
|
1006
|
+
"P-goal-plan/delegated-execution.md",
|
|
1007
|
+
"P-goal-plan/delegated-execution-template.md",
|
|
1008
|
+
"P-goal-plan/workspace-execution-template.md",
|
|
1009
|
+
"I-implement/delegated-evidence-template.md",
|
|
934
1010
|
"schemas",
|
|
935
1011
|
"tools",
|
|
936
1012
|
"skills",
|
|
@@ -1156,8 +1232,8 @@ function validateSpec(path, errors, warnings) {
|
|
|
1156
1232
|
}
|
|
1157
1233
|
const { meta, body } = parseFrontmatter(path);
|
|
1158
1234
|
const label = basename(path);
|
|
1159
|
-
if (meta.artifact !== "spec" || meta.schema_version !==
|
|
1160
|
-
errors.push(`${label}: artifact/schema_version must be spec/${
|
|
1235
|
+
if (meta.artifact !== "spec" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1236
|
+
errors.push(`${label}: artifact/schema_version must be spec/${DOMAIN_SCHEMA_VERSION}`);
|
|
1161
1237
|
}
|
|
1162
1238
|
const sources = requireList(meta, "sources", label, errors);
|
|
1163
1239
|
if (!sources.length) errors.push(`${label}: sources must contain at least one source`);
|
|
@@ -1185,8 +1261,8 @@ function validateMap(path, errors) {
|
|
|
1185
1261
|
return null;
|
|
1186
1262
|
}
|
|
1187
1263
|
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/${
|
|
1264
|
+
if (meta.artifact !== "tickets-map" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1265
|
+
errors.push(`${basename(path)}: artifact/schema_version must be tickets-map/${DOMAIN_SCHEMA_VERSION}`);
|
|
1190
1266
|
}
|
|
1191
1267
|
for (const heading of [
|
|
1192
1268
|
"## 2. 执行清单",
|
|
@@ -1199,71 +1275,86 @@ function validateMap(path, errors) {
|
|
|
1199
1275
|
return { path, meta, body };
|
|
1200
1276
|
}
|
|
1201
1277
|
|
|
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
1278
|
function validateGoalPlan(path, errors) {
|
|
1220
1279
|
if (!isFile(path)) return null;
|
|
1221
1280
|
const { meta, body } = parseFrontmatter(path);
|
|
1222
|
-
|
|
1223
|
-
|
|
1281
|
+
const required = [
|
|
1282
|
+
"schema_version",
|
|
1283
|
+
"artifact",
|
|
1284
|
+
"change",
|
|
1285
|
+
"status",
|
|
1286
|
+
"modes",
|
|
1287
|
+
"orchestration",
|
|
1288
|
+
"lead",
|
|
1289
|
+
"implementation_agent_limit",
|
|
1290
|
+
"integration_attempt_limit",
|
|
1291
|
+
"ticket_workspace_policy",
|
|
1292
|
+
"integration_gate",
|
|
1293
|
+
"ready_for_execution",
|
|
1294
|
+
];
|
|
1295
|
+
const missing = required.filter((key) => !(key in meta));
|
|
1296
|
+
if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
|
|
1297
|
+
const unexpected = Object.keys(meta).filter((key) => !required.includes(key));
|
|
1298
|
+
if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
1299
|
+
if (meta.artifact !== "goal-plan" || meta.schema_version !== GOAL_PLAN_SCHEMA_VERSION) {
|
|
1300
|
+
errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/${GOAL_PLAN_SCHEMA_VERSION}`);
|
|
1224
1301
|
}
|
|
1225
1302
|
const modes = requireList(meta, "modes", basename(path), errors);
|
|
1226
1303
|
const invalidModes = modes.filter((mode) => !VALID_PLAN_MODES.has(mode));
|
|
1227
1304
|
if (invalidModes.length) {
|
|
1228
1305
|
errors.push(`${basename(path)}: invalid modes ${JSON.stringify(invalidModes)}`);
|
|
1229
1306
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if ((coordinationMode === undefined) !== (workspaceStrategy === undefined)) {
|
|
1233
|
-
errors.push(`${basename(path)}: coordination_mode and workspace_strategy must appear together`);
|
|
1307
|
+
if (meta.orchestration !== "lead-directed") {
|
|
1308
|
+
errors.push(`${basename(path)}: orchestration must be lead-directed`);
|
|
1234
1309
|
}
|
|
1235
|
-
if (
|
|
1236
|
-
errors.push(`${basename(path)}:
|
|
1310
|
+
if (typeof meta.lead !== "string" || !meta.lead.trim()) {
|
|
1311
|
+
errors.push(`${basename(path)}: lead must be a non-empty recoverable locator`);
|
|
1237
1312
|
}
|
|
1238
|
-
if (
|
|
1239
|
-
errors.push(`${basename(path)}:
|
|
1313
|
+
if (!Number.isInteger(meta.implementation_agent_limit) || meta.implementation_agent_limit < 1) {
|
|
1314
|
+
errors.push(`${basename(path)}: implementation_agent_limit must be a positive integer`);
|
|
1240
1315
|
}
|
|
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`);
|
|
1316
|
+
if (!Number.isInteger(meta.integration_attempt_limit) || meta.integration_attempt_limit < 1) {
|
|
1317
|
+
errors.push(`${basename(path)}: integration_attempt_limit must be a positive integer`);
|
|
1247
1318
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (new Set(["
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1319
|
+
if (!new Set(["current", "required"]).has(meta.ticket_workspace_policy)) {
|
|
1320
|
+
errors.push(`${basename(path)}: ticket_workspace_policy must be current or required`);
|
|
1321
|
+
}
|
|
1322
|
+
if (!new Set(["direct-parent", "candidate-merge"]).has(meta.integration_gate)) {
|
|
1323
|
+
errors.push(`${basename(path)}: integration_gate must be direct-parent or candidate-merge`);
|
|
1324
|
+
}
|
|
1325
|
+
if (
|
|
1326
|
+
(meta.ticket_workspace_policy === "current" && meta.integration_gate !== "direct-parent") ||
|
|
1327
|
+
(meta.ticket_workspace_policy === "required" && meta.integration_gate !== "candidate-merge")
|
|
1328
|
+
) {
|
|
1329
|
+
errors.push(`${basename(path)}: ticket_workspace_policy and integration_gate must use current/direct-parent or required/candidate-merge`);
|
|
1330
|
+
}
|
|
1331
|
+
for (const obsolete of ["coordination_mode", "workspace_strategy", "terminal_action"]) {
|
|
1332
|
+
if (obsolete in meta) errors.push(`${basename(path)}: obsolete Goal Plan field ${obsolete} is not allowed`);
|
|
1333
|
+
}
|
|
1334
|
+
for (const obsolete of ["## Delegated Execution Addendum", "## Isolated Workspace Addendum"]) {
|
|
1335
|
+
if (body.includes(obsolete)) errors.push(`${basename(path)}: obsolete Goal Plan addendum '${obsolete}' is not allowed`);
|
|
1336
|
+
}
|
|
1337
|
+
const validReadyStates = new Set(["ready", "in_progress"]);
|
|
1338
|
+
const validNotReadyStates = new Set(["draft", "blocked", "completed"]);
|
|
1339
|
+
if (
|
|
1340
|
+
(meta.ready_for_execution === true && !validReadyStates.has(meta.status)) ||
|
|
1341
|
+
(meta.ready_for_execution === false && !validNotReadyStates.has(meta.status))
|
|
1342
|
+
) {
|
|
1343
|
+
errors.push(`${basename(path)}: ready_for_execution must match status`);
|
|
1257
1344
|
}
|
|
1258
1345
|
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
1346
|
for (const heading of REQUIRED_GOAL_PLAN_SECTIONS) {
|
|
1263
1347
|
if (!body.includes(heading)) {
|
|
1264
1348
|
errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
|
|
1265
1349
|
}
|
|
1266
1350
|
}
|
|
1351
|
+
const requiredMarkers = meta.ticket_workspace_policy === "current"
|
|
1352
|
+
? [...REQUIRED_LEAD_GOAL_PLAN_MARKERS, "Local direct-parent verification and parent update"]
|
|
1353
|
+
: [...REQUIRED_LEAD_GOAL_PLAN_MARKERS, "source worktree 不运行 E2E", "Local candidate integration and parent update"];
|
|
1354
|
+
const missingLeadMarkers = requiredMarkers.filter((marker) => !body.includes(marker));
|
|
1355
|
+
if (missingLeadMarkers.length) {
|
|
1356
|
+
errors.push(`${basename(path)}: ready Goal Plan missing Lead/workspace markers ${JSON.stringify(missingLeadMarkers)}`);
|
|
1357
|
+
}
|
|
1267
1358
|
const assumptions = sectionBody(body, "## Assumptions");
|
|
1268
1359
|
if (assumptions && assumptions.includes("高影响") && !assumptions.includes("必须为 `false`")) {
|
|
1269
1360
|
errors.push(`${basename(path)}: high-impact assumptions cannot remain in a ready Goal Plan`);
|
|
@@ -1272,6 +1363,27 @@ function validateGoalPlan(path, errors) {
|
|
|
1272
1363
|
return { path, meta, body };
|
|
1273
1364
|
}
|
|
1274
1365
|
|
|
1366
|
+
function validateGoalPlanRuntimeLimits(path, change, goalPlan, errors) {
|
|
1367
|
+
if (!goalPlan) return;
|
|
1368
|
+
const config = findSpecdevConfig(change);
|
|
1369
|
+
if (!config) {
|
|
1370
|
+
errors.push(`${basename(path)}: SpecDev config.json is required to validate execution limits`);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const configuredAgents = positiveConfigLimit(config, "max_implementation_agents", 0);
|
|
1374
|
+
const configuredAttempts = positiveConfigLimit(config, "max_integration_attempts", 0);
|
|
1375
|
+
if (config.schema_version !== CONFIG_SCHEMA_VERSION || configuredAgents === 0 || configuredAttempts === 0) {
|
|
1376
|
+
errors.push(`${basename(path)}: SpecDev config.json must use schema v${CONFIG_SCHEMA_VERSION} with positive execution limits`);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (goalPlan.meta.implementation_agent_limit > configuredAgents) {
|
|
1380
|
+
errors.push(`${basename(path)}: implementation_agent_limit ${goalPlan.meta.implementation_agent_limit} exceeds config max_implementation_agents ${configuredAgents}`);
|
|
1381
|
+
}
|
|
1382
|
+
if (goalPlan.meta.integration_attempt_limit > configuredAttempts) {
|
|
1383
|
+
errors.push(`${basename(path)}: integration_attempt_limit ${goalPlan.meta.integration_attempt_limit} exceeds config max_integration_attempts ${configuredAttempts}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1275
1387
|
function validateDesignTree(path, change, errors) {
|
|
1276
1388
|
if (!isFile(path)) return null;
|
|
1277
1389
|
let data;
|
|
@@ -1449,8 +1561,8 @@ function validateTicket(path, errors) {
|
|
|
1449
1561
|
errors.push(`${basename(path)}: missing frontmatter keys ${JSON.stringify(missing.sort())}`);
|
|
1450
1562
|
return null;
|
|
1451
1563
|
}
|
|
1452
|
-
if (meta.artifact !== "ticket" || meta.schema_version !==
|
|
1453
|
-
errors.push(`${basename(path)}: artifact/schema_version must be ticket/${
|
|
1564
|
+
if (meta.artifact !== "ticket" || meta.schema_version !== DOMAIN_SCHEMA_VERSION) {
|
|
1565
|
+
errors.push(`${basename(path)}: artifact/schema_version must be ticket/${DOMAIN_SCHEMA_VERSION}`);
|
|
1454
1566
|
}
|
|
1455
1567
|
const ticketId = String(meta.id);
|
|
1456
1568
|
if (!/^T-\d{2,}$/.test(ticketId)) errors.push(`${basename(path)}: invalid Ticket id ${ticketId}`);
|
|
@@ -1503,13 +1615,7 @@ function validateTicket(path, errors) {
|
|
|
1503
1615
|
if (unresolved && !/^无[。.]?$/.test(unresolved.trim())) {
|
|
1504
1616
|
errors.push(`${basename(path)}: Ready Ticket has unresolved questions`);
|
|
1505
1617
|
}
|
|
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
|
-
}
|
|
1618
|
+
if (!lists.writable_paths.length) errors.push(`${basename(path)}: Ready implementation Ticket requires writable_paths`);
|
|
1513
1619
|
if (new Set(["standard", "deep"]).has(meta.planning_depth)) {
|
|
1514
1620
|
for (const heading of ["## 5. 实现契约", "## 6. 执行路线"]) {
|
|
1515
1621
|
if (!body.includes(heading)) {
|
|
@@ -1524,6 +1630,9 @@ function validateTicket(path, errors) {
|
|
|
1524
1630
|
if (!verification || !verification.includes("|")) {
|
|
1525
1631
|
errors.push(`${basename(path)}: Ready Ticket has no usable verification matrix`);
|
|
1526
1632
|
}
|
|
1633
|
+
if (!verification.includes("E2E disposition") || !/(current-workspace|source-worktree|parent-candidate|direct-parent)/.test(verification)) {
|
|
1634
|
+
errors.push(`${basename(path)}: Ready Ticket must define E2E disposition and an explicit execution environment`);
|
|
1635
|
+
}
|
|
1527
1636
|
const acceptance = sectionBody(body, "## 10. 验收标准").toLowerCase();
|
|
1528
1637
|
if (!acceptance.includes("- [ ]") && !acceptance.includes("- [x]")) {
|
|
1529
1638
|
errors.push(`${basename(path)}: Ready Ticket has no acceptance checklist`);
|
|
@@ -1550,6 +1659,10 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1550
1659
|
"change",
|
|
1551
1660
|
"change_status",
|
|
1552
1661
|
"current_work",
|
|
1662
|
+
"works_run",
|
|
1663
|
+
"claimed_investigations",
|
|
1664
|
+
"execution_authorization",
|
|
1665
|
+
"leadership",
|
|
1553
1666
|
"created_at",
|
|
1554
1667
|
"updated_at",
|
|
1555
1668
|
"completed_at",
|
|
@@ -1557,11 +1670,14 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1557
1670
|
"archive_path",
|
|
1558
1671
|
"blockers",
|
|
1559
1672
|
"deviations",
|
|
1673
|
+
"worktrees",
|
|
1560
1674
|
];
|
|
1561
1675
|
const missing = required.filter((key) => !(key in data));
|
|
1562
1676
|
if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
|
|
1563
|
-
|
|
1564
|
-
|
|
1677
|
+
const unexpected = Object.keys(data).filter((key) => !required.includes(key));
|
|
1678
|
+
if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
1679
|
+
if (data.schema_version !== CHANGE_STATUS_SCHEMA_VERSION || data.artifact !== "change-status") {
|
|
1680
|
+
errors.push(`${basename(path)}: artifact/schema_version must be change-status/${CHANGE_STATUS_SCHEMA_VERSION}`);
|
|
1565
1681
|
}
|
|
1566
1682
|
if (data.change !== expectedChange) {
|
|
1567
1683
|
errors.push(`${basename(path)}: change must equal directory name ${expectedChange}`);
|
|
@@ -1569,17 +1685,45 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1569
1685
|
if (!new Set(["active", "blocked", "completed", "archived"]).has(data.change_status)) {
|
|
1570
1686
|
errors.push(`${basename(path)}: invalid change_status ${data.change_status}`);
|
|
1571
1687
|
}
|
|
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`);
|
|
1688
|
+
if (!Array.isArray(data.blockers) || !Array.isArray(data.deviations) || !Array.isArray(data.worktrees)) {
|
|
1689
|
+
errors.push(`${basename(path)}: blockers, deviations, and worktrees must be arrays`);
|
|
1577
1690
|
}
|
|
1691
|
+
const seenWorktreeTickets = new Set();
|
|
1578
1692
|
for (const worktree of Array.isArray(data.worktrees) ? data.worktrees : []) {
|
|
1579
1693
|
if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
|
|
1580
1694
|
errors.push(`${basename(path)}: invalid worktree entry`);
|
|
1581
1695
|
continue;
|
|
1582
1696
|
}
|
|
1697
|
+
const requiredWorktreeKeys = [
|
|
1698
|
+
"ticket_id",
|
|
1699
|
+
"owner",
|
|
1700
|
+
"implementation_owner",
|
|
1701
|
+
"integration_owner",
|
|
1702
|
+
"provider",
|
|
1703
|
+
"base_sha",
|
|
1704
|
+
"parent_branch",
|
|
1705
|
+
"branch",
|
|
1706
|
+
"workspace_ref",
|
|
1707
|
+
"source_checkpoint",
|
|
1708
|
+
"integration",
|
|
1709
|
+
"status",
|
|
1710
|
+
"updated_at",
|
|
1711
|
+
];
|
|
1712
|
+
const missingWorktreeKeys = requiredWorktreeKeys.filter((key) => !(key in worktree));
|
|
1713
|
+
if (missingWorktreeKeys.length) {
|
|
1714
|
+
errors.push(`${basename(path)}: worktree integration contract missing ${JSON.stringify(missingWorktreeKeys)}`);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const unexpectedWorktreeKeys = Object.keys(worktree).filter((key) => !requiredWorktreeKeys.includes(key));
|
|
1718
|
+
if (unexpectedWorktreeKeys.length) {
|
|
1719
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has unexpected keys ${JSON.stringify(unexpectedWorktreeKeys.sort())}`);
|
|
1720
|
+
}
|
|
1721
|
+
if (!/^T-[0-9]{2,}$/.test(String(worktree.ticket_id))) {
|
|
1722
|
+
errors.push(`${basename(path)}: invalid worktree ticket_id ${worktree.ticket_id}`);
|
|
1723
|
+
} else if (seenWorktreeTickets.has(worktree.ticket_id)) {
|
|
1724
|
+
errors.push(`${basename(path)}: duplicate worktree for ${worktree.ticket_id}`);
|
|
1725
|
+
}
|
|
1726
|
+
seenWorktreeTickets.add(worktree.ticket_id);
|
|
1583
1727
|
const ref = worktree.workspace_ref;
|
|
1584
1728
|
if (
|
|
1585
1729
|
typeof ref !== "string" ||
|
|
@@ -1592,21 +1736,21 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1592
1736
|
if (!VALID_WORKTREE_STATUS.has(worktree.status)) {
|
|
1593
1737
|
errors.push(`${basename(path)}: invalid worktree status ${worktree.status}`);
|
|
1594
1738
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1739
|
+
const currentWorkspace = ref === "current";
|
|
1740
|
+
if (worktree.provider !== "git") {
|
|
1741
|
+
errors.push(`${basename(path)}: Ticket worktree ${worktree.ticket_id} provider must be git`);
|
|
1742
|
+
}
|
|
1743
|
+
const expectedRef = `specdev-worktree/${expectedChange}/${worktree.ticket_id}`;
|
|
1744
|
+
if (!currentWorkspace && ref !== expectedRef) {
|
|
1745
|
+
errors.push(`${basename(path)}: git workspace_ref must equal ${expectedRef}`);
|
|
1600
1746
|
}
|
|
1601
1747
|
|
|
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;
|
|
1748
|
+
if ("terminal_action" in worktree) {
|
|
1749
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} obsolete terminal_action is not allowed`);
|
|
1608
1750
|
}
|
|
1609
1751
|
const requiredIntegrationKeys = [
|
|
1752
|
+
"owner",
|
|
1753
|
+
"implementation_owner",
|
|
1610
1754
|
"integration_owner",
|
|
1611
1755
|
"parent_branch",
|
|
1612
1756
|
"source_checkpoint",
|
|
@@ -1617,19 +1761,20 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1617
1761
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration contract missing ${JSON.stringify(missingIntegrationKeys)}`);
|
|
1618
1762
|
continue;
|
|
1619
1763
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration_owner is required`);
|
|
1764
|
+
for (const key of ["owner", "implementation_owner", "integration_owner", "base_sha", "parent_branch", "branch", "updated_at"]) {
|
|
1765
|
+
if (typeof worktree[key] !== "string" || !worktree[key].trim()) {
|
|
1766
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${key} is required`);
|
|
1767
|
+
}
|
|
1625
1768
|
}
|
|
1626
1769
|
if (typeof worktree.parent_branch !== "string" || !worktree.parent_branch.trim()) {
|
|
1627
1770
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch is required`);
|
|
1628
|
-
} else if (worktree.parent_branch
|
|
1771
|
+
} else if (currentWorkspace && worktree.parent_branch !== worktree.branch) {
|
|
1772
|
+
errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} branch must equal parent_branch`);
|
|
1773
|
+
} else if (!currentWorkspace && worktree.parent_branch === worktree.branch) {
|
|
1629
1774
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} parent_branch must differ from branch`);
|
|
1630
1775
|
}
|
|
1631
1776
|
|
|
1632
|
-
const sourceRequired = new Set(["review", "integrating", "integrated"]).has(worktree.status);
|
|
1777
|
+
const sourceRequired = new Set(["review", "integrating", "integrated", "removed"]).has(worktree.status);
|
|
1633
1778
|
if (sourceRequired && (typeof worktree.source_checkpoint !== "string" || !worktree.source_checkpoint.trim())) {
|
|
1634
1779
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires source_checkpoint`);
|
|
1635
1780
|
} else if (
|
|
@@ -1646,19 +1791,31 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1646
1791
|
}
|
|
1647
1792
|
const requiredResultKeys = [
|
|
1648
1793
|
"status",
|
|
1794
|
+
"parent_ref",
|
|
1649
1795
|
"parent_before_sha",
|
|
1650
1796
|
"source_sha",
|
|
1797
|
+
"candidate_sha",
|
|
1798
|
+
"candidate_tree_sha",
|
|
1799
|
+
"candidate_branch",
|
|
1800
|
+
"candidate_workspace_ref",
|
|
1651
1801
|
"result_sha",
|
|
1652
1802
|
"method",
|
|
1653
1803
|
"conflict_paths",
|
|
1654
1804
|
"verification",
|
|
1805
|
+
"full_suite",
|
|
1806
|
+
"e2e",
|
|
1655
1807
|
"evidence",
|
|
1656
1808
|
"attempts",
|
|
1809
|
+
"promotion_status",
|
|
1657
1810
|
];
|
|
1658
1811
|
const missingResultKeys = requiredResultKeys.filter((key) => !(key in integration));
|
|
1659
1812
|
if (missingResultKeys.length) {
|
|
1660
1813
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration missing ${JSON.stringify(missingResultKeys)}`);
|
|
1661
1814
|
}
|
|
1815
|
+
const unexpectedResultKeys = Object.keys(integration).filter((key) => !requiredResultKeys.includes(key));
|
|
1816
|
+
if (unexpectedResultKeys.length) {
|
|
1817
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration has unexpected keys ${JSON.stringify(unexpectedResultKeys.sort())}`);
|
|
1818
|
+
}
|
|
1662
1819
|
if (!VALID_INTEGRATION_STATUS.has(integration.status)) {
|
|
1663
1820
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid integration status ${integration.status}`);
|
|
1664
1821
|
}
|
|
@@ -1680,14 +1837,53 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1680
1837
|
) {
|
|
1681
1838
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration evidence must be a rooted Ticket Evidence path`);
|
|
1682
1839
|
}
|
|
1683
|
-
if (
|
|
1684
|
-
errors.push(`${basename(path)}:
|
|
1840
|
+
if (!new Set(["pending", "applying", "applied", "failed", "stale"]).has(integration.promotion_status)) {
|
|
1841
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} has invalid promotion_status ${integration.promotion_status}`);
|
|
1685
1842
|
}
|
|
1686
|
-
const
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1843
|
+
for (const [name, suite] of [["full_suite", integration.full_suite], ["e2e", integration.e2e]]) {
|
|
1844
|
+
if (!suite || typeof suite !== "object" || Array.isArray(suite) || typeof suite.required !== "boolean" || !VALID_E2E_STATUS.has(suite.status)) {
|
|
1845
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} contract is invalid`);
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const unexpectedSuiteKeys = Object.keys(suite).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
|
|
1849
|
+
if (unexpectedSuiteKeys.length) errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${name} has unexpected keys ${JSON.stringify(unexpectedSuiteKeys.sort())}`);
|
|
1850
|
+
if (suite.required === false && (suite.status !== "not-required" || typeof suite.reason !== "string" || !suite.reason.trim())) {
|
|
1851
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required ${name} needs not-required status and reason`);
|
|
1852
|
+
}
|
|
1853
|
+
if (suite.required === true && suite.status === "not-required") {
|
|
1854
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required ${name} cannot be not-required`);
|
|
1690
1855
|
}
|
|
1856
|
+
}
|
|
1857
|
+
const e2e = integration.e2e;
|
|
1858
|
+
if (
|
|
1859
|
+
!e2e ||
|
|
1860
|
+
typeof e2e !== "object" ||
|
|
1861
|
+
Array.isArray(e2e) ||
|
|
1862
|
+
typeof e2e.required !== "boolean" ||
|
|
1863
|
+
!VALID_E2E_STATUS.has(e2e.status)
|
|
1864
|
+
) {
|
|
1865
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e contract is invalid`);
|
|
1866
|
+
} else {
|
|
1867
|
+
const unexpectedE2eKeys = Object.keys(e2e).filter((key) => !new Set(["required", "status", "reason", "evidence"]).has(key));
|
|
1868
|
+
if (unexpectedE2eKeys.length) {
|
|
1869
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} integration e2e has unexpected keys ${JSON.stringify(unexpectedE2eKeys.sort())}`);
|
|
1870
|
+
}
|
|
1871
|
+
if (e2e.required === false && e2e.status !== "not-required") {
|
|
1872
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} non-required E2E must use status=not-required`);
|
|
1873
|
+
}
|
|
1874
|
+
if (e2e.required === true && e2e.status === "not-required") {
|
|
1875
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} required E2E cannot be not-required`);
|
|
1876
|
+
}
|
|
1877
|
+
if (
|
|
1878
|
+
e2e.required === true &&
|
|
1879
|
+
e2e.status === "passed" &&
|
|
1880
|
+
(typeof e2e.evidence !== "string" || !e2e.evidence.trim())
|
|
1881
|
+
) {
|
|
1882
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} passed required E2E needs evidence`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const integrationLifecycleActive = new Set(["integrating", "integrated", "removed"]).has(worktree.status);
|
|
1886
|
+
if (integrationLifecycleActive) {
|
|
1691
1887
|
if (typeof integration.parent_before_sha !== "string" || !integration.parent_before_sha.trim()) {
|
|
1692
1888
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires parent_before_sha`);
|
|
1693
1889
|
}
|
|
@@ -1696,36 +1892,69 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1696
1892
|
} else if (integration.source_sha !== worktree.source_checkpoint) {
|
|
1697
1893
|
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} source_sha must equal source_checkpoint`);
|
|
1698
1894
|
}
|
|
1699
|
-
if (
|
|
1700
|
-
|
|
1895
|
+
if (currentWorkspace) {
|
|
1896
|
+
if (integration.candidate_sha !== null || integration.candidate_tree_sha !== null || integration.candidate_branch !== null || integration.candidate_workspace_ref !== null) {
|
|
1897
|
+
errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} cannot record candidate workspace fields`);
|
|
1898
|
+
}
|
|
1899
|
+
if (integration.method !== "direct-parent") {
|
|
1900
|
+
errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} requires integration.method=direct-parent`);
|
|
1901
|
+
}
|
|
1902
|
+
} else {
|
|
1903
|
+
if (typeof integration.candidate_sha !== "string" || !integration.candidate_sha.trim()) {
|
|
1904
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires candidate_sha`);
|
|
1905
|
+
}
|
|
1906
|
+
const expectedCandidateBranch = `speculo/integration/${expectedChange}/${worktree.ticket_id}`;
|
|
1907
|
+
if (integration.candidate_branch !== expectedCandidateBranch) {
|
|
1908
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_branch must equal ${expectedCandidateBranch}`);
|
|
1909
|
+
}
|
|
1910
|
+
const expectedCandidateRef = `specdev-worktree/.integration/${expectedChange}/${worktree.ticket_id}`;
|
|
1911
|
+
if (integration.candidate_workspace_ref !== expectedCandidateRef) {
|
|
1912
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} candidate_workspace_ref must equal ${expectedCandidateRef}`);
|
|
1913
|
+
}
|
|
1914
|
+
if (!new Set(["fast-forward", "merge-commit"]).has(integration.method)) {
|
|
1915
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires an integration method`);
|
|
1916
|
+
}
|
|
1701
1917
|
}
|
|
1918
|
+
if (!Number.isInteger(integration.attempts) || integration.attempts < 1) {
|
|
1919
|
+
errors.push(`${basename(path)}: worktree ${worktree.ticket_id} ${worktree.status} requires attempts >= 1`);
|
|
1920
|
+
}
|
|
1702
1921
|
}
|
|
1703
|
-
if (worktree.status === "integrating" && integration.status !== "
|
|
1704
|
-
errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=
|
|
1922
|
+
if (worktree.status === "integrating" && integration.status !== "candidate") {
|
|
1923
|
+
errors.push(`${basename(path)}: integrating worktree ${worktree.ticket_id} requires integration.status=candidate`);
|
|
1705
1924
|
}
|
|
1706
|
-
if (
|
|
1925
|
+
if (new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
1707
1926
|
if (
|
|
1708
1927
|
integration.status !== "passed" ||
|
|
1709
1928
|
integration.verification !== "passed" ||
|
|
1710
|
-
!new Set(["fast-forward", "merge-commit"]).has(integration.method) ||
|
|
1929
|
+
!(currentWorkspace ? integration.method === "direct-parent" : new Set(["fast-forward", "merge-commit"]).has(integration.method)) ||
|
|
1711
1930
|
typeof integration.result_sha !== "string" ||
|
|
1712
|
-
!integration.result_sha.trim()
|
|
1931
|
+
!integration.result_sha.trim() ||
|
|
1932
|
+
(currentWorkspace ? integration.result_sha !== worktree.source_checkpoint : integration.result_sha !== integration.candidate_sha) ||
|
|
1933
|
+
!e2e ||
|
|
1934
|
+
!new Set(["not-required", "passed"]).has(e2e.status)
|
|
1713
1935
|
) {
|
|
1714
|
-
errors.push(`${basename(path)}:
|
|
1936
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} requires passed candidate/result/E2E state`);
|
|
1715
1937
|
}
|
|
1716
|
-
if (integration.method === "
|
|
1938
|
+
if (currentWorkspace && integration.method === "direct-parent") {
|
|
1939
|
+
if (integration.candidate_sha !== null || integration.candidate_tree_sha !== null || integration.candidate_branch !== null || integration.candidate_workspace_ref !== null) {
|
|
1940
|
+
errors.push(`${basename(path)}: current workspace ${worktree.ticket_id} direct-parent cannot record candidate fields`);
|
|
1941
|
+
}
|
|
1717
1942
|
if (integration.result_sha !== worktree.source_checkpoint) {
|
|
1718
|
-
errors.push(`${basename(path)}:
|
|
1943
|
+
errors.push(`${basename(path)}: current workspace ${worktree.status} ${worktree.ticket_id} result_sha must equal source_checkpoint`);
|
|
1944
|
+
}
|
|
1945
|
+
} else if (integration.method === "fast-forward") {
|
|
1946
|
+
if (integration.candidate_sha !== worktree.source_checkpoint) {
|
|
1947
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward candidate/result must equal source_checkpoint`);
|
|
1719
1948
|
}
|
|
1720
1949
|
if (Array.isArray(integration.conflict_paths) && integration.conflict_paths.length > 0) {
|
|
1721
|
-
errors.push(`${basename(path)}:
|
|
1950
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} fast-forward cannot record conflict_paths`);
|
|
1722
1951
|
}
|
|
1723
1952
|
} else if (
|
|
1724
1953
|
integration.method === "merge-commit" &&
|
|
1725
|
-
typeof integration.
|
|
1726
|
-
(integration.
|
|
1954
|
+
typeof integration.candidate_sha === "string" &&
|
|
1955
|
+
(integration.candidate_sha === worktree.source_checkpoint || integration.candidate_sha === integration.parent_before_sha)
|
|
1727
1956
|
) {
|
|
1728
|
-
errors.push(`${basename(path)}:
|
|
1957
|
+
errors.push(`${basename(path)}: ${worktree.status} worktree ${worktree.ticket_id} merge-commit candidate must be a distinct checkpoint`);
|
|
1729
1958
|
}
|
|
1730
1959
|
}
|
|
1731
1960
|
}
|
|
@@ -1741,7 +1970,96 @@ function validateChangeStatus(path, expectedChange, errors) {
|
|
|
1741
1970
|
return data;
|
|
1742
1971
|
}
|
|
1743
1972
|
|
|
1744
|
-
function
|
|
1973
|
+
function validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors) {
|
|
1974
|
+
if (goalPlan?.meta.ticket_workspace_policy !== "current") return;
|
|
1975
|
+
const entries = Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : [];
|
|
1976
|
+
const active = [];
|
|
1977
|
+
for (const worktree of entries) {
|
|
1978
|
+
if (worktree.workspace_ref !== "current") {
|
|
1979
|
+
errors.push(`${worktree.ticket_id}: current Goal Plan requires workspace_ref=current; source worktrees are not allowed`);
|
|
1980
|
+
}
|
|
1981
|
+
if (worktree.parent_branch !== worktree.branch) {
|
|
1982
|
+
errors.push(`${worktree.ticket_id}: current workspace execution must commit directly on parent_branch`);
|
|
1983
|
+
}
|
|
1984
|
+
if (new Set(["active", "review", "integrating", "blocked"]).has(worktree.status)) {
|
|
1985
|
+
active.push(worktree.ticket_id);
|
|
1986
|
+
}
|
|
1987
|
+
const integration = worktree.integration;
|
|
1988
|
+
if (integration && typeof integration === "object" && !Array.isArray(integration)) {
|
|
1989
|
+
if (["candidate_sha", "candidate_tree_sha", "candidate_branch", "candidate_workspace_ref"].some((key) => integration[key] !== null)) {
|
|
1990
|
+
errors.push(`${worktree.ticket_id}: current workspace execution cannot record candidate workspace fields`);
|
|
1991
|
+
}
|
|
1992
|
+
if (integration.method !== null && integration.method !== "direct-parent") {
|
|
1993
|
+
errors.push(`${worktree.ticket_id}: current workspace execution requires direct-parent integration`);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (active.length > 1) {
|
|
1998
|
+
errors.push(`current Goal Plan requires strictly serial Ticket execution; active worktrees: ${active.join(", ")}`);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function gitOutput(repoRoot, args) {
|
|
2003
|
+
try {
|
|
2004
|
+
return execFileSync("git", ["-C", repoRoot, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
2005
|
+
} catch {
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function gitCommitExists(repoRoot, sha) {
|
|
2011
|
+
return typeof sha === "string" && gitSucceeds(repoRoot, ["cat-file", "-e", `${sha}^{commit}`]);
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function gitSucceeds(repoRoot, args) {
|
|
2015
|
+
try {
|
|
2016
|
+
execFileSync("git", ["-C", repoRoot, ...args], { stdio: ["ignore", "ignore", "ignore"] });
|
|
2017
|
+
return true;
|
|
2018
|
+
} catch {
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function validateGitEvidence(repoRoot, changeStatus, errors) {
|
|
2024
|
+
if (!repoRoot) return;
|
|
2025
|
+
const resolvedRoot = resolve(repoRoot);
|
|
2026
|
+
if (gitOutput(resolvedRoot, ["rev-parse", "--is-inside-work-tree"]) !== "true") {
|
|
2027
|
+
errors.push(`--repo is not a Git worktree: ${resolvedRoot}`);
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const currentBranch = gitOutput(resolvedRoot, ["branch", "--show-current"]);
|
|
2031
|
+
for (const worktree of Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : []) {
|
|
2032
|
+
const label = String(worktree.ticket_id ?? "worktree");
|
|
2033
|
+
for (const [name, sha] of [
|
|
2034
|
+
["base_sha", worktree.base_sha],
|
|
2035
|
+
["source_checkpoint", worktree.source_checkpoint],
|
|
2036
|
+
["parent_before_sha", worktree.integration?.parent_before_sha],
|
|
2037
|
+
["source_sha", worktree.integration?.source_sha],
|
|
2038
|
+
["candidate_sha", worktree.integration?.candidate_sha],
|
|
2039
|
+
["result_sha", worktree.integration?.result_sha],
|
|
2040
|
+
]) {
|
|
2041
|
+
if (sha !== null && sha !== undefined && sha !== "" && !gitCommitExists(resolvedRoot, sha)) {
|
|
2042
|
+
errors.push(`${label}: ${name} is not a resolvable Git commit: ${sha}`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
if (worktree.workspace_ref === "current") {
|
|
2046
|
+
if (currentBranch !== worktree.parent_branch) errors.push(`${label}: current branch ${currentBranch ?? "<detached>"} must equal ${worktree.parent_branch}`);
|
|
2047
|
+
if (new Set(["integrated", "removed"]).has(worktree.status) && worktree.integration?.result_sha && gitOutput(resolvedRoot, ["rev-parse", worktree.parent_branch]) !== worktree.integration.result_sha) {
|
|
2048
|
+
errors.push(`${label}: parent branch HEAD must equal recorded result_sha`);
|
|
2049
|
+
}
|
|
2050
|
+
if (worktree.integration?.source_sha && worktree.base_sha && !gitSucceeds(resolvedRoot, ["merge-base", "--is-ancestor", worktree.base_sha, worktree.integration.source_sha])) {
|
|
2051
|
+
errors.push(`${label}: source_sha must descend from base_sha`);
|
|
2052
|
+
}
|
|
2053
|
+
} else if (worktree.integration?.source_sha && worktree.base_sha && !gitSucceeds(resolvedRoot, ["merge-base", "--is-ancestor", worktree.base_sha, worktree.integration.source_sha])) {
|
|
2054
|
+
errors.push(`${label}: source_sha must descend from base_sha`);
|
|
2055
|
+
}
|
|
2056
|
+
if (new Set(["integrated", "removed"]).has(worktree.status) && gitOutput(resolvedRoot, ["status", "--porcelain"])) {
|
|
2057
|
+
errors.push(`${label}: repository is dirty while Ticket is recorded as ${worktree.status}`);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function validateChange(change, stage = null, repoRoot = null) {
|
|
1745
2063
|
const errors = [];
|
|
1746
2064
|
const warnings = [];
|
|
1747
2065
|
if (!isDirectory(change)) {
|
|
@@ -1837,6 +2155,38 @@ function validateChange(change, stage = null) {
|
|
|
1837
2155
|
}
|
|
1838
2156
|
}
|
|
1839
2157
|
|
|
2158
|
+
if (stage === "implement" && ticketMode && changeStatus) {
|
|
2159
|
+
const worktreesByTicket = new Map(
|
|
2160
|
+
(Array.isArray(changeStatus.worktrees) ? changeStatus.worktrees : [])
|
|
2161
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
2162
|
+
.map((entry) => [String(entry.ticket_id), entry]),
|
|
2163
|
+
);
|
|
2164
|
+
for (const [ticketId, artifact] of tickets) {
|
|
2165
|
+
if (new Set(["draft", "cancelled"]).has(artifact.meta.status)) continue;
|
|
2166
|
+
if (!worktreesByTicket.has(ticketId)) {
|
|
2167
|
+
errors.push(`${ticketId}: Implement stage requires one Ticket workspace execution record`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
for (const ticketId of worktreesByTicket.keys()) {
|
|
2171
|
+
if (!tickets.has(ticketId)) {
|
|
2172
|
+
errors.push(`${ticketId}: worktree record has no matching Ticket`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (changeStatus && goalPlan) {
|
|
2177
|
+
validateGoalPlanRuntimeLimits(goalPlan.path, change, goalPlan, errors);
|
|
2178
|
+
const attemptLimit = Number.isInteger(goalPlan.meta.integration_attempt_limit) ? goalPlan.meta.integration_attempt_limit : null;
|
|
2179
|
+
if (attemptLimit !== null) {
|
|
2180
|
+
for (const worktree of changeStatus.worktrees ?? []) {
|
|
2181
|
+
if (worktree?.integration && Number.isInteger(worktree.integration.attempts) && worktree.integration.attempts > attemptLimit) {
|
|
2182
|
+
errors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration.attempts} exceed Goal Plan limit ${attemptLimit}`);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors);
|
|
2187
|
+
}
|
|
2188
|
+
validateGitEvidence(repoRoot, changeStatus, errors);
|
|
2189
|
+
|
|
1840
2190
|
if (spec) {
|
|
1841
2191
|
const declaredContracts = new Set(spec.body.match(/\bAC-\d+\b/g) ?? []);
|
|
1842
2192
|
const coveredContracts = new Set(
|
|
@@ -1922,19 +2272,35 @@ function validateChange(change, stage = null) {
|
|
|
1922
2272
|
continue;
|
|
1923
2273
|
}
|
|
1924
2274
|
const text = readText(evidence);
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
"##
|
|
1928
|
-
"##
|
|
1929
|
-
"##
|
|
1930
|
-
"##
|
|
1931
|
-
"##
|
|
1932
|
-
"##
|
|
1933
|
-
|
|
2275
|
+
const currentWorkspace = goalPlan?.meta.ticket_workspace_policy === "current";
|
|
2276
|
+
const requiredHeadings = [
|
|
2277
|
+
"## 2. Lead Dispatch And Candidate Return",
|
|
2278
|
+
"## 3. 修改范围与路径所有权",
|
|
2279
|
+
"## 4. 验收与合同映射",
|
|
2280
|
+
"## 5. Workspace Verification",
|
|
2281
|
+
"## 6. 双轴审查",
|
|
2282
|
+
"## 7. Integration Verification",
|
|
2283
|
+
"## 8. 偏差与决策",
|
|
2284
|
+
"## 9. 残余风险与交付定位",
|
|
2285
|
+
];
|
|
2286
|
+
for (const heading of requiredHeadings) {
|
|
1934
2287
|
if (!text.includes(heading)) {
|
|
1935
2288
|
errors.push(`${toPosix(relative(change, evidence))}: missing '${heading}'`);
|
|
1936
2289
|
}
|
|
1937
2290
|
}
|
|
2291
|
+
const workspaceVerification = sectionBody(text, "## 5. Workspace Verification");
|
|
2292
|
+
if (!/(current-workspace|source-worktree)/.test(workspaceVerification)) {
|
|
2293
|
+
errors.push(`${toPosix(relative(change, evidence))}: Workspace Verification must name current-workspace or source-worktree`);
|
|
2294
|
+
}
|
|
2295
|
+
if (!currentWorkspace && /E2E[^\n]*(?:\bpass(?:ed)?\b|通过)/i.test(workspaceVerification) && !/不得|禁止|not-run/i.test(workspaceVerification)) {
|
|
2296
|
+
errors.push(`${toPosix(relative(change, evidence))}: E2E pass cannot come from source-worktree`);
|
|
2297
|
+
}
|
|
2298
|
+
const worktree = Array.isArray(changeStatus?.worktrees)
|
|
2299
|
+
? changeStatus.worktrees.find((entry) => entry?.ticket_id === ticketId)
|
|
2300
|
+
: null;
|
|
2301
|
+
if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
2302
|
+
errors.push(`${ticketId}: status is done but no integrated or removed Ticket worktree exists`);
|
|
2303
|
+
}
|
|
1938
2304
|
}
|
|
1939
2305
|
|
|
1940
2306
|
if (goalPlan?.meta.ready_for_execution === true) {
|
|
@@ -2014,13 +2380,14 @@ function printResults(errors, warnings) {
|
|
|
2014
2380
|
}
|
|
2015
2381
|
|
|
2016
2382
|
function usage() {
|
|
2017
|
-
console.error("Usage: node validate-specdev.mjs [--stage <stage>] <change-directory> | --self-check");
|
|
2383
|
+
console.error("Usage: node validate-specdev.mjs [--stage <stage>] [--repo <project-root>] <change-directory> | --self-check");
|
|
2018
2384
|
return 2;
|
|
2019
2385
|
}
|
|
2020
2386
|
|
|
2021
2387
|
function main(argv) {
|
|
2022
2388
|
let selfCheckRequested = false;
|
|
2023
2389
|
let stage = null;
|
|
2390
|
+
let repoRoot = null;
|
|
2024
2391
|
const positional = [];
|
|
2025
2392
|
for (let index = 0; index < argv.length; index += 1) {
|
|
2026
2393
|
const arg = argv[index];
|
|
@@ -2031,6 +2398,11 @@ function main(argv) {
|
|
|
2031
2398
|
index += 1;
|
|
2032
2399
|
} else if (arg.startsWith("--stage=")) {
|
|
2033
2400
|
stage = arg.slice("--stage=".length);
|
|
2401
|
+
} else if (arg === "--repo") {
|
|
2402
|
+
repoRoot = argv[index + 1] ?? null;
|
|
2403
|
+
index += 1;
|
|
2404
|
+
} else if (arg.startsWith("--repo=")) {
|
|
2405
|
+
repoRoot = arg.slice("--repo=".length);
|
|
2034
2406
|
} else if (arg.startsWith("--")) {
|
|
2035
2407
|
return usage();
|
|
2036
2408
|
} else {
|
|
@@ -2046,7 +2418,7 @@ function main(argv) {
|
|
|
2046
2418
|
return printResults(result.errors, result.warnings);
|
|
2047
2419
|
}
|
|
2048
2420
|
if (positional.length === 1) {
|
|
2049
|
-
const result = validateChange(resolve(positional[0]), stage);
|
|
2421
|
+
const result = validateChange(resolve(positional[0]), stage, repoRoot);
|
|
2050
2422
|
return printResults(result.errors, result.warnings);
|
|
2051
2423
|
}
|
|
2052
2424
|
return usage();
|