@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
@@ -4,11 +4,11 @@
4
4
 
5
5
  ## 冻结输入
6
6
 
7
- 在报告中建立独立的 `review-input` 锚点,用一个 JSON 对象保存 `scope`(review-scope 的完整原始返回值)、`sources` 和适用的 `exclusions`。sources 每项为 `kind: standards|specification|instructions`、项目根相对 `locator` 和实际 UTF-8 原文字节的 `sha256`。locator 可带显式锚点;轻量 README 只摘要其 scope/acceptance 等合同段,不能摘要包含审查记录的整份 README。来源必须覆盖本轮实际规范、标准及适用指令。
7
+ 在报告中建立独立的 `review-input` 锚点,用一个 JSON 对象保存 `scope`(review-scope 的完整原始返回值)、`sources` 和适用的 `exclusions`。sources 每项为 `kind: standards|specification|instructions`、项目根相对 `locator` 和实际 UTF-8 原文字节的 `sha256`。locator 可带显式锚点;轻量 README 只摘要其 scope/acceptance 等合同段,不能摘要包含审查记录的整份 README。来源必须覆盖本轮实际规范、标准及适用指令。规范来源 locator 相对明确项目根,代码 scope 相对选定 change 工作区;两者不同时,校验器分别使用 sourceRoot 与 repoRoot,不复制规范进 worktree、不用 .. 穿越 locator。
8
8
 
9
9
  `Input-SHA256` 为该对象经 JSON.parse 后再 JSON.stringify 的 UTF-8 SHA256,由工具计算,不手写猜测。将同一对象及摘要交给 reviewer。摘要证明输入一致,不证明审查真的发生。
10
10
 
11
- 未提交的 Direct Spec 先读取 <Path>{roots.workflows}/specdev/common/skills/code-review/references/working-tree-review.md</Path>。Git diff 使用实际工具返回的不可变端点;规范/标准和相关指令变化也会使受影响结论失效。
11
+ 未提交的 Ticket 或 Direct Spec 先读取 <Path>{roots.workflows}/specdev/common/skills/code-review/references/working-tree-review.md</Path>。Git diff 使用实际工具返回的不可变端点;规范/标准和相关指令变化也会使受影响结论失效。
12
12
 
13
13
  ## 最小记录
14
14
 
@@ -31,7 +31,7 @@ Verification: evidence/T-01.md#review-receipt
31
31
 
32
32
  Mode 为 native/herdr/orca/cli。Implementers 包含本范围全部实现及修复者;Lead 参与实现也列入。每轴 Report 指向 reviewer 实际输出的独立段,含 `Reviewer`、`Axis`、`Result`、`Input-SHA256`、`Observed` 字段及具体 findings/依据。不能指回汇总表自证。请求修正或未验证不得改写为 pass;原报告不覆盖。
33
33
 
34
- Verification 指向 Lead 回读真实派遣和返回事实后的独立核验段,含 `Verified-By`、`Mode`、`Reviewers`(逗号分隔真实身份)、`Input-SHA256`、`Result: verified`、`Observed`。Observed 说明工具返回的实际身份、派遣/返回定位、只读范围和核验事实;工具不提供的事实保持未验证。需要深查时沿原历史会话 locator 检索,不以 JSONL 文件存在代替审查。
34
+ Verification 指向 Lead 回读真实派遣和返回事实后的独立核验段,含 `Verified-By`、`Mode`、`Reviewers`(逗号分隔真实身份)、`Input-SHA256`、`Result: verified`、`Observed`。Observed 说明工具返回的实际身份、派遣/返回定位、只读范围和核验事实;工具不提供的事实保持未验证。需要深查时沿对应任务 session-locator.json 的 path,结合 TASK 内容检索(旧任务沿原历史 locator),不以 JSONL 文件存在代替审查。
35
35
 
36
36
  ## 完成与复用
37
37
 
@@ -17,6 +17,8 @@ UI 范围按 <Path>{roots.workflows}/specdev/common/rules/design-contract.md</Pa
17
17
 
18
18
  穷尽仓库中声明代码写法的文件:适用的 AGENTS/CLAUDE、CONTRIBUTING、编码标准、lint/type/test 配置和项目生成的 standards skill。记录适用范围;工具已经机械执行的格式项不重复生成人工噪声。
19
19
 
20
+ 项目 CODING_STANDARDS.md 或已有 standards Skill 的 reviewer 分支是判断性标准的入口;包含从 RETRO 升级的规则时沿其来源与适用范围读取。实现入口只需相应导航,不要求实现者预加载整份审查标准。机械规则是否真正接入检查由配置和执行证据判断,文字禁令不替代 guardrail。
21
+
20
22
  ## 完成标准
21
23
 
22
24
  - 每个候选来源有 found/not-found/not-applicable 结论;
@@ -1,6 +1,6 @@
1
1
  # 未提交工作副本审查
2
2
 
3
- I 的 Direct Spec current 未获提交授权时使用本分支。它覆盖实际交付差异,不以 HEAD 代替未提交内容;C 的 source-files 仍只承诺指定文件阅读。
3
+ I 的 Ticket 或 Direct Spec 未提交交付使用本分支;current 与 change worktree 均适用。它覆盖实际交付差异,不以 HEAD 代替未提交内容;C 的 source-files 仍只承诺指定文件阅读。
4
4
 
5
5
  1. 停止相关 writer,回读实施前 Git 基线、原有 dirty 改动、当前授权写集与实际变更。确认项目 Git 根;无已存在的基线、冲突 index、不可读取文件或不支持的子模块时保持未验证,不伪造提交。
6
6
  2. 调用以下只读工具。`--file` 可重复,使用已核对的完整交付文件清单,包含删除路径;不传时覆盖相对基线的全部实际变化和非忽略未跟踪文件。只读冻结不创建 commit、不暂存、不修改 index。
@@ -1,53 +1,34 @@
1
1
  ---
2
2
  name: dev-worktree
3
- description: 管理 SpecDev required Ticket 或 Direct Spec change 的 Git worktree;创建隔离来源 workspace、固定实现 commit,并由 Lead 在 parent-candidate checkout 验证后推进父分支。
3
+ description: 为 SpecDev change 创建、复用、恢复和核验一个选定 Git worktree;仅在 change 明确选择独立工作区或授权清理时使用。
4
4
  compatibility: Requires the owning Speculo workflow and its workspace path resolver.
5
5
  ---
6
6
 
7
7
  # Dev Worktree
8
8
 
9
- 本 Skill 由 T-tickets、P-goal-plan 和 I-implement 复用。按 <Path>{roots.workflows}/specdev/common/rules/execution-policy.md</Path> 选择 `required` 时使用 source → candidate → parent 状态机,无需先建 Goal;`current` 不调用本 Skill。原生执行与 Herdr 使用同一工作区合同。
9
+ 由 P-goal-plan 和 I-implement 复用;T 只规划并引用工作区选择。按 <Path>{roots.workflows}/specdev/common/rules/execution-policy.md</Path>,一个 change 的全部票据与协作者使用同一工作区;current 不创建 worktree。
10
10
 
