@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.
Files changed (89) hide show
  1. package/package.json +1 -1
  2. package/template/canonical/canonical-specdev-goal-plan.md +2272 -1069
  3. package/template/canonical/canonical-specdev-grill-with-docs.md +2164 -661
  4. package/template/canonical/canonical-specdev-spec.md +2163 -660
  5. package/template/canonical/canonical-specdev-tickets.md +2095 -574
  6. package/template/canonical/canonical-specdev-wayfinder.md +2339 -836
  7. package/template/references/coordination/coordination-contract.md +8 -6
  8. package/template/references/coordination/modes/cli.md +1 -1
  9. package/template/references/coordination/modes/herdr.md +2 -2
  10. package/template/references/coordination/modes/orca.md +3 -3
  11. package/template/references/coordination/report-template.md +18 -0
  12. package/template/references/coordination/result-and-recovery.md +4 -4
  13. package/template/references/coordination/session-files.md +49 -56
  14. package/template/references/coordination/task-brief.md +7 -7
  15. package/template/references/coordination/task-template.md +25 -0
  16. package/template/references/retrospective/references/friction-taxonomy.md +1 -1
  17. package/template/references/writing-for-agents/references/project-skill-maintenance.md +29 -0
  18. package/template/references/writing-for-agents/writing-contract.md +2 -0
  19. package/template/workflows/specdev/SKILL.md +1 -1
  20. package/template/workflows/specdev/common/README.md +1 -1
  21. package/template/workflows/specdev/common/rules/artifact-contract.md +1 -1
  22. package/template/workflows/specdev/common/rules/change-completion.md +6 -6
  23. package/template/workflows/specdev/common/rules/domain-runtime.md +2 -2
  24. package/template/workflows/specdev/common/rules/evidence-and-verification.md +17 -34
  25. package/template/workflows/specdev/common/rules/execution-policy.md +24 -16
  26. package/template/workflows/specdev/common/rules/operating-governance.md +1 -1
  27. package/template/workflows/specdev/common/rules/parent-implementation-orchestration.md +4 -2
  28. package/template/workflows/specdev/common/rules/path-ownership.md +9 -7
  29. package/template/workflows/specdev/common/rules/path-reference-contract.md +1 -1
  30. package/template/workflows/specdev/common/rules/workflow-routing.md +1 -1
  31. package/template/workflows/specdev/common/rules/workflow-state-and-lifecycle.md +7 -3
  32. package/template/workflows/specdev/common/schemas/execution-context.schema.json +67 -0
  33. package/template/workflows/specdev/common/schemas/execution-record.schema.json +154 -0
  34. package/template/workflows/specdev/common/schemas/goal-plan.schema.json +15 -55
  35. package/template/workflows/specdev/common/schemas/implementation-plan.schema.json +15 -55
  36. package/template/workflows/specdev/common/schemas/retro-approval.schema.json +44 -0
  37. package/template/workflows/specdev/common/schemas/retro-archive.schema.json +99 -0
  38. package/template/workflows/specdev/common/schemas/retro-execution.schema.json +112 -0
  39. package/template/workflows/specdev/common/schemas/retro-upgrade.schema.json +163 -0
  40. package/template/workflows/specdev/common/schemas/retrospective.schema.json +119 -14
  41. package/template/workflows/specdev/common/schemas/session-locator.schema.json +15 -0
  42. package/template/workflows/specdev/common/skills/code-review/references/review-evidence.md +3 -3
  43. package/template/workflows/specdev/common/skills/code-review/references/source-discovery.md +2 -0
  44. package/template/workflows/specdev/common/skills/code-review/references/working-tree-review.md +1 -1
  45. package/template/workflows/specdev/common/skills/dev-worktree/SKILL.md +15 -34
  46. package/template/workflows/specdev/common/skills/dev-worktree/references/create.md +27 -56
  47. package/template/workflows/specdev/common/skills/dev-worktree/references/finalize.md +10 -54
  48. package/template/workflows/specdev/common/tools/README.md +16 -0
  49. package/template/workflows/specdev/common/tools/delivery-artifacts.mjs +5 -5
  50. package/template/workflows/specdev/common/tools/execution-context.mjs +104 -0
  51. package/template/workflows/specdev/common/tools/plan-contract.mjs +3 -1
  52. package/template/workflows/specdev/common/tools/review-evidence.mjs +2 -2
  53. package/template/workflows/specdev/common/tools/tests/engineering.test.mjs +2 -2
  54. package/template/workflows/specdev/common/tools/tests/review-fixture.mjs +4 -4
  55. package/template/workflows/specdev/common/tools/ticket-control.mjs +17 -12
  56. package/template/workflows/specdev/common/tools/validate-handoff.mjs +108 -0
  57. package/template/workflows/specdev/common/tools/validate-retro.mjs +331 -0
  58. package/template/workflows/specdev/common/tools/validate-specdev.mjs +135 -105
  59. package/template/workflows/specdev/specdev-a-archive-and-consolidate/SKILL.md +2 -1
  60. package/template/workflows/specdev/specdev-a-archive-and-consolidate/references/operation-contract.md +2 -0
  61. package/template/workflows/specdev/specdev-a-archive-and-consolidate/references/retro-archive.md +27 -0
  62. package/template/workflows/specdev/specdev-c-code-review/SKILL.md +1 -1
  63. package/template/workflows/specdev/specdev-i-implement/SKILL.md +3 -3
  64. package/template/workflows/specdev/specdev-i-implement/assets/evidence-template.md +39 -31
  65. package/template/workflows/specdev/specdev-i-implement/references/execution-preflight.md +16 -16
  66. package/template/workflows/specdev/specdev-i-implement/references/implementation-procedure.md +21 -37
  67. package/template/workflows/specdev/specdev-i-implement/references/merge-conflict-protocol.md +8 -18
  68. package/template/workflows/specdev/specdev-p-goal-plan/SKILL.md +3 -3
  69. package/template/workflows/specdev/specdev-p-goal-plan/assets/goal-plan-template.md +23 -26
  70. package/template/workflows/specdev/specdev-p-goal-plan/references/completion-control.md +11 -14
  71. package/template/workflows/specdev/specdev-p-goal-plan/references/lead-orchestration.md +12 -33
  72. package/template/workflows/specdev/specdev-p-goal-plan/references/map-control.md +1 -1
  73. package/template/workflows/specdev/specdev-p-goal-plan/references/multi-change-plan.md +11 -16
  74. package/template/workflows/specdev/specdev-p-goal-plan/references/multi-conflict-and-drift.md +2 -2
  75. package/template/workflows/specdev/specdev-p-goal-plan/references/multi-execution-loop.md +6 -6
  76. package/template/workflows/specdev/specdev-p-goal-plan/references/orchestration-protocol.md +13 -44
  77. package/template/workflows/specdev/specdev-p-goal-plan/references/planning-modes.md +18 -25
  78. package/template/workflows/specdev/specdev-p-goal-plan/references/single-change-plan.md +16 -16
  79. package/template/workflows/specdev/specdev-r-retro/SKILL.md +15 -11
  80. package/template/workflows/specdev/specdev-r-retro/assets/retro-template.md +26 -14
  81. package/template/workflows/specdev/specdev-r-retro/assets/upgrade-plan-template.md +41 -0
  82. package/template/workflows/specdev/specdev-r-retro/references/development-retrospective.md +42 -0
  83. package/template/workflows/specdev/specdev-r-retro/references/records.md +47 -0
  84. package/template/workflows/specdev/specdev-r-retro/references/skill-upgrade.md +13 -0
  85. package/template/workflows/specdev/specdev-t-tickets/SKILL.md +2 -0
  86. package/template/workflows/specdev/specdev-t-tickets/assets/ticket-template.md +1 -1
  87. package/template/workflows/specdev/specdev-t-tickets/assets/tickets-map-template.md +6 -6
  88. package/template/workflows/specdev/specdev-t-tickets/references/planning-procedure.md +2 -2
  89. 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 = 7;
