@namewta/speculo 2.0.4 → 2.0.6
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/package.json +1 -1
- package/template/canonical/canonical-specdev-goal-plan.md +2272 -1069
- package/template/canonical/canonical-specdev-grill-with-docs.md +2164 -661
- package/template/canonical/canonical-specdev-spec.md +2163 -660
- package/template/canonical/canonical-specdev-tickets.md +2095 -574
- package/template/canonical/canonical-specdev-wayfinder.md +2339 -836
- package/template/references/coordination/coordination-contract.md +8 -6
- package/template/references/coordination/modes/cli.md +1 -1
- package/template/references/coordination/modes/herdr.md +2 -2
- package/template/references/coordination/modes/orca.md +3 -3
- package/template/references/coordination/report-template.md +18 -0
- package/template/references/coordination/result-and-recovery.md +4 -4
- package/template/references/coordination/session-files.md +49 -56
- package/template/references/coordination/task-brief.md +7 -7
- package/template/references/coordination/task-template.md +25 -0
- package/template/references/retrospective/references/friction-taxonomy.md +1 -1
- package/template/references/writing-for-agents/references/project-skill-maintenance.md +29 -0
- package/template/references/writing-for-agents/writing-contract.md +2 -0
- package/template/workflows/specdev/SKILL.md +1 -1
- package/template/workflows/specdev/common/README.md +1 -1
- package/template/workflows/specdev/common/rules/artifact-contract.md +1 -1
- package/template/workflows/specdev/common/rules/change-completion.md +6 -6
- package/template/workflows/specdev/common/rules/domain-runtime.md +2 -2
- package/template/workflows/specdev/common/rules/evidence-and-verification.md +17 -34
- package/template/workflows/specdev/common/rules/execution-policy.md +24 -16
- package/template/workflows/specdev/common/rules/operating-governance.md +1 -1
- package/template/workflows/specdev/common/rules/parent-implementation-orchestration.md +4 -2
- package/template/workflows/specdev/common/rules/path-ownership.md +9 -7
- package/template/workflows/specdev/common/rules/path-reference-contract.md +1 -1
- package/template/workflows/specdev/common/rules/workflow-routing.md +1 -1
- package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +7 -3
- package/template/workflows/specdev/common/schemas/execution-context.schema.json +67 -0
- package/template/workflows/specdev/common/schemas/execution-record.schema.json +154 -0
- package/template/workflows/specdev/common/schemas/goal-plan.schema.json +15 -55
- package/template/workflows/specdev/common/schemas/implementation-plan.schema.json +15 -55
- package/template/workflows/specdev/common/schemas/retro-approval.schema.json +44 -0
- package/template/workflows/specdev/common/schemas/retro-archive.schema.json +99 -0
- package/template/workflows/specdev/common/schemas/retro-execution.schema.json +112 -0
- package/template/workflows/specdev/common/schemas/retro-upgrade.schema.json +163 -0
- package/template/workflows/specdev/common/schemas/retrospective.schema.json +119 -14
- package/template/workflows/specdev/common/schemas/session-locator.schema.json +15 -0
- package/template/workflows/specdev/common/skills/code-review/references/review-evidence.md +3 -3
- package/template/workflows/specdev/common/skills/code-review/references/source-discovery.md +2 -0
- package/template/workflows/specdev/common/skills/code-review/references/working-tree-review.md +1 -1
- package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +15 -34
- package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +27 -56
- package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +10 -54
- package/template/workflows/specdev/common/tools/README.md +16 -0
- package/template/workflows/specdev/common/tools/delivery-artifacts.mjs +5 -5
- package/template/workflows/specdev/common/tools/execution-context.mjs +104 -0
- package/template/workflows/specdev/common/tools/plan-contract.mjs +3 -1
- package/template/workflows/specdev/common/tools/review-evidence.mjs +2 -2
- package/template/workflows/specdev/common/tools/tests/engineering.test.mjs +2 -2
- package/template/workflows/specdev/common/tools/tests/review-fixture.mjs +4 -4
- package/template/workflows/specdev/common/tools/ticket-control.mjs +17 -12
- package/template/workflows/specdev/common/tools/validate-handoff.mjs +108 -0
- package/template/workflows/specdev/common/tools/validate-retro.mjs +331 -0
- package/template/workflows/specdev/common/tools/validate-specdev.mjs +135 -105
- package/template/workflows/specdev/specdev-a-archive-and-consolidate/SKILL.md +2 -1
- package/template/workflows/specdev/specdev-a-archive-and-consolidate/references/operation-contract.md +2 -0
- package/template/workflows/specdev/specdev-a-archive-and-consolidate/references/retro-archive.md +27 -0
- package/template/workflows/specdev/specdev-c-code-review/SKILL.md +1 -1
- package/template/workflows/specdev/specdev-i-implement/SKILL.md +3 -3
- package/template/workflows/specdev/specdev-i-implement/assets/evidence-template.md +39 -31
- package/template/workflows/specdev/specdev-i-implement/references/execution-preflight.md +16 -16
- package/template/workflows/specdev/specdev-i-implement/references/implementation-procedure.md +21 -37
- package/template/workflows/specdev/specdev-i-implement/references/merge-conflict-protocol.md +8 -18
- package/template/workflows/specdev/specdev-p-goal-plan/SKILL.md +3 -3
- package/template/workflows/specdev/specdev-p-goal-plan/assets/goal-plan-template.md +23 -26
- package/template/workflows/specdev/specdev-p-goal-plan/references/completion-control.md +11 -14
- package/template/workflows/specdev/specdev-p-goal-plan/references/lead-orchestration.md +12 -33
- package/template/workflows/specdev/specdev-p-goal-plan/references/map-control.md +1 -1
- package/template/workflows/specdev/specdev-p-goal-plan/references/multi-change-plan.md +11 -16
- package/template/workflows/specdev/specdev-p-goal-plan/references/multi-conflict-and-drift.md +2 -2
- package/template/workflows/specdev/specdev-p-goal-plan/references/multi-execution-loop.md +6 -6
- package/template/workflows/specdev/specdev-p-goal-plan/references/orchestration-protocol.md +13 -44
- package/template/workflows/specdev/specdev-p-goal-plan/references/planning-modes.md +18 -25
- package/template/workflows/specdev/specdev-p-goal-plan/references/single-change-plan.md +16 -16
- package/template/workflows/specdev/specdev-r-retro/SKILL.md +15 -11
- package/template/workflows/specdev/specdev-r-retro/assets/retro-template.md +26 -14
- package/template/workflows/specdev/specdev-r-retro/assets/upgrade-plan-template.md +41 -0
- package/template/workflows/specdev/specdev-r-retro/references/development-retrospective.md +42 -0
- package/template/workflows/specdev/specdev-r-retro/references/records.md +47 -0
- package/template/workflows/specdev/specdev-r-retro/references/skill-upgrade.md +13 -0
- package/template/workflows/specdev/specdev-t-tickets/SKILL.md +2 -0
- package/template/workflows/specdev/specdev-t-tickets/assets/ticket-template.md +1 -1
- package/template/workflows/specdev/specdev-t-tickets/assets/tickets-map-template.md +6 -6
- package/template/workflows/specdev/specdev-t-tickets/references/planning-procedure.md +2 -2
- package/template/workflows/specdev/specdev-t-tickets/references/ticket-readiness.md +3 -3
|
@@ -30,15 +30,16 @@ import {
|
|
|
30
30
|
|
|
31
31
|
import { validateChangeClosure, readChangeOwner } from "./change-artifacts.mjs";
|
|
32
32
|
import { validateRecord, validateRecords } from "./delivery-artifacts.mjs";
|
|
33
|
+
import { readExecutionContext, ticketPolicy, resolveExecutionWorkspace, validateExecutionRecord } from './execution-context.mjs';
|
|
33
34
|
import { reviewScopeCLI } from "./review-scope.mjs";
|
|
34
35
|
import { hasIndependentReview, validateIndependentReview } from "./review-evidence.mjs";
|
|
35
36
|
import { ENGINEERING_KINDS, validateEngineeringArtifacts, validateEngineeringPackage } from "./engineering-evidence.mjs";
|
|
36
37
|
|
|
37
38
|
const DOMAIN_SCHEMA_VERSION = 3;
|
|
38
39
|
const CONFIG_SCHEMA_VERSION = 7;
|
|
39
|
-
const GOAL_PLAN_SCHEMA_VERSION =
|
|
40
|
+
const GOAL_PLAN_SCHEMA_VERSION = 8;
|
|
40
41
|
const IMPLEMENTATION_MAP_SCHEMA_VERSION = 1;
|
|
41
|
-
const IMPLEMENTATION_PLAN_SCHEMA_VERSION =
|
|
42
|
+
const IMPLEMENTATION_PLAN_SCHEMA_VERSION = 3;
|
|
42
43
|
const CHANGE_STATUS_SCHEMA_VERSION = 6;
|
|
43
44
|
const GLOBAL_STATUS_SCHEMA_VERSION = 5;
|
|
44
45
|
const WORKFLOW_PREFIX = "{roots.workflows}/specdev/";
|
|
@@ -174,7 +175,6 @@ const VALID_STAGES = new Set([
|
|
|
174
175
|
"goal-plan",
|
|
175
176
|
"implement",
|
|
176
177
|
"learn",
|
|
177
|
-
"retro",
|
|
178
178
|
"pr-delivery",
|
|
179
179
|
"review",
|
|
180
180
|
"prototype",
|
|
@@ -213,10 +213,10 @@ const REQUIRED_GOAL_PLAN_SECTIONS = new Set([
|
|
|
213
213
|
]);
|
|
214
214
|
const REQUIRED_LEAD_GOAL_PLAN_MARKERS = [
|
|
215
215
|
"### Lead Orchestration",
|
|
216
|
-
"
|
|
216
|
+
"Ticket scheduling",
|
|
217
217
|
"Read-only agents",
|
|
218
218
|
"execution-time dynamic",
|
|
219
|
-
"###
|
|
219
|
+
"### Change Workspace and Verification",
|
|
220
220
|
"### Authorization Matrix",
|
|
221
221
|
"Implementation commit",
|
|
222
222
|
];
|
|
@@ -602,6 +602,8 @@ function validateDocumentReferences(root) {
|
|
|
602
602
|
.replace(/`?\s*<Path>.*?<\/Path>\s*`?/gs, "")
|
|
603
603
|
.replace(/`?\s*<Url>.*?<\/Url>\s*`?/gs, "");
|
|
604
604
|
for (const link of acceptedLinks) withoutTags = withoutTags.replaceAll(link, "");
|
|
605
|
+
// This exact JSON field is a change-local data locator, not a static dependency.
|
|
606
|
+
withoutTags = withoutTags.replace(/"workspace_context":\s*"README\.md#execution-context"/g, '"workspace_context": "change-local-anchor"');
|
|
605
607
|
|
|
606
608
|
for (const match of withoutTags.matchAll(/\{roots\.(?:workflows|state)\}/g)) {
|
|
607
609
|
errors.push(
|
|
@@ -755,13 +757,13 @@ function validateExecutionContractAssets(root) {
|
|
|
755
757
|
|
|
756
758
|
const goalPlanSchema = JSON.parse(readText(goalPlanSchemaPath));
|
|
757
759
|
if (
|
|
758
|
-
goalPlanSchema.$id !== "urn:speculo:specdev:goal-plan:
|
|
760
|
+
goalPlanSchema.$id !== "urn:speculo:specdev:goal-plan:v8" ||
|
|
759
761
|
goalPlanSchema.properties?.schema_version?.const !== GOAL_PLAN_SCHEMA_VERSION ||
|
|
760
762
|
goalPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
|
|
761
763
|
goalPlanSchema.properties?.concurrent_implementation_limit?.minimum !== 1 ||
|
|
762
764
|
goalPlanSchema.properties?.integration_attempt_limit?.minimum !== 1 ||
|
|
763
|
-
!["
|
|
764
|
-
|
|
765
|
+
!["auto", "serial", "parallel"].every((value) => goalPlanSchema.properties?.ticket_execution?.enum?.includes(value)) ||
|
|
766
|
+
goalPlanSchema.properties?.max_parallel_tickets?.minimum !== 1 ||
|
|
765
767
|
goalPlanSchema.additionalProperties !== false
|
|
766
768
|
) {
|
|
767
769
|
errors.push("goal-plan.schema.json must define the strict Lead-directed Goal Plan v7 workspace strategy contract");
|
|
@@ -791,12 +793,12 @@ function validateExecutionContractAssets(root) {
|
|
|
791
793
|
}
|
|
792
794
|
const implementationPlanSchema = JSON.parse(readText(implementationPlanSchemaPath));
|
|
793
795
|
if (
|
|
794
|
-
implementationPlanSchema.$id !== "urn:speculo:specdev:implementation-plan:
|
|
796
|
+
implementationPlanSchema.$id !== "urn:speculo:specdev:implementation-plan:v3" ||
|
|
795
797
|
implementationPlanSchema.properties?.schema_version?.const !== IMPLEMENTATION_PLAN_SCHEMA_VERSION ||
|
|
796
798
|
implementationPlanSchema.properties?.orchestration?.const !== "lead-directed" ||
|
|
797
799
|
implementationPlanSchema.properties?.concurrent_implementation_limit?.minimum !== 1 ||
|
|
798
800
|
implementationPlanSchema.properties?.integration_attempt_limit?.minimum !== 1 ||
|
|
799
|
-
!["
|
|
801
|
+
!["auto", "serial", "parallel"].every((value) => implementationPlanSchema.properties?.ticket_execution?.enum?.includes(value)) ||
|
|
800
802
|
implementationPlanSchema.additionalProperties !== false
|
|
801
803
|
) {
|
|
802
804
|
errors.push("implementation-plan.schema.json must define the strict parent implementation projection contract v1");
|
|
@@ -852,7 +854,7 @@ function capabilityChecks(root) {
|
|
|
852
854
|
"Wave",
|
|
853
855
|
"lead-directed",
|
|
854
856
|
"concurrent_implementation_limit",
|
|
855
|
-
"
|
|
857
|
+
"ticket_execution",
|
|
856
858
|
"lead-orchestration.md",
|
|
857
859
|
"Definition of Done",
|
|
858
860
|
],
|
|
@@ -862,7 +864,7 @@ function capabilityChecks(root) {
|
|
|
862
864
|
"implement",
|
|
863
865
|
[
|
|
864
866
|
join(root, "specdev-i-implement", "SKILL.md"),
|
|
865
|
-
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "
|
|
867
|
+
["codebase-design", "design-it-twice", "TDD", "双轴审查", "Evidence", "change 工作区", "共享编排方法", "checkpoint"],
|
|
866
868
|
],
|
|
867
869
|
],
|
|
868
870
|
[
|
|
@@ -972,14 +974,13 @@ function capabilityChecks(root) {
|
|
|
972
974
|
} else {
|
|
973
975
|
const text = readText(goalPlanTemplate);
|
|
974
976
|
for (const required of [
|
|
975
|
-
"schema_version:
|
|
977
|
+
"schema_version: 8",
|
|
976
978
|
"orchestration: lead-directed",
|
|
977
|
-
"
|
|
979
|
+
"ticket_execution: auto",
|
|
978
980
|
"integration_attempt_limit: 3",
|
|
979
|
-
|
|
980
|
-
"integration_gate: direct-parent",
|
|
981
|
+
|
|
981
982
|
...REQUIRED_LEAD_GOAL_PLAN_MARKERS,
|
|
982
|
-
"
|
|
983
|
+
"Change workspace verification",
|
|
983
984
|
]) {
|
|
984
985
|
if (!text.includes(required)) errors.push(`Goal Plan template lost marker ${required}`);
|
|
985
986
|
}
|
|
@@ -1861,6 +1862,27 @@ function validateMap(path, errors, repoRoot = null) {
|
|
|
1861
1862
|
}
|
|
1862
1863
|
|
|
1863
1864
|
function validateGoalPlan(path, errors) {
|
|
1865
|
+
if (!isFile(path)) { errors.push(`missing Goal Plan: ${basename(path)}`); return null; }
|
|
1866
|
+
const {meta,body} = parseFrontmatter(path);
|
|
1867
|
+
if (meta.schema_version === 7 && /^Outcome: completed$/m.test(readText(join(dirname(path),'README.md')))) return validateLegacyGoalPlan(path,errors);
|
|
1868
|
+
const schema = JSON.parse(readText(fileURLToPath(new URL('../schemas/goal-plan.schema.json', import.meta.url))));
|
|
1869
|
+
if (/^## (?:Delegated Execution|Isolated Workspace) Addendum$/m.test(body)) errors.push('goal-plan.md: obsolete Goal Plan addendum');
|
|
1870
|
+
for (const key of schema.required) if (!(key in meta)) errors.push(`${basename(path)}: missing ${key}`);
|
|
1871
|
+
for (const key of Object.keys(meta)) if (!(key in schema.properties)) errors.push(`${basename(path)}: obsolete or unexpected field ${key}; explicit reconciliation required`);
|
|
1872
|
+
if (meta.schema_version !== GOAL_PLAN_SCHEMA_VERSION || meta.artifact !== 'goal-plan') errors.push('goal-plan.md: requires schema v8; explicit reconciliation required');
|
|
1873
|
+
if (meta.orchestration !== 'lead-directed' || typeof meta.lead !== 'string' || !meta.lead.trim()) errors.push('goal-plan.md: needs a recoverable Lead');
|
|
1874
|
+
if (!Array.isArray(meta.modes) || meta.modes.some(mode=>!VALID_PLAN_MODES.has(mode)) || new Set(meta.modes).size !== meta.modes.length) errors.push('goal-plan.md: has invalid/duplicate modes');
|
|
1875
|
+
if (!['draft','ready','in_progress','completed','blocked'].includes(meta.status) || typeof meta.ready_for_execution !== 'boolean' || meta.ready_for_execution !== ['ready','in_progress'].includes(meta.status)) errors.push('goal-plan.md: ready_for_execution must match status');
|
|
1876
|
+
errors.push(...validateExecutionPolicy({meta,body}));
|
|
1877
|
+
if (meta.ready_for_execution) {
|
|
1878
|
+
for (const heading of [...REQUIRED_GOAL_PLAN_SECTIONS,...REQUIRED_LEAD_GOAL_PLAN_MARKERS]) if (!body.includes(heading)) errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
|
|
1879
|
+
const assumptions=sectionBody(body,'## Assumptions');
|
|
1880
|
+
if (assumptions && assumptions.includes('高影响') && !assumptions.includes('必须为 `false`')) errors.push(`${basename(path)}: high-impact assumptions cannot remain in a ready Goal Plan`);
|
|
1881
|
+
}
|
|
1882
|
+
return {path,meta,body};
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function validateLegacyGoalPlan(path, errors) {
|
|
1864
1886
|
if (!isFile(path)) {
|
|
1865
1887
|
errors.push(`missing Goal Plan: ${basename(path)}`);
|
|
1866
1888
|
return null;
|
|
@@ -1884,8 +1906,8 @@ function validateGoalPlan(path, errors) {
|
|
|
1884
1906
|
if (missing.length) errors.push(`${basename(path)}: missing keys ${JSON.stringify(missing.sort())}`);
|
|
1885
1907
|
const unexpected = Object.keys(meta).filter((key) => !required.includes(key));
|
|
1886
1908
|
if (unexpected.length) errors.push(`${basename(path)}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
1887
|
-
if (meta.artifact !== "goal-plan" || meta.schema_version !==
|
|
1888
|
-
errors.push(`${basename(path)}: artifact/schema_version must be goal-plan
|
|
1909
|
+
if (meta.artifact !== "goal-plan" || meta.schema_version !== 7) {
|
|
1910
|
+
errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/7`);
|
|
1889
1911
|
}
|
|
1890
1912
|
const modes = requireList(meta, "modes", basename(path), errors);
|
|
1891
1913
|
const invalidModes = modes.filter((mode) => !VALID_PLAN_MODES.has(mode));
|
|
@@ -1937,9 +1959,10 @@ function validateGoalPlan(path, errors) {
|
|
|
1937
1959
|
errors.push(`${basename(path)}: ready Goal Plan missing '${heading}'`);
|
|
1938
1960
|
}
|
|
1939
1961
|
}
|
|
1962
|
+
const legacyMarkers = REQUIRED_LEAD_GOAL_PLAN_MARKERS.map(marker=>marker === 'Ticket scheduling' ? 'Concurrent implementations' : marker === '### Change Workspace and Verification' ? '### Ticket Workspace and Integration' : marker);
|
|
1940
1963
|
const requiredMarkers = meta.ticket_workspace_policy === "current"
|
|
1941
|
-
? [...
|
|
1942
|
-
: [...
|
|
1964
|
+
? [...legacyMarkers, "Local direct-parent verification and parent update"]
|
|
1965
|
+
: [...legacyMarkers, "source worktree 不运行 E2E", "Local candidate integration and parent update"];
|
|
1943
1966
|
const missingLeadMarkers = requiredMarkers.filter((marker) => !body.includes(marker));
|
|
1944
1967
|
if (missingLeadMarkers.length) {
|
|
1945
1968
|
errors.push(`${basename(path)}: ready Goal Plan missing Lead/workspace markers ${JSON.stringify(missingLeadMarkers)}`);
|
|
@@ -1954,6 +1977,7 @@ function validateGoalPlan(path, errors) {
|
|
|
1954
1977
|
|
|
1955
1978
|
function validateGoalPlanRuntimeLimits(path, change, goalPlan, errors, repoRoot = null, options = {}) {
|
|
1956
1979
|
if (!goalPlan) return;
|
|
1980
|
+
if (goalPlan.meta.schema_version === 7 && /^Outcome: completed$/m.test(readText(join(change,'README.md')))) return;
|
|
1957
1981
|
errors.push(...validateExecutionPolicy(goalPlan, findSpecdevConfig(change, repoRoot, options)).map(error => `${basename(path)}: ${error}`));
|
|
1958
1982
|
}
|
|
1959
1983
|
|
|
@@ -2200,7 +2224,7 @@ function validateTicket(path, errors) {
|
|
|
2200
2224
|
if (!verification || !verification.includes("|")) {
|
|
2201
2225
|
errors.push(`${basename(path)}: Ready Ticket has no usable verification matrix`);
|
|
2202
2226
|
}
|
|
2203
|
-
if (!verification.includes("E2E disposition") || !/(current-workspace|source-worktree|parent-candidate|direct-parent)/.test(verification)) {
|
|
2227
|
+
if (!verification.includes("E2E disposition") || !/(change-workspace|current-workspace|source-worktree|parent-candidate|direct-parent)/.test(verification)) {
|
|
2204
2228
|
errors.push(`${basename(path)}: Ready Ticket must define E2E disposition and an explicit execution environment`);
|
|
2205
2229
|
}
|
|
2206
2230
|
const acceptance = semanticSection(body, ["验收标准"]).toLowerCase();
|
|
@@ -2217,10 +2241,24 @@ function validateChangeExecution(change, errors, context) {
|
|
|
2217
2241
|
try {
|
|
2218
2242
|
const facts = runtime.readChangeFactsSync(context, basename(change));
|
|
2219
2243
|
if (facts.location.root !== resolve(change)) throw new Error('change identity does not match requested location');
|
|
2220
|
-
data = { ...facts, worktrees:readChangeExecution(change) };
|
|
2244
|
+
data = { ...facts, worktrees:readChangeExecution(change), executionContext:readExecutionContext(change) };
|
|
2221
2245
|
} catch (error) { errors.push(`${change}: ${error.message}`); return null; }
|
|
2246
|
+
const modern = data.worktrees.filter(record=>record.schema_version === 1);
|
|
2247
|
+
const legacy = data.worktrees.filter(record=>record.schema_version !== 1);
|
|
2248
|
+
if (legacy.length && (data.outcome === 'active' || modern.length || data.executionContext)) errors.push('legacy-active-execution: explicit reconciliation by original owner required; preserve old worktrees and evidence');
|
|
2249
|
+
if (data.executionContext) {
|
|
2250
|
+
try {
|
|
2251
|
+
const owner = readChangeOwner(context,expectedChange).owner;
|
|
2252
|
+
if (owner !== data.executionContext.owner) errors.push('Execution Context owner differs from original Lead');
|
|
2253
|
+
data.workspaceRoot = resolveExecutionWorkspace(context.projectRoot,data.executionContext);
|
|
2254
|
+
} catch(error) { errors.push(error.message); }
|
|
2255
|
+
}
|
|
2256
|
+
for (const record of modern) {
|
|
2257
|
+
errors.push(...validateExecutionRecord(change,record,data.executionContext,{repoRoot:data.workspaceRoot ?? context.projectRoot,sourceRoot:context.projectRoot}));
|
|
2258
|
+
if (record.purpose === 'direct-spec' && record.status === 'cancelled' && data.outcome !== 'cancelled') errors.push('cancelled Direct Spec execution requires the original whole-change cancellation');
|
|
2259
|
+
}
|
|
2222
2260
|
const seenWorktreeTickets = new Set();
|
|
2223
|
-
for (const worktree of
|
|
2261
|
+
for (const worktree of legacy) {
|
|
2224
2262
|
if (!worktree || typeof worktree !== "object" || Array.isArray(worktree)) {
|
|
2225
2263
|
errors.push(`${basename(path)}: invalid worktree entry`);
|
|
2226
2264
|
continue;
|
|
@@ -2505,7 +2543,7 @@ function validateChangeExecution(change, errors, context) {
|
|
|
2505
2543
|
}
|
|
2506
2544
|
|
|
2507
2545
|
function validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors) {
|
|
2508
|
-
const allEntries =
|
|
2546
|
+
const allEntries = (changeStatus?.worktrees ?? []).filter(record=>record.schema_version !== 1);
|
|
2509
2547
|
const entries = goalPlan?.meta.ticket_workspace_policy === "current" ? allEntries : allEntries.filter(entry => entry.workspace_ref === "current");
|
|
2510
2548
|
const active = [];
|
|
2511
2549
|
for (const worktree of entries) {
|
|
@@ -2562,7 +2600,7 @@ function validateGitEvidence(repoRoot, changeStatus, errors) {
|
|
|
2562
2600
|
return;
|
|
2563
2601
|
}
|
|
2564
2602
|
const currentBranch = gitOutput(resolvedRoot, ["branch", "--show-current"]);
|
|
2565
|
-
for (const worktree of
|
|
2603
|
+
for (const worktree of (changeStatus?.worktrees ?? []).filter(record=>record.schema_version !== 1)) {
|
|
2566
2604
|
const label = String(worktree.ticket_id ?? worktree.purpose ?? "worktree");
|
|
2567
2605
|
for (const [name, sha] of [
|
|
2568
2606
|
["base_sha", worktree.base_sha],
|
|
@@ -2647,13 +2685,13 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2647
2685
|
const map = parseFrontmatter(mapPath);
|
|
2648
2686
|
const plan = parseFrontmatter(planPath);
|
|
2649
2687
|
const mapKeys = ["schema_version", "artifact", "change", "status", "revision", "members", "tasks", "dependencies", "serializations"];
|
|
2650
|
-
const planKeys = ["schema_version", "artifact", "change", "status", "source_map_revision", "orchestration", "lead", "concurrent_implementation_limit", "integration_attempt_limit", "
|
|
2688
|
+
const planKeys = ["schema_version", "artifact", "change", "status", "source_map_revision", "orchestration", "lead", "concurrent_implementation_limit", "integration_attempt_limit", "ticket_execution", "ready_for_execution"];
|
|
2651
2689
|
for (const [label, meta, expected] of [
|
|
2652
2690
|
["implementation-map.md", map.meta, mapKeys],
|
|
2653
2691
|
["implementation-plan.md", plan.meta, planKeys],
|
|
2654
2692
|
]) {
|
|
2655
|
-
const missing = expected.filter((key) => !(key in meta));
|
|
2656
|
-
const unexpected = Object.keys(meta).filter((key) => !expected.includes(key));
|
|
2693
|
+
const missing = expected.filter((key) => key !== "concurrent_implementation_limit" && !(key in meta));
|
|
2694
|
+
const unexpected = Object.keys(meta).filter((key) => !expected.includes(key) && key !== "max_parallel_tickets");
|
|
2657
2695
|
if (missing.length) errors.push(`${label}: missing keys ${JSON.stringify(missing)}`);
|
|
2658
2696
|
if (unexpected.length) errors.push(`${label}: unexpected keys ${JSON.stringify(unexpected.sort())}`);
|
|
2659
2697
|
}
|
|
@@ -2661,7 +2699,7 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2661
2699
|
errors.push("implementation-map.md: artifact/schema_version must be implementation-map/1");
|
|
2662
2700
|
}
|
|
2663
2701
|
if (plan.meta.schema_version !== IMPLEMENTATION_PLAN_SCHEMA_VERSION || plan.meta.artifact !== "implementation-plan") {
|
|
2664
|
-
errors.push("implementation-plan.md: artifact/schema_version must be implementation-plan/
|
|
2702
|
+
errors.push("implementation-plan.md: artifact/schema_version must be implementation-plan/3");
|
|
2665
2703
|
}
|
|
2666
2704
|
if (map.meta.change !== parentName || plan.meta.change !== parentName) {
|
|
2667
2705
|
errors.push("parent implementation artifacts must name their containing change");
|
|
@@ -2678,17 +2716,7 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2678
2716
|
if (plan.meta.orchestration !== "lead-directed" || typeof plan.meta.lead !== "string" || !plan.meta.lead.trim()) {
|
|
2679
2717
|
errors.push("implementation-plan.md: lead-directed orchestration requires a recoverable Lead");
|
|
2680
2718
|
}
|
|
2681
|
-
|
|
2682
|
-
if (!Number.isInteger(plan.meta[key]) || plan.meta[key] < 1) {
|
|
2683
|
-
errors.push(`implementation-plan.md: ${key} must be a positive integer`);
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
const validStrategy =
|
|
2687
|
-
(plan.meta.ticket_workspace_policy === "current" && plan.meta.integration_gate === "direct-parent") ||
|
|
2688
|
-
(plan.meta.ticket_workspace_policy === "required" && plan.meta.integration_gate === "candidate-merge");
|
|
2689
|
-
if (!validStrategy) {
|
|
2690
|
-
errors.push("implementation-plan.md: workspace/integration strategy must be current/direct-parent or required/candidate-merge");
|
|
2691
|
-
}
|
|
2719
|
+
errors.push(...validateExecutionPolicy(plan, findSpecdevConfig(change, repoRoot, options)));
|
|
2692
2720
|
const readyStatuses = new Set(["ready", "in_progress"]);
|
|
2693
2721
|
if (
|
|
2694
2722
|
typeof plan.meta.ready_for_execution !== "boolean" ||
|
|
@@ -2736,8 +2764,6 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2736
2764
|
const expectedTasks = new Set();
|
|
2737
2765
|
const expectedInternalEdges = new Set();
|
|
2738
2766
|
let activeImplementations = 0;
|
|
2739
|
-
let activeCurrentWriters = 0;
|
|
2740
|
-
const integratingByRef = new Map();
|
|
2741
2767
|
|
|
2742
2768
|
for (const member of members) {
|
|
2743
2769
|
if (!CHANGE_NAME.test(member)) {
|
|
@@ -2761,24 +2787,8 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2761
2787
|
memberStatuses.set(member, status);
|
|
2762
2788
|
if (parentStatus?.outcome === "completed") validateGitEvidence(options.context.projectRoot,status,memberErrors);
|
|
2763
2789
|
if (parentStatus?.location.kind !== "archive" && status.location.kind !== "active") memberErrors.push("archived change cannot be an implementation member");
|
|
2764
|
-
for (const
|
|
2765
|
-
if (
|
|
2766
|
-
activeImplementations += 1;
|
|
2767
|
-
if (worktree.workspace_ref === "current") activeCurrentWriters += 1;
|
|
2768
|
-
if (plan.meta.ticket_workspace_policy === "current" && worktree.workspace_ref !== "current") {
|
|
2769
|
-
memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent current policy`);
|
|
2770
|
-
}
|
|
2771
|
-
if (plan.meta.ticket_workspace_policy === "required" && worktree.workspace_ref === "current") {
|
|
2772
|
-
memberErrors.push(`${worktree.ticket_id}: active workspace must follow parent required policy`);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
if (worktree?.status === "integrating") {
|
|
2776
|
-
const key = String(worktree.parent_branch ?? "<unknown-ref>");
|
|
2777
|
-
integratingByRef.set(key, (integratingByRef.get(key) ?? 0) + 1);
|
|
2778
|
-
}
|
|
2779
|
-
if (Number.isInteger(worktree?.integration?.attempts) && worktree.integration.attempts > plan.meta.integration_attempt_limit) {
|
|
2780
|
-
memberErrors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration.attempts} exceed parent limit ${plan.meta.integration_attempt_limit}`);
|
|
2781
|
-
}
|
|
2790
|
+
for (const record of status.worktrees ?? []) {
|
|
2791
|
+
if ((record.attempts ?? record.integration?.attempts ?? 0) > plan.meta.integration_attempt_limit) memberErrors.push(`${record.ticket_id}: verification attempts exceed parent limit`);
|
|
2782
2792
|
}
|
|
2783
2793
|
}
|
|
2784
2794
|
|
|
@@ -2875,15 +2885,14 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2875
2885
|
if (unfinished.length) memberErrors.push(`completed member has unfinished Tickets: ${JSON.stringify(unfinished)}`);
|
|
2876
2886
|
}
|
|
2877
2887
|
|
|
2888
|
+
activeImplementations += [...childTickets.values()].filter(ticket=>['in_progress','review'].includes(ticket.meta.status)).length;
|
|
2878
2889
|
const goalPath = join(memberRoot, "goal-plan.md");
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
))
|
|
2885
|
-
memberErrors.push("Goal Plan workspace/integration strategy conflicts with parent Implementation Plan");
|
|
2886
|
-
}
|
|
2890
|
+
const goal = isFile(goalPath) ? validateGoalPlan(goalPath, memberErrors) : null;
|
|
2891
|
+
const childPolicy = ticketPolicy(plan.meta,goal?.meta,status?.executionContext);
|
|
2892
|
+
const active = [...childTickets.values()].filter(ticket=>['in_progress','review'].includes(ticket.meta.status));
|
|
2893
|
+
if (childPolicy.max_parallel_tickets !== null && active.length > childPolicy.max_parallel_tickets) memberErrors.push('child concurrent Ticket limit exceeded');
|
|
2894
|
+
if (stage === 'complete' && status?.outcome === 'completed' && !isFile(join(memberRoot,'implementation-map.md'))) {
|
|
2895
|
+
memberErrors.push(...validateChange(memberRoot,'complete',repoRoot,null,null,options).errors);
|
|
2887
2896
|
}
|
|
2888
2897
|
errors.push(...memberErrors.map((message) => `${member}: ${message}`));
|
|
2889
2898
|
warnings.push(...memberWarnings.map((message) => `${member}: ${message}`));
|
|
@@ -2954,6 +2963,16 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2954
2963
|
const unfinishedTasks = [...ticketByTask]
|
|
2955
2964
|
.filter(([, value]) => !new Set(["done", "cancelled"]).has(value.artifact.meta.status))
|
|
2956
2965
|
.map(([task]) => task);
|
|
2966
|
+
const activeTasks = [...ticketByTask].filter(([,value])=>['in_progress','review'].includes(value.artifact.meta.status)).map(([task])=>task);
|
|
2967
|
+
for (const task of activeTasks) {
|
|
2968
|
+
for (const prerequisite of graph.get(task) ?? []) if (ticketByTask.get(prerequisite)?.artifact.meta.status !== 'done') errors.push(`${task}: active Ticket prerequisite is not done: ${prerequisite}`);
|
|
2969
|
+
}
|
|
2970
|
+
for (let index=0; index<activeTasks.length; index++) for (const right of activeTasks.slice(index+1)) {
|
|
2971
|
+
const left=activeTasks[index], a=ticketByTask.get(left).artifact.meta, b=ticketByTask.get(right).artifact.meta;
|
|
2972
|
+
if (serializationPairs.has([left,right].sort().join('\0'))) errors.push(`${left}/${right}: serialized Tickets cannot be active together`);
|
|
2973
|
+
if ((a.writable_paths ?? []).some(x=>(b.writable_paths ?? []).some(y=>pathsOverlap(String(x),String(y))))) errors.push(`${left}/${right}: active Tickets have conflicting writable paths`);
|
|
2974
|
+
if ((a.resource_claims ?? []).some(x=>(b.resource_claims ?? []).some(y=>x === y || x.endsWith('*') && y.startsWith(x.slice(0,-1)) || y.endsWith('*') && x.startsWith(y.slice(0,-1))))) errors.push(`${left}/${right}: active Tickets have conflicting semantic resources`);
|
|
2975
|
+
}
|
|
2957
2976
|
for (let index = 0; index < unfinishedTasks.length; index += 1) {
|
|
2958
2977
|
const leftId = unfinishedTasks[index];
|
|
2959
2978
|
const left = ticketByTask.get(leftId).artifact;
|
|
@@ -2995,15 +3014,8 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
2995
3014
|
}
|
|
2996
3015
|
|
|
2997
3016
|
errors.push(...validateExecutionPolicy(plan, findSpecdevConfig(change, repoRoot, options)).map(error => `implementation-plan.md: ${error}`));
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
}
|
|
3001
|
-
if (plan.meta.ticket_workspace_policy === "current" && activeCurrentWriters > 1) {
|
|
3002
|
-
errors.push(`parent current policy permits only one active implementation writer; found ${activeCurrentWriters}`);
|
|
3003
|
-
}
|
|
3004
|
-
for (const [ref, count] of integratingByRef) {
|
|
3005
|
-
if (count > 1) errors.push(`repository/ref integration must be serialized for ${ref}; found ${count}`);
|
|
3006
|
-
}
|
|
3017
|
+
const parentTicketPolicy = ticketPolicy(plan.meta);
|
|
3018
|
+
if (parentTicketPolicy.max_parallel_tickets !== null && activeImplementations > parentTicketPolicy.max_parallel_tickets) errors.push('parent concurrent Ticket limit exceeded');
|
|
3007
3019
|
|
|
3008
3020
|
if (parentStatus?.outcome === "completed") {
|
|
3009
3021
|
const incomplete = members.filter((member) => memberStatuses.get(member)?.outcome !== "completed");
|
|
@@ -3026,8 +3038,8 @@ function validateParentImplementation(change, parentStatus, stage, errors, warni
|
|
|
3026
3038
|
errors.push(`${task}: completed parent requires child Ticket Evidence`);
|
|
3027
3039
|
}
|
|
3028
3040
|
const worktree = value.status?.worktrees?.find((entry) => entry?.ticket_id === value.artifact.meta.id);
|
|
3029
|
-
if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
3030
|
-
errors.push(`${task}: completed parent requires
|
|
3041
|
+
if (!worktree || !new Set(["verified", "integrated", "removed"]).has(worktree.status)) {
|
|
3042
|
+
errors.push(`${task}: completed parent requires verified child execution evidence`);
|
|
3031
3043
|
}
|
|
3032
3044
|
}
|
|
3033
3045
|
const evidencePath = join(change, "evidence", "implementation-orchestration.md");
|
|
@@ -3128,21 +3140,18 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3128
3140
|
validatePrototypes(change, stage === "prototype", errors, prototypeId, new Set(["spec", "tickets", "goal-plan", "implement", "review", "complete"]).has(stage), workspace.context);
|
|
3129
3141
|
if (new Set(["tickets", "goal-plan", "implement", "review", "complete"]).has(stage)) runDesignCheck("--handoff", change, errors, workspace.context);
|
|
3130
3142
|
validateChangeLearning(change, stage === "learn", errors);
|
|
3131
|
-
const retro = validateRecords(change, "retrospective", parseFrontmatter);
|
|
3132
3143
|
const prs = validateRecords(change, "pull-request", parseFrontmatter);
|
|
3133
|
-
errors.push(...
|
|
3134
|
-
if (stage === "retro" && !retro.records.some(r => r.meta.status === "completed")) errors.push("retro stage requires a completed retrospective");
|
|
3144
|
+
errors.push(...prs.errors);
|
|
3135
3145
|
if (stage === "pr-delivery" && !prs.records.length) errors.push("pr-delivery stage requires a PR record");
|
|
3136
3146
|
if (archiveReady && prs.records.some(r => r.meta.requested === true && (["planned", "failed"].includes(r.meta.status) || (r.meta.delivery_target === "ready" && r.meta.status === "draft")))) errors.push("complete stage cannot archive pending PR delivery");
|
|
3137
|
-
|
|
3138
|
-
!["spec.md", "ticket", "tickets-map.md", "goal-plan.md", "implementation-map.md", "implementation-plan.md", "evidence"].some(p => existsSync(join(change,p))) && !changeStatus.worktrees?.length;
|
|
3147
|
+
|
|
3139
3148
|
|
|
3140
3149
|
let simpleGoal = null;
|
|
3141
3150
|
try { simpleGoal = readLightweightGoal(change); } catch (error) { errors.push(error.message); }
|
|
3142
|
-
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage) && !isParentImplementation
|
|
3151
|
+
const specRequired = new Set(["spec", "tickets", "goal-plan", "implement", "complete"]).has(stage) && !isParentImplementation;
|
|
3143
3152
|
const specPath = join(change, "spec.md");
|
|
3144
3153
|
let specification = null;
|
|
3145
|
-
if (!isParentImplementation &&
|
|
3154
|
+
if (!isParentImplementation && (isFile(specPath) || specRequired)) {
|
|
3146
3155
|
try { specification = readSpecification(change, {allowLightweight: stage !== "tickets" && (stage !== "goal-plan" || Boolean(simpleGoal)), requiredArtifact:options.requiredArtifact, depth:options.depth}); }
|
|
3147
3156
|
catch(error) { errors.push(error.message); }
|
|
3148
3157
|
}
|
|
@@ -3229,7 +3238,7 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3229
3238
|
.map((entry) => [String(entry.ticket_id), entry]),
|
|
3230
3239
|
);
|
|
3231
3240
|
for (const [ticketId, artifact] of tickets) {
|
|
3232
|
-
if (new Set(["draft", "cancelled"]).has(artifact.meta.status)) continue;
|
|
3241
|
+
if (new Set(["draft", "ready", "cancelled"]).has(artifact.meta.status)) continue;
|
|
3233
3242
|
if (!worktreesByTicket.has(ticketId)) {
|
|
3234
3243
|
errors.push(`${ticketId}: Implement stage requires one Ticket workspace execution record`);
|
|
3235
3244
|
}
|
|
@@ -3245,23 +3254,28 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3245
3254
|
const attemptLimit = Number.isInteger(goalPlan.meta.integration_attempt_limit) ? goalPlan.meta.integration_attempt_limit : null;
|
|
3246
3255
|
if (attemptLimit !== null) {
|
|
3247
3256
|
for (const worktree of changeStatus.worktrees ?? []) {
|
|
3248
|
-
if (
|
|
3249
|
-
errors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration
|
|
3257
|
+
if ((worktree.attempts ?? worktree.integration?.attempts ?? 0) > attemptLimit) {
|
|
3258
|
+
errors.push(`${worktree.ticket_id}: integration attempts ${worktree.attempts ?? worktree.integration?.attempts} exceed Goal Plan limit ${attemptLimit}`);
|
|
3250
3259
|
}
|
|
3251
3260
|
}
|
|
3252
3261
|
}
|
|
3253
3262
|
}
|
|
3254
3263
|
validateCurrentWorkspaceExecution(changeStatus, goalPlan, errors);
|
|
3255
3264
|
const directExecution = changeStatus?.worktrees?.find(entry => entry.purpose === "direct-spec");
|
|
3256
|
-
if (
|
|
3265
|
+
if (changeStatus?.executionContext && workspace.config) {
|
|
3266
|
+
const execution=changeStatus.executionContext,limits=workspace.config.execution;
|
|
3267
|
+
if (execution.integration_attempt_limit > limits.max_integration_attempts) errors.push('Execution Context exceeds config max_integration_attempts');
|
|
3268
|
+
if (execution.concurrent_implementation_limit > limits.max_concurrent_implementations) errors.push('Execution Context exceeds config max_concurrent_implementations');
|
|
3269
|
+
for (const record of changeStatus.worktrees ?? []) if (record.attempts > limits.max_integration_attempts) errors.push(`${record.ticket_id ?? record.purpose}: verification attempts exceed config limit`);
|
|
3270
|
+
}
|
|
3257
3271
|
if (directExecution && (ticketMode || isParentImplementation)) errors.push("Direct Spec execution cannot coexist with Ticket or parent implementation artifacts");
|
|
3258
|
-
if (directExecution && (stage === "complete" || changeStatus.outcome === "completed") && !new Set(["integrated", "removed"]).has(directExecution.status)) errors.push("Direct Spec completion requires
|
|
3272
|
+
if (directExecution && (stage === "complete" || changeStatus.outcome === "completed") && !new Set(["verified", "integrated", "removed"]).has(directExecution.status)) errors.push("Direct Spec completion requires verified execution; active or failed checks cannot complete");
|
|
3259
3273
|
validateGitEvidence(stage === "complete" ? workspace.context.projectRoot : repoRoot, changeStatus, errors);
|
|
3260
3274
|
|
|
3261
3275
|
if (stage === "implement" && spec && !ticketMode && (spec.meta.ready_for_tickets !== true || spec.meta.status !== "ready")) errors.push("Direct Spec implementation requires a ready Spec with ready_for_tickets=true");
|
|
3262
3276
|
if (spec?.meta.ready_for_tickets === true && !/\bAC-\d+\b/.test(spec.body)) errors.push("ready Spec must define at least one AC-### acceptance contract");
|
|
3263
3277
|
|
|
3264
|
-
if (stage === "complete" && spec && !ticketMode && !isParentImplementation
|
|
3278
|
+
if (stage === "complete" && spec && !ticketMode && !isParentImplementation) errors.push(...validateAcceptanceEvidence(change,"evidence/direct-spec.md",[...new Set(spec.body.match(/\bAC-\d+\b/g) ?? [])]));
|
|
3265
3279
|
|
|
3266
3280
|
// New completion/archival consumes actual review evidence. Historical records
|
|
3267
3281
|
// remain readable outside these gates; archived validation returned earlier.
|
|
@@ -3275,17 +3289,18 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3275
3289
|
} else if (owner.work === 'specdev/triage' && source && triage?.meta.ready_for_implementation === false) nonImplementation = true;
|
|
3276
3290
|
} catch(error) { errors.push(error.message); }
|
|
3277
3291
|
}
|
|
3278
|
-
if (stage === 'complete' && changeStatus?.location.kind === 'active' && changeStatus?.outcome !== 'cancelled' && !
|
|
3292
|
+
if (stage === 'complete' && changeStatus?.location.kind === 'active' && changeStatus?.outcome !== 'cancelled' && !nonImplementation) {
|
|
3279
3293
|
const units = [...tickets.values()].filter(ticket => ticket.meta.status !== 'cancelled');
|
|
3280
3294
|
const finalArtifact = isParentImplementation ? 'evidence/implementation-orchestration.md'
|
|
3281
3295
|
: units.length > 1 ? 'evidence/final-review.md'
|
|
3282
3296
|
: units.length === 1 ? `evidence/${units[0].meta.id}.md`
|
|
3283
3297
|
: lightweight ? 'README.md' : 'evidence/direct-spec.md';
|
|
3284
3298
|
try {
|
|
3285
|
-
const
|
|
3299
|
+
const deliveryRoot = changeStatus.workspaceRoot ?? workspace.context.projectRoot;
|
|
3300
|
+
const head = execFileSync('git',['-C',deliveryRoot,'rev-parse','--verify','HEAD^{commit}'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
|
|
3286
3301
|
const executions = [...(changeStatus.worktrees ?? []), ...[...(parentImplementation?.memberStatuses?.values() ?? [])].flatMap(status=>status.worktrees ?? [])];
|
|
3287
|
-
const owners = executions.flatMap(record => [record.implementation_owner, ...(record.integration?.conflict_paths?.length ? [record.integration_owner] : [])]).filter(value => typeof value === 'string' && /^[a-z][a-z0-9-]*:\S+$/i.test(value));
|
|
3288
|
-
const bases = [...new Set(executions.map(record=>record.base_sha).filter(value=>/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(value ?? '')))];
|
|
3302
|
+
const owners = executions.flatMap(record => [...(record.implementation_owners ?? []), record.implementation_owner, ...(record.integration?.conflict_paths?.length ? [record.integration_owner] : [])]).filter(value => typeof value === 'string' && /^[a-z][a-z0-9-]*:\S+$/i.test(value));
|
|
3303
|
+
const bases = [...new Set([changeStatus.executionContext?.base_sha, ...executions.map(record=>record.base_sha)].filter(value=>/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(value ?? '')))];
|
|
3289
3304
|
const expectedBase = bases.length > 1 ? execFileSync('git',['-C',workspace.context.projectRoot,'merge-base','--octopus',...bases],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim() : bases[0] ?? null;
|
|
3290
3305
|
const sourceLocator = path => {
|
|
3291
3306
|
const snapshot = workspace.context.readOnlySnapshot;
|
|
@@ -3294,9 +3309,12 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3294
3309
|
};
|
|
3295
3310
|
const specLocator = sourceLocator(spec ? spec.path : join(change,'README.md'));
|
|
3296
3311
|
const requiredSpecifications = isParentImplementation
|
|
3297
|
-
? [sourceLocator(join(change,'implementation-plan.md')),sourceLocator(join(change,'implementation-map.md')),...new Set([...(parentImplementation?.ticketByTask?.values() ?? [])].map(unit=>sourceLocator(join(unit.memberRoot,'spec.md'))))]
|
|
3312
|
+
? [sourceLocator(join(change,'implementation-plan.md')),sourceLocator(join(change,'implementation-map.md')),...new Set([...(parentImplementation?.ticketByTask?.values() ?? [])].map(unit=>sourceLocator(join(unit.memberRoot,'spec.md')))),...[...(parentImplementation?.memberStatuses ?? [])].filter(([,status])=>status.executionContext).map(([member])=>{
|
|
3313
|
+
const units=[...parentImplementation.ticketByTask.values()].filter(unit=>unit.member===member && unit.artifact.meta.status !== 'cancelled');
|
|
3314
|
+
return units.length ? `${sourceLocator(join(units[0].memberRoot,'evidence',units.length>1?'final-review.md':`${units[0].artifact.meta.id}.md`))}#review-input` : null;
|
|
3315
|
+
}).filter(Boolean)]
|
|
3298
3316
|
: spec ? [specLocator] : [`${specLocator}#scope`,`${specLocator}#acceptance`];
|
|
3299
|
-
errors.push(...validateIndependentReview(change,finalArtifact,{repoRoot:workspace.context.projectRoot,expectedHead:head,expectedBase,requiredImplementers:owners,requiredSpecifications,checkCurrent:true,requireDelivery:
|
|
3317
|
+
errors.push(...validateIndependentReview(change,finalArtifact,{repoRoot:deliveryRoot,sourceRoot:workspace.context.projectRoot,expectedHead:isParentImplementation?null:head,expectedBase:isParentImplementation?null:expectedBase,requiredImplementers:owners,requiredSpecifications,checkCurrent:true,requireDelivery:!isParentImplementation,allowSourceFiles:isParentImplementation}));
|
|
3300
3318
|
} catch(error) { errors.push(`independent-review: final delivery checkpoint unavailable: ${error.message}`); }
|
|
3301
3319
|
}
|
|
3302
3320
|
|
|
@@ -3376,6 +3394,17 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3376
3394
|
}
|
|
3377
3395
|
}
|
|
3378
3396
|
|
|
3397
|
+
const scheduling = ticketPolicy(goalPlan?.meta,changeStatus?.executionContext);
|
|
3398
|
+
const inFlight = [...tickets.values()].filter(ticket=>['in_progress','review'].includes(ticket.meta.status));
|
|
3399
|
+
if (scheduling.max_parallel_tickets !== null && inFlight.length > scheduling.max_parallel_tickets) errors.push('concurrent Ticket limit exceeded');
|
|
3400
|
+
for (const ticket of inFlight) for (const dependency of graph.get(ticket.meta.id) ?? []) if (tickets.get(dependency)?.meta.status !== 'done') errors.push(`${ticket.meta.id}: active Ticket prerequisite is not done: ${dependency}`);
|
|
3401
|
+
for (const record of changeStatus?.worktrees ?? []) if (record.schema_version === 1 && record.ticket_id && ['active','review'].includes(record.status) && !['in_progress','review'].includes(tickets.get(record.ticket_id)?.meta.status)) errors.push(`${record.ticket_id}: active execution requires an in_progress/review Ticket`);
|
|
3402
|
+
for (let i=0;i<inFlight.length;i++) for (const other of inFlight.slice(i+1)) {
|
|
3403
|
+
const ticket = inFlight[i];
|
|
3404
|
+
if ((ticket.meta.writable_paths ?? []).some(a=>(other.meta.writable_paths ?? []).some(b=>pathsOverlap(a,b)))) errors.push(`active Tickets ${ticket.meta.id}/${other.meta.id} have conflicting writable paths`);
|
|
3405
|
+
if ((ticket.meta.resource_claims ?? []).some(a=>(other.meta.resource_claims ?? []).some(b=>a === b || a.endsWith('*') && b.startsWith(a.slice(0,-1)) || b.endsWith('*') && a.startsWith(b.slice(0,-1))))) errors.push(`active Tickets ${ticket.meta.id}/${other.meta.id} have conflicting semantic resources`);
|
|
3406
|
+
if (transitivelyDepends(graph,ticket.meta.id,other.meta.id) || transitivelyDepends(graph,other.meta.id,ticket.meta.id)) errors.push('active Ticket dependency is not completed');
|
|
3407
|
+
}
|
|
3379
3408
|
const evidenceDir = join(change, "evidence");
|
|
3380
3409
|
for (const [ticketId, artifact] of tickets) {
|
|
3381
3410
|
if (artifact.meta.status !== "done") continue;
|
|
@@ -3385,6 +3414,7 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3385
3414
|
continue;
|
|
3386
3415
|
}
|
|
3387
3416
|
const text = readText(evidence);
|
|
3417
|
+
if (changeStatus?.worktrees?.find(entry=>entry.ticket_id === ticketId)?.schema_version === 1) errors.push(...validateAcceptanceEvidence(change,`evidence/${ticketId}.md`,artifact.meta.contract_ids ?? []));
|
|
3388
3418
|
if (stage === 'complete' && changeStatus?.location.kind === 'active' || hasIndependentReview(evidence)) {
|
|
3389
3419
|
const execution = changeStatus?.worktrees?.find(entry => entry.ticket_id === ticketId);
|
|
3390
3420
|
errors.push(...validateIndependentReview(change, `evidence/${ticketId}.md`, {
|
|
@@ -3410,17 +3440,17 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3410
3440
|
}
|
|
3411
3441
|
}
|
|
3412
3442
|
const workspaceVerification = sectionBody(text, "## 5. Workspace Verification");
|
|
3413
|
-
if (!/(current-workspace|source-worktree)/.test(workspaceVerification)) {
|
|
3414
|
-
errors.push(`${toPosix(relative(change, evidence))}: Workspace Verification must name
|
|
3443
|
+
if (!/(change-workspace|current-workspace|source-worktree)/.test(workspaceVerification)) {
|
|
3444
|
+
errors.push(`${toPosix(relative(change, evidence))}: Workspace Verification must name change-workspace`);
|
|
3415
3445
|
}
|
|
3416
|
-
if (!currentWorkspace && /E2E[^\n]*(?:\bpass(?:ed)?\b|通过)/i.test(workspaceVerification) && !/不得|禁止|not-run/i.test(workspaceVerification)) {
|
|
3446
|
+
if (changeStatus?.worktrees?.find(entry=>entry.ticket_id === ticketId)?.schema_version !== 1 && !currentWorkspace && /E2E[^\n]*(?:\bpass(?:ed)?\b|通过)/i.test(workspaceVerification) && !/不得|禁止|not-run/i.test(workspaceVerification)) {
|
|
3417
3447
|
errors.push(`${toPosix(relative(change, evidence))}: E2E pass cannot come from source-worktree`);
|
|
3418
3448
|
}
|
|
3419
3449
|
const worktree = Array.isArray(changeStatus?.worktrees)
|
|
3420
3450
|
? changeStatus.worktrees.find((entry) => entry?.ticket_id === ticketId)
|
|
3421
3451
|
: null;
|
|
3422
|
-
if (!worktree || !new Set(["integrated", "removed"]).has(worktree.status)) {
|
|
3423
|
-
errors.push(`${ticketId}: status is done but no
|
|
3452
|
+
if (!worktree || !new Set(["verified", "integrated", "removed"]).has(worktree.status)) {
|
|
3453
|
+
errors.push(`${ticketId}: status is done but no verified Ticket execution exists`);
|
|
3424
3454
|
}
|
|
3425
3455
|
}
|
|
3426
3456
|
|
|
@@ -3454,7 +3484,7 @@ function validateChange(change, stage = null, repoRoot = null, engineeringKind =
|
|
|
3454
3484
|
}
|
|
3455
3485
|
if (
|
|
3456
3486
|
stage === "complete" &&
|
|
3457
|
-
!ticketFiles.length && !
|
|
3487
|
+
!ticketFiles.length && !lightweight &&
|
|
3458
3488
|
!isFile(join(change, "implementation-map.md")) &&
|
|
3459
3489
|
!isFile(join(change, "evidence", "direct-spec.md"))
|
|
3460
3490
|
) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specdev-a-archive-and-consolidate
|
|
3
|
-
description: "核对并封存已完成或取消的 change,或独立审计维护 ADR、context 与当前 docs
|
|
3
|
+
description: "核对并封存已完成或取消的 change、已处置 RETRO,或独立审计维护 ADR、context 与当前 docs;拥有设计基线网关,明确要求时从代码访谈形成知识 change。"
|
|
4
4
|
metadata:
|
|
5
5
|
speculo-kind: work-skill
|
|
6
6
|
speculo-id: specdev/archive-and-consolidate
|
|
@@ -14,6 +14,7 @@ metadata:
|
|
|
14
14
|
|
|
15
15
|
## 选择范围
|
|
16
16
|
|
|
17
|
+
- **archive-retro**:R 的项目 Skill 升级已验证后,按 <Path>{roots.workflows}/specdev/specdev-a-archive-and-consolidate/references/retro-archive.md</Path> 执行固定报告及方案的归档;复用具体批准。该窄分支使用独立 RETRO 门,不执行下方 change 专属流程。
|
|
17
18
|
- **archive-single / archive-batch**:用户指定 completed/cancelled 对象;batch 必须明确请求并固定清单。无候选返回 no-candidates,不创建 change。
|
|
18
19
|
- **knowledge-audit / knowledge-maintenance**:按用户指定主题检查/维护既有知识,无需新 change;只读审计默认会话结果。
|
|
19
20
|
- **consolidate-from-code**:明确要求从代码访谈形成新知识,按 <Path>{roots.workflows}/specdev/specdev-a-archive-and-consolidate/references/consolidation-interview.md</Path> 建立真实验收载体,验证后结束并走归档。
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A 单一拥有本合同;Command 只路由领域入口,不作为通用知识执行器。默认 dry-run 返回会话计划、零文件写入;confirmed 仅执行真实具体批准覆盖的动作。
|
|
4
4
|
|
|
5
|
+
R 调用的 RETRO 归档沿 [RETRO 窄网关](retro-archive.md),总决策/执行回执留在原升级方案,逐对象移动回执随对象保存;不创建 change 回执或触发以下四类维护。
|
|
6
|
+
|
|
5
7
|
## 输入、范围与唯一回执
|
|
6
8
|
|
|
7
9
|
输入是验证过的 WorkContext(projectRoot/methodRoot/stateRoot)、固定目标/主题清单、模式、批准来源和写集。未指定 change 时只读目录/README 候选消歧,不读旧全局索引决定新身份。独立知识审计/维护无需虚构 change。资料缺失或网关不明只阻塞依赖动作。归档没有候选返回 no-candidates;不创建访谈 change 或空回执。
|
package/template/workflows/specdev/specdev-a-archive-and-consolidate/references/retro-archive.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# RETRO 升级后的归档网关
|
|
2
|
+
|
|
3
|
+
A 唯一拥有移动与封存;R 提供原升级方案、具体批准和已验证执行回执。本分支不接管 Skill 写入,不创建 change,也不运行 change 的实现完成门或四类资产维护。授权须覆盖 PLAN 中固定的 archive_reports 及方案对象本身,已确认内容直接复用。
|
|
4
|
+
|
|
5
|
+
读取 <Path>{roots.workflows}/specdev/specdev-r-retro/references/records.md</Path> 的身份和定位合同,以及 <Path>{roots.workflows}/specdev/common/schemas/retro-archive.schema.json</Path>。以下复用 [原归档规则](archive-rules.md) 的无覆盖、完整性与恢复原则,源/目标替换为 retro 对象,不把报告当成 completed change。
|
|
6
|
+
|
|
7
|
+
## 预检与顺序
|
|
8
|
+
|
|
9
|
+
1. 回读 PLAN.md、approval.json 与实际确认原文,验证候选和批准摘要、执行 owner、实际 writer、相关 lock/pending/recovery;数据字段不是授权。运行独立校验器的升级 `--stage verified`,全部计划内 Skill 项通过才继续。
|
|
10
|
+
2. 固定 archive_reports 清单。每份报告均 completed,所有发现已有 adopt 且执行通过或 reject 且有明确理由;存在 defer 的报告继续现役。拒绝、无发现、证据不足报告也需要具体归档决定,不能因没有候选自动移动。仅复用仍然匹配原方案的证据。
|
|
11
|
+
3. 对全部待移动报告先运行 `--report <id> --upgrade-plan <upgrade-id> --stage archive-ready`。确认源与唯一目标、同文件系统、无覆盖能力、目录/模式/链接及稳定来源可解析,预检全部通过才开始批量移动。
|
|
12
|
+
4. 在每个原对象内创建 archive.json,保存实际字节、目录模式及链接清单。使用校验器导出的 `retroInventory(objectRoot)` 生成实际清单;它不跟随链接,只排除 archive.json 自身。不得手填摘要或漏掉候选、批准、证据及恢复材料。
|
|
13
|
+
5. 重验批准、源摘要、目标缺失及唯一 writer。使用宿主提供的同文件系统无覆盖移动,或可证明单 writer/隔离边界下的移动;不能靠“先检查再 rename”宣称并发无覆盖。跨文件系统不降级复制删除。整体移动至 <Path>{roots.state}/specdev/retro/archive/{retro}/</Path>,保持原目录名和正文。
|
|
14
|
+
6. 原回执 result=moved,从实际目标运行 `--stage archived`,确认源消失、唯一目标、清单、字节、模式、链接、原批准及来源可解析。通过后原回执记 verified,再运行 `--stage archived --sealed`。只有通过才封存;不能用目录已移动代替成功。
|
|
15
|
+
7. 全部获批报告封存后,最后对方案运行 `--upgrade <upgrade-id> --stage archive-ready`,按相同过程移动至 <Path>{roots.state}/specdev/retro/archive/{upgrade}/</Path> 并 sealed。仍有 defer 的报告留在现役;后续按报告/F-ID 定位本方案既往处置即可,不复制待办状态。
|
|
16
|
+
|
|
17
|
+
命令均使用 <Path>{roots.workflows}/specdev/common/tools/validate-retro.mjs</Path> 并传明确 `--repo`、`--method-root`、`--state-root`;最后一个是 specdev 的父数据根。报告的 archived 检查仍传 `--upgrade-plan`,方案本身不传。
|
|
18
|
+
|
|
19
|
+
## archive.json
|
|
20
|
+
|
|
21
|
+
记录 schema_version=1、id、对象原 owner、upgrade ID、source=`retro/<id>`、target=`retro/archive/<id>`、PLAN.md 摘要、approval.json 摘要、result=moved/verified、实际 time 与 entries。entries 每项为对象内 path、type=file/directory/symlink、整数 mode;file 另有 sha256,symlink 另有原始 target。归档前在隔离写集内固定清单,移动后复核;清单字段不替代真实移动观察。
|
|
22
|
+
|
|
23
|
+
## 恢复与冻结
|
|
24
|
+
|
|
25
|
+
普通批量操作不具有整体原子性。部分移动失败保留已落地事实,从源/目标和各自原回执继续;源和目标同时存在、摘要漂移或结果不明时停止该分支,不覆盖、不重复移动。方案对象必须最后归档,避免丢失未完操作的负责位置。
|
|
26
|
+
|
|
27
|
+
封存后报告、方案、候选和回执只读。后续 Skill 再次升级不改变已封存事实:历史验证核对当时获批候选、执行与归档清单,不要求当前 Skill 永远保持旧摘要。来源 change 移动依赖稳定 ID 解析;纠错另建后继记录,不能借恢复重写历史。
|
|
@@ -50,6 +50,6 @@ UI diff 按 <Path>{roots.workflows}/specdev/common/rules/design-contract.md</Pat
|
|
|
50
50
|
|
|
51
51
|
进入实现或审查时读取 <Path>{roots.references}/coordination/coordination-contract.md</Path>;未明确指定时按当前宿主正常执行,App、CLI 等均可,subagent/agent team 属于宿主能力。明确指定 CLI 交互、Herdr 等方式时按选择执行并展开对应参考;编排与 current/worktree 独立,已有授权和选择直接复用。只读取所选工具的当前指南,不固定工具或模型。
|
|
52
52
|
|
|
53
|
-
`lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或擅自跨 CLI
|
|
53
|
+
`lead` 上下文维护原业务状态和 Evidence;`task` 上下文只处理 Lead 指定目标和阶段,执行同一方法并返回候选、findings、检查和未验证项,不修改正式 Ticket、Map、Goal、Evidence,不递归启动 Goal 或擅自跨 CLI 派单;宿主原生协作遵守原写集和共享资源 owner 约束。未曝光到宿主列表不影响读取真实入口。
|
|
54
54
|
|
|
55
55
|
P 在已授权 run/resume 中完成当前实现、审查、验证和同范围修复后继续下一 Ready 任务,不逐票询问“继续”。只读分析或 plan 不执行。双轴审查必须由未参与本范围实现的 Agent 执行,默认派遣一位原生 subagent;已有编排选择沿用于 review,不能由实现者或参与实现的 Lead 自审替代。
|