11
11
  ## 输入
12
12
 
13
- - `operation=create | restore | finalize | remove`;
14
- - `purpose=ticket | direct-spec`;
15
- - repository、父分支、`base_sha`、branch、portable workspace locator;
16
- - workspace、implementation 和 integration owner;
17
- - 允许动作、路径合同、验证合同、调用方状态记录位置。
13
+ - `operation=create | restore | finalize | remove`,`purpose=change`;
14
+ - 已授权 change、Spec/实际 Ticket、项目根、基线、选定 branch/workspace;
15
+ - 原 owner、授权写集、验证合同、原 README/Evidence;
16
+ - 创建或删除的具体授权;创建不要求预先授权 commit/merge。
18
17
 
19
- required Ticket 提供 Ready Ticket;Direct Spec 提供获批完整规格及验证合同。两者都提供实际存在的 Goal/父计划、原 Evidence 路径、Lead、尝试上限来源、implementation commit 与 local candidate integration/父分支更新授权。缺失时返回 blocked;不为这些输入补造 Ticket/Goal。
18
+ ## 创建或恢复
20
19
 
21
- ## 1. 创建或恢复
20
+ create/restore 读取 <Path>{roots.workflows}/specdev/common/skills/dev-worktree/references/create.md</Path>。先检查已有记录和实际 Git,精确匹配则复用。没有指定名称的新 change worktree 使用 `specdev-worktree/<change>` 和 `speculo/<change>`;不按 Ticket、Agent、重试或审查拆分目录。
22
21
 
23
- `operation=create` 时加载 `<Path>{roots.workflows}/specdev/common/skills/dev-worktree/references/create.md</Path>`。Ticket 使用 `specdev-worktree/<change>/<ticket-id>`;Direct Spec 使用 `specdev-worktree/<change>/direct-spec`。每个实现单元只有一个来源 worktree。`operation=restore` 时重读实际 Git worktree/branch/tip/dirty 状态并与调用方记录核对,漂移时停止。
22
+ **完成标准**:唯一 workspace/branch/base 与真实 Git 和原授权一致,未覆盖用户改动。
24
23
 
25
- **完成标准**:来源基线、branch、locator、owners 和实际 Git 状态一致;现有用户改动未被覆盖。
24
+ ## 核验
26
25
 
27
- ## 2. 来源实现门
26
+ finalize 读取 <Path>{roots.workflows}/specdev/common/skills/dev-worktree/references/finalize.md</Path>。它只验收选定工作区,不创建 candidate checkout、不合并或推进其他分支。每票和整个 change 通过各自 AC、独立审查及验证门。
28
27
 
29
- implementation owner 只在来源 worktree 修改授权项目路径,运行规格或 Ticket 要求的单元、组件、静态、类型、lint/build 等非 E2E 检查。进入 `review` 前,worktree 必须 clean,branch tip 必须是已授权的 `source_checkpoint` commit,实际 diff 必须符合路径合同。
28
+ ## 授权清理
30
29
 
31
- **完成标准**:source checkpoint 不可变且可达;来源 worktree 没有 E2E pass 声明。
30
+ remove 只在明确授权的精确目标上执行:writer 已停止、工作区 clean、未提交产物已另有可恢复处置,已承诺交付完成或获明确处置。先核验真实 Git,再删除并重读 worktree list/refs;保留全部 Evidence。强制删除需专门授权,不能因 done 自动清理。
32
31
 
33
- ## 3. 候选合并与父分支推进
32
+ 清理前完成仍需读取该工作区的父完成和归档验收。若用户明确要求提前删除,保留原上下文、可恢复 checkpoint 和尚未验收事项;相关验收保持阻塞,恢复时只重建原选定工作区,不能将不存在的工作区当作已核验交付。
34
33
 
35
- `operation=finalize` 仅由 Lead/integration owner 调用,并加载 `<Path>{roots.workflows}/specdev/common/skills/dev-worktree/references/finalize.md</Path>`。Lead 在独立 parent-candidate checkout 组合最新父分支与 source checkpoint,运行集成检查和适用 E2E,通过后才推进父分支。
36
-
37
- 本地 candidate checkout/branch 的创建、重建和回收属于已授权 local candidate integration;来源 branch/worktree 的删除仍需要独立 cleanup 授权。push、PR、remote merge、deploy、migration 和生产动作不从本 Skill 继承。
38
-
39
- **完成标准**:执行单元 `integrated` 时父 HEAD 精确等于记录的 result SHA,并包含 source checkpoint;失败或 stale 时父分支未变化。后续 `removed` 只表示来源 branch/worktree 已清理,不撤销该集成事实。
40
-
41
- ## 4. 移除
42
-
43
- `operation=remove` 先验证执行单元已 `integrated`、目标 worktree clean、checkpoint 可恢复且删除目标精确。只有明确 cleanup 授权时删除来源 branch/worktree;强制删除需要单独确认。删除后重读 `git worktree list` 与 refs,并只把调用方生命周期状态更新为 `removed`;`base_sha`、source checkpoint、candidate/result、验证、E2E 与 Evidence 字段必须原样保留。
44
-
45
- **完成标准**:只删除精确授权目标;失败保留现场与恢复命令。
46
-
47
- ## 固定规则
48
-
49
- - 执行方式与工作区正交;获批策略决定是否使用 worktree,Ticket 或 Direct Spec change 决定工作单元;
50
- - required E2E 只在 Lead-owned parent-candidate checkout 运行;
51
- - 每个 required 实现单元完成前必须有 source commit 与父分支 result,worktree 状态为 `integrated` 或其清理后终态 `removed`;
52
- - candidate 失败保留来源 worktree 修正,父分支不动;
53
- - 成功集成不自动清理来源 branch/worktree。
34
+ **完成标准**:只处理授权的 change 工作区,失败保留现场和下一步。push、PR、merge、部署和永久知识写入仍属于原 owning 入口。
@@ -1,71 +1,42 @@
1
- # Create Or Restore Worktree
1
+ # 创建、复用与恢复 change 工作区
2
2
 
3
- ## 前置条件
3
+ ## 前置与创建
4
4
 