40
+ const GOAL_PLAN_SCHEMA_VERSION = 8;
40
41
  const IMPLEMENTATION_MAP_SCHEMA_VERSION = 1;
41
- const IMPLEMENTATION_PLAN_SCHEMA_VERSION = 2;
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
- "Concurrent implementations",
216
+ "Ticket scheduling",
217
217
  "Read-only agents",
218
218
  "execution-time dynamic",
219
- "### Ticket Workspace and Integration",
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:v7" ||
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
- !["current", "required"].every((value) => goalPlanSchema.properties?.ticket_workspace_policy?.enum?.includes(value)) ||
764
- !["direct-parent", "candidate-merge"].every((value) => goalPlanSchema.properties?.integration_gate?.enum?.includes(value)) ||
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:v2" ||
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
- !["current", "required"].every((value) => implementationPlanSchema.properties?.ticket_workspace_policy?.enum?.includes(value)) ||
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
- "candidate-merge",
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", "parent-candidate", "共享编排方法", "实现 commit"],
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: 7",
977
+ "schema_version: 8",
976
978
  "orchestration: lead-directed",
977
- "concurrent_implementation_limit: 3",
979
+ "ticket_execution: auto",
978
980
  "integration_attempt_limit: 3",
979
- "ticket_workspace_policy: current",
980
- "integration_gate: direct-parent",
981
+
981
982
  ...REQUIRED_LEAD_GOAL_PLAN_MARKERS,