5
- - Ticket Ready 或完整 Direct Spec 已获批且规格就绪,项目根是有效 Git repository,父分支和 `base_sha` 可解析;
6
- - implementation commit 与 local candidate integration/父分支更新已授权;
7
- - workspace、implementation、integration owner 唯一;integration owner 必须为 Lead;
8
- - 检查 `specdev-worktree/` 已被 Git ignore;未初始化项目由当前 I 在授权写集内补充该忽略规则并记录,不能强制运行 init;
9
- - 目标 branch/worktree 不覆盖现有用户 workspace,路径合同无冲突。
5
+ 1. 核对获批 change、原 owner、项目根、允许写集和工作区选择。复用既有授权,不把创建 worktree 当作提交或合并授权。
6
+ 2. 重读 README Execution Context、Git worktree list、branch/HEAD/dirty 状态。已有选择与 Git 一致就复用;冲突时停止,不换路径无限新建。
7
+ 3. current 使用当前项目 checkout,允许 main 或已有工作分支/worktree。worktree 新建默认 branch `speculo/<change>`、locator `specdev-worktree/<change>`;用户指定位置时使用可解析的项目相对 locator。已有外部 checkout 可作为明确 project-root 以 current 复用,不持久化机器绝对路径。
8
+ 4. 创建前检查目标不存在或精确属于本 change,检查 `specdev-worktree/` 被忽略;只在授权范围补充 ignore。固定真实 base SHA 后创建一个 worktree。恢复不执行创建,不覆盖现有 branch/目录。
9
+ 5. 在选定工作区读取项目 Agent 指令、适用 Skill、依赖与验证合同,运行实际需要的最小基线。
10
+ 6. Lead 重验旧字节后原子更新 README 唯一记录并回读;不建立全局镜像。没有安装元数据也可按明确根运行。
10
11
 
11
- ## 创建来源 worktree
12
+ ## Execution Context
12
13
 
13
- 先确定 `unit-id`:Ticket 使用真实 ID(如 T-01),Direct Spec 固定为 `direct-spec`;change 使用已解析的 change 名。下列路径由两者共同限定。
14
+ 首次真实执行时添加以下锚点和唯一 JSON object;值来自实际操作,不预造空记录。current 同样保存上下文,但不执行 worktree create。
14
15
 
15
- 1. 重读父分支 HEAD、工作树、现有 worktrees 与 refs;父 HEAD 与计划基线不一致时由 Lead决定更新 `base_sha` 或阻塞;
16
- 2. 固定 branch `speculo/<change>/<unit-id>` 与 locator `specdev-worktree/<change>/<unit-id>`;
17
- 3. 确认目标 branch/path 不存在,或其实际记录精确匹配当前实现单元;
18
- 4. 从 `base_sha` 创建 Git worktree,不复用其他实现单元目录;
19
- 5. 在来源 worktree 读取项目 Agent 指令、依赖、构建与路径合同;
20
- 6. 安装实际需要的依赖,运行最小非 E2E 基线;
21
- 7. Lead 将实际 workspace、owner、checkpoint 和 `status: active` 记录在本单元原 Evidence 的 `## Workspace Execution Record` 唯一 JSON object 中,保留原执行字段;不创建全局 worktree 镜像。
22
-
23
- 初始记录:
16
+ ````markdown
17
+ <a id="execution-context"></a>
18
+ ## Execution Context
24
19
 
25
20
  ```json
26
21
  {
27
- "ticket_id": "T-01",
28
- "owner": "lead",
29
- "implementation_owner": "lead-or-dynamic-agent",
30
- "integration_owner": "lead",
31
- "provider": "git",
32
- "base_sha": "<immutable-sha>",
33
- "parent_branch": "<parent-branch>",
34
- "branch": "speculo/<change>/T-01",
35
- "workspace_ref": "specdev-worktree/<change>/T-01",
36
- "source_checkpoint": null,
37
- "integration": {
38
- "status": "pending",
39
- "parent_ref": "<parent-branch>",
40
- "parent_before_sha": null,
41
- "source_sha": null,
42
- "candidate_sha": null,
43
- "candidate_tree_sha": null,
44
- "candidate_branch": null,
45
- "candidate_workspace_ref": null,
46
- "result_sha": null,
47
- "method": null,
48
- "conflict_paths": [],
49
- "verification": "pending",
50
- "full_suite": {"required": true, "status": "pending", "reason": null, "evidence": null},
51
- "e2e": {"required": false, "status": "not-required", "reason": "实际范围无端到端风险时填写依据", "evidence": null},
52
- "evidence": "<Path>{roots.state}/specdev/changes/<change>/evidence/T-01.md</Path>",
53
- "attempts": 0,
54
- "promotion_status": "pending"
55
- },
56
- "status": "active",
57
- "updated_at": "<ISO-8601>"
22
+ "schema_version": 1,
23
+ "change": "2026-10-15-example",
24
+ "workspace_policy": "worktree",
25
+ "workspace_ref": "specdev-worktree/2026-10-15-example",
26
+ "branch": "speculo/2026-10-15-example",
27
+ "base_sha": "<actual-immutable-git-sha>",
28
+ "owner": "native:actual-lead",
29
+ "ticket_execution": "auto"
58
30
  }
59
31
  ```
32
+ ````
60
33
 
61
- Direct Spec 将 `ticket_id` 替换为 `purpose: "direct-spec"`、`change: "<change>"`、`integration_attempt_limit: <实际正整数上限>`;branch 与 workspace 的单元部分替换为 direct-spec,integration.evidence 指向 <Path>{roots.state}/specdev/changes/{change}/evidence/direct-spec.md</Path>。不额外创建 Ticket。
62
-
63
- `full_suite` 与 `e2e` 的 required/reason 依据 Spec/Ticket/有效计划的实际验证合同;required 初始为 pending,not-required 必须给真实理由。未发生操作不预填通过。
34
+ current 使用 `workspace_policy: current`、`workspace_ref: current` 和实际 branch。`ticket_execution` 为无计划时的选择;有计划时计划拥有调度,恢复核对约束不冲突。可选 `max_parallel_tickets` 计量票据;可选 `concurrent_implementation_limit` 计量实际实现者,互不换算;可选 `integration_attempt_limit` 记录验证尝试上限,缺省 3。相关 schema:<Path>{roots.workflows}/specdev/common/schemas/execution-context.schema.json</Path>。
64
35
 
65
- ## 恢复
36
+ Ticket/Direct Spec 的 workspace_context 统一引用 `<Path>{roots.state}/specdev/changes/{change}/README.md</Path>` 的 execution-context 锚点,不复制 branch/locator/base。实际实现者写原执行记录,正式状态只由 Lead 更新。
66
37
 
67
- 恢复时核对 repository、branch、locator、`base_sha`、实际 HEAD、dirty 状态和 owner。状态记录与 Git 不一致、branch 被其他 worktree 占用或出现越界修改时停止;Lead 写 blocker,不重建覆盖。
38
+ ## 恢复与迁移
68
39
 
69
- 进入 `review` 前必须由 implementation owner 创建最终 commit;Lead 重读 branch tip、diff 与 `git status`,把精确 SHA 写入 `source_checkpoint`。
40
+ 核验 checkout 所属 Git common directory、branch、base 可达、工作区路径和实际 writer。未知 owner、路径链接、漂移或旧活动 per-Ticket worktree 记录先阻塞受影响写入。旧记录按 <Path>{roots.workflows}/specdev/common/rules/execution-policy.md</Path> 由原 owner 显式整理;历史与旧工作区保留,不自动归并或删除。
70
41
 
71
- **完成标准**:来源 worktree 可定位且唯一;基线、记录与 Git 一致;source 检查不含 E2E;失败时保留现场。
42
+ **完成标准**:change 唯一工作区可定位,记录与 Git 一致,创建次数不随票据/Agent 数增长。
@@ -1,58 +1,14 @@
1
- # Candidate Merge And Parent Integration
1
+ # 在选定 change 工作区验收
2
2
 
3
- 仅由 Lead/integration owner 对处于 review 的 Ticket 或 Direct Spec 执行单元调用。Ticket 状态来自原票;Direct Spec 状态来自原 Evidence 的 Workspace Execution Record。unit-id 为真实 Ticket ID 或 direct-spec。
3
+ Lead 在原 change workspace 上执行;本过程不创建 candidate worktree、不提交、不合并、不推进父分支。
4
4
 
5
- ## 1. 接收 source checkpoint
5
+ 1. 核对原 README 执行上下文、实际 Git、允许路径、原票据/Spec、owner、验证合同和已有授权。
6
+ 2. 协调相关 writer 停写,冻结实际 commit 或未提交工作副本快照。检查全部交付范围,保留其他票和原有用户改动的归属,不暂存或覆盖它们。
7
+ 3. 对固定输入执行必要集成/回归、类型/lint/build 和 Lead-owned E2E;记录命令、退出码和观察,not-required 给真实理由。
8
+ 4. 按 <Path>{roots.workflows}/specdev/common/skills/code-review/references/review-evidence.md</Path> 核验独立双轴审查;冲突修正者和参与实现的 Lead 均不能作为独立 reviewer。输入变化只复用未失效的证据。
9
+ 5. 回读真实交付与固定输入,验证通过后由 Lead 将本票执行记录标 verified、Ticket 标 done;整个 change 仍需整体 AC/Gate 和最终审查。没有提交授权时快照足够,不伪造 commit/result。
10
+ 6. 失败保留实际工作区、快照、报告和命令;同一 blocker 无新证据或 attempts 达到有效上限时返回 Lead 复盘。记录共同失败模式、可能原因、下一轮实质改变及 owner/路由后才可重试,保留旧轮次。
6
11
 
7
- 1. 核对获批 Spec/Ready Ticket、实际存在的计划、Evidence 目标、owner 与本地 integration 授权;
8
- 2. 验证来源 worktree clean,branch tip 精确等于 `source_checkpoint`,commit 从 `base_sha` 可达;
9
- 3. 审计实际 diff 未越过 writable/shared owner 合同;
10
- 4. 确认 source-worktree 必跑非 E2E 检查已执行,且没有把 E2E 自报为通过;
11
- 5. 重读父分支 checkout clean、HEAD 与 remote/本地约定,记录 `parent_before_sha`。
12
+ 明确请求的 Git/发布交付由原 owning 入口按具体授权串行执行,并重新核验变化后的交付。完成验收不自动授予 commit、merge、push、PR 或工作区清理权限。
12
13
 
13
- 建立新 candidate 前先比较原记录 `attempts` 与有效 Plan 或无 Goal 执行合同的 `integration_attempt_limit`。若前一轮尚未通过且当前 attempts 已达到上限,不创建或重建 candidate、不增加 attempts;保留 source workspace、旧 candidate 与失败记录,将执行记录及已有 Ticket 标为 `blocked`,向有效 Lead 返回 `integration-attempt-limit`。
14
-
15
- 其他预检失败时保持 `review`/`blocked`,不开始候选合并。
16
-
17
- ## 2. 建立 parent-candidate checkout
18
-
19
- 1. 使用 branch `speculo/integration/<change>/<unit-id>` 和 locator `specdev-worktree/.integration/<change>/<unit-id>`,从最新 `parent_before_sha` 建立 Lead-owned integration worktree;
20
- 2. 如果父 SHA 是 source checkpoint 的祖先,在 candidate checkout 执行 `git merge --ff-only <source_checkpoint>`,`method=fast-forward`;
21
- 3. 否则执行 `git merge --no-ff --no-commit <source_checkpoint>`;
22
- 4. 冲突按 `<Path>{roots.workflows}/specdev/specdev-i-implement/references/merge-conflict-protocol.md</Path>` 处理。需要新产品决定时执行 `git merge --abort`,记录 blocker 并返回来源 worktree;
23
- 5. 对分叉结果创建一次 Lead-owned candidate merge commit,`method=merge-commit`;
24
- 6. 记录 candidate branch/locator、`candidate_sha`、实际 Git tree SHA `candidate_tree_sha`、`source_sha`、冲突路径与 attempts,worktree 状态改为 `integrating`、integration 状态改为 `candidate`。
25
-
26
- 重试前从最新父分支重建 candidate branch/worktree;旧 candidate SHA 保存在 Evidence。候选生命周期的重建/回收包含在 local candidate integration 授权中。
27
-
28
- ## 3. 在候选父状态验证
29
-
30
- 在 candidate checkout 运行:
31
-
32
- - 当前实现单元受影响集成与回归;
33
- - 项目要求的 typecheck/lint/build 或其他父状态检查;
34
- - 仅当 Spec/Ticket/有效执行合同 `e2e.required=true` 时运行对应 E2E。
35
-
36
- 每条命令记录运行环境 `parent-candidate`、退出码与摘要。E2E required 未运行或失败时 integration `verification=failed`、`status=failed`;父分支保持 `parent_before_sha`。当本轮失败使 attempts 达到有效执行合同的 `integration_attempt_limit` 时,保存本轮失败并返回 Lead 复盘;不得继续机械修正、放宽断言、删除检查或发明行为。上限是 Lead 复盘触发点,不是永久禁止恢复。
37
-
38
- 候选的代码、规范或相关配置与已审输入不同,尤其发生合并或冲突修正时,按 <Path>{roots.workflows}/specdev/common/skills/code-review/references/review-evidence.md</Path> 在此实际 candidate 上完成独立双轴审查。修正合并冲突的 Lead/Agent 也属于实现者;两轴通过前不得推进。完全相同的输入沿原证据复用门核对,不以 source 检查代替 candidate 审查。
39
-
40
- ## 4. 推进父分支
41
-
42
- 全部 required 检查通过后:
43
-
44
- 1. 重读父分支 HEAD;不等于 `parent_before_sha` 时将 candidate 标记 `stale`,不推进父分支并从步骤 2 重建;
45
- 2. 在父分支 checkout 执行 `git merge --ff-only <candidate_sha>`;候选 merge commit 本身已以父 SHA 为第一祖先,因此不再创建第二个 merge commit;
46
- 3. 重读父 HEAD、tree 与 ancestor 关系,确认 HEAD 精确等于 candidate SHA 且包含 source checkpoint;
47
- 4. 写入 `result_sha=candidate_sha`、`verification=passed`、full_suite/E2E 最终状态、`promotion_status=applied` 和 Evidence;
48
- 5. integration/status 改为 `passed`/`integrated`,再由 Lead 标记 Ticket Done 或在 Direct Spec 全部 AC 通过后完成 change。
49
-
50
- ## 5. 失败、清理与恢复
51
-
52
- - candidate 检查失败:父分支不动,已有 Ticket 回 `in_progress` 或 `blocked`,来源 worktree 保留;
53
- - 达到 integration attempt 上限:保留全部 source/candidate checkpoint 与失败记录,等待 Lead 在原 Evidence 写明共同失败模式、最可能原因、下一轮改变和下一 owner/路由;只有形成有实质变化的新 Dispatch Packet 后,Lead 才可将当前执行记录 `attempts` 重置为 `0` 并重新进入 finalize;
54
- - 父 HEAD 漂移:旧 candidate 记 `stale`,完整重建并重跑;
55
- - 成功后可按 candidate integration 授权回收 transient integration worktree/branch;来源 branch/worktree 不自动清理。获得独立 cleanup 授权并清理后,只将生命周期状态改为 `removed`,完整保留已经通过的集成与 E2E 证据;
56
- - push、PR、remote merge、deploy、migration 和生产动作仍需各自授权。
57
-
58
- **完成标准**:passed 时父 HEAD=result/candidate SHA 且包含 source commit;failed/stale 时父 HEAD 仍为开始该轮记录的父状态或更新后的外部事实,没有本轮候选污染。
14
+ **完成标准**:证据覆盖实际交付,verified 不等于已合并;失败不掩盖未知状态,不增加工作区。
@@ -77,3 +77,19 @@ UI 原型阶段用 --stage prototype --prototype-id UI-NNN;逻辑原型可指
77
77
  `--review-scope --repo <project-root> --mode working-tree --base <baseline-sha> [--file <relative-path> ...]` 固定真实 Git 根的工作副本。无 file 参数覆盖全部相对基线的变化和非忽略新增;显式 file 包含删除路径,排除项返回供 Lead 核对。输出保留已暂存/未暂存状态、实际内容摘要、类型、模式、链接、基线和观察到的 HEAD,不写项目文件、commit 或 index。