982
- "Local direct-parent verification and parent update",
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 !== GOAL_PLAN_SCHEMA_VERSION) {
1888
- errors.push(`${basename(path)}: artifact/schema_version must be goal-plan/${GOAL_PLAN_SCHEMA_VERSION}`);
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
- ? [...REQUIRED_LEAD_GOAL_PLAN_MARKERS, "Local direct-parent verification and parent update"]
1942
- : [...REQUIRED_LEAD_GOAL_PLAN_MARKERS, "source worktree 不运行 E2E", "Local candidate integration and parent update"];
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 Array.isArray(data.worktrees) ? data.worktrees : []) {
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 = Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : [];
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 Array.isArray(changeStatus?.worktrees) ? changeStatus.worktrees : []) {
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", "ticket_workspace_policy", "integration_gate", "ready_for_execution"];
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/1");
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
- for (const key of ["concurrent_implementation_limit", "integration_attempt_limit"]) {
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 worktree of Array.isArray(status.worktrees) ? status.worktrees : []) {
2765
- if (worktree?.status === "active") {
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
- if (isFile(goalPath)) {
2880
- const goal = validateGoalPlan(goalPath, memberErrors);
2881
- if (goal && (
2882
- goal.meta.ticket_workspace_policy !== plan.meta.ticket_workspace_policy ||
2883
- goal.meta.integration_gate !== plan.meta.integration_gate
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
- if (activeImplementations > plan.meta.concurrent_implementation_limit) {
2999
- errors.push(`parent implementation agent limit exceeded: ${activeImplementations} active for limit ${plan.meta.concurrent_implementation_limit}`);
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 integrated or removed child workspace record`);
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(...retro.errors, ...prs.errors);
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
- const retroOnly = retro.records.length > 0 && retro.errors.length === 0 && retro.records.every(r => r.meta.status === "completed") &&
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 && !retroOnly;
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 && !retroOnly && (isFile(specPath) || specRequired)) {
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 (worktree?.integration && Number.isInteger(worktree.integration.attempts) && worktree.integration.attempts > attemptLimit) {
3249
- errors.push(`${worktree.ticket_id}: integration attempts ${worktree.integration.attempts} exceed Goal Plan limit ${attemptLimit}`);
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 (directExecution && workspace.config) errors.push(...validateExecutionPolicy({meta:{concurrent_implementation_limit:1,integration_attempt_limit:directExecution.integration_attempt_limit}},workspace.config).map(error => `Direct Spec: ${error}`));
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 integrated or removed execution; active or failed candidates cannot complete");
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 && !retroOnly) errors.push(...validateAcceptanceEvidence(change,"evidence/direct-spec.md",[...new Set(spec.body.match(/\bAC-\d+\b/g) ?? [])]));
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' && !retroOnly && !nonImplementation) {
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 head = execFileSync('git',['-C',workspace.context.projectRoot,'rev-parse','--verify','HEAD^{commit}'],{encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
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:true}));
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 current-workspace or source-worktree`);
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 integrated or removed Ticket worktree exists`);
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 && !retroOnly && !lightweight &&
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;拥有项目设计基线批准与提升网关,仅在用户明确要求时从代码访谈形成知识 change。"
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 或空回执。
@@ -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 派单;宿主原生协作遵守原写集和单 writer 约束。未曝光到宿主列表不影响读取真实入口。
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 自审替代。