78
78
 
79
79
  新增文件必须按 working_snapshot 阅读,不能只看 tracked_diff_command。子模块、冲突 index、越界或目录链接保持未验证。停止 writer 后固定输入,结束与完成前重验;显式范围外新增报告不使代码摘要自引用,遗漏实现仍由 Lead 的完整范围审计阻塞。详见 <Path>{roots.workflows}/specdev/common/skills/code-review/references/working-tree-review.md</Path> 与 <Path>{roots.workflows}/specdev/common/skills/code-review/references/review-evidence.md</Path>。
80
+
81
+ ## 票据调度与 change 工作区
82
+
83
+ Goal schema v8、父 Implementation Plan v3 使用 ticket_execution: auto|serial|parallel 和可选 max_parallel_tickets。ticket-control 输出 schema_version:2,返回 ticket_execution/max_parallel_tickets;取消由 current 推导 implementation_limit 的旧接口。人数限制仍由 Lead 按真实工具状态独立执行,不从票数或历史实现者名单推断。
84
+
85
+ 实际执行的 README Execution Context v1 是唯一工作区事实,原 Ticket/Direct Spec 的 Workspace Execution Record v1 引用它。相关格式见 <Path>{roots.workflows}/specdev/common/schemas/execution-context.schema.json</Path>、<Path>{roots.workflows}/specdev/common/schemas/execution-record.schema.json</Path>;校验器只读核对 Git、固定审查输入、AC 与验证,不创建 worktree/Agent 或授予权限。无 Goal 的合法 Map 仍可分析,默认 auto;运行前 Lead 核对真实授权。旧活动记录先由原 owner 显式整理,未知版本和混合协议拒绝继续,原件不改写。
86
+
87
+ ## 独立开发复盘
88
+
89
+ <Path>{roots.workflows}/specdev/common/tools/validate-retro.mjs</Path> 只读枚举/校验独立 RETRO、升级方案、批准、实际执行与归档完整性;使用 `--help` 查看 report/upgrade 和各 stage。数据根使用 WorkContext 的领域共同父根。协议见 <Path>{roots.workflows}/specdev/specdev-r-retro/references/records.md</Path>;工具不写 Skill、不执行验证命令、不认证授权或宿主行为。
90
+
91
+ ## 派遣交接与可选会话定位
92
+
93
+ <Path>{roots.workflows}/specdev/common/tools/validate-handoff.mjs</Path> 在新派遣投递前/回收时只读校验 TASK YAML、REPORT、目录身份与可选 session-locator.json,协议见 <Path>{roots.references}/coordination/session-files.md</Path>。参数:`--repo <project> [--method-root <methods> --state-root <data-parent>] --change <id> --session <round> --task <id> --stage dispatch|return`,无安装时必须给出三个根。
94
+
95
+ 没有准确日志地址就不创建定位文件,缺失时正常通过;已有文件仅含非空绝对路径 path,须符合 <Path>{roots.workflows}/specdev/common/schemas/session-locator.schema.json</Path>。不扫描工具缓存、不读取外部历史,不认证 provider 身份、授权或完成。结构错误退出 1,参数错误退出 2。存量交接按原合同回收,归档用原完整性清单,不强制补新版字段。
@@ -14,7 +14,7 @@ function check(value, rule, at, errors) {
14
14
  if (rule.type) {
15
15
  const types = Array.isArray(rule.type) ? rule.type : [rule.type];
16
16
  const t = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
17
- if (!types.includes(t)) { errors.push(at + ": invalid type"); return; }
17
+ if (!types.includes(t) && !(types.includes("integer") && Number.isInteger(value))) { errors.push(at + ": invalid type"); return; }
18
18
  }
19
19
  if (typeof value === "string") {
20
20
  if (rule.minLength && value.trim().length < rule.minLength) errors.push(at + ": empty");
@@ -28,14 +28,13 @@ function check(value, rule, at, errors) {
28
28
  } else if (value && typeof value === "object") {
29
29
  for (const k of rule.required ?? []) if (!(k in value)) errors.push(at + ": missing " + k);
30
30
  for (const [k,v] of Object.entries(value)) {
31
- if (rule.properties?.[k]) check(v,rule.properties[k],at+"."+k,errors);
31
+ if (Object.hasOwn(rule.properties ?? {},k)) check(v,rule.properties[k],at+"."+k,errors);
32
32
  else if (rule.additionalProperties === false) errors.push(at + ": unknown " + k);
33
33
  }
34
34
  }
35
35
  }
36
36
  const specs = {
37
37
  "pull-request": ["pull-request.schema.json", ["Summary","Evidence","Before / After","Merge Risk","Authorization","Receipt","Recovery"]],
38
- retrospective: ["retrospective.schema.json", ["Scope","Evidence","Findings","Acceptance","Routes"]],
39
38
  "logic-prototype": ["logic-prototype.schema.json", ["Model","Scenarios","Evidence","Decisions","Handoff"]],
40
39
  "triage-run": ["triage-run.schema.json", ["Targets","Plan","Evidence","Authorization","Receipts","Recovery"]],
41
40
  };
@@ -54,7 +53,6 @@ export function validateRecord(path, kind, parse, expectedChange = null) {
54
53
  if (meta.status === "ready" && meta.verification !== "passed") errors.push(path + ": ready PR requires passed verification");
55
54
  if (["draft","ready"].includes(meta.status) && !new RegExp("^https://github\\.com/" + String(meta.repo).replaceAll(".","\\.") + "/pull/[1-9]\\d*$").test(meta.url ?? "")) errors.push(path + ": missing matching PR URL");
56
55
  }
57
- if (kind === "retrospective" && (meta.sources ?? []).some(s => typeof s !== "string" || !s.trim() || /(?:[A-Za-z]:[\\/]|\b(?:token|secret)=)/i.test(s))) errors.push(path + ": unsafe source");
58
56
  if (kind === "triage-run" && meta.status === "completed" && meta.mode !== "ci-security") {
59
57
  if (meta.npm_target === "unknown") errors.push(path + ": completed release target cannot be unknown");
60
58
  if (!meta.commit_sha) errors.push(path + ": completed release requires fixed commit");
@@ -83,7 +81,7 @@ export function validateRecord(path, kind, parse, expectedChange = null) {
83
81
  return {errors,meta,body};
84
82
  }
85
83
  export function validateRecords(change, kind, parse) {
86
- const folder = kind === "retrospective" ? "retro" : "pull-requests", root=join(change,folder);
84
+ const root=join(change,"pull-requests");
87
85
  const errors=[], records=[];
88
86
  if (!existsSync(root)) return {errors,records};
89
87
  if (lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) return {errors:[root + ": unsafe directory"],records};
@@ -93,3 +91,5 @@ export function validateRecords(change, kind, parse) {
93
91
  }
94
92
  return {errors,records};
95
93
  }
94
+
95
+ export { check as checkSchema };
@@ -0,0 +1,104 @@
1
+ /** Change workspace and Ticket scheduling contracts. Inspection only; never creates worktrees or agents. */
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { basename, join, relative, resolve } from 'node:path';
5
+ import * as runtime from '../../../../lib/speculo-runtime.mjs';
6
+ import { validateIndependentReview } from './review-evidence.mjs';
7
+
8
+ export const EXECUTION_VERSION = 1;
9
+ export const EXECUTION_CONTEXT = 'README.md#execution-context';
10
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ const text = value => typeof value === 'string' && value.trim().length > 0;
12
+ const sha = value => /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(value ?? '');
13
+ const positive = value => Number.isInteger(value) && value > 0;
14
+ const git = (root, args) => execFileSync('git', ['-C', root, ...args], {encoding:'utf8',stdio:['ignore','pipe','pipe']}).trim();
15
+
16
+ export function readExecutionContext(change) {
17
+ const path = runtime.safeWorkPathSync(change, 'README.md', {mustExist:true,type:'file'});
18
+ const source = readFileSync(path, 'utf8');
19
+ const headings = runtime.markdownBodyLines(source, path).filter(line => line.text === '## Execution Context');
20
+ if (!headings.length) return null;
21
+ if (headings.length !== 1) throw new Error('duplicate Execution Context');
22
+ const section = runtime.readArtifactSectionSync(path, 'execution-context').text;
23
+ const blocks = [...section.matchAll(/```json\s*\n([\s\S]*?)\n```/g)];
24
+ if (blocks.length !== 1) throw new Error('Execution Context requires one JSON object');
25
+ const value = JSON.parse(blocks[0][1]);
26
+ const required = ['schema_version','change','workspace_policy','workspace_ref','branch','base_sha','owner','ticket_execution'];
27
+ const allowed = [...required,'max_parallel_tickets','concurrent_implementation_limit','integration_attempt_limit'];
28
+ if (!object(value) || required.some(key => !(key in value)) || Object.keys(value).some(key => !allowed.includes(key))) throw new Error('invalid Execution Context fields');
29
+ if (value.schema_version !== EXECUTION_VERSION || value.change !== basename(change)) throw new Error('Execution Context version/change mismatch; explicit reconciliation required');
30
+ if (!['current','worktree'].includes(value.workspace_policy) || !['auto','serial','parallel'].includes(value.ticket_execution)) throw new Error('invalid workspace_policy or ticket_execution');
31
+ if (!text(value.branch) || !text(value.owner) || !sha(value.base_sha)) throw new Error('Execution Context requires branch, owner and immutable base_sha');
32
+ for (const key of ['max_parallel_tickets','concurrent_implementation_limit','integration_attempt_limit']) if (key in value && !positive(value[key])) throw new Error(`${key} must be a positive integer`);
33
+ if (value.workspace_policy === 'current' ? value.workspace_ref !== 'current' : !text(value.workspace_ref) || value.workspace_ref === 'current' || /[\\:\x00-\x1f]/.test(value.workspace_ref) || value.workspace_ref.split('/').some(part => !part || part === '.' || part === '..' || part === '.git')) throw new Error('workspace_ref must identify the selected portable change workspace');
34
+ return value;
35
+ }
36
+
37
+ /** Defaults do not impose an Agent count. Parent and child Ticket caps remain separate from it. */
38
+ export function ticketPolicy(...sources) {
39
+ const values = sources.filter(Boolean);
40
+ const modes = values.map(value => value.ticket_execution).filter(Boolean);
41
+ const limits = values.map(value => value.max_parallel_tickets).filter(value => value !== undefined);
42
+ if (modes.some(mode => !['auto','serial','parallel'].includes(mode)) || limits.some(limit => !positive(limit))) throw new Error('invalid Ticket scheduling policy');
43
+ return {ticket_execution:modes.includes('serial') ? 'serial' : modes[0] ?? 'auto', max_parallel_tickets:modes.includes('serial') ? 1 : limits.length ? Math.min(...limits) : null};
44
+ }
45
+
46
+ export function resolveExecutionWorkspace(repoRoot, context) {
47
+ if (!context || !repoRoot) return repoRoot;
48
+ const root = realpathSync.native(repoRoot);
49
+ const workspace = context.workspace_ref === 'current' ? root : runtime.safeWorkPathSync(root, context.workspace_ref, {mustExist:true,type:'directory'});
50
+ if (relative(realpathSync.native(workspace), realpathSync.native(git(workspace,['rev-parse','--show-toplevel']))) !== '') throw new Error('selected workspace is not a real Git checkout');
51
+ const common = cwd => realpathSync.native(resolve(cwd, git(cwd,['rev-parse','--git-common-dir'])));
52
+ if (common(root) !== common(workspace)) throw new Error('selected workspace belongs to another repository');
53
+ if (git(workspace,['symbolic-ref','--short','HEAD']) !== context.branch) throw new Error('selected workspace branch drifted');
54
+ git(workspace,['cat-file','-e',`${context.base_sha}^{commit}`]);
55
+ git(workspace,['merge-base','--is-ancestor',context.base_sha,'HEAD']);
56
+ if (context.workspace_policy === 'worktree') {
57
+ const paths = git(root,['worktree','list','--porcelain','-z']).split('\0').filter(row=>row.startsWith('worktree ')).map(row=>realpathSync.native(row.slice(9)));
58
+ if (!paths.includes(realpathSync.native(workspace)) || realpathSync.native(workspace) === root) throw new Error('selected change worktree is not registered');
59
+ }
60
+ return workspace;
61
+ }
62
+
63
+ function evidenceText(change, locator) {
64
+ if (!text(locator) || !locator.includes('#')) throw new Error('verification evidence requires an explicit artifact anchor');
65
+ const [file, anchor, extra] = locator.split('#');
66
+ if (!anchor || extra !== undefined) throw new Error('invalid evidence locator');
67
+ const path = runtime.safeWorkPathSync(change, file, {mustExist:true,type:'file'});
68
+ return runtime.readArtifactSectionSync(path, anchor).text;
69
+ }
70
+
71
+ export function validateExecutionRecord(change, record, context, {repoRoot=null, sourceRoot=repoRoot, checkCurrent=false} = {}) {
72
+ const errors = [], unit = record.ticket_id ?? record.purpose;
73
+ try {
74
+ const identity = record.ticket_id ? ['ticket_id'] : ['purpose','change'];
75
+ const keys = ['schema_version',...identity,'workspace_context','implementation_owners','status','checkpoint','verification','attempts'];
76
+ if (record.schema_version !== EXECUTION_VERSION || keys.some(key=>!(key in record)) || Object.keys(record).some(key=>!keys.includes(key))) throw new Error('legacy or invalid execution record; explicit reconciliation required');
77
+ if (record.ticket_id ? !/^T-\d{2,}$/.test(record.ticket_id) : record.purpose !== 'direct-spec' || record.change !== basename(change)) throw new Error('execution identity does not match this change');
78
+ if (!context || record.workspace_context !== EXECUTION_CONTEXT) throw new Error('execution must reference the unique change Execution Context');
79
+ if (!['active','review','verified','blocked','cancelled'].includes(record.status)) throw new Error('invalid execution status');
80
+ if (!Array.isArray(record.implementation_owners) || !record.implementation_owners.length || record.implementation_owners.some(owner=>!text(owner)) || new Set(record.implementation_owners).size !== record.implementation_owners.length) throw new Error('implementation_owners must list the actual distinct implementers');
81
+ if (!Number.isInteger(record.attempts) || record.attempts < 0 || record.attempts > (context.integration_attempt_limit ?? 3)) throw new Error('verification attempts exceed the effective limit');
82
+ if (record.checkpoint !== null && !text(record.checkpoint)) throw new Error('checkpoint must be null or a frozen review input locator');
83
+ const verification = record.verification;
84
+ if (!object(verification) || Object.keys(verification).sort().join(',') !== 'e2e,evidence,status' || !['pending','passed','failed'].includes(verification.status)) throw new Error('invalid workspace verification');
85
+ const e2e = verification.e2e;
86
+ if (!object(e2e) || Object.keys(e2e).sort().join(',') !== 'evidence,reason,required,status' || typeof e2e.required !== 'boolean' || !['pending','passed','failed','not-required'].includes(e2e.status)) throw new Error('invalid E2E disposition');
87
+ for (const value of [verification.evidence,e2e.evidence,e2e.reason]) if (value !== null && !text(value)) throw new Error('evidence/reason must be null or non-empty text');
88
+ if (!e2e.required && (e2e.status !== 'not-required' || !text(e2e.reason)) || e2e.required && e2e.status === 'not-required') throw new Error('E2E disposition needs a truthful result/reason');
89
+ if (record.status === 'verified') {
90
+ if (verification.status !== 'passed' || !['passed','not-required'].includes(e2e.status) || !record.checkpoint) throw new Error('verified execution requires passed checks, E2E and a frozen checkpoint');
91
+ const observed = evidenceText(change,verification.evidence);
92
+ if (!/^Result: passed\s*$/m.test(observed) || !/^Observed: \S/m.test(observed) || /^Command: /m.test(observed) && !/^Exit: 0\s*$/m.test(observed)) throw new Error('verification requires actual observations and successful command exits');
93
+ if (e2e.required) {
94
+ const result = evidenceText(change,e2e.evidence);
95
+ if (!/^Result: passed\s*$/m.test(result) || !/^Observed: \S/m.test(result) || /^Command: /m.test(result) && !/^Exit: 0\s*$/m.test(result) || !result.split('\n').some(line=>line.trim() === `Owner: ${context.owner}`)) throw new Error('required E2E needs observed passing evidence owned by the Lead');
96
+ }
97
+ const artifact = `evidence/${record.ticket_id ?? 'direct-spec'}.md`;
98
+ const source = readFileSync(runtime.safeWorkPathSync(change,artifact,{mustExist:true,type:'file'}),'utf8');
99
+ if (!source.split('\n').some(line=>line.trim() === `Input: ${record.checkpoint}`)) throw new Error('checkpoint must match the actual Independent Review input');
100
+ errors.push(...validateIndependentReview(change,artifact,{repoRoot,sourceRoot,requiredImplementers:record.implementation_owners,checkCurrent,requireDelivery:checkCurrent}));
101
+ }
102
+ } catch(error) { errors.push(`${unit}: ${error.message}`); }
103
+ return errors;
104
+ }
@@ -319,7 +319,9 @@ export function readSpecification(changeRoot, { allowLightweight = false, requir
319
319
  export function validateExecutionPolicy(plan, config = null) {
320
320
  const errors = [];
321
321
  const meta = plan.meta;
322
- if (!Number.isInteger(meta.concurrent_implementation_limit) || meta.concurrent_implementation_limit < 1 || !Number.isInteger(meta.integration_attempt_limit) || meta.integration_attempt_limit < 1) return ['plan must declare positive execution limits'];
322
+ if (!['auto','serial','parallel'].includes(meta.ticket_execution)) errors.push('plan must declare ticket_execution: auto, serial or parallel');
323
+ for (const key of ['concurrent_implementation_limit','max_parallel_tickets']) if (meta[key] !== undefined && (!Number.isInteger(meta[key]) || meta[key] < 1)) errors.push(`${key} must be a positive integer when specified`);
324
+ if (!Number.isInteger(meta.integration_attempt_limit) || meta.integration_attempt_limit < 1) errors.push('plan must declare a positive integration_attempt_limit');
323
325
  if (config) {
324
326
  if (config.schema_version !== 7 || !Number.isInteger(config.execution?.max_concurrent_implementations) || !Number.isInteger(config.execution?.max_integration_attempts)) return ['existing tool config requires schema v7 and explicit execution limits'];
325
327
  if (meta.concurrent_implementation_limit > config.execution.max_concurrent_implementations) errors.push(`concurrent_implementation_limit ${meta.concurrent_implementation_limit} exceeds config max_concurrent_implementations ${config.execution.max_concurrent_implementations}`);
@@ -34,7 +34,7 @@ function readLocator(root, locator) {
34
34
  export function reviewInputDigest(input) { return hash(JSON.stringify(input)); }
35
35
 
36
36
  /** Sources use project-relative locators; an anchor avoids a README hashing itself. */
37
- export function validateIndependentReview(root, artifact, {repoRoot=null, requirePassed=true, expectedHead=null, expectedBase=null, expectedResults=null, requiredImplementers=[], requiredSpecifications=[], checkCurrent=false, requireDelivery=false, allowSourceFiles=false} = {}) {
37
+ export function validateIndependentReview(root, artifact, {repoRoot=null, sourceRoot=repoRoot, requirePassed=true, expectedHead=null, expectedBase=null, expectedResults=null, requiredImplementers=[], requiredSpecifications=[], checkCurrent=false, requireDelivery=false, allowSourceFiles=false} = {}) {
38
38
  const errors = [], label = `independent-review: ${artifact}`;
39
39
  try {
40
40
  const path = safeWorkPathSync(root, artifact, {mustExist:true, type:'file'});
@@ -54,7 +54,7 @@ export function validateIndependentReview(root, artifact, {repoRoot=null, requir
54
54
  for (const source of input.sources) {
55
55
  if (!['standards','specification','instructions'].includes(source.kind) || !real(source.locator) || !/^[a-f0-9]{64}$/.test(source.sha256 ?? '')) throw new Error('invalid review source');
56
56
  sourceKinds.add(source.kind);
57
- if (repoRoot && checkCurrent && hash(readLocator(repoRoot,source.locator).text) !== source.sha256) throw new Error(`source drift: ${source.locator}`);
57
+ if (repoRoot && checkCurrent && hash(readLocator(sourceRoot,source.locator).text) !== source.sha256) throw new Error(`source drift: ${source.locator}`);
58
58
  }
59
59
  if (!sourceKinds.has('standards') || requirePassed && !sourceKinds.has('specification')) throw new Error('missing required review sources');
60
60
  if (requiredSpecifications.some(locator=>!input.sources.some(source=>source.kind === 'specification' && source.locator === locator))) throw new Error('review omits the authoritative specification/acceptance source');
@@ -223,11 +223,11 @@ test('range comparison and merge-base comparison have distinct semantics', t =>
223
223
  const range = resolveReviewScope({ repo: r.root, mode: 'range', base: r.c, head: side });
224
224
  assert.deepEqual(baseline.changed_paths, ['side.txt']); assert.deepEqual(range.changed_paths, ['b.txt', 'c.txt', 'side.txt']);
225
225
  });
226
- test('fifteen Work IDs match the current registry and persisted schema versions remain unchanged', () => {
226
+ test('fifteen Work IDs match the current registry and persisted schema versions match their contracts', () => {
227
227
  const entries = readdirSync(workflow).filter(name => /^specdev-[a-z]-/.test(name)); assert.equal(entries.length, 15);
228
228
  const ids = entries.map(name => /speculo-id: (.+)$/m.exec(readFileSync(join(workflow, name, 'SKILL.md'), 'utf8'))?.[1]);
229
229
  assert.deepEqual(ids.sort(), ['archive-and-consolidate','code-review','diagnose-bugs','goal-plan','grill-with-docs','implement','init-setup','learn','prototype','retro','review-architecture','spec','tickets','triage','wayfinder'].map(id => 'specdev/' + id).sort());
230
- for (const [file, version] of [['config', 7], ['status', 5], ['change-status', 6], ['goal-plan', 7]]) {
230
+ for (const [file, version] of [['config', 7], ['status', 5], ['change-status', 6], ['goal-plan', 8], ['implementation-plan', 3], ['execution-context', 1], ['execution-record', 1]]) {
231
231
  const schema = JSON.parse(readFileSync(join(workflow, 'common/schemas', `${file}.schema.json`), 'utf8'));
232
232
  assert.equal(schema.properties.schema_version.const, version);
233
233
  }
@@ -6,7 +6,7 @@ import {resolveReviewScope} from '../review-scope.mjs';
6
6
  import {reviewInputDigest} from '../review-evidence.mjs';
7
7
  import {readArtifactSectionSync} from '../../../../../lib/speculo-runtime.mjs';
8
8
  const hash=text=>createHash('sha256').update(text).digest('hex');
9
- export function appendReviewFixture({repo,root,artifact='evidence/direct-spec.md',base,head=null,files=['value.txt'],mode='native',reviewer='native:reviewer',implementers=['native:implementer'],spec=null,sourceLocators=null}) {
9
+ export function appendReviewFixture({repo,sourceRoot=repo,root,artifact='evidence/direct-spec.md',base,head=null,files=['value.txt'],sourceFiles=false,mode='native',reviewer='native:reviewer',implementers=['native:implementer'],spec=null,sourceLocators=null}) {
10
10
  const path=join(root,artifact);mkdirSync(dirname(path),{recursive:true});
11
11
  let prior=existsSync(path)?readFileSync(path,'utf8'):'';
12
12
  prior=prior.split('\n<!-- synthetic-independent-review -->')[0];
@@ -14,14 +14,14 @@ export function appendReviewFixture({repo,root,artifact='evidence/direct-spec.md
14
14
  let sources;
15
15
  if(sourceLocators) sources=sourceLocators.map(({kind,locator})=>{
16
16
  const [file,anchor]=locator.split('#');
17
- return {kind,locator,sha256:hash(anchor?readArtifactSectionSync(join(repo,file),anchor).text:readFileSync(join(repo,file),'utf8'))};
17
+ return {kind,locator,sha256:hash(anchor?readArtifactSectionSync(join(sourceRoot,file),anchor).text:readFileSync(join(sourceRoot,file),'utf8'))};
18
18
  });
19
19
  else {
20
20
  const standards=join(root,'evidence/fixture-standards.md');mkdirSync(dirname(standards),{recursive:true});writeFileSync(standards,'# Fixture standards\nPreserve existing behavior and test changes.\n');
21
21
  if(!spec) {spec=join(root,'evidence/fixture-spec.md');writeFileSync(spec,'# Fixture Spec\nAC-001: deliver the accepted behavior.\n');}
22
- sources=[['standards',standards],['specification',spec]].map(([kind,file])=>({kind,locator:relative(repo,file).split('\\').join('/'),sha256:hash(readFileSync(file,'utf8'))}));
22
+ sources=[['standards',standards],['specification',spec]].map(([kind,file])=>({kind,locator:relative(sourceRoot,file).split('\\').join('/'),sha256:hash(readFileSync(file,'utf8'))}));
23
23
  }
24
- const scope=resolveReviewScope(head?{repo,mode:'range',base,head}:{repo,mode:'working-tree',base,files});
24
+ const scope=resolveReviewScope(sourceFiles?{repo,mode:'source-files',files}:head?{repo,mode:'range',base,head}:{repo,mode:'working-tree',base,files});
25
25
  const input={scope,sources,exclusions:'Fixture-only records and unrelated inputs are outside the implementation scope.'};
26
26
  const digest=reviewInputDigest(input);
27
27
  const report=axis=>`<a id="${axis}-output"></a>\n## Original ${axis} output\nReviewer: ${reviewer}\nAxis: ${axis}\nResult: pass\nInput-SHA256: ${digest}\nObserved: synthetic fixture reviewer inspected the fixed inputs; no blocking findings.\n`;