From 6c376fffc4fce011ae8297545a93c80a1957564b Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 21:03:30 +0800 Subject: [PATCH 01/17] docs: translate workflow simplification design --- ...26-08-18-workflow-simplification-design.md | 276 +++++++++--------- 1 file changed, 138 insertions(+), 138 deletions(-) diff --git a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md index 4f72b14..fca7ec5 100644 --- a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md +++ b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md @@ -1,242 +1,242 @@ -# Polaris Workflow Simplification Design +# Polaris 工作流精简设计 -## Status +## 状态 -Approved direction: implement the workflow simplifications identified in the repository audit. +已批准方向:实施仓库审核中识别出的工作流精简项。 -Target protocol version: `0.1.20` -Target workflow version: `0.1.3` +目标协议版本:`0.1.20` +目标工作流版本:`0.1.3` -## Problem +## 问题 -The current `0.1.2` workflow preserves strong governance boundaries, but several persisted states and gates do not introduce new authority, evidence, or a human decision. The largest problem is that ignored local telemetry in `runtime/progress.json` is a hard prerequisite for the durable `FINISH_IMPLEMENTATION` transition even though the protocol says runtime state does not participate in phase gates or Fresh Clone recovery. +当前 `0.1.2` 工作流保留了严格的治理边界,但部分持久状态和门禁并没有引入新的 Authority、证据或人工决策。最突出的问题是:`runtime/progress.json` 中的本地瞬时遥测被设为耐久 `FINISH_IMPLEMENTATION` 转换的硬前提,而协议同时又声明 runtime 状态不参与阶段门禁或 Fresh Clone 恢复。 -The happy path also contains mechanically adjacent transitions that can be combined: +Happy path 还包含多组可以合并的相邻机械转换: -- `START_IMPLEMENTATION` followed by the `DISPATCH_IMPLEMENTATION` self-transition; -- `FINISH_IMPLEMENTATION` followed by resuming the same Implementer for `SYNC_DOCS`; -- `ACCEPT_REVIEW` followed by `START_VALIDATION`, with the same Reviews checked twice; -- `PASS_VALIDATION` followed by `CLOSE` for R0/R1, where no final Human approval exists. +- `START_IMPLEMENTATION` 后紧接 `DISPATCH_IMPLEMENTATION` 自转换; +- `FINISH_IMPLEMENTATION` 后续接同一个 Implementer 执行 `SYNC_DOCS`; +- `ACCEPT_REVIEW` 后紧接 `START_VALIDATION`,且两次检查相同 Review; +- R0/R1 的 `PASS_VALIDATION` 后紧接 `CLOSE`,中间不存在最终人工批准。 -Code Intelligence is optional and non-blocking, but every stage currently writes an unavailable or skipped record even when the Provider is not used. This creates durable noise without strengthening a gate. +Code Intelligence 是可选、非阻断能力,但当前每个阶段即使 Provider 未使用,也会写入 unavailable 或 skipped 记录。这会产生耐久噪声,却不会增强任何门禁。 -Finally, the product authority says closure requires a full task validation pass, while the implemented closure gate currently checks only Result and the optional R2 final approval. +此外,产品 Authority 规定关闭任务前必须完整通过任务校验,但当前实现的关闭门禁只检查 Result 和可选的 R2 最终批准。 -## Goals +## 目标 -1. Remove persisted states and transitions that have no distinct governance boundary. -2. Make ignored live progress optional telemetry rather than durable authority. -3. Keep Work Item, Plan decisions, Implementation handoff, Implementation, Knowledge Delta, Review handoff, Review, Validation, Result, event ledger, and state projection as durable artifacts. -4. Preserve independent Review and acceptance-driven Validation as separate stages. -5. Preserve an explicit `VERIFIED` waiting state only for R2 final Human approval. -6. Make closure validate the complete candidate task projection before committing the transition. -7. Provide an explicit, recoverable migration from workflow `0.1.2` to `0.1.3`. -8. Keep the runtime dependency-free beyond the Python standard library. +1. 删除没有独立治理边界的持久状态和转换。 +2. 将 ignored 的实时进度降为可选遥测,而不是耐久 Authority。 +3. 保留 Work Item、Plan 决策、Implementation handoff、Implementation、Knowledge Delta、Review handoff、Review、Validation、Result、事件账本和状态投影等耐久产物。 +4. 保持独立 Review 与基于验收标准的 Validation 为两个不同阶段。 +5. 仅为等待 R2 最终人工批准保留显式 `VERIFIED` 状态。 +6. 在提交关闭转换前,完整校验候选任务投影。 +7. 提供从工作流 `0.1.2` 到 `0.1.3` 的显式、可恢复迁移。 +8. 运行时继续只依赖 Python 标准库。 -## Non-goals +## 非目标 -- Removing independent Implementer or Reviewer isolation. -- Removing Work Item confirmation, Plan decisions, Knowledge Delta, Review, or Validation. -- Introducing a daemon, scheduler, task DAG, database, or custom Agent Runtime. -- Automatically pushing, merging, publishing, or running remote CI. -- Rewriting or deleting historical artifacts or events. +- 删除独立 Implementer 或 Reviewer 的隔离要求。 +- 删除 Work Item 确认、Plan 决策、Knowledge Delta、Review 或 Validation。 +- 引入 daemon、scheduler、Task DAG、数据库或自定义 Agent Runtime。 +- 自动 push、merge、发布或编排远程 CI。 +- 改写或删除历史 artifact 或事件。 -## Options Considered +## 备选方案 -### Option A: Change only Skills and documentation +### 方案 A:只修改 Skills 和文档 -This would reduce conversational ceremony but leave the persisted workflow and gates unchanged. Existing frozen workflow projects would still require the old transitions. It would also leave the ignored-progress hard gate in place. +这可以减少对话层面的仪式,但持久工作流和门禁仍保持不变。已有冻结工作流的项目仍然必须执行旧转换,而且 ignored progress 的硬门禁仍然存在。 -Rejected because it does not solve the mechanical redundancy. +不采用,因为它没有解决机械层面的冗余。 -### Option B: Keep all states but automatically chain transitions +### 方案 B:保留所有状态,但自动串联转换 -The controller could immediately run `DISPATCH_IMPLEMENTATION`, `START_VALIDATION`, and `CLOSE` after their predecessors. This reduces user-visible pauses but retains duplicate events, repeated validation, intermediate checkpoint commits, and recovery states with no independent meaning. +Controller 可以在前一个转换后立即执行 `DISPATCH_IMPLEMENTATION`、`START_VALIDATION` 和 `CLOSE`。这能减少用户可见的暂停,但仍然保留重复事件、重复校验、中间 checkpoint commit,以及没有独立含义的恢复状态。 -Rejected because it hides rather than removes the complexity. +不采用,因为它只是隐藏复杂度,没有消除复杂度。 -### Option C: Version and simplify the workflow +### 方案 C:升级版本并精简工作流 -Introduce workflow `0.1.3`, remove redundant states and events, loosen the telemetry dependency, and explicitly migrate existing tasks. +引入工作流 `0.1.3`,删除冗余状态和事件,解除遥测依赖,并显式迁移已有任务。 -Selected because it aligns persisted control flow with actual governance boundaries while preserving auditability. +采用该方案,因为它让持久控制流与真实治理边界保持一致,同时保留可审计性。 -## Target Workflow +## 目标工作流 -The normal persisted path becomes: +普通持久主路径调整为: ```text DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → REVIEWING → VALIDATING → CLOSED ``` -R2 uses an additional final approval state: +R2 增加最终批准状态: ```text VALIDATING → VERIFIED → CLOSED ``` -The states `IMPLEMENTED`, `DOCS_SYNCED`, and `REVIEWED` are removed from workflow `0.1.3`. +工作流 `0.1.3` 删除 `IMPLEMENTED`、`DOCS_SYNCED` 和 `REVIEWED` 状态。 -The following governance loops remain: +以下治理回路保持不变: ```text REVIEWING -- REJECT_REVIEW --> IMPLEMENTING VALIDATING -- FAIL_IMPLEMENTATION --> IMPLEMENTING VALIDATING -- FAIL_PLAN --> PLANNED -non-terminal -- NEW_REVISION --> QUALIFIED -non-terminal -- BLOCK --> BLOCKED +任意非终态 -- NEW_REVISION --> QUALIFIED +任意非终态 -- BLOCK --> BLOCKED BLOCKED -- RESOLVE_BLOCK --> blocked_from -non-terminal -- CANCEL --> CANCELLED +任意非终态 -- CANCEL --> CANCELLED ``` -## Transition Design +## 转换设计 -### Start Implementation +### 开始 Implementation -`START_IMPLEMENTATION` moves `PLANNED → IMPLEMENTING` and requires the Implementation handoff in the same transition. Its gate combines: +`START_IMPLEMENTATION` 执行 `PLANNED → IMPLEMENTING`,并要求在同一次转换中提交 Implementation handoff。门禁合并检查: -- R2 pre-approval validation; -- handoff identity, revision, attempt, Plan, Working Set, and package validation. +- R2 实施前批准; +- handoff 的身份、revision、attempt、Plan、Working Set 和 package。 -`DISPATCH_IMPLEMENTATION` is removed. Worker dispatch remains a host action performed after the handoff is registered; it is not a persisted workflow state. +删除 `DISPATCH_IMPLEMENTATION`。handoff 注册后由宿主执行 Worker 派发;Worker 派发是宿主动作,不是持久工作流状态。 -### Finish Implementation and Start Review +### 完成 Implementation 并开始 Review -The Implementer completes code, tests, required project documentation, final checks, the Implementation artifact, and Knowledge Delta before returning. Both artifacts bind the same final subject commit and diff hash. +Implementer 返回前完成代码、测试、必要项目文档、最终检查、Implementation artifact 和 Knowledge Delta。两个 artifact 绑定相同的最终 subject commit 和 diff hash。 -The main controller then builds the immutable Review handoff and runs `START_REVIEW` directly from `IMPLEMENTING`. The transition registers: +主 Controller 随后构建不可变 Review handoff,并直接从 `IMPLEMENTING` 执行 `START_REVIEW`。该转换注册: -- `implementation`; -- `knowledge_delta`; -- `review_handoff`; -- the final subject base/head commits. +- `implementation`; +- `knowledge_delta`; +- `review_handoff`; +- 最终 subject base/head commits。 -Its combined gate checks the Implementation handoff binding, Implementation artifact, Knowledge Delta, documentation impact, final subject, and Review handoff. It then moves `IMPLEMENTING → REVIEWING`. +合并后的门禁检查 Implementation handoff 绑定、Implementation artifact、Knowledge Delta、文档影响、最终 subject 和 Review handoff,然后执行 `IMPLEMENTING → REVIEWING`。 -There is no separate Implementation checkpoint commit before documentation. The final subject checkpoint already includes code, tests, build configuration, and project documentation. +Documentation 前不再创建单独的 Implementation checkpoint commit。最终 subject checkpoint 已同时包含代码、测试、构建配置和项目文档。 -### Accept Review and Start Validation +### 接受 Review 并开始 Validation -`ACCEPT_REVIEW` validates all required Review artifacts once and moves `REVIEWING → VALIDATING`. `START_VALIDATION` is removed. Validation remains a separate stage and produces a new immutable Validation artifact. +`ACCEPT_REVIEW` 只校验一次全部必需 Review artifact,并执行 `REVIEWING → VALIDATING`。删除 `START_VALIDATION`。Validation 仍是独立阶段,并生成新的不可变 Validation artifact。 -### Pass Validation and Close +### Validation 通过并关闭 -Two explicit pass events avoid conditional destinations hidden in code: +使用两个显式通过事件,避免在代码中隐藏条件目标状态: -- `PASS_AND_CLOSE`: valid only for R0/R1, registers Validation and Result, validates the complete candidate CLOSED projection, and moves `VALIDATING → CLOSED`. -- `PASS_VALIDATION`: valid only for R2, registers Validation, validates all acceptance criteria, and moves `VALIDATING → VERIFIED`. +- `PASS_AND_CLOSE`:只允许 R0/R1 使用;注册 Validation 和 Result,完整校验候选 CLOSED 投影,然后执行 `VALIDATING → CLOSED`。 +- `PASS_VALIDATION`:只允许 R2 使用;注册 Validation,校验全部验收标准,然后执行 `VALIDATING → VERIFIED`。 -R2 then records final Human approval and Result before `CLOSE` moves `VERIFIED → CLOSED`. Both closing paths execute the same complete candidate-task validator before appending the event. +R2 随后记录最终人工批准和 Result,再通过 `CLOSE` 执行 `VERIFIED → CLOSED`。两个关闭路径都必须在追加事件前调用相同的完整候选任务校验器。 -## Candidate Projection Validation +## 候选投影校验 -Task validation will be refactored so the same rules can validate either: +重构任务校验,使相同规则既可以校验: -- the projection currently stored in `state.json`; or -- a candidate projection prepared by `transition_task.py` before an event is appended. +- 当前保存在 `state.json` 中的投影;或 +- `transition_task.py` 在追加事件前准备的候选投影。 -The public `validate_task.py` command continues to validate the stored state and event reconstruction. Closing gates call the shared candidate validator with the proposed CLOSED state and registered artifacts. No transition may append a CLOSED event and validate afterward. +公开的 `validate_task.py` 命令继续校验已保存状态和事件重建结果。关闭门禁使用拟议的 CLOSED 状态及已注册 artifact 调用共享候选校验器。不得先追加 CLOSED 事件再进行事后校验。 -This eliminates the current discrepancy between `plan.md` and `closure_ready` without duplicating a second set of closure rules. +这会消除 `plan.md` 与当前 `closure_ready` 实现之间的偏差,同时避免复制第二套关闭规则。 -## Live Implementation Progress +## Implementation 实时进度 -`runtime/progress.json` remains available for hosts that can expose live progress, but it is explicitly best-effort and optional: +`runtime/progress.json` 继续供能够展示实时进度的宿主使用,但明确降为 best-effort、可选遥测: -- it remains Git ignored; -- its absence never blocks `START_REVIEW`, recovery, or closure; -- R0 does not require initialization or step events; -- R1/R2 may use ordered steps for status reporting, but the final Implementation artifact is authoritative; -- if a valid progress snapshot exists, the controller may compare it with the Implementation summary and report discrepancies as a warning, not a transition failure; -- Implementation `step_results` remain required durable summaries and are written directly into the Implementation artifact. +- 继续保持 Git ignored; +- 文件缺失不得阻断 `START_REVIEW`、恢复或关闭; +- R0 不要求初始化或写入步骤事件; +- R1/R2 可以使用有序步骤展示状态,但最终 Implementation artifact 才是 Authority; +- 如果存在有效 progress 快照,Controller 可以将其与 Implementation summary 对比,并把不一致报告为警告,而不是转换失败; +- Implementation `step_results` 继续作为必填耐久摘要,由 Implementer 直接写入 Implementation artifact。 -The progress updater continues to reject corrupt or conflicting updates when it is used. Its local state machine is not part of the project workflow graph. +使用 progress updater 时,它仍然拒绝损坏或冲突的更新。其本地状态机不属于项目工作流图。 -## Code Intelligence Records +## Code Intelligence 记录 -Code Intelligence remains optional, provider-neutral at artifact boundaries, and non-blocking. +Code Intelligence 继续保持可选、在 artifact 边界上 Provider-neutral,并且非阻断。 -- Stage artifacts may omit the Code Intelligence reference when no query or freshness-relevant operation was performed. -- Missing marker, disabled policy, or a Provider known to be unavailable in the current session does not require a new durable stage record. -- A durable record is written only when a stage performed a Provider status, sync, or explore operation whose result is useful audit evidence. -- Source and Git fallbacks remain mandatory whenever Provider evidence is stale or insufficient. -- Validation continues to exclude Code Intelligence as acceptance evidence. +- 阶段没有执行 query 或与 freshness 有关的操作时,artifact 可以省略 Code Intelligence 引用。 +- marker 缺失、策略禁用,或当前会话已经确认 Provider 不可用时,不要求生成新的耐久阶段记录。 +- 只有阶段实际执行 Provider status、sync 或 explore,且结果具有审计价值时,才写入耐久记录。 +- Provider 证据过期或不足时,仍必须执行源码和 Git 回退。 +- Validation 继续禁止把 Code Intelligence 当作验收证据。 -Historical v1 and v2 records remain immutable and readable. +历史 v1 和 v2 记录保持不可变、可读取。 -## Migration from Workflow 0.1.2 +## 从工作流 0.1.2 迁移 -Protocol `0.1.20` adds an explicit migration strategy capable of replacing the frozen workflow and mapping task projections. The migration remains adjacent, append-only, resumable, and lock-protected. +协议 `0.1.20` 增加能够替换冻结工作流并映射任务投影的显式迁移策略。迁移继续保持相邻、append-only、可恢复且受锁保护。 -State mapping: +状态映射如下: -| Old state | New state | +| 旧状态 | 新状态 | |---|---| | `DRAFT` | `DRAFT` | | `QUALIFIED` | `QUALIFIED` | | `PLANNED` | `PLANNED` | -| `IMPLEMENTING` with registered handoff | `IMPLEMENTING` | -| `IMPLEMENTING` without registered handoff | `PLANNED` | +| 已注册 handoff 的 `IMPLEMENTING` | `IMPLEMENTING` | +| 未注册 handoff 的 `IMPLEMENTING` | `PLANNED` | | `IMPLEMENTED` | `IMPLEMENTING` | | `DOCS_SYNCED` | `IMPLEMENTING` | | `REVIEWING` | `REVIEWING` | | `REVIEWED` | `VALIDATING` | | `VALIDATING` | `VALIDATING` | | `VERIFIED` | `VERIFIED` | -| `BLOCKED` | `BLOCKED`, with `blocked_from` mapped by the same rules | +| `BLOCKED` | `BLOCKED`,并按相同规则映射 `blocked_from` | | `CLOSED` | `CLOSED` | | `CANCELLED` | `CANCELLED` | -Artifacts are preserved. Mapping `DOCS_SYNCED → IMPLEMENTING` lets the new `START_REVIEW` gate reuse the existing Implementation and Knowledge Delta and generate only the missing Review handoff. Mapping `IMPLEMENTED → IMPLEMENTING` lets the same Implementer finish documentation without relying on the ignored progress file. +保留全部 artifact。`DOCS_SYNCED → IMPLEMENTING` 使新 `START_REVIEW` 门禁能够复用已有 Implementation 和 Knowledge Delta,只生成缺失的 Review handoff。`IMPLEMENTED → IMPLEMENTING` 允许同一个 Implementer 完成文档工作,而不依赖 ignored progress 文件。 -Each migrated task receives one `MIGRATE_POLARIS` event containing old/new protocol version, old/new workflow version, and old/new state. The migration record stores before/after event sequence and mapped status. Reruns reuse an already appended matching event and reject inconsistent partial state. +每个迁移任务追加一个 `MIGRATE_POLARIS` 事件,其中包含新旧协议版本、新旧工作流版本和新旧状态。迁移记录保存转换前后的 event sequence 和映射状态。重跑时复用已追加且匹配的事件,并拒绝不一致的部分状态。 -## Authority and Artifact Compatibility +## Authority 与 artifact 兼容性 -- Historical `events.jsonl` entries may name removed states; they remain valid historical events. -- The rebuilt current projection uses the final migration event and workflow `0.1.3`. -- Existing immutable artifacts are never rewritten merely to adopt the new workflow. -- New Implementation and Knowledge Delta artifacts bind one final subject. -- `state.json` continues to store only current artifact pointers. -- Result remains a durable closure summary, but R0/R1 controllers generate it before `PASS_AND_CLOSE` rather than through a separate VERIFIED checkpoint. +- 历史 `events.jsonl` 可以包含已删除的状态;这些仍是合法历史事件。 +- 重建出的当前投影使用最终迁移事件和工作流 `0.1.3`。 +- 不得仅为了采用新工作流而改写已有不可变 artifact。 +- 新 Implementation 和 Knowledge Delta 绑定同一个最终 subject。 +- `state.json` 继续只保存当前 artifact 指针。 +- Result 继续作为耐久关闭摘要;R0/R1 Controller 在 `PASS_AND_CLOSE` 前生成 Result,不再经过单独的 VERIFIED checkpoint。 -## Skills and User-Facing Contract +## Skills 与用户对话契约 -The stable nine-field Polaris status block remains unchanged. Removed checkpoint markers are no longer emitted on new workflow tasks: +稳定的九字段 Polaris 状态块保持不变。新工作流任务不再输出已删除的阶段标记: -- `IMPLEMENTATION_FINISHED` and `DOCS_SYNCED` collapse into `REVIEW_HANDOFF_READY` after the final subject is ready; -- `REVIEW_ACCEPTED` reports state `VALIDATING` and immediately identifies Validation as the next action; -- R0/R1 `VALIDATION_PASS` is followed by the same successful transition result at `CLOSED`, so the controller emits only `TASK_CLOSED`; -- R2 still emits `VALIDATION_PASS` at `VERIFIED`, requesting final approval. +- `IMPLEMENTATION_FINISHED` 和 `DOCS_SYNCED` 合并为最终 subject 就绪后的 `REVIEW_HANDOFF_READY`; +- `REVIEW_ACCEPTED` 报告状态 `VALIDATING`,并立即把 Validation 标记为下一动作; +- R0/R1 的 Validation PASS 与成功关闭属于同一次转换结果,因此 Controller 只输出 `TASK_CLOSED`; +- R2 仍在 `VERIFIED` 输出 `VALIDATION_PASS`,并请求最终批准。 -Recovery recommendations and documentation are updated to describe the new states and legal next actions. +恢复建议和用户文档同步更新为新的状态及合法下一动作。 -## Testing Strategy +## 测试策略 -Tests are written before implementation changes and must cover: +必须先编写测试,再修改实现,并覆盖: -1. `START_IMPLEMENTATION` atomically registers and validates its handoff. -2. `DISPATCH_IMPLEMENTATION` is absent and rejected. -3. Missing `runtime/progress.json` does not block the final implementation-to-review transition. -4. `START_REVIEW` requires matching Implementation, Knowledge Delta, documentation check, final subject, and Review handoff. -5. Implementation and Knowledge Delta bind the same final subject. -6. `ACCEPT_REVIEW` moves directly to `VALIDATING`; `START_VALIDATION` is absent. -7. R0/R1 `PASS_AND_CLOSE` requires Validation, Result, and a complete candidate-task validation pass. -8. R2 cannot use `PASS_AND_CLOSE`, reaches `VERIFIED` through `PASS_VALIDATION`, and still requires final approval to close. -9. Code Intelligence references may be omitted when unused, while present records remain fully validated. -10. Every old workflow state migrates deterministically, including `BLOCKED.blocked_from` and the pre-handoff IMPLEMENTING edge case. -11. Interrupted migration resumes without duplicate events. -12. Documentation, templates, schemas, host-rendered Skills, and the full R1/R2 flow match workflow `0.1.3`. +1. `START_IMPLEMENTATION` 原子注册并校验 handoff。 +2. `DISPATCH_IMPLEMENTATION` 不再存在且会被拒绝。 +3. 缺少 `runtime/progress.json` 不阻断最终 Implementation-to-Review 转换。 +4. `START_REVIEW` 必须检查匹配的 Implementation、Knowledge Delta、文档检查、最终 subject 和 Review handoff。 +5. Implementation 与 Knowledge Delta 绑定相同最终 subject。 +6. `ACCEPT_REVIEW` 直接进入 `VALIDATING`;`START_VALIDATION` 不再存在。 +7. R0/R1 `PASS_AND_CLOSE` 要求 Validation、Result 和完整候选任务校验通过。 +8. R2 禁止使用 `PASS_AND_CLOSE`,通过 `PASS_VALIDATION` 进入 `VERIFIED`,关闭前仍要求最终批准。 +9. 未使用 Code Intelligence 时允许省略引用;存在记录时仍执行完整校验。 +10. 所有旧工作流状态都能确定性迁移,包括 `BLOCKED.blocked_from` 和未注册 handoff 的旧 `IMPLEMENTING` 边界情况。 +11. 中断迁移可恢复,且不会重复追加事件。 +12. 文档、模板、Schema、宿主渲染 Skills 以及完整 R1/R2 流程都与工作流 `0.1.3` 一致。 -The full repository test suite, compile check, template materialization check, and clean-worktree inspection are required before completion. +完成前必须运行完整仓库测试、compile 检查、模板物化检查和 clean-worktree 检查。 -## Success Criteria +## 成功标准 -- New R1 happy paths require one start-implementation transition, one start-review transition, one review-accept transition, and one pass-and-close transition after planning. -- No ignored runtime file is required by a durable gate. -- Review and Validation remain independently evidenced. -- R2 retains both Human approval gates. -- Existing `0.1.2` projects have an explicit adjacent migration path. -- `validate_task.py` and closing transitions share one legality implementation. -- All tests pass using only Python standard-library runtime code. +- 新 R1 happy path 在 Planning 后只需要一次开始 Implementation 转换、一次开始 Review 转换、一次 Review 接受转换和一次通过并关闭转换。 +- ignored runtime 文件不再是任何耐久门禁的前提。 +- Review 和 Validation 继续保留独立证据。 +- R2 保留两个人工批准门禁。 +- 已有 `0.1.2` 项目拥有显式相邻迁移路径。 +- `validate_task.py` 与关闭转换共享同一套合法性实现。 +- 全部测试通过,运行时代码只使用 Python 标准库。 From 58c528800d5c63209a07256a1cb114ad2588d0b2 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:38:22 +0800 Subject: [PATCH 02/17] docs: plan simplified Polaris workflow --- .../2026-08-18-workflow-simplification.md | 583 ++++++++++++++++++ 1 file changed, 583 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-18-workflow-simplification.md diff --git a/docs/superpowers/plans/2026-08-18-workflow-simplification.md b/docs/superpowers/plans/2026-08-18-workflow-simplification.md new file mode 100644 index 0000000..28a0e86 --- /dev/null +++ b/docs/superpowers/plans/2026-08-18-workflow-simplification.md @@ -0,0 +1,583 @@ +# Polaris 工作流精简实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将 Polaris 升级到协议 `0.1.20` / workflow `0.1.3`,删除没有独立治理边界的状态和转换,并让本机实时进度不再阻断耐久流程。 + +**Architecture:** 以新的声明式 workflow 为控制面,把 Implementation、Documentation 和 Review handoff 合并进 `START_REVIEW` 门禁,把 Review 接受直接推进到 Validation,并为 R0/R1 提供原子 `PASS_AND_CLOSE`。任务校验拆成“已保存投影校验”和“候选投影校验”两层;迁移协议升级为 v2,显式替换冻结 workflow 并映射旧任务状态。 + +**Tech Stack:** Python 3.10+ 标准库、JSON Schema 有限子集、`unittest`、Git fixture。 + +**Spec:** `docs/superpowers/specs/2026-08-18-workflow-simplification-design.md` + +## Global Constraints + +- `plan.md` 是当前 v0.1 产品与实现 Authority,最终必须同步更新。 +- 运行时不得新增 Python 标准库以外的依赖。 +- 所有 JSON 使用四空格缩进,并由现有原子写入工具生成。 +- 每个门禁、状态转换、迁移分支和 Validator 规则必须先有失败测试。 +- Agent 不得直接写入 `VERIFIED` 或 `CLOSED`;仍由 `transition_task.py` 通过门禁转换。 +- 历史 event 和 artifact 保持不可变;新版本只追加迁移事件并更新投影。 +- Windows、macOS 与 Linux 路径和进程语义继续使用现有跨平台抽象。 + +--- + +### Task 1: 冻结 workflow 0.1.3 与新状态契约 + +**Files:** +- Modify: `workflow/default-workflow.json` +- Modify: `schemas/task-state.schema.json` +- Modify: `schemas/project-index.schema.json` +- Modify: `templates/project.json` +- Modify: `templates/task-sources/state.json` +- Generated: `templates/task/state.json` +- Test: `tests/test_core.py` + +**Interfaces:** +- Consumes: 现有 `transition_task.transition(...)` 对声明式 `event/from/to/gate` 的解释。 +- Produces: workflow `0.1.3`;事件 `START_IMPLEMENTATION`、`START_REVIEW`、`ACCEPT_REVIEW`、`PASS_AND_CLOSE`、`PASS_VALIDATION`、`CLOSE` 的唯一合法边。 + +- [ ] **Step 1: 写入失败的 workflow 契约测试** + +在 `tests/test_core.py` 新增: + +```python +def test_workflow_013_contains_only_governance_states(self) -> None: + workflow = read_json(ROOT / "workflow" / "default-workflow.json") + self.assertEqual(workflow["workflow_version"], "0.1.3") + self.assertNotIn("IMPLEMENTED", workflow["states"]) + self.assertNotIn("DOCS_SYNCED", workflow["states"]) + self.assertNotIn("REVIEWED", workflow["states"]) + events = {item["event"]: item for item in workflow["transitions"]} + self.assertNotIn("DISPATCH_IMPLEMENTATION", events) + self.assertNotIn("FINISH_IMPLEMENTATION", events) + self.assertNotIn("SYNC_DOCS", events) + self.assertNotIn("START_VALIDATION", events) + self.assertEqual(events["START_REVIEW"]["from"], ["IMPLEMENTING"]) + self.assertEqual(events["ACCEPT_REVIEW"]["to"], "VALIDATING") + self.assertEqual(events["PASS_AND_CLOSE"]["to"], "CLOSED") +``` + +- [ ] **Step 2: 运行测试并确认因旧 workflow 失败** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_workflow_013_contains_only_governance_states -v` + +Expected: FAIL,报告 `0.1.2 != 0.1.3` 或旧状态仍存在。 + +- [ ] **Step 3: 最小修改 workflow、状态 Schema 和版本模板** + +将主路径转换定义为: + +```json +{ + "event": "START_IMPLEMENTATION", + "from": ["PLANNED"], + "to": "IMPLEMENTING", + "gate": "implementation_start_ready" +} +``` + +```json +{ + "event": "START_REVIEW", + "from": ["IMPLEMENTING"], + "to": "REVIEWING", + "gate": "review_start_ready" +} +``` + +```json +{ + "event": "ACCEPT_REVIEW", + "from": ["REVIEWING"], + "to": "VALIDATING", + "gate": "review_accepted" +} +``` + +并增加 `PASS_AND_CLOSE` 的 `validation_passed_and_closure_ready` gate;`PASS_VALIDATION` 保留给 R2,`CLOSE` 保留为 `VERIFIED → CLOSED`。 + +- [ ] **Step 4: 物化任务模板并运行契约测试** + +Run: `python3 scripts/materialize_task_layout.py` + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_workflow_013_contains_only_governance_states -v` + +Expected: PASS。 + +- [ ] **Step 5: 提交声明式契约** + +```bash +git add workflow/default-workflow.json schemas/task-state.schema.json schemas/project-index.schema.json templates/project.json templates/task-sources/state.json templates/task/state.json tests/test_core.py +git commit -m "feat: define simplified workflow 0.1.3" +``` + +### Task 2: 合并 Implementation、Documentation 与 Review 启动门禁 + +**Files:** +- Modify: `scripts/internal/transition_gates.py` +- Modify: `scripts/internal/transition_effects.py` +- Modify: `scripts/build_review_handoff.py` +- Modify: `scripts/internal/implementation_protocol.py` +- Modify: `scripts/update_implementation_progress.py` +- Modify: `scripts/validate_task.py` +- Test: `tests/test_core.py` + +**Interfaces:** +- Consumes: `validate_implementation_handoff(...)`、`validate_handoff(...)`、`check_docs.check(...)`、artifact registration from `prepare_next_state(...)`。 +- Produces: `check_implementation_artifact(...)`、`check_knowledge_delta(...)` 等可由 transition gate 和 task validator 复用的只读校验;`build_review_handoff.build(...)` 接受 `IMPLEMENTING` 且已有最终 artifact/subject 的状态。 + +- [ ] **Step 1: 写失败测试,证明 handoff 在 START_IMPLEMENTATION 中原子注册** + +```python +def test_start_implementation_atomically_registers_handoff(self) -> None: + self.freeze_and_plan() + handoff = build_implementation_handoff(self.repo, "TASK-0001") + result = transition( + self.repo, + "TASK-0001", + "START_IMPLEMENTATION", + [f"implementation_handoff={Path(handoff['path']).relative_to(self.task).as_posix()}"], + None, None, None, None, None, None, + ) + self.assertEqual(result["to"], "IMPLEMENTING") + state = read_json(self.task / "state.json") + self.assertIn("implementation_handoff", state["artifacts"]) + with self.assertRaisesRegex(RuleFailure, "unknown workflow event"): + transition( + self.repo, "TASK-0001", "DISPATCH_IMPLEMENTATION", [], + None, None, None, None, None, None, + ) +``` + +- [ ] **Step 2: 写失败测试,证明 progress 缺失不阻断 START_REVIEW** + +使用现有 fixture helper 生成最终 subject、Implementation、Knowledge Delta 和 Review handoff,删除 `runtime/progress.json` 后执行: + +```python +result = transition( + self.repo, + "TASK-0001", + "START_REVIEW", + [ + "implementation=implementations/r001/attempt-001.json", + "knowledge_delta=knowledge/r001/knowledge-delta-001.json", + "review_handoff=reviews/r001/handoff-001.json", + ], + None, + base, + head, + None, + None, + None, +) +self.assertEqual(result["to"], "REVIEWING") +``` + +- [ ] **Step 3: 运行两项测试并确认失败原因来自旧转换/旧 progress 门禁** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_start_implementation_atomically_registers_handoff tests.test_core.PolarisCoreTests.test_start_review_does_not_require_live_progress -v` + +Expected: FAIL;旧 `START_IMPLEMENTATION` 不接受 handoff 或 `build_review_handoff` 只允许 `DOCS_SYNCED`。 + +- [ ] **Step 4: 实现组合 gate 与共享 artifact 校验** + +在 `transition_gates.check_gate(...)` 中用两个组合分支替代旧四个分支: + +```python +if gate == "implementation_start_ready": + if state["rigor"] == "R2": + artifact_file(directory, state, "pre_approval") + validate_implementation_handoff(repo, root, directory, state, True) +elif gate == "review_start_ready": + validate_implementation_record(repo, root, directory, state) + validate_knowledge_delta(repo, root, directory, state) + validate_handoff(repo, root, directory, state) +``` + +移除 `validate_progress(...)` 对耐久 gate 的调用。`update_implementation_progress.py` 和 `implementation_protocol.validate_progress(...)` 只接受当前 `IMPLEMENTING`;允许 `DOCUMENTING/COMPLETED` phase 在该状态内发生。 + +- [ ] **Step 5: 更新 Review handoff 构建入口与 validator 状态顺序** + +`build_review_handoff.build(...)` 从 `IMPLEMENTING` 读取已注册 Implementation、Knowledge Delta 和最终 subject。`validate_task.py` 不再使用线性 `ORDER/at_least` 推断已删除状态,而按当前状态需要的 artifact 集合校验。 + +- [ ] **Step 6: 运行 Implementation/Review/Progress 相关测试** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_start_implementation_atomically_registers_handoff tests.test_core.PolarisCoreTests.test_start_review_does_not_require_live_progress tests.test_core.PolarisCoreTests.test_implementation_steps_are_linear_append_only_and_acceptance_bound tests.test_core.PolarisCoreTests.test_live_progress_rejects_session_takeover_and_invalid_blocker -v` + +Expected: PASS。 + +- [ ] **Step 7: 提交合并门禁** + +```bash +git add scripts/internal/transition_gates.py scripts/internal/transition_effects.py scripts/build_review_handoff.py scripts/internal/implementation_protocol.py scripts/update_implementation_progress.py scripts/validate_task.py tests/test_core.py +git commit -m "feat: combine implementation and review readiness gates" +``` + +### Task 3: 候选投影校验与 R0/R1 原子关闭 + +**Files:** +- Modify: `scripts/validate_task.py` +- Modify: `scripts/internal/transition_gates.py` +- Modify: `scripts/transition_task.py` +- Test: `tests/test_core.py` + +**Interfaces:** +- Produces: `validate_projection(repo: Path, task_id: str, state: dict[str, Any], *, check_event_projection: bool) -> dict[str, Any]`。 +- Consumes: `transition_task.prepare_next_state(...)` 生成的候选 state,以及 `apply_event_effects(...)` 计算后的最终 destination。 + +- [ ] **Step 1: 写失败的 Review 直达 Validation 测试** + +```python +accepted = transition( + self.repo, + "TASK-0001", + "ACCEPT_REVIEW", + ["review=reviews/r001/review-001.json"], + None, None, None, None, None, None, +) +self.assertEqual(accepted["to"], "VALIDATING") +with self.assertRaisesRegex(RuleFailure, "unknown workflow event"): + transition( + self.repo, "TASK-0001", "START_VALIDATION", [], + None, None, None, None, None, None, + ) +``` + +- [ ] **Step 2: 写失败的 R1 PASS_AND_CLOSE 候选投影测试** + +```python +closed = transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, None, None, None, None, None, +) +self.assertEqual(closed["to"], "CLOSED") +self.assertEqual(validate(self.repo, "TASK-0001")["state"], "CLOSED") +``` + +再加入反例:篡改已注册 Implementation hash 后,`PASS_AND_CLOSE` 必须在追加事件前失败,并保持 sequence/status 不变。 + +- [ ] **Step 3: 写失败的 R2 分流测试** + +```python +with self.assertRaisesRegex(RuleFailure, "R0/R1"): + transition( + self.repo, "TASK-0001", "PASS_AND_CLOSE", artifacts, + None, None, None, None, None, None, + ) +verified = transition( + self.repo, "TASK-0001", "PASS_VALIDATION", + ["validation=validations/r001/validation-001.json"], + None, None, None, None, None, None, +) +self.assertEqual(verified["to"], "VERIFIED") +``` + +- [ ] **Step 4: 运行测试并确认旧流程失败** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_review_acceptance_enters_validation tests.test_core.PolarisCoreTests.test_r1_pass_and_close_validates_candidate_projection tests.test_core.PolarisCoreTests.test_r2_keeps_verified_final_approval_gate -v` + +Expected: FAIL,事件缺失或目标状态仍为旧值。 + +- [ ] **Step 5: 抽取共享投影校验并在关闭 gate 中调用** + +`validate(...)` 继续执行 event/state 重建一致性,然后调用 +`validate_projection(repo: Path, task_id: str, state: dict[str, Any]) -> dict[str, Any]`。 +该函数依次校验协议与 workflow 版本、Work Item 身份和 rigor、当前状态要求的 +Plan/Working Set、Implementation/Knowledge Delta、Review、Validation、Result、subject +绑定及 R2 approval,并返回包含 `message`、`task` 和 `state` 的结果对象。 + +`transition_task.transition(...)` 在 `apply_event_effects(...)` 后先将 candidate `status` 设为 destination,再把候选传给 closure gate 或统一的 post-gate candidate validator,只有成功后才增加 sequence、追加事件并写 state。 + +- [ ] **Step 6: 实现严格 rigor 分流** + +`validation_passed_and_closure_ready` 拒绝 R2,并要求 PASS Validation、完整 AC、Result 和 candidate CLOSED 投影;`validation_passed` 拒绝非 R2。`closure_ready` 只接受 R2 的 VERIFIED 状态、Result、final approval 和完整 candidate CLOSED 投影。 + +- [ ] **Step 7: 运行关闭与完整 R1/R2 测试** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_review_acceptance_enters_validation tests.test_core.PolarisCoreTests.test_r1_pass_and_close_validates_candidate_projection tests.test_core.PolarisCoreTests.test_r2_keeps_verified_final_approval_gate tests.test_core.PolarisCoreTests.test_full_r1_flow_closes_only_after_review_and_validation -v` + +Expected: PASS。 + +- [ ] **Step 8: 提交候选校验和关闭路径** + +```bash +git add scripts/validate_task.py scripts/internal/transition_gates.py scripts/transition_task.py tests/test_core.py +git commit -m "feat: validate candidate projection before task closure" +``` + +### Task 4: 升级迁移协议并映射 workflow 0.1.2 任务 + +**Files:** +- Modify: `schemas/migration-protocol.schema.json` +- Modify: `schemas/migration-record.schema.json` +- Modify: `schemas/event.schema.json` +- Modify: `workflow/migrations.json` +- Modify: `scripts/internal/migration_protocol.py` +- Test: `tests/test_core.py` +- Test: `tests/test_codegraph.py` + +**Interfaces:** +- Produces: migration protocol v2 strategy `replace_version_and_workflow` / `append_mapped_workflow_event`。 +- Produces: `map_migrated_status(state: dict[str, Any]) -> tuple[str, str | None]`,返回新 status 和映射后的 blocked_from。 + +- [ ] **Step 1: 写状态映射表驱动失败测试** + +```python +cases = { + "DRAFT": "DRAFT", + "QUALIFIED": "QUALIFIED", + "PLANNED": "PLANNED", + "IMPLEMENTED": "IMPLEMENTING", + "DOCS_SYNCED": "IMPLEMENTING", + "REVIEWING": "REVIEWING", + "REVIEWED": "VALIDATING", + "VALIDATING": "VALIDATING", + "VERIFIED": "VERIFIED", + "CLOSED": "CLOSED", + "CANCELLED": "CANCELLED", +} +for old, expected in cases.items(): + with self.subTest(old=old): + state = {"status": old, "blocked_from": None, "artifacts": {}} + self.assertEqual(map_migrated_status(state)[0], expected) +``` + +另测 `IMPLEMENTING` 无 handoff 映射到 `PLANNED`、有 handoff保持 `IMPLEMENTING`,以及 `BLOCKED.blocked_from` 同步映射。 + +- [ ] **Step 2: 写完整迁移失败测试** + +从协议 `0.1.19` / workflow `0.1.2` fixture 运行 `migrate_project(...)`,断言: + +```python +self.assertEqual(project["polaris_version"], "0.1.20") +self.assertEqual(project["workflow_version"], "0.1.3") +self.assertEqual(frozen_workflow["workflow_version"], "0.1.3") +self.assertEqual(event["from"], "DOCS_SYNCED") +self.assertEqual(event["to"], "IMPLEMENTING") +self.assertEqual(event["previous_workflow_version"], "0.1.2") +``` + +- [ ] **Step 3: 运行测试并确认 v1 协议拒绝 workflow 变化** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_workflow_migration_maps_every_legacy_state tests.test_core.PolarisCoreTests.test_migration_replaces_frozen_workflow_and_maps_tasks -v` + +Expected: FAIL,报告 v1 migration protocol cannot change workflow versions。 + +- [ ] **Step 4: 实现 migration protocol v2 Schema 与注册步骤** + +新增 `0.1.19-to-0.1.20`: + +```json +{ + "migration_id": "0.1.19-to-0.1.20", + "from_polaris_version": "0.1.19", + "to_polaris_version": "0.1.20", + "from_workflow_version": "0.1.2", + "to_workflow_version": "0.1.3", + "project_strategy": "replace_version_and_workflow", + "task_strategy": "append_mapped_workflow_event" +} +``` + +旧步骤继续使用 v1 strategy 值,协议 loader 按 strategy 验证是否允许 workflow 变化。 + +- [ ] **Step 5: 实现事件、记录和冻结 workflow 替换** + +迁移事件记录 `from/to` 状态和 `previous_polaris_version`、`previous_workflow_version`;migration record task entry增加 `source_status`、`target_status`。在持有所有任务锁且写入 IN_PROGRESS record 后,用 vendored `default-workflow.json` 原子替换 `.polaris/workflow.json`,再更新 project 版本。 + +- [ ] **Step 6: 验证崩溃恢复和历史 Code Intelligence 迁移不回归** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_migration_replaces_frozen_workflow_and_maps_tasks tests.test_core.PolarisCoreTests.test_migration_resumes_after_event_append_without_duplication tests.test_codegraph.CodeGraphTests.test_migration_retires_v1_records_without_rewriting_them -v` + +Expected: PASS。 + +- [ ] **Step 7: 提交 workflow 迁移** + +```bash +git add schemas/migration-protocol.schema.json schemas/migration-record.schema.json schemas/event.schema.json workflow/migrations.json scripts/internal/migration_protocol.py tests/test_core.py tests/test_codegraph.py +git commit -m "feat: migrate frozen projects to workflow 0.1.3" +``` + +### Task 5: 同步恢复、Skills、可选 Code Intelligence 与宿主表面 + +**Files:** +- Modify: `scripts/internal/recovery_protocol.py` +- Modify: `scripts/recover_task.py` +- Modify: `skills/engineering-task/SKILL.md` +- Modify: `skills/implementation/SKILL.md` +- Modify: `skills/documentation-sync/SKILL.md` +- Modify: `skills/adversarial-review/SKILL.md` +- Modify: `skills/validation/SKILL.md` +- Modify: `skills/architecture-planning/SKILL.md` +- Modify: `skills/code-intelligence/SKILL.md` +- Modify: `hosts/codex/skill-appendices/engineering-task.md` +- Modify: `hosts/claude-code/skill-appendices/engineering-task.md` +- Test: `tests/test_core.py` +- Test: `tests/test_codegraph.py` + +**Interfaces:** +- Consumes: 新 workflow event/state 名称。 +- Produces: 新的稳定对话标记、Worker prompt、恢复 next action,以及“只有实际 Provider 操作才写耐久 record”的阶段规则。 + +- [ ] **Step 1: 写失败的 Skill/恢复表面测试** + +```python +surfaces = [ + ROOT / "skills/engineering-task/SKILL.md", + ROOT / "skills/validation/SKILL.md", + ROOT / "docs/USAGE.md", +] +for path in surfaces: + text = path.read_text(encoding="utf-8") + self.assertNotIn("DISPATCH_IMPLEMENTATION", text) + self.assertNotIn("START_VALIDATION", text) +self.assertIn("PASS_AND_CLOSE", (ROOT / "skills/validation/SKILL.md").read_text()) +``` + +为 Code Intelligence 增加测试:缺少 `.codegraph/` 时,阶段 Skill 明确允许省略 record,而不是要求 durable `UNAVAILABLE` artifact。 + +- [ ] **Step 2: 运行表面测试并确认旧文案失败** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_workflow_surfaces_use_simplified_events tests.test_codegraph.CodeGraphTests.test_stage_surfaces_do_not_require_unused_provider_records -v` + +Expected: FAIL,旧事件名和 mandatory unavailable record 仍存在。 + +- [ ] **Step 3: 更新恢复动作和 progress 读取边界** + +`NEXT_ACTIONS` 只包含新状态;`IMPLEMENTING` 同时覆盖实现、文档和 Review handoff 准备。`recover_task.py` 仅在 `IMPLEMENTING` 且 progress 文件存在时尝试读取;无文件返回 `None`,不视为 blocker。 + +- [ ] **Step 4: 更新 canonical Skills 与宿主 appendix** + +主 Controller 的新顺序是:构建 handoff并 `START_IMPLEMENTATION`、派发 Implementer、等待最终 Implementation + Knowledge Delta、构建 Review handoff并 `START_REVIEW`、Reviewer ACCEPT 后进入 VALIDATING、R0/R1 `PASS_AND_CLOSE`、R2 `PASS_VALIDATION` 后等待最终批准再 `CLOSE`。 + +Documentation Sync 继续作为同一 Implementer 内部 Skill,但在 `IMPLEMENTING` 内完成且不再对应 Graph transition。 + +- [ ] **Step 5: 放宽未使用 Code Intelligence 的 durable record 要求** + +Planning、Implementation、Review、Documentation Sync 在未执行 status/sync/explore 时不生成 record;一旦执行 Provider 操作,继续使用 v2 record、freshness 和源码回退校验。Artifact Schema 中已有可选 `code_intelligence` 字段,不新增空占位字段。 + +- [ ] **Step 6: 运行 Skill 渲染、宿主和 Code Intelligence 测试** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_workflow_surfaces_use_simplified_events tests.test_core.PolarisCoreTests.test_host_adapters_render_from_one_host_neutral_skill_source tests.test_codegraph.CodeGraphTests.test_stage_surfaces_do_not_require_unused_provider_records tests.test_codegraph.CodeGraphTests.test_all_agent_surfaces_share_codegraph_fallback_rules -v` + +Expected: PASS。 + +- [ ] **Step 7: 提交执行表面更新** + +```bash +git add scripts/internal/recovery_protocol.py scripts/recover_task.py skills hosts tests/test_core.py tests/test_codegraph.py +git commit -m "docs: align workflow skills with simplified states" +``` + +### Task 6: 更新 Authority、用户文档、版本与生成物 + +**Files:** +- Modify: `VERSION` +- Modify: `plan.md` +- Modify: `README.md` +- Modify: `README.zh-CN.md` +- Modify: `docs/USAGE.md` +- Modify: `templates/task-sources/*` as required by changed defaults +- Generated: `templates/task/*` +- Test: `tests/test_core.py` +- Test: `tests/test_codegraph.py` + +**Interfaces:** +- Produces: 所有产品 Authority、用户说明、模板和版本字符串一致指向协议 `0.1.20` / workflow `0.1.3`。 + +- [ ] **Step 1: 更新版本一致性测试并确认失败** + +把现有版本表面断言改为: + +```python +for path in version_surfaces: + text = path.read_text(encoding="utf-8") + self.assertIn("0.1.20", text, path.as_posix()) + self.assertIn("0.1.3", text, path.as_posix()) +``` + +Run: `python3 -m unittest tests.test_codegraph.CodeGraphTests.test_managed_surfaces_only_name_the_official_codegraph -v` + +Expected: FAIL,当前版本仍为 `0.1.19` / `0.1.2`。 + +- [ ] **Step 2: 更新 VERSION、plan.md、README 和 USAGE** + +删除旧 happy path、中间 checkpoint 和 mandatory progress/Code Intelligence record 叙述;写入迁移 `0.1.19 → 0.1.20`、状态映射、R0/R1 `PASS_AND_CLOSE` 与 R2 `VERIFIED` 最终批准路径。 + +- [ ] **Step 3: 重新物化模板并检查生成漂移** + +Run: `python3 scripts/materialize_task_layout.py` + +Run: `python3 scripts/materialize_task_layout.py --check` + +Expected: PASS;如果脚本不提供 `--check`,运行 `python3 -m unittest tests.test_core.PolarisCoreTests.test_task_layout_is_single_source_and_templates_mirror_it -v` 作为机械检查。 + +- [ ] **Step 4: 运行文档、版本和模板测试** + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_task_layout_is_single_source_and_templates_mirror_it tests.test_core.PolarisCoreTests.test_skills_define_stable_conversation_checkpoints tests.test_codegraph.CodeGraphTests.test_readmes_keep_codegraph_operational_boundaries -v` + +Expected: PASS。 + +- [ ] **Step 5: 提交 Authority 和文档** + +```bash +git add VERSION plan.md README.md README.zh-CN.md docs/USAGE.md templates tests/test_core.py tests/test_codegraph.py +git commit -m "docs: publish Polaris 0.1.20 workflow 0.1.3" +``` + +### Task 7: 全量回归、编译和干净交付 + +**Files:** +- Modify: only files required by failures attributable to this workflow change +- Test: `tests/run_tests.py` + +**Interfaces:** +- Produces: 一个标准库运行时、模板一致、完整测试通过的协议版本。 + +- [ ] **Step 1: 运行完整自动化测试** + +Run: `python3 tests/run_tests.py` + +Expected: `150+` tests,失败 `0`、错误 `0`。 + +- [ ] **Step 2: 运行 unittest discovery** + +Run: `python3 -m unittest discover -s tests -v` + +Expected: PASS。 + +- [ ] **Step 3: 运行编译检查** + +Run: `python3 -m compileall -q polaris_cli.py scripts tests` + +Expected: exit `0`,无输出。 + +- [ ] **Step 4: 运行文档和模板检查** + +Run: `python3 scripts/check_docs.py --help` + +Run: `python3 -m unittest tests.test_core.PolarisCoreTests.test_task_layout_is_single_source_and_templates_mirror_it -v` + +Expected: PASS。 + +- [ ] **Step 5: 检查最终 diff 与工作区** + +Run: `git diff --check` + +Run: `git status --short` + +Expected: 没有 whitespace error;只有本计划范围内的预期修改,或在最终提交后 clean。 + +- [ ] **Step 6: 最终提交(仅在仍有已验证修改时)** + +```bash +git add -A +git commit -m "test: verify simplified Polaris workflow" +``` From e3af9dc823be4d750282b0cc50b3a5364a01d45c Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:41:40 +0800 Subject: [PATCH 03/17] feat: define simplified workflow 0.1.3 --- schemas/project-index.schema.json | 3 -- schemas/task-state.schema.json | 3 -- templates/project.json | 2 +- templates/task-sources/state.json | 2 +- templates/task/state.json | 2 +- tests/test_core.py | 16 +++++++++ workflow/default-workflow.json | 56 ++++++------------------------- 7 files changed, 29 insertions(+), 55 deletions(-) diff --git a/schemas/project-index.schema.json b/schemas/project-index.schema.json index 18a91b9..d9c40c7 100644 --- a/schemas/project-index.schema.json +++ b/schemas/project-index.schema.json @@ -55,10 +55,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "BLOCKED", diff --git a/schemas/task-state.schema.json b/schemas/task-state.schema.json index 4a40832..359de91 100644 --- a/schemas/task-state.schema.json +++ b/schemas/task-state.schema.json @@ -37,10 +37,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "BLOCKED", diff --git a/templates/project.json b/templates/project.json index 5cb42f7..c30e407 100644 --- a/templates/project.json +++ b/templates/project.json @@ -1,6 +1,6 @@ { "project_id": "PROJECT_ID", "polaris_version": "0.1.19", - "workflow_version": "0.1.2", + "workflow_version": "0.1.3", "active_tasks": [] } diff --git a/templates/task-sources/state.json b/templates/task-sources/state.json index e059bc6..024913f 100644 --- a/templates/task-sources/state.json +++ b/templates/task-sources/state.json @@ -1,7 +1,7 @@ { "task_id": "TASK-0001", "polaris_version": "0.1.19", - "workflow_version": "0.1.2", + "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", "rigor": "R1", diff --git a/templates/task/state.json b/templates/task/state.json index e059bc6..024913f 100644 --- a/templates/task/state.json +++ b/templates/task/state.json @@ -1,7 +1,7 @@ { "task_id": "TASK-0001", "polaris_version": "0.1.19", - "workflow_version": "0.1.2", + "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", "rigor": "R1", diff --git a/tests/test_core.py b/tests/test_core.py index 8e874e1..6951550 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -172,6 +172,22 @@ def test_init_project_defaults_to_repository_directory_name(self) -> None: def task(self) -> Path: return self.repo / ".polaris" / "tasks" / "TASK-0001" + def test_workflow_013_contains_only_governance_states(self) -> None: + """0.1.3 只持久化具有独立治理边界的状态和转换。""" + workflow = read_json(ROOT / "workflow" / "default-workflow.json") + self.assertEqual(workflow["workflow_version"], "0.1.3") + self.assertNotIn("IMPLEMENTED", workflow["states"]) + self.assertNotIn("DOCS_SYNCED", workflow["states"]) + self.assertNotIn("REVIEWED", workflow["states"]) + events = {item["event"]: item for item in workflow["transitions"]} + self.assertNotIn("DISPATCH_IMPLEMENTATION", events) + self.assertNotIn("FINISH_IMPLEMENTATION", events) + self.assertNotIn("SYNC_DOCS", events) + self.assertNotIn("START_VALIDATION", events) + self.assertEqual(events["START_REVIEW"]["from"], ["IMPLEMENTING"]) + self.assertEqual(events["ACCEPT_REVIEW"]["to"], "VALIDATING") + self.assertEqual(events["PASS_AND_CLOSE"]["to"], "CLOSED") + def freeze_work_item(self) -> None: path = self.task / "revisions" / "work-item-r001.json" value = read_json(path) diff --git a/workflow/default-workflow.json b/workflow/default-workflow.json index 79f67ac..e6ab3f7 100644 --- a/workflow/default-workflow.json +++ b/workflow/default-workflow.json @@ -1,6 +1,6 @@ { - "schema_version": "0.1.2", - "workflow_version": "0.1.2", + "schema_version": "0.1.3", + "workflow_version": "0.1.3", "initial_state": "DRAFT", "terminal_states": [ "CLOSED", @@ -11,10 +11,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "BLOCKED", @@ -45,46 +42,22 @@ "PLANNED" ], "to": "IMPLEMENTING", - "gate": "implementation_approved" - }, - { - "event": "DISPATCH_IMPLEMENTATION", - "from": [ - "IMPLEMENTING" - ], - "to": "IMPLEMENTING", - "gate": "implementation_handoff_ready" - }, - { - "event": "FINISH_IMPLEMENTATION", - "from": [ - "IMPLEMENTING" - ], - "to": "IMPLEMENTED", - "gate": "implementation_ready" - }, - { - "event": "SYNC_DOCS", - "from": [ - "IMPLEMENTED" - ], - "to": "DOCS_SYNCED", - "gate": "docs_ready" + "gate": "implementation_start_ready" }, { "event": "START_REVIEW", "from": [ - "DOCS_SYNCED" + "IMPLEMENTING" ], "to": "REVIEWING", - "gate": "review_package_ready" + "gate": "review_start_ready" }, { "event": "ACCEPT_REVIEW", "from": [ "REVIEWING" ], - "to": "REVIEWED", + "to": "VALIDATING", "gate": "review_accepted" }, { @@ -98,12 +71,12 @@ "on_max_attempts_to": "BLOCKED" }, { - "event": "START_VALIDATION", + "event": "PASS_AND_CLOSE", "from": [ - "REVIEWED" + "VALIDATING" ], - "to": "VALIDATING", - "gate": "validation_ready" + "to": "CLOSED", + "gate": "validation_passed_and_closure_ready" }, { "event": "PASS_VALIDATION", @@ -143,10 +116,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "BLOCKED" @@ -161,10 +131,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED" ], @@ -186,10 +153,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "BLOCKED" From 70952c442bf97ff7277f81f04deafc4a8109fa02 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:45:10 +0800 Subject: [PATCH 04/17] feat: combine implementation and review readiness gates --- scripts/build_implementation_handoff.py | 6 +- scripts/build_review_handoff.py | 57 ++++- scripts/internal/implementation_protocol.py | 6 +- scripts/internal/transition_gates.py | 24 +- scripts/update_implementation_progress.py | 6 +- tests/test_core.py | 234 +++++++++++++++----- 6 files changed, 248 insertions(+), 85 deletions(-) diff --git a/scripts/build_implementation_handoff.py b/scripts/build_implementation_handoff.py index 56c460c..541e81a 100644 --- a/scripts/build_implementation_handoff.py +++ b/scripts/build_implementation_handoff.py @@ -54,8 +54,10 @@ def build(repo: Path, task_id: str) -> dict[str, Any]: directory = task_dir(repo, task_id) state = read_json(state_path(directory)) require_protocol_compatible(repo, state) - if state["status"] != "IMPLEMENTING": - raise RuleFailure("Implementation handoff can only be built from IMPLEMENTING") + if state["status"] not in {"PLANNED", "IMPLEMENTING"}: + raise RuleFailure( + "Implementation handoff can only be built from PLANNED or IMPLEMENTING" + ) revision = state["current_revision"] attempt, previous_review, base_commit = expected_attempt(root, directory, state) plan = normalized_reference(directory, state["artifacts"].get("plan")) diff --git a/scripts/build_review_handoff.py b/scripts/build_review_handoff.py index f607993..c63cfb8 100644 --- a/scripts/build_review_handoff.py +++ b/scripts/build_review_handoff.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import copy import sys from pathlib import Path from typing import Any @@ -14,11 +15,13 @@ current_work_item_path, directory_sha256, file_sha256, + full_commit, protocol_root, read_json, require_protocol_compatible, run_main, task_dir, + subject_diff_hash, utc_now, validate_json_file, write_json_atomic, @@ -86,13 +89,51 @@ def build( task_id: str, implementer_session_id: str, isolation_mode: str | None, + implementation_path: Path | None = None, + knowledge_path: Path | None = None, + subject_base: str | None = None, + subject_head: str | None = None, ) -> dict[str, Any]: root = protocol_root(repo) directory = task_dir(repo, task_id) - state = read_json(state_path(directory)) - require_protocol_compatible(repo, state) - if state["status"] != "DOCS_SYNCED": - raise RuleFailure("review handoff can only be built from DOCS_SYNCED") + stored_state = read_json(state_path(directory)) + require_protocol_compatible(repo, stored_state) + if stored_state["status"] != "IMPLEMENTING": + raise RuleFailure("review handoff can only be built from IMPLEMENTING") + supplied = (implementation_path, knowledge_path, subject_base, subject_head) + if any(value is not None for value in supplied) and not all( + value is not None for value in supplied + ): + raise InputFailure( + "review handoff requires implementation, knowledge, subject base, and subject head" + ) + state = copy.deepcopy(stored_state) + if all(value is not None for value in supplied): + base = full_commit(repo, str(subject_base)) + head = full_commit(repo, str(subject_head)) + for name, supplied_path in ( + ("implementation", implementation_path), + ("knowledge_delta", knowledge_path), + ): + assert supplied_path is not None + resolved = supplied_path.resolve() + try: + relative = resolved.relative_to(directory.resolve()) + except ValueError as exc: + raise RuleFailure( + f"review handoff artifact must be inside the task directory: {resolved}" + ) from exc + if not resolved.is_file(): + raise RuleFailure(f"review handoff artifact does not exist: {resolved}") + state["artifacts"][name] = { + "path": relative.as_posix(), + "sha256": file_sha256(resolved), + } + state["subject"] = { + "base_commit": base, + "head_commit": head, + "diff_hash": subject_diff_hash(repo, base, head), + } implementation_reference = normalized_reference( directory, state["artifacts"].get("implementation") ) @@ -241,6 +282,10 @@ def main() -> int: "r0_isolated_same_session", ], ) + parser.add_argument("--implementation", type=Path) + parser.add_argument("--knowledge-delta", type=Path) + parser.add_argument("--subject-base") + parser.add_argument("--subject-head") parser.add_argument("--repo", type=Path, default=Path.cwd()) parser.add_argument("--json", action="store_true") args = parser.parse_args() @@ -250,6 +295,10 @@ def main() -> int: args.task_id, args.implementer_session_id, args.isolation, + args.implementation, + args.knowledge_delta, + args.subject_base, + args.subject_head, ), args.json, ) diff --git a/scripts/internal/implementation_protocol.py b/scripts/internal/implementation_protocol.py index 9130aea..082867e 100644 --- a/scripts/internal/implementation_protocol.py +++ b/scripts/internal/implementation_protocol.py @@ -281,10 +281,8 @@ def validate_progress(repo: Path, task_id: str) -> dict[str, Any]: root = protocol_root(repo) directory = task_dir(repo, task_id) state = read_json(state_path(directory)) - if state["status"] not in {"IMPLEMENTING", "IMPLEMENTED"}: - raise RuleFailure( - "Live implementation progress is valid only while IMPLEMENTING or IMPLEMENTED" - ) + if state["status"] != "IMPLEMENTING": + raise RuleFailure("Live implementation progress is valid only while IMPLEMENTING") handoff, reference = validate_handoff(repo, root, directory, state) path = resolve_repo_reference(repo, handoff["progress_json_path"]) progress = validate_json_file( diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index 2acda91..c61abb8 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -5,11 +5,7 @@ from pathlib import Path from typing import Any -from .implementation_protocol import ( - step_results, - validate_handoff as validate_implementation_handoff, - validate_progress, -) +from .implementation_protocol import validate_handoff as validate_implementation_handoff from .code_intelligence_protocol import record_reference from .polaris_core import ( RuleFailure, @@ -113,10 +109,9 @@ def check_gate( validate_plan_decisions(repo, root, directory, state, True) working_set_path = artifact_file(directory, state, "working_set") validate_working_set(repo, state["task_id"], working_set_path) - elif gate == "implementation_approved": + elif gate == "implementation_start_ready": if state["rigor"] == "R2": artifact_file(directory, state, "pre_approval") - elif gate == "implementation_handoff_ready": validate_implementation_handoff(repo, root, directory, state, True) elif gate == "implementation_ready": handoff, handoff_reference = validate_implementation_handoff( @@ -156,13 +151,6 @@ def check_gate( != implementation["subject_diff_hash"] ): raise RuleFailure("Implementation Code Intelligence record targets the wrong subject") - progress = validate_progress(repo, state["task_id"]) - if progress["phase"] != "CHECKPOINTING": - raise RuleFailure("FINISH_IMPLEMENTATION requires CHECKPOINTING live progress") - if progress["implementer_session_id"] != implementation["implementer_session_id"]: - raise RuleFailure("Implementation and live progress have different sessions") - if implementation["step_results"] != step_results(progress): - raise RuleFailure("Implementation step_results do not match live progress") validate_review_response(root, directory, state, implementation) elif gate == "docs_ready": knowledge_path = artifact_file(directory, state, "knowledge_delta") @@ -204,6 +192,14 @@ def check_gate( state["subject"]["base_commit"], state["subject"]["head_commit"], ) + elif gate == "review_start_ready": + check_gate( + repo, root, directory, state, "implementation_ready", blocker, workflow + ) + check_gate(repo, root, directory, state, "docs_ready", blocker, workflow) + check_gate( + repo, root, directory, state, "review_package_ready", blocker, workflow + ) elif gate == "review_package_ready": artifact_file(directory, state, "knowledge_delta") check_subject(repo, state.get("subject")) diff --git a/scripts/update_implementation_progress.py b/scripts/update_implementation_progress.py index 4d3d1c7..008240e 100644 --- a/scripts/update_implementation_progress.py +++ b/scripts/update_implementation_progress.py @@ -114,8 +114,8 @@ def update( directory = task_dir(repo, task_id) state = read_json(state_path(directory)) require_protocol_compatible(repo, state) - if state["status"] not in {"IMPLEMENTING", "IMPLEMENTED"}: - raise RuleFailure("Implementation progress can only update while IMPLEMENTING or IMPLEMENTED") + if state["status"] != "IMPLEMENTING": + raise RuleFailure("Implementation progress can only update while IMPLEMENTING") handoff, reference = validate_handoff(repo, root, directory, state) progress_path = resolve_repo_reference(repo, handoff["progress_json_path"]) existing = read_json(progress_path) if progress_path.exists() else None @@ -253,8 +253,6 @@ def update( raise RuleFailure( f"SET_PHASE cannot move {progress['phase']} progress to {phase}" ) - if phase in {"DOCUMENTING", "COMPLETED"} and state["status"] != "IMPLEMENTED": - raise RuleFailure(f"{phase} progress requires task state IMPLEMENTED") progress["phase"] = phase if phase == "FAILED": if not blocker or not user_action: diff --git a/tests/test_core.py b/tests/test_core.py index 6951550..2a6fa5a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -232,29 +232,11 @@ def dispatch_implementation(self) -> tuple[dict[str, object], dict[str, str]]: state = read_json(self.task / "state.json") existing = state["artifacts"].get("implementation_handoff") if existing is None: - result = build_implementation_handoff(self.repo, "TASK-0001") - handoff_path = Path(result["path"]) - transition( - self.repo, - "TASK-0001", - "DISPATCH_IMPLEMENTATION", - [ - "implementation_handoff=" - + handoff_path.relative_to(self.task).as_posix() - ], - None, - None, - None, - None, - None, - None, - ) - state = read_json(self.task / "state.json") - existing = state["artifacts"]["implementation_handoff"] + raise AssertionError("Implementation handoff was not registered at stage start") handoff_path = self.task / existing["path"] return read_json(handoff_path), existing - def enter_implementing(self) -> None: + def enter_planned(self) -> None: self.freeze_work_item() build_working_set(self.repo, "TASK-0001", True) transition( @@ -276,11 +258,19 @@ def enter_implementing(self) -> None: None, None, ) + + def enter_implementing(self) -> None: + self.enter_planned() + handoff = build_implementation_handoff(self.repo, "TASK-0001") + handoff_path = Path(handoff["path"]) transition( self.repo, "TASK-0001", "START_IMPLEMENTATION", - [], + [ + "implementation_handoff=" + + handoff_path.relative_to(self.task).as_posix() + ], None, None, None, @@ -289,6 +279,26 @@ def enter_implementing(self) -> None: None, ) + def test_start_implementation_atomically_registers_handoff(self) -> None: + """START_IMPLEMENTATION 同时校验并注册不可变 Implementer 输入。""" + self.enter_implementing() + state = read_json(self.task / "state.json") + self.assertEqual(state["status"], "IMPLEMENTING") + self.assertIn("implementation_handoff", state["artifacts"]) + with self.assertRaisesRegex(RuleFailure, "unknown workflow event"): + transition( + self.repo, + "TASK-0001", + "DISPATCH_IMPLEMENTATION", + [], + None, + None, + None, + None, + None, + None, + ) + def implementation_value( self, base: str, head: str, session_id: str ) -> dict[str, object]: @@ -340,6 +350,31 @@ def implementation_value( ) return value + def implementation_value_without_progress( + self, base: str, head: str, session_id: str + ) -> dict[str, object]: + handoff, reference = self.dispatch_implementation() + value = read_json(template_path(ROOT, "implementation")) + value.update( + { + "artifact_attempt": handoff["artifact_attempt"], + "implementer_session_id": session_id, + "implementation_handoff_path": reference["path"], + "implementation_handoff_sha256": reference["sha256"], + "subject_base_commit": base, + "subject_head_commit": head, + "subject_diff_hash": subject_diff_hash(self.repo, base, head), + "step_results": [ + { + "id": "STEP-001", + "status": "COMPLETED", + "result": "Implemented and checked the accepted change", + } + ], + } + ) + return value + def knowledge_value( self, attempt: int, base: str, head: str ) -> dict[str, object]: @@ -381,6 +416,124 @@ def start_review( state = read_json(self.task / "state.json") return read_json(handoff_path), state + def test_start_review_does_not_require_live_progress(self) -> None: + """耐久 Review 门禁不依赖 ignored 的本机进度快照。""" + self.enter_implementing() + base = run_git(self.repo, "rev-parse", "HEAD") + (self.repo / "subject.txt").write_text("final subject\n", encoding="utf-8") + run_git(self.repo, "add", "subject.txt") + run_git(self.repo, "commit", "-q", "-m", "final subject") + head = run_git(self.repo, "rev-parse", "HEAD") + + implementation_path = ( + self.task / "implementations" / "r001" / "attempt-001.json" + ) + write_json_atomic( + implementation_path, + self.implementation_value_without_progress(base, head, "impl-session"), + ) + knowledge_path = ( + self.task / "knowledge" / "r001" / "knowledge-delta-001.json" + ) + knowledge = self.knowledge_value(1, base, head) + knowledge["entries"][0].update( + { + "changed_paths": ["subject.txt"], + "evidence": "No project documentation impact", + } + ) + write_json_atomic(knowledge_path, knowledge) + + handoff_result = build_review_handoff( + self.repo, + "TASK-0001", + "impl-session", + "fresh_session", + implementation_path, + knowledge_path, + base, + head, + ) + handoff_path = Path(handoff_result["path"]) + self.assertFalse((self.task / "runtime" / "progress.json").exists()) + result = transition( + self.repo, + "TASK-0001", + "START_REVIEW", + [ + "implementation=implementations/r001/attempt-001.json", + "knowledge_delta=knowledge/r001/knowledge-delta-001.json", + "review_handoff=" + handoff_path.relative_to(self.task).as_posix(), + ], + None, + base, + head, + None, + None, + None, + ) + self.assertEqual(result["to"], "REVIEWING") + + def test_live_progress_can_document_inside_implementing(self) -> None: + """可选实时进度在同一个 IMPLEMENTING 节点内覆盖文档阶段。""" + self.enter_implementing() + title = "Polaris Implement · TASK-0001 · r001 · attempt 1" + update_implementation_progress( + self.repo, "TASK-0001", title, "Pending", "INITIALIZE" + ) + update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "DEFINE_STEPS", + defined_steps=[ + {"title": "Complete subject", "acceptance_ids": ["AC-01"]} + ], + ) + update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "START_STEP", + step_id="STEP-001", + ) + update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "COMPLETE_STEP", + step_id="STEP-001", + result="Subject and checks completed", + ) + update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "SET_PHASE", + phase="CHECKPOINTING", + ) + update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "SET_PHASE", + phase="DOCUMENTING", + ) + result = update_implementation_progress( + self.repo, + "TASK-0001", + title, + "impl-session", + "SET_PHASE", + phase="COMPLETED", + ) + self.assertEqual(result["value"]["phase"], "COMPLETED") + def review_value( self, handoff: dict[str, object], @@ -3039,10 +3192,10 @@ def test_implementation_steps_are_linear_append_only_and_acceptance_bound(self) ) self.assertEqual(progress["implementation_steps"][0]["status"], "SKIPPED") - def test_checkpoint_requires_terminal_steps_and_freezes_step_results(self) -> None: - """未完成步骤不能进入 checkpoint,最终 artifact 必须精确冻结步骤结果。""" + def test_optional_progress_requires_terminal_steps_before_checkpointing(self) -> None: + """可选实时进度仍拒绝在步骤未终结时进入 checkpoint。""" self.enter_implementing() - handoff, reference = self.dispatch_implementation() + self.dispatch_implementation() title = "Polaris Implement · TASK-0001 · r001 · attempt 1" update_implementation_progress( self.repo, "TASK-0001", title, "Pending", "INITIALIZE" @@ -3068,39 +3221,6 @@ def test_checkpoint_requires_terminal_steps_and_freezes_step_results(self) -> No self.repo, "TASK-0001", title, "impl-freeze-session", "SET_PHASE", phase="CHECKPOINTING", ) - base = run_git(self.repo, "rev-parse", "HEAD") - (self.repo / "freeze.txt").write_text("done\n", encoding="utf-8") - run_git(self.repo, "add", "freeze.txt") - run_git(self.repo, "commit", "-q", "-m", "freeze implementation") - head = run_git(self.repo, "rev-parse", "HEAD") - implementation = read_json(template_path(ROOT, "implementation")) - implementation.update({ - "artifact_attempt": handoff["artifact_attempt"], - "implementer_session_id": "impl-freeze-session", - "implementation_handoff_path": reference["path"], - "implementation_handoff_sha256": reference["sha256"], - "subject_base_commit": base, - "subject_head_commit": head, - "subject_diff_hash": subject_diff_hash(self.repo, base, head), - "step_results": [{"id": "STEP-001", "status": "COMPLETED", "result": "wrong"}], - }) - path = self.task / handoff["output_path"] - write_json_atomic(path, implementation) - with self.assertRaises(RuleFailure): - transition( - self.repo, "TASK-0001", "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-001.json"], - None, base, head, None, None, None, - ) - implementation["step_results"] = [ - {"id": "STEP-001", "status": "COMPLETED", "result": "Finished work"} - ] - write_json_atomic(path, implementation) - transition( - self.repo, "TASK-0001", "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-001.json"], - None, base, head, None, None, None, - ) update_implementation_progress( self.repo, "TASK-0001", title, "impl-freeze-session", "SET_PHASE", phase="DOCUMENTING", From 86fe68cf11dbea3a95b895fad3f30cba970f812c Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:50:25 +0800 Subject: [PATCH 05/17] feat: validate atomic closure projections --- scripts/internal/transition_gates.py | 29 ++- scripts/validate_task.py | 111 ++++++--- tests/test_core.py | 331 ++++++++++++++++++++++++++- 3 files changed, 420 insertions(+), 51 deletions(-) diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index c61abb8..893d34c 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy from pathlib import Path from typing import Any @@ -233,7 +234,11 @@ def check_gate( if review["verdict"] != "ACCEPT": raise RuleFailure("Validation requires all mandated Reviews to ACCEPT") validate_review(repo, root, directory, state, review, work_item) - elif gate == "validation_passed": + elif gate in {"validation_passed", "validation_passed_and_closure_ready"}: + if gate == "validation_passed" and state["rigor"] != "R2": + raise RuleFailure("R0/R1 must use PASS_AND_CLOSE") + if gate == "validation_passed_and_closure_ready" and state["rigor"] == "R2": + raise RuleFailure("R2 must pass Validation before final approval and closure") validation = load_validation(root, directory, state) if validation["verdict"] != "PASS": raise RuleFailure("Validation verdict must be PASS") @@ -247,6 +252,12 @@ def check_gate( raise RuleFailure("Validation must PASS every acceptance criterion") if validation["subject_diff_hash"] != state["subject"]["diff_hash"]: raise RuleFailure("Validation targets the wrong subject") + if gate == "validation_passed_and_closure_ready": + from validate_task import validate_projection + + candidate = copy.deepcopy(state) + candidate["status"] = "CLOSED" + validate_projection(repo, state["task_id"], candidate) elif gate in {"validation_failed_implementation", "validation_failed_plan"}: validation = load_validation(root, directory, state) if validation["verdict"] != "FAIL": @@ -260,14 +271,14 @@ def check_gate( ): raise RuleFailure("failed Validation targets the wrong revision or subject") elif gate == "closure_ready": - result = validate_json_file( - artifact_file(directory, state, "result"), - root / "schemas" / "result.schema.json", - ) - if result["subject_diff_hash"] != state["subject"]["diff_hash"]: - raise RuleFailure("Result targets the wrong subject") - if state["rigor"] == "R2": - artifact_file(directory, state, "final_approval") + if state["rigor"] != "R2": + raise RuleFailure("only R2 closes from VERIFIED") + artifact_file(directory, state, "final_approval") + from validate_task import validate_projection + + candidate = copy.deepcopy(state) + candidate["status"] = "CLOSED" + validate_projection(repo, state["task_id"], candidate) elif gate == "new_revision_ready": validate_json_file(work_item_path, root / "schemas" / "work-item.schema.json") elif gate == "blocker_recorded": diff --git a/scripts/validate_task.py b/scripts/validate_task.py index 2991166..686daab 100644 --- a/scripts/validate_task.py +++ b/scripts/validate_task.py @@ -38,10 +38,7 @@ "QUALIFIED", "PLANNED", "IMPLEMENTING", - "IMPLEMENTED", - "DOCS_SYNCED", "REVIEWING", - "REVIEWED", "VALIDATING", "VERIFIED", "CLOSED", @@ -52,6 +49,17 @@ def at_least(status: str, threshold: str) -> bool: return status in ORDER and ORDER.index(status) >= ORDER.index(threshold) +def authority_status(state: dict[str, Any]) -> str: + """Return the durable stage whose invariants a projection must satisfy.""" + status = state["status"] + if status == "BLOCKED": + blocked_from = state.get("blocked_from") + return blocked_from if blocked_from in ORDER else "DRAFT" + if status == "CANCELLED": + return "DRAFT" + return status + + def artifact_path(directory: Path, reference: Any) -> Path: if isinstance(reference, str): return directory / reference @@ -72,22 +80,17 @@ def require_artifact(state: dict[str, Any], directory: Path, name: str) -> Path: return path -def validate(repo: Path, task_id: str) -> dict[str, Any]: +def validate_projection( + repo: Path, task_id: str, state: dict[str, Any] +) -> dict[str, Any]: + """Validate a prospective authority projection without reading its event log.""" root = protocol_root(repo) directory = task_dir(repo, task_id) - state_file = task_state_path(directory) - state = validate_json_file(state_file, root / "schemas" / "task-state.schema.json") - event_schema = read_json(root / "schemas" / "event.schema.json") - for event in load_events_checked(events_path(directory)): - errors = validate_schema(event, event_schema) - if errors: - raise RuleFailure( - f"event {event.get('sequence')} failed schema validation:\n- " - + "\n- ".join(errors) - ) - rebuilt = rebuild_state_value(events_path(directory)) - if rebuilt != state: - raise RuleFailure("state.json does not match the state reconstructed from events.jsonl") + state_errors = validate_schema( + state, read_json(root / "schemas" / "task-state.schema.json") + ) + if state_errors: + raise RuleFailure("candidate task state failed schema validation:\n- " + "\n- ".join(state_errors)) version = (root / "VERSION").read_text(encoding="utf-8").strip() workflow = read_json(repo / ".polaris" / "workflow.json") @@ -122,7 +125,8 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: directory / prior_reference["path"], root / "schemas" / "review.schema.json" ) - status = state["status"] + projected_status = state["status"] + status = authority_status(state) if at_least(status, "PLANNED"): require_artifact(state, directory, "plan") working_set_path = require_artifact(state, directory, "working_set") @@ -130,9 +134,11 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: if "plan_decisions" in state["artifacts"]: require_artifact(state, directory, "plan_decisions") validate_plan_decisions(repo, root, directory, state, True) - if status == "IMPLEMENTING" and "implementation_handoff" in state["artifacts"]: + if at_least(status, "IMPLEMENTING"): validate_implementation_handoff(repo, root, directory, state) - if at_least(status, "IMPLEMENTED"): + if state["rigor"] == "R2": + require_artifact(state, directory, "pre_approval") + if at_least(status, "REVIEWING"): handoff, handoff_reference = validate_implementation_handoff( repo, root, directory, state ) @@ -162,6 +168,9 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: != handoff_reference["path"] or implementation["implementation_handoff_sha256"] != handoff_reference["sha256"] + or implementation["subject_base_commit"] != subject["base_commit"] + or implementation["subject_head_commit"] != subject["head_commit"] + or implementation["subject_diff_hash"] != subject["diff_hash"] ) if identity_mismatch: raise RuleFailure("Implementation artifact targets the wrong revision or subject") @@ -180,15 +189,8 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: != implementation["subject_diff_hash"] ): raise RuleFailure("Implementation Code Intelligence record targets the wrong subject") - if implementation["subject_base_commit"] != subject["base_commit"]: - raise RuleFailure("Implementation artifact has the wrong subject base") - if status == "IMPLEMENTED" and ( - implementation["subject_head_commit"] != subject["head_commit"] - or implementation["subject_diff_hash"] != subject["diff_hash"] - ): - raise RuleFailure("Implementation artifact targets the wrong implementation subject") validate_review_response(root, directory, state, implementation) - if at_least(status, "DOCS_SYNCED"): + knowledge_path = require_artifact(state, directory, "knowledge_delta") knowledge = validate_json_file( knowledge_path, root / "schemas" / "knowledge-delta.schema.json" @@ -220,9 +222,9 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: != knowledge["subject_diff_hash"] ): raise RuleFailure("Knowledge Delta Code Intelligence record targets the wrong subject") - if status == "REVIEWING" or at_least(status, "REVIEWED"): + validate_handoff(repo, root, directory, state) - if at_least(status, "REVIEWED"): + if at_least(status, "VALIDATING"): review_names = ["review"] if any( work_item["risk_flags"].get(flag, False) @@ -234,7 +236,7 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: review_path = require_artifact(state, directory, name) review = validate_json_file(review_path, root / "schemas" / "review.schema.json") if review["verdict"] != "ACCEPT": - raise RuleFailure("REVIEWED requires every mandated Review to ACCEPT") + raise RuleFailure("VALIDATING requires every mandated Review to ACCEPT") validate_review(repo, root, directory, state, review, work_item) if review["reviewer_session_id"] in reviewer_ids: raise RuleFailure("mandated Reviews must use distinct Reviewer sessions") @@ -247,27 +249,60 @@ def validate(repo: Path, task_id: str) -> dict[str, Any]: if validation["verdict"] != "PASS": raise RuleFailure("VERIFIED requires a PASS Validation") expected = {item["id"] for item in work_item["acceptance"]} - actual = { + actual = [ item["acceptance_id"] for item in validation["acceptance_results"] if item["result"] == "PASS" - } - if actual != expected: + ] + if len(actual) != len(expected) or set(actual) != expected: raise RuleFailure("Validation does not PASS every acceptance criterion exactly once") - if validation["subject_diff_hash"] != state["subject"]["diff_hash"]: - raise RuleFailure("Validation targets the wrong subject") + if ( + validation["task_id"] != task_id + or validation["work_item_revision"] != state["current_revision"] + or validation["artifact_attempt"] != implementation["artifact_attempt"] + or validation["subject_base_commit"] != state["subject"]["base_commit"] + or validation["subject_head_commit"] != state["subject"]["head_commit"] + or validation["subject_diff_hash"] != state["subject"]["diff_hash"] + ): + raise RuleFailure("Validation targets the wrong revision or subject") if status == "CLOSED": result_path = require_artifact(state, directory, "result") result = validate_json_file(result_path, root / "schemas" / "result.schema.json") if ( - result["work_item_revision"] != state["current_revision"] + result["task_id"] != task_id + or result["work_item_revision"] != state["current_revision"] + or result["subject_base_commit"] != state["subject"]["base_commit"] + or result["subject_head_commit"] != state["subject"]["head_commit"] or result["subject_diff_hash"] != state["subject"]["diff_hash"] ): raise RuleFailure("Result targets the wrong revision or subject") if state["rigor"] == "R2": require_artifact(state, directory, "final_approval") - return {"message": f"{task_id} is valid at {status}", "task": task_id, "state": status} + return { + "message": f"{task_id} is valid at {projected_status}", + "task": task_id, + "state": projected_status, + } + + +def validate(repo: Path, task_id: str) -> dict[str, Any]: + root = protocol_root(repo) + directory = task_dir(repo, task_id) + state_file = task_state_path(directory) + state = validate_json_file(state_file, root / "schemas" / "task-state.schema.json") + event_schema = read_json(root / "schemas" / "event.schema.json") + for event in load_events_checked(events_path(directory)): + errors = validate_schema(event, event_schema) + if errors: + raise RuleFailure( + f"event {event.get('sequence')} failed schema validation:\n- " + + "\n- ".join(errors) + ) + rebuilt = rebuild_state_value(events_path(directory)) + if rebuilt != state: + raise RuleFailure("state.json does not match the state reconstructed from events.jsonl") + return validate_projection(repo, task_id, state) def main() -> int: diff --git a/tests/test_core.py b/tests/test_core.py index 2a6fa5a..db48204 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -206,6 +206,28 @@ def freeze_work_item(self) -> None: value["review_dispatch"]["authorized"] = True write_json_atomic(path, value) + def set_task_rigor(self, rigor: str) -> None: + """Keep the initial Work Item, state, and event projection rigor-aligned.""" + work_item_path = self.task / "revisions" / "work-item-r001.json" + work_item = read_json(work_item_path) + work_item["rigor"] = rigor + write_json_atomic(work_item_path, work_item) + state_path = self.task / "state.json" + state = read_json(state_path) + state["rigor"] = rigor + write_json_atomic(state_path, state) + event_path = self.task / "events.jsonl" + events = read_jsonl(event_path) + for event in events: + event["rigor"] = rigor + write_text_atomic( + event_path, + "".join( + json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n" + for event in events + ), + ) + def set_protocol_version(self, version: str) -> None: """Rewrite the minimal test fixture as an older internally consistent project.""" project_path = self.repo / ".polaris" / "project.json" @@ -263,14 +285,19 @@ def enter_implementing(self) -> None: self.enter_planned() handoff = build_implementation_handoff(self.repo, "TASK-0001") handoff_path = Path(handoff["path"]) + artifacts = [ + "implementation_handoff=" + handoff_path.relative_to(self.task).as_posix() + ] + if read_json(self.task / "state.json")["rigor"] == "R2": + approval_path = self.task / "approvals" / "r001" / "pre-approval.txt" + approval_path.parent.mkdir(parents=True, exist_ok=True) + approval_path.write_text("Approved before implementation\n", encoding="utf-8") + artifacts.append("pre_approval=approvals/r001/pre-approval.txt") transition( self.repo, "TASK-0001", "START_IMPLEMENTATION", - [ - "implementation_handoff=" - + handoff_path.relative_to(self.task).as_posix() - ], + artifacts, None, None, None, @@ -389,6 +416,87 @@ def knowledge_value( ) return value + def enter_reviewing_without_progress( + self, + ) -> tuple[dict[str, object], dict[str, object]]: + """Build and register the durable review package without runtime telemetry.""" + self.enter_implementing() + base = run_git(self.repo, "rev-parse", "HEAD") + (self.repo / "subject.txt").write_text("final subject\n", encoding="utf-8") + run_git(self.repo, "add", "subject.txt") + run_git(self.repo, "commit", "-q", "-m", "final subject") + head = run_git(self.repo, "rev-parse", "HEAD") + + implementation_path = ( + self.task / "implementations" / "r001" / "attempt-001.json" + ) + write_json_atomic( + implementation_path, + self.implementation_value_without_progress(base, head, "impl-session"), + ) + knowledge_path = ( + self.task / "knowledge" / "r001" / "knowledge-delta-001.json" + ) + knowledge = self.knowledge_value(1, base, head) + knowledge["entries"][0].update( + { + "changed_paths": ["subject.txt"], + "evidence": "No project documentation impact", + } + ) + write_json_atomic(knowledge_path, knowledge) + + handoff_result = build_review_handoff( + self.repo, + "TASK-0001", + "impl-session", + "fresh_session", + implementation_path, + knowledge_path, + base, + head, + ) + handoff_path = Path(handoff_result["path"]) + transition( + self.repo, + "TASK-0001", + "START_REVIEW", + [ + "implementation=implementations/r001/attempt-001.json", + "knowledge_delta=knowledge/r001/knowledge-delta-001.json", + "review_handoff=" + handoff_path.relative_to(self.task).as_posix(), + ], + None, + base, + head, + None, + None, + None, + ) + return read_json(handoff_path), read_json(self.task / "state.json") + + def accept_current_review( + self, handoff: dict[str, object], state: dict[str, object] + ) -> dict[str, object]: + review_path = self.task / "reviews" / "r001" / "review-001.json" + write_json_atomic( + review_path, + self.review_value(handoff, state, "review-session", "ACCEPT"), + ) + transition( + self.repo, + "TASK-0001", + "ACCEPT_REVIEW", + ["review=reviews/r001/review-001.json"], + None, + None, + None, + None, + None, + None, + ) + return read_json(self.task / "state.json") + def start_review( self, implementer_session_id: str = "impl-session", @@ -569,6 +677,221 @@ def review_value( ) return review + def test_review_acceptance_enters_validation_directly(self) -> None: + """ACCEPT_REVIEW 直接进入 VALIDATING,不保留空壳 REVIEWED 节点。""" + handoff, state = self.enter_reviewing_without_progress() + accepted = self.accept_current_review(handoff, state) + self.assertEqual(accepted["status"], "VALIDATING") + with self.assertRaisesRegex(RuleFailure, "unknown workflow event"): + transition( + self.repo, + "TASK-0001", + "START_VALIDATION", + [], + None, + None, + None, + None, + None, + None, + ) + + def test_r1_pass_and_close_validates_candidate_projection(self) -> None: + """R1 闭环先完整校验候选 CLOSED 投影,失败时不得写事件或状态。""" + handoff, state = self.enter_reviewing_without_progress() + validating = self.accept_current_review(handoff, state) + subject = validating["subject"] + + validation_path = self.task / "validations" / "r001" / "validation-001.json" + validation = read_json(template_path(ROOT, "validation")) + validation.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "validated_at": "2026-08-18T00:00:00Z", + "verdict": "PASS", + "acceptance_results": [ + { + "acceptance_id": "AC-01", + "command_or_check": "state validation", + "cwd": ".", + "environment_summary": "test", + "started_at": "2026-08-18T00:00:00Z", + "exit_code": 0, + "result": "PASS", + "output_path_or_hash": "inline:test", + } + ], + } + ) + write_json_atomic(validation_path, validation) + result_path = self.task / "results" / "r001" / "result-001.json" + result = read_json(template_path(ROOT, "result")) + result.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "summary": "Validated smoke task", + } + ) + write_json_atomic(result_path, result) + + implementation_path = ( + self.task / "implementations" / "r001" / "attempt-001.json" + ) + implementation = read_json(implementation_path) + original_result = implementation["step_results"][0]["result"] + implementation["step_results"][0]["result"] = "tampered after review" + write_json_atomic(implementation_path, implementation) + sequence = validating["sequence"] + with self.assertRaisesRegex(RuleFailure, "changed after.*registered"): + transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + unchanged = read_json(self.task / "state.json") + self.assertEqual(unchanged["status"], "VALIDATING") + self.assertEqual(unchanged["sequence"], sequence) + + implementation["step_results"][0]["result"] = original_result + write_json_atomic(implementation_path, implementation) + closed = transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual(closed["to"], "CLOSED") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "CLOSED") + + def test_r2_keeps_verified_and_final_approval_gate(self) -> None: + """R2 必须先到 VERIFIED,再由最终人类批准关闭。""" + self.set_task_rigor("R2") + handoff, state = self.enter_reviewing_without_progress() + validating = self.accept_current_review(handoff, state) + subject = validating["subject"] + + validation_path = self.task / "validations" / "r001" / "validation-001.json" + validation = read_json(template_path(ROOT, "validation")) + validation.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "validated_at": "2026-08-18T00:00:00Z", + "verdict": "PASS", + "acceptance_results": [ + { + "acceptance_id": "AC-01", + "command_or_check": "state validation", + "cwd": ".", + "environment_summary": "test", + "started_at": "2026-08-18T00:00:00Z", + "exit_code": 0, + "result": "PASS", + "output_path_or_hash": "inline:test", + } + ], + } + ) + write_json_atomic(validation_path, validation) + result_path = self.task / "results" / "r001" / "result-001.json" + result = read_json(template_path(ROOT, "result")) + result.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "summary": "Validated R2 smoke task", + } + ) + write_json_atomic(result_path, result) + + with self.assertRaisesRegex(RuleFailure, "R2 must pass Validation"): + transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + verified = transition( + self.repo, + "TASK-0001", + "PASS_VALIDATION", + ["validation=validations/r001/validation-001.json"], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual(verified["to"], "VERIFIED") + with self.assertRaisesRegex(RuleFailure, "final_approval"): + transition( + self.repo, + "TASK-0001", + "CLOSE", + ["result=results/r001/result-001.json"], + None, + None, + None, + None, + None, + None, + ) + + final_approval = self.task / "approvals" / "r001" / "final-approval.txt" + final_approval.write_text("Approved for closure\n", encoding="utf-8") + closed = transition( + self.repo, + "TASK-0001", + "CLOSE", + [ + "result=results/r001/result-001.json", + "final_approval=approvals/r001/final-approval.txt", + ], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual(closed["to"], "CLOSED") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "CLOSED") + def finish_and_reject_attempt( self, attempt: int, From b1ae67591d0c8059f01418c1972d2d72fb59a118 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:55:30 +0800 Subject: [PATCH 06/17] feat: migrate frozen projects to workflow 0.1.3 --- VERSION | 2 +- schemas/event.schema.json | 6 + schemas/migration-protocol.schema.json | 14 +- schemas/migration-record.schema.json | 14 +- scripts/internal/migration_protocol.py | 108 +++++++++++++-- templates/project.json | 2 +- templates/task-sources/state.json | 2 +- templates/task/state.json | 2 +- tests/test_codegraph.py | 47 +++++-- tests/test_core.py | 178 ++++++++++++++++++++++--- workflow/migrations.json | 11 +- 11 files changed, 341 insertions(+), 45 deletions(-) diff --git a/VERSION b/VERSION index d8a023e..baa9837 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.19 +0.1.20 diff --git a/schemas/event.schema.json b/schemas/event.schema.json index 9bdb203..844594a 100644 --- a/schemas/event.schema.json +++ b/schemas/event.schema.json @@ -35,6 +35,12 @@ "migration_id": { "type": "string" }, + "previous_polaris_version": { + "type": "string" + }, + "previous_workflow_version": { + "type": "string" + }, "from": { "type": [ "string", diff --git a/schemas/migration-protocol.schema.json b/schemas/migration-protocol.schema.json index 619dd35..d425d2e 100644 --- a/schemas/migration-protocol.schema.json +++ b/schemas/migration-protocol.schema.json @@ -9,7 +9,7 @@ "additionalProperties": false, "properties": { "protocol_version": { - "const": 1 + "const": 2 }, "steps": { "type": "array", @@ -48,10 +48,18 @@ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, "project_strategy": { - "const": "replace_version" + "type": "string", + "enum": [ + "replace_version", + "replace_version_and_workflow" + ] }, "task_strategy": { - "const": "append_version_event" + "type": "string", + "enum": [ + "append_version_event", + "append_mapped_workflow_event" + ] } } } diff --git a/schemas/migration-record.schema.json b/schemas/migration-record.schema.json index 7c96b35..0b39a29 100644 --- a/schemas/migration-record.schema.json +++ b/schemas/migration-record.schema.json @@ -17,7 +17,11 @@ "additionalProperties": false, "properties": { "record_version": { - "const": 1 + "type": "integer", + "enum": [ + 1, + 2 + ] }, "migration_id": { "type": "string", @@ -78,6 +82,14 @@ "migration_sequence": { "type": "integer", "minimum": 1 + }, + "source_status": { + "type": "string", + "minLength": 1 + }, + "target_status": { + "type": "string", + "minLength": 1 } } } diff --git a/scripts/internal/migration_protocol.py b/scripts/internal/migration_protocol.py index 52c3af0..8800e77 100644 --- a/scripts/internal/migration_protocol.py +++ b/scripts/internal/migration_protocol.py @@ -39,6 +39,45 @@ MIGRATIONS_ROOT = Path(".polaris/migrations") +LEGACY_STATUS_MAP = { + "DRAFT": "DRAFT", + "QUALIFIED": "QUALIFIED", + "PLANNED": "PLANNED", + "IMPLEMENTED": "IMPLEMENTING", + "DOCS_SYNCED": "IMPLEMENTING", + "REVIEWING": "REVIEWING", + "REVIEWED": "VALIDATING", + "VALIDATING": "VALIDATING", + "VERIFIED": "VERIFIED", + "CLOSED": "CLOSED", + "CANCELLED": "CANCELLED", +} + + +def _map_legacy_stage(status: str, artifacts: dict[str, Any]) -> str: + if status == "IMPLEMENTING": + return "IMPLEMENTING" if "implementation_handoff" in artifacts else "PLANNED" + mapped = LEGACY_STATUS_MAP.get(status) + if mapped is None: + raise RuleFailure(f"cannot map legacy workflow state: {status}") + return mapped + + +def map_migrated_status(state: dict[str, Any]) -> tuple[str, str | None]: + """Map a workflow 0.1.2 task projection into workflow 0.1.3.""" + artifacts = state.get("artifacts") + if not isinstance(artifacts, dict): + raise RuleFailure("legacy task artifacts must be an object") + status = state.get("status") + if status == "BLOCKED": + blocked_from = state.get("blocked_from") + if not isinstance(blocked_from, str): + raise RuleFailure("legacy BLOCKED task has no blocked_from state") + return "BLOCKED", _map_legacy_stage(blocked_from, artifacts) + if not isinstance(status, str): + raise RuleFailure("legacy task status must be a string") + return _map_legacy_stage(status, artifacts), None + def load_migration_protocol(protocol_root: Path) -> dict[str, Any]: protocol = validate_json_file( @@ -57,9 +96,23 @@ def load_migration_protocol(protocol_root: Path) -> dict[str, Any]: for step in protocol["steps"]: if step["from_polaris_version"] == step["to_polaris_version"]: raise RuleFailure(f"migration route does not advance: {step['migration_id']}") - if step["from_workflow_version"] != step["to_workflow_version"]: + changes_workflow = ( + step["from_workflow_version"] != step["to_workflow_version"] + ) + if changes_workflow and ( + step["project_strategy"] != "replace_version_and_workflow" + or step["task_strategy"] != "append_mapped_workflow_event" + ): + raise RuleFailure( + "workflow migration requires replacement and mapped-event strategies: " + f"{step['migration_id']}" + ) + if not changes_workflow and ( + step["project_strategy"] != "replace_version" + or step["task_strategy"] != "append_version_event" + ): raise RuleFailure( - "v1 migration protocol cannot change workflow versions: " + "version-only migration requires version-only strategies: " f"{step['migration_id']}" ) return protocol @@ -95,6 +148,15 @@ def load_migration_records(repo: Path, protocol_root: Path) -> list[dict[str, An f"migration record contains a non-adjacent task sequence: " f"{record['migration_id']}" ) + if record["record_version"] == 2 and any( + not isinstance(item.get("source_status"), str) + or not isinstance(item.get("target_status"), str) + for item in record["tasks"] + ): + raise RuleFailure( + f"v2 migration record lacks task status mapping: " + f"{record['migration_id']}" + ) if record["status"] == "COMPLETED" and record["completed_at"] is None: raise RuleFailure( f"completed migration lacks completion time: {record['migration_id']}" @@ -126,12 +188,15 @@ def validate_completed_migrations(repo: Path, protocol_root: Path) -> None: ) event = events[sequence] prior = events[sequence - 1] + expected_from = item.get("source_status", event.get("to")) + expected_to = item.get("target_status", expected_from) if ( event.get("event") != "MIGRATE_POLARIS" or event.get("migration_id") != record["migration_id"] or event.get("polaris_version") != record["to_polaris_version"] or event.get("workflow_version") != record["to_workflow_version"] - or event.get("from") != event.get("to") + or event.get("from") != expected_from + or event.get("to") != expected_to or prior.get("polaris_version") != record["from_polaris_version"] or prior.get("workflow_version") @@ -146,23 +211,29 @@ def validate_completed_migrations(repo: Path, protocol_root: Path) -> None: def _migration_event( state: dict[str, Any], step: dict[str, Any], timestamp: str ) -> dict[str, Any]: + target_status = state["status"] + target_blocked_from = state.get("blocked_from") + if step["task_strategy"] == "append_mapped_workflow_event": + target_status, target_blocked_from = map_migrated_status(state) return { "sequence": state["sequence"] + 1, "timestamp": timestamp, "event": "MIGRATE_POLARIS", "gate": "explicit_protocol_migration", "from": state["status"], - "to": state["status"], + "to": target_status, "task_id": state["task_id"], "polaris_version": step["to_polaris_version"], "workflow_version": step["to_workflow_version"], "current_revision": state["current_revision"], "rigor": state["rigor"], - "blocked_from": state.get("blocked_from"), + "blocked_from": target_blocked_from, "blocker": state.get("blocker"), "artifacts": state["artifacts"], "subject": state.get("subject"), "migration_id": step["migration_id"], + "previous_polaris_version": step["from_polaris_version"], + "previous_workflow_version": step["from_workflow_version"], } @@ -266,13 +337,17 @@ def _new_record( "task_id": task_id, "source_sequence": state["sequence"], "migration_sequence": state["sequence"] + 1, + "source_status": state["status"], + "target_status": map_migrated_status(state)[0] + if step["task_strategy"] == "append_mapped_workflow_event" + else state["status"], } ) retired_code_intelligence_records.extend( _retired_code_intelligence_records(repo, task_id, directory, protocol_root) ) return { - "record_version": 1, + "record_version": 2, "migration_id": step["migration_id"], "from_polaris_version": step["from_polaris_version"], "to_polaris_version": step["to_polaris_version"], @@ -310,9 +385,12 @@ def migrate_project(repo: Path, protocol_root: Path) -> dict[str, Any]: step["to_polaris_version"], }: raise RuleFailure("project version is outside the in-progress migration") - if project["workflow_version"] != step["from_workflow_version"]: + allowed_workflow_versions = {step["from_workflow_version"]} + if step["project_strategy"] == "replace_version_and_workflow": + allowed_workflow_versions.add(step["to_workflow_version"]) + if project["workflow_version"] not in allowed_workflow_versions: raise RuleFailure("project workflow version changed during migration") - if workflow["workflow_version"] != step["from_workflow_version"]: + if workflow["workflow_version"] not in allowed_workflow_versions: raise RuleFailure("frozen workflow changed during migration") elif project["polaris_version"] == target_version: return { @@ -371,6 +449,15 @@ def migrate_project(repo: Path, protocol_root: Path) -> dict[str, Any]: if incomplete is None: write_json_atomic(record_path, record) + if step["project_strategy"] == "replace_version_and_workflow": + target_workflow = validate_json_file( + protocol_root / "workflow" / "default-workflow.json", + protocol_root / "schemas" / "workflow.schema.json", + ) + if target_workflow["workflow_version"] != step["to_workflow_version"]: + raise RuleFailure("vendored workflow does not match migration target") + write_json_atomic(repo / ".polaris" / "workflow.json", target_workflow) + for item in record["tasks"]: directory = task_dir(repo, item["task_id"]) ledger_path = events_path(directory) @@ -396,10 +483,15 @@ def migrate_project(repo: Path, protocol_root: Path) -> dict[str, Any]: f"task advanced during migration: {item['task_id']}" ) event = events[sequence] + expected_from = item.get("source_status", event.get("to")) + expected_to = item.get("target_status", expected_from) if ( event.get("event") != "MIGRATE_POLARIS" or event.get("migration_id") != record["migration_id"] or event.get("polaris_version") != step["to_polaris_version"] + or event.get("workflow_version") != step["to_workflow_version"] + or event.get("from") != expected_from + or event.get("to") != expected_to ): raise RuleFailure( f"task has an unexpected migration event: {item['task_id']}" diff --git a/templates/project.json b/templates/project.json index c30e407..4bfc7aa 100644 --- a/templates/project.json +++ b/templates/project.json @@ -1,6 +1,6 @@ { "project_id": "PROJECT_ID", - "polaris_version": "0.1.19", + "polaris_version": "0.1.20", "workflow_version": "0.1.3", "active_tasks": [] } diff --git a/templates/task-sources/state.json b/templates/task-sources/state.json index 024913f..a623be4 100644 --- a/templates/task-sources/state.json +++ b/templates/task-sources/state.json @@ -1,6 +1,6 @@ { "task_id": "TASK-0001", - "polaris_version": "0.1.19", + "polaris_version": "0.1.20", "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", diff --git a/templates/task/state.json b/templates/task/state.json index 024913f..a623be4 100644 --- a/templates/task/state.json +++ b/templates/task/state.json @@ -1,6 +1,6 @@ { "task_id": "TASK-0001", - "polaris_version": "0.1.19", + "polaris_version": "0.1.20", "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", diff --git a/tests/test_codegraph.py b/tests/test_codegraph.py index 9c79112..773e385 100644 --- a/tests/test_codegraph.py +++ b/tests/test_codegraph.py @@ -266,6 +266,29 @@ def set_protocol_version(self, version: str) -> None: "".join(json.dumps(event, separators=(",", ":")) + "\n" for event in events), ) + def set_workflow_version(self, version: str) -> None: + project_path = self.repo / ".polaris/project.json" + project = json.loads(project_path.read_text(encoding="utf-8")) + project["workflow_version"] = version + write_json_atomic(project_path, project) + workflow_path = self.repo / ".polaris/workflow.json" + workflow = json.loads(workflow_path.read_text(encoding="utf-8")) + workflow["schema_version"] = version + workflow["workflow_version"] = version + write_json_atomic(workflow_path, workflow) + state_path = self.repo / ".polaris/tasks/TASK-0001/state.json" + state = json.loads(state_path.read_text(encoding="utf-8")) + state["workflow_version"] = version + write_json_atomic(state_path, state) + event_path = self.repo / ".polaris/tasks/TASK-0001/events.jsonl" + events = [json.loads(line) for line in event_path.read_text(encoding="utf-8").splitlines()] + for event in events: + event["workflow_version"] = version + write_text_atomic( + event_path, + "".join(json.dumps(event, separators=(",", ":")) + "\n" for event in events), + ) + def test_legacy_v1_records_remain_readable_but_cannot_be_written(self) -> None: self.initialize_task() protocol = importlib.import_module("internal.code_intelligence_protocol") @@ -309,9 +332,10 @@ def test_legacy_v1_records_remain_readable_but_cannot_be_written(self) -> None: record(self.repo, "TASK-0001", value, ROOT) def test_migration_retires_v1_records_without_rewriting_them(self) -> None: - """0.1.19 inventories frozen v1 evidence while leaving its bytes intact.""" + """0.1.20 inventories frozen v1 evidence while leaving its bytes intact.""" self.initialize_task() - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") legacy_path = ( self.repo / ".polaris/tasks/TASK-0001/code-intelligence/r001/planning.json" @@ -348,16 +372,16 @@ def test_migration_retires_v1_records_without_rewriting_them(self) -> None: result = migrate_project(self.repo) - self.assertEqual(result["from"], "0.1.18") - self.assertEqual(result["to"], "0.1.19") + self.assertEqual(result["from"], "0.1.19") + self.assertEqual(result["to"], "0.1.20") self.assertEqual( json.loads((self.repo / ".polaris/project.json").read_text(encoding="utf-8"))["workflow_version"], - "0.1.2", + "0.1.3", ) migration = json.loads( ( self.repo - / ".polaris/migrations/MIG-0.1.18-to-0.1.19.json" + / ".polaris/migrations/MIG-0.1.19-to-0.1.20.json" ).read_text(encoding="utf-8") ) self.assertEqual( @@ -379,7 +403,8 @@ def test_migration_retires_v1_records_without_rewriting_them(self) -> None: def test_migration_rejects_noncanonical_v2_record_paths(self) -> None: """Migration scans only the canonical Code Intelligence record layout.""" self.initialize_task() - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") noncanonical = ( self.repo / ".polaris/tasks/TASK-0001/code-intelligence/r001/not-a-stage.json" @@ -405,7 +430,8 @@ def test_migration_inventories_v1_records_from_prior_revisions(self) -> None: events_path, "".join(json.dumps(event, separators=(",", ":")) + "\n" for event in events), ) - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") legacy_path = ( self.repo / ".polaris/tasks/TASK-0001/code-intelligence/r001/planning.json" @@ -451,7 +477,7 @@ def test_migration_inventories_v1_records_from_prior_revisions(self) -> None: migration = json.loads( ( self.repo - / ".polaris/migrations/MIG-0.1.18-to-0.1.19.json" + / ".polaris/migrations/MIG-0.1.19-to-0.1.20.json" ).read_text(encoding="utf-8") ) self.assertEqual( @@ -467,7 +493,8 @@ def test_migration_inventories_v1_records_from_prior_revisions(self) -> None: def test_migration_rejects_a_dangling_code_intelligence_symlink(self) -> None: """A dangling record-root symlink is rejected rather than treated as absent.""" self.initialize_task() - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") records_root = self.repo / ".polaris/tasks/TASK-0001/code-intelligence" shutil.rmtree(records_root) records_root.symlink_to(self.repo / "missing-code-intelligence") diff --git a/tests/test_core.py b/tests/test_core.py index db48204..2316f32 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -43,6 +43,7 @@ TEXT_HASH_MODE, managed_file_sha256, ) +from internal.migration_protocol import map_migrated_status # noqa: E402 from build_working_set import build as build_working_set # noqa: E402 from build_implementation_handoff import build as build_implementation_handoff # noqa: E402 from build_review_handoff import build as build_review_handoff # noqa: E402 @@ -250,6 +251,33 @@ def set_protocol_version(self, version: str) -> None: ), ) + def set_workflow_version(self, version: str) -> None: + """Rewrite the project, frozen workflow, and task projection workflow version.""" + project_path = self.repo / ".polaris" / "project.json" + project = read_json(project_path) + project["workflow_version"] = version + write_json_atomic(project_path, project) + workflow_path = self.repo / ".polaris" / "workflow.json" + workflow = read_json(workflow_path) + workflow["schema_version"] = version + workflow["workflow_version"] = version + write_json_atomic(workflow_path, workflow) + state_path = self.task / "state.json" + state = read_json(state_path) + state["workflow_version"] = version + write_json_atomic(state_path, state) + event_path = self.task / "events.jsonl" + events = read_jsonl(event_path) + for event in events: + event["workflow_version"] = version + write_text_atomic( + event_path, + "".join( + json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n" + for event in events + ), + ) + def dispatch_implementation(self) -> tuple[dict[str, object], dict[str, str]]: state = read_json(self.task / "state.json") existing = state["artifacts"].get("implementation_handoff") @@ -1893,25 +1921,27 @@ def test_every_normal_writer_uses_the_protocol_compatibility_gate(self) -> None: def test_explicit_migration_appends_task_event_and_records_completion(self) -> None: """相邻版本迁移追加审计事件,不改写任务历史,并留下完成记录。""" - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") vendor(ROOT, self.repo, False) result = migrate_project(self.repo) - self.assertEqual(result["from"], "0.1.18") - self.assertEqual(result["to"], "0.1.19") + self.assertEqual(result["from"], "0.1.19") + self.assertEqual(result["to"], "0.1.20") self.assertEqual(result["migrated_tasks"], 1) events = read_jsonl(self.task / "events.jsonl") self.assertEqual(len(events), 2) - self.assertEqual(events[0]["polaris_version"], "0.1.18") + self.assertEqual(events[0]["polaris_version"], "0.1.19") self.assertEqual(events[1]["event"], "MIGRATE_POLARIS") self.assertEqual(events[1]["from"], events[1]["to"]) - self.assertEqual(events[1]["polaris_version"], "0.1.19") + self.assertEqual(events[1]["polaris_version"], "0.1.20") + self.assertEqual(events[1]["workflow_version"], "0.1.3") record = read_json( self.repo / ".polaris" / "migrations" - / "MIG-0.1.18-to-0.1.19.json" + / "MIG-0.1.19-to-0.1.20.json" ) self.assertEqual(record["status"], "COMPLETED") self.assertIsNotNone(record["completed_at"]) @@ -1921,19 +1951,126 @@ def test_explicit_migration_appends_task_event_and_records_completion(self) -> N ) self.assertEqual(validate_project(self.repo)["active_tasks"], 1) + def test_workflow_migration_maps_every_legacy_state(self) -> None: + """0.1.2 状态按治理含义映射,BLOCKED 的恢复目标同步转换。""" + cases = { + "DRAFT": "DRAFT", + "QUALIFIED": "QUALIFIED", + "PLANNED": "PLANNED", + "IMPLEMENTED": "IMPLEMENTING", + "DOCS_SYNCED": "IMPLEMENTING", + "REVIEWING": "REVIEWING", + "REVIEWED": "VALIDATING", + "VALIDATING": "VALIDATING", + "VERIFIED": "VERIFIED", + "CLOSED": "CLOSED", + "CANCELLED": "CANCELLED", + } + for legacy, expected in cases.items(): + with self.subTest(legacy=legacy): + self.assertEqual( + map_migrated_status( + {"status": legacy, "blocked_from": None, "artifacts": {}} + ), + (expected, None), + ) + self.assertEqual( + map_migrated_status( + {"status": "IMPLEMENTING", "blocked_from": None, "artifacts": {}} + ), + ("PLANNED", None), + ) + self.assertEqual( + map_migrated_status( + { + "status": "IMPLEMENTING", + "blocked_from": None, + "artifacts": {"implementation_handoff": {"path": "handoff.json"}}, + } + ), + ("IMPLEMENTING", None), + ) + self.assertEqual( + map_migrated_status( + { + "status": "BLOCKED", + "blocked_from": "DOCS_SYNCED", + "artifacts": {}, + } + ), + ("BLOCKED", "IMPLEMENTING"), + ) + + def test_migration_replaces_frozen_workflow_and_maps_tasks(self) -> None: + """0.1.19 迁移替换冻结 workflow,并用单个事件映射旧状态。""" + self.enter_implementing() + self.set_protocol_version("0.1.19") + project_path = self.repo / ".polaris" / "project.json" + project = read_json(project_path) + project["workflow_version"] = "0.1.2" + write_json_atomic(project_path, project) + workflow_path = self.repo / ".polaris" / "workflow.json" + workflow = read_json(workflow_path) + workflow["schema_version"] = "0.1.2" + workflow["workflow_version"] = "0.1.2" + write_json_atomic(workflow_path, workflow) + state_path = self.task / "state.json" + state = read_json(state_path) + state["status"] = "DOCS_SYNCED" + state["workflow_version"] = "0.1.2" + write_json_atomic(state_path, state) + event_path = self.task / "events.jsonl" + events = read_jsonl(event_path) + for event in events: + event["workflow_version"] = "0.1.2" + events[-1]["to"] = "DOCS_SYNCED" + write_text_atomic( + event_path, + "".join( + json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n" + for event in events + ), + ) + vendor(ROOT, self.repo, False) + + result = migrate_project(self.repo) + + self.assertEqual(result["from"], "0.1.19") + self.assertEqual(result["to"], "0.1.20") + project = read_json(project_path) + self.assertEqual(project["polaris_version"], "0.1.20") + self.assertEqual(project["workflow_version"], "0.1.3") + self.assertEqual(read_json(workflow_path)["workflow_version"], "0.1.3") + state = read_json(state_path) + self.assertEqual(state["status"], "IMPLEMENTING") + event = read_jsonl(event_path)[-1] + self.assertEqual(event["from"], "DOCS_SYNCED") + self.assertEqual(event["to"], "IMPLEMENTING") + self.assertEqual(event["previous_polaris_version"], "0.1.19") + self.assertEqual(event["previous_workflow_version"], "0.1.2") + record = read_json( + self.repo + / ".polaris" + / "migrations" + / "MIG-0.1.19-to-0.1.20.json" + ) + self.assertEqual(record["tasks"][0]["source_status"], "DOCS_SYNCED") + self.assertEqual(record["tasks"][0]["target_status"], "IMPLEMENTING") + def test_migration_resumes_after_event_append_without_duplication(self) -> None: """中断后重跑会采用已追加的迁移事件并完成投影,不重复写事件。""" - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") vendor(ROOT, self.repo, False) state = read_json(self.task / "state.json") started_at = "2026-08-15T00:00:00Z" record = { - "record_version": 1, - "migration_id": "0.1.18-to-0.1.19", - "from_polaris_version": "0.1.18", - "to_polaris_version": "0.1.19", + "record_version": 2, + "migration_id": "0.1.19-to-0.1.20", + "from_polaris_version": "0.1.19", + "to_polaris_version": "0.1.20", "from_workflow_version": "0.1.2", - "to_workflow_version": "0.1.2", + "to_workflow_version": "0.1.3", "status": "IN_PROGRESS", "started_at": started_at, "completed_at": None, @@ -1942,6 +2079,8 @@ def test_migration_resumes_after_event_append_without_duplication(self) -> None: "task_id": "TASK-0001", "source_sequence": 0, "migration_sequence": 1, + "source_status": "DRAFT", + "target_status": "DRAFT", } ], } @@ -1949,7 +2088,7 @@ def test_migration_resumes_after_event_append_without_duplication(self) -> None: self.repo / ".polaris" / "migrations" - / "MIG-0.1.18-to-0.1.19.json", + / "MIG-0.1.19-to-0.1.20.json", record, ) append_jsonl( @@ -1962,15 +2101,17 @@ def test_migration_resumes_after_event_append_without_duplication(self) -> None: "from": state["status"], "to": state["status"], "task_id": "TASK-0001", - "polaris_version": "0.1.19", - "workflow_version": "0.1.2", + "polaris_version": "0.1.20", + "workflow_version": "0.1.3", "current_revision": state["current_revision"], "rigor": state["rigor"], "blocked_from": state["blocked_from"], "blocker": state["blocker"], "artifacts": state["artifacts"], "subject": state["subject"], - "migration_id": "0.1.18-to-0.1.19", + "migration_id": "0.1.19-to-0.1.20", + "previous_polaris_version": "0.1.19", + "previous_workflow_version": "0.1.2", }, ) @@ -1982,7 +2123,8 @@ def test_migration_resumes_after_event_append_without_duplication(self) -> None: def test_migration_reclaims_only_its_own_dead_process_lock(self) -> None: """迁移可接管同一迁移的崩溃锁,但不能抢占仍存活的进程。""" - self.set_protocol_version("0.1.18") + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") vendor(ROOT, self.repo, False) lock_path = self.task / ".transition.lock" write_json_atomic( @@ -1990,7 +2132,7 @@ def test_migration_reclaims_only_its_own_dead_process_lock(self) -> None: { "lock_version": 1, "kind": "polaris_migration", - "migration_id": "0.1.18-to-0.1.19", + "migration_id": "0.1.19-to-0.1.20", "task_id": "TASK-0001", "hostname": socket.gethostname(), "pid": 2147483647, diff --git a/workflow/migrations.json b/workflow/migrations.json index 20fed56..3582a1b 100644 --- a/workflow/migrations.json +++ b/workflow/migrations.json @@ -1,5 +1,5 @@ { - "protocol_version": 1, + "protocol_version": 2, "steps": [ { "migration_id": "0.1.9-to-0.1.10", @@ -90,6 +90,15 @@ "to_workflow_version": "0.1.2", "project_strategy": "replace_version", "task_strategy": "append_version_event" + }, + { + "migration_id": "0.1.19-to-0.1.20", + "from_polaris_version": "0.1.19", + "to_polaris_version": "0.1.20", + "from_workflow_version": "0.1.2", + "to_workflow_version": "0.1.3", + "project_strategy": "replace_version_and_workflow", + "task_strategy": "append_mapped_workflow_event" } ] } From a75b625476a3c8f39c1e886c5e8e90543c8595a8 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 22:59:40 +0800 Subject: [PATCH 07/17] docs: align workflow skills with simplified states --- .../claude-code/agents/polaris-implementer.md | 2 +- scripts/internal/recovery_protocol.py | 11 ++--- scripts/recover_task.py | 2 +- skills/adversarial-review/SKILL.md | 2 +- skills/architecture-planning/SKILL.md | 2 +- skills/code-intelligence/SKILL.md | 6 +-- skills/documentation-sync/SKILL.md | 10 ++--- skills/engineering-task/SKILL.md | 16 ++++---- skills/implementation/SKILL.md | 10 ++--- skills/validation/SKILL.md | 6 +-- tests/test_codegraph.py | 12 ++++++ tests/test_core.py | 40 ++++++++++++++++++- 12 files changed, 82 insertions(+), 37 deletions(-) diff --git a/hosts/claude-code/agents/polaris-implementer.md b/hosts/claude-code/agents/polaris-implementer.md index 98228c3..226cdf9 100644 --- a/hosts/claude-code/agents/polaris-implementer.md +++ b/hosts/claude-code/agents/polaris-implementer.md @@ -11,4 +11,4 @@ You are an isolated Polaris Implementer working in the main Claude Code session' On the first run, execute only the preloaded `implementation` Skill from the registered handoff. Do not read the parent conversation or accept implementation advice outside that handoff. Return the immutable Implementation artifact path and your agent ID as the Implementer session ID. -When the parent resumes this same agent after `FINISH_IMPLEMENTATION`, execute only the preloaded `documentation-sync` Skill. Return the Knowledge Delta path and final subject checkpoint. Never run workflow transitions, Review, Validation, or task closure. +When the parent resumes this same agent after receiving the Implementation artifact, execute only the preloaded `documentation-sync` Skill while task authority remains `IMPLEMENTING`. Return the Knowledge Delta path and final subject checkpoint. Never run workflow transitions, Review, Validation, or task closure. diff --git a/scripts/internal/recovery_protocol.py b/scripts/internal/recovery_protocol.py index 96e10c6..6d74633 100644 --- a/scripts/internal/recovery_protocol.py +++ b/scripts/internal/recovery_protocol.py @@ -12,14 +12,11 @@ NEXT_ACTIONS = { "DRAFT": "complete and freeze the Work Item, then QUALIFY", "QUALIFIED": "refresh the bounded Working Set, finish PLAN.md, then PLAN", - "PLANNED": "satisfy any pre-approval, START_IMPLEMENTATION, and dispatch the Implementer handoff", - "IMPLEMENTING": "reuse or dispatch the deterministic Implementer task and read its live progress", - "IMPLEMENTED": "continue the same Implementer task for Documentation Sync, then register its artifact", - "DOCS_SYNCED": "build the immutable Review handoff and dispatch the required Reviewer task", + "PLANNED": "build the Implementer handoff and register it atomically with START_IMPLEMENTATION", + "IMPLEMENTING": "complete implementation and documentation, then build the Review handoff and START_REVIEW", "REVIEWING": "use a fresh or isolated Reviewer session to review only the registered handoff", - "REVIEWED": "prepare the acceptance evidence plan and START_VALIDATION", - "VALIDATING": "run reproducible acceptance checks and record PASS or the correct failure edge", - "VERIFIED": "write the Result artifact and request CLOSE through the mechanical gate", + "VALIDATING": "run reproducible acceptance checks; PASS_AND_CLOSE for R0/R1 or PASS_VALIDATION for R2", + "VERIFIED": "obtain R2 final approval, write the Result, and request CLOSE through the mechanical gate", "BLOCKED": "resolve the recorded blocker with its Decision Owner, then RESOLVE_BLOCK or create a new revision", "CLOSED": "no action; the task is closed", "CANCELLED": "no action; the task is cancelled", diff --git a/scripts/recover_task.py b/scripts/recover_task.py index 6759450..1d26ec2 100644 --- a/scripts/recover_task.py +++ b/scripts/recover_task.py @@ -89,7 +89,7 @@ def recover(repo: Path, task_id: str) -> dict[str, Any]: except (RuleFailure, InputFailure) as exc: working_set_status = {"available": False, "reason": str(exc)} live_progress: dict[str, Any] | None = None - if state["status"] in {"IMPLEMENTING", "IMPLEMENTED"}: + if state["status"] == "IMPLEMENTING": task_progress_path = progress_path(directory) if task_progress_path.is_file(): try: diff --git a/skills/adversarial-review/SKILL.md b/skills/adversarial-review/SKILL.md index 86de770..3d8a611 100644 --- a/skills/adversarial-review/SKILL.md +++ b/skills/adversarial-review/SKILL.md @@ -15,7 +15,7 @@ For R1/R2, run only in the fresh Reviewer context defined by the active host ada 6. Check engineering quality second: correctness, failure paths, lifetime, concurrency, security, performance, compatibility, maintainability, test gaps, and counterexamples. 7. Preserve every prior Finding ID in a follow-up Review. Read the registered author response, recheck the entire new patch, and record a concrete `reviewer_resolution` for each carried Finding. 8. Give new Findings monotonic IDs and mark critical/high, acceptance failures, and scope violations as blocking. -9. Finalize an immutable v2 Review Code Intelligence record, including `UNAVAILABLE`, `FAILED`, or `SKIPPED` when appropriate. Resolve the output with `task_layout.review_path` from the handoff revision, attempt, and Reviewer slot. Write a new immutable Review JSON bound to the handoff, slot, session attestation, and optional record. Never assemble the path independently or overwrite an existing artifact. Reject while any blocking Finding remains open; Code Intelligence cannot determine the verdict. +9. If this stage actually performed a Provider status, sync, or explore operation, finalize an immutable v2 Review Code Intelligence record. If no Provider operation ran, omit the Code Intelligence record and its optional artifact reference. Resolve the output with `task_layout.review_path` from the handoff revision, attempt, and Reviewer slot. Write a new immutable Review JSON bound to the handoff, slot, session attestation, and optional record. Never assemble the path independently or overwrite an existing artifact. Reject while any blocking Finding remains open; Code Intelligence cannot determine the verdict. 10. Return the verdict and exact Review path to the dispatching `{{skill:engineering-task}}` context. Do not run `ACCEPT_REVIEW` or `REJECT_REVIEW`; the dispatcher validates and registers all required Review artifacts before applying the graph transition. Never modify implementation code or start another Reviewer task during Review. Return a concise structured result to the dispatcher with verdict, Review attempt, Reviewer slot, reviewer session ID, subject commits/diff hash, every Finding ID and status, and the immutable Review path. Do not emit a Polaris checkpoint marker from the child task. The dispatching context emits `[POLARIS:REVIEW_ACCEPTED]` or `[POLARIS:REVIEW_REJECTED]` with the nine fixed fields only after the corresponding transition succeeds. If isolation or handoff validation prevents review, do not write a Review; report the exact required fresh-session or handoff action to the dispatcher. diff --git a/skills/architecture-planning/SKILL.md b/skills/architecture-planning/SKILL.md index b0a3bd8..bbb2bee 100644 --- a/skills/architecture-planning/SKILL.md +++ b/skills/architecture-planning/SKILL.md @@ -6,7 +6,7 @@ description: Internal Polaris stage for an explicitly started `{{skill:engineeri # Architecture Planning 1. Read the frozen Work Item and project rules. -2. Refresh `working-set.json` with `build_working_set.py`. At the Planning boundary invoke `{{skill:code-intelligence}}` for optional frozen-task relationship discovery, first using `status` or `sync-if-needed`. Use CodeGraph only when `.codegraph/` already exists: prefer `codegraph_explore`, with `codegraph explore` as the non-MCP fallback. A bounded `codegraph sync` is non-blocking. Record a compact v2 Planning Code Intelligence record even when unavailable. Confirm every returned path from repository source before adding it with the query ID as `discovered_from`; provider failure immediately falls back to the original repository search path and never blocks Planning. Include `.polaris/code-intelligence.json` when it exists and the finalized Planning record in the Working Set. Record every entry as section, path, reason, and discovery source; add explicit entries only for concrete dependencies. Do not parse or create a duplicate Markdown Working Set. +2. Refresh `working-set.json` with `build_working_set.py`. At the Planning boundary use `{{skill:code-intelligence}}` only when optional frozen-task relationship discovery is useful and a Provider operation can run. Use CodeGraph only when `.codegraph/` already exists: prefer `codegraph_explore`, with `codegraph explore` as the non-MCP fallback. A bounded `codegraph sync` is non-blocking. When a status, sync, or explore operation runs, write a compact v2 Planning record; otherwise omit the Code Intelligence record. Confirm every returned path from repository source before adding it with the query ID as `discovered_from`; provider failure immediately falls back to the original repository search path and never blocks Planning. Include `.polaris/code-intelligence.json` and the finalized Planning record in the Working Set only when they exist. Record every entry as section, path, reason, and discovery source; add explicit entries only for concrete dependencies. Do not parse or create a duplicate Markdown Working Set. 3. Investigate only paths justified by the task or a discovered dependency. Provider observations cannot expand frozen scope. 4. Write `PLAN.md` as a delta from `base_commit`, including alternatives, risks, affected invariants, and expected documentation changes. Keep rationale in Markdown; do not use it as decision authority. 5. Map every acceptance criterion to a planned validation command or Human check. Code Intelligence observations are not acceptance evidence. diff --git a/skills/code-intelligence/SKILL.md b/skills/code-intelligence/SKILL.md index d24375e..cbe7a39 100644 --- a/skills/code-intelligence/SKILL.md +++ b/skills/code-intelligence/SKILL.md @@ -7,19 +7,19 @@ description: Internal optional Polaris stage support for bounded CodeGraph relat Treat Code Intelligence as read-only, best-effort evidence. Source, Git, builds, tests, and frozen Polaris artifacts remain authority. -1. Load `.polaris/code-intelligence.json` and project rules. If the policy disables Code Intelligence, record `UNAVAILABLE` and use the stage's source path. Use CodeGraph only when the repository root has an existing `.codegraph/` directory. When it is absent, record `UNAVAILABLE`, stop CodeGraph calls for this project for the session, and tell the user they may choose to initialize it; never run `codegraph init`. +1. Load `.polaris/code-intelligence.json` when present and project rules. If policy disables Code Intelligence or the repository has no `.codegraph/` directory, use the stage's source path and omit the Code Intelligence record because no Provider operation ran. Stop CodeGraph calls for this project for the session and tell the user they may choose to initialize it; never run `codegraph init`. 2. At the calling stage's declared boundary, run `code_intelligence_runtime.py status` or `sync-if-needed`. The latter may run one bounded `codegraph sync` only when status reports pending changes; it never loops, waits for a watcher, or treats a successful command as a gate. 3. For an allowed frozen-scope relationship query, use only `codegraph_explore` when MCP exposes it. If MCP is unavailable and the executable is available, use `codegraph explore` as the non-MCP fallback. Do not select retired narrow operations. Bound the query to the Work Item, Working Set, registered subject, or a confirmed dependency; graph output cannot expand frozen scope, authorize change, satisfy acceptance, or determine a Review verdict. 4. Save each raw explore response only below the task's ignored `runtime/code-intelligence/` directory, then run `code_intelligence_runtime.py classify-response` for it. When `RESPONSE_BANNER` is a freshness basis, persist that successful explore response hash as `freshness.response_sha256`; final records contain the response hash and finite summary, never the response itself. 5. On `PARTIAL_STALE`, process every named path by its current safe state. If it is a current confined regular file, directly read it and record `READ_SOURCE` with its current SHA-256. If a safe path is missing/deleted, inspect the registered subject Git diff and record `INSPECT_GIT_DIFF` with null observed SHA-256 and bound base/head/diff evidence. For unsafe paths, record `NOT_VERIFIED` and use source search. The remaining graph response may still be navigation evidence, but never a conclusion about a stale path. 6. On `INDEX_STALE` or `NOT_VERIFIED`, use repository source search and Git evidence, record the `SEARCH_SOURCE` fallback, and stop repeated graph calls for that stage. Every `SEARCH_SOURCE` fallback records `result_paths`: zero or at most 100 unique POSIX paths, each a current confined regular file with its current SHA-256; non-`SEARCH_SOURCE` fallbacks use empty `result_paths`. On malformed, missing, or unavailable Provider output, continue the same source fallback without blocking the stage. 7. Never initialize, install, start, authenticate, or reconfigure CodeGraph. Do not manage its watcher, daemon, lock, or host MCP settings. -8. Finalize an immutable v2 Code Intelligence record with the stage's actual freshness, stale points, and source fallbacks. Code Intelligence is never a workflow gate. +8. Finalize an immutable v2 Code Intelligence record only after an actual Provider status, sync, or explore operation, including its real freshness, stale points, and source fallbacks. If no operation ran, omit the Code Intelligence record. Code Intelligence is never a workflow gate. Stage policy: - Planning: at the Planning boundary, request only frozen-task relationship discovery needed to justify Working Set entries; confirm every returned path in repository source and record its query ID as `discovered_from`. - Implementation: before editing, request only handoff-scoped edit relationships. Query again mid-stage only when a later declared implementation step depends on relationships changed by the current subject. -- Documentation Sync: run `sync-if-needed` once only when the final subject changed supported source files; otherwise record `SKIPPED`. +- Documentation Sync: run `sync-if-needed` once only when the final subject changed supported source files and the Provider is available; otherwise omit the Code Intelligence record. - Review: independently request only registered-subject impact relationships. Do not reuse Implementer query conclusions. - Validation: do not invoke this Skill; use builds, tests, static checks, and Human Checks as the acceptance evidence. diff --git a/skills/documentation-sync/SKILL.md b/skills/documentation-sync/SKILL.md index e22a2ea..b906cc5 100644 --- a/skills/documentation-sync/SKILL.md +++ b/skills/documentation-sync/SKILL.md @@ -1,22 +1,22 @@ --- name: documentation-sync -description: Internal Polaris worker stage for an explicitly started `{{skill:engineering-task}}` workflow. Invoke only by continuing the same dedicated Implementer task in IMPLEMENTED to reconcile documentation; do not activate from ordinary documentation requests. +description: Internal Polaris worker stage for an explicitly started `{{skill:engineering-task}}` workflow. Invoke only by continuing the same dedicated Implementer task in IMPLEMENTING to reconcile documentation; do not activate from ordinary documentation requests. --- # Documentation Sync -1. Continue in the same Implementer conversation and reload the registered Implementation artifact and live progress. Use the progress updater's `SET_PHASE` event to enter `DOCUMENTING`; do not alter the terminal Implementation steps. +1. Continue in the same Implementer conversation and reload the Implementation artifact. If live progress exists, validate it and use the updater's `SET_PHASE` event to enter `DOCUMENTING`; otherwise continue without creating telemetry. Do not alter the terminal Implementation steps. 2. Compare changed subject paths with project documentation and the frozen Work Item. 3. Write a Knowledge Delta JSON with an entry for every affected knowledge area: `ADD`, `UPDATE`, `STALE`, or `NO_CHANGE`. 4. Update confirmed project documentation. Do not promote unverified inference to authority. 5. Record failed attempts with `record_exploration.py`. Keep task-only conclusions in the task; promote reusable, evidence-backed conclusions to `.polaris/explorations/` with the same script. 6. Leave no unresolved `STALE` entry. 7. Create the final subject checkpoint and recompute the subject diff hash. -8. When the final subject includes supported source changes, invoke `{{skill:code-intelligence}}` once at the Documentation Sync boundary with `sync-if-needed`; otherwise record `SKIPPED`. Use CodeGraph only with an existing `.codegraph/` directory: prefer `codegraph_explore`, with `codegraph explore` as the non-MCP fallback. A bounded `codegraph sync` is non-blocking. Record `UNAVAILABLE` or `FAILED` and continue when CodeGraph cannot run. Reference the immutable v2 Documentation Sync Code Intelligence record from the Knowledge Delta; never claim commit-exact freshness. +8. When the final subject includes supported source changes and the Provider is available, invoke `{{skill:code-intelligence}}` once at the Documentation Sync boundary with `sync-if-needed`. Use CodeGraph only with an existing `.codegraph/` directory: prefer `codegraph_explore`, with `codegraph explore` as the non-MCP fallback. A bounded `codegraph sync` is non-blocking. If a Provider operation ran, reference its immutable v2 record from the Knowledge Delta; otherwise omit the Code Intelligence record and its optional artifact reference. Never claim commit-exact freshness. 9. Refresh the Working Set if a promoted exploration, documentation change, or confirmed Code Intelligence dependency alters the next stage's justified inputs. -10. Run `check_docs.py` with the final subject base/head, append its result with `ADD_CHECK`, then use `SET_PHASE` to enter `COMPLETED` with no blocker. Return the Knowledge Delta path, final subject base/head, diff hash, changed documentation, promoted explorations, Code Intelligence refresh status, and check result. +10. Run `check_docs.py` with the final subject base/head. When live telemetry exists, append its result with `ADD_CHECK`, then use `SET_PHASE` to enter `COMPLETED` with no blocker. Return the Knowledge Delta path, final subject base/head, diff hash, changed documentation, promoted explorations, Code Intelligence refresh status, and check result. -Do not run `SYNC_DOCS` or emit a Polaris checkpoint marker. The main `{{skill:engineering-task}}` validates and registers the artifact, advances the graph, reloads state, and emits `[POLARIS:DOCS_SYNCED]`. +Do not run workflow transitions or emit a Polaris checkpoint marker. The main `{{skill:engineering-task}}` combines the Knowledge Delta with the Implementation and final subject when it starts Review. Do not edit Review, Validation, Result, event, or state artifacts directly. diff --git a/skills/engineering-task/SKILL.md b/skills/engineering-task/SKILL.md index 94ea9bc..b24e327 100644 --- a/skills/engineering-task/SKILL.md +++ b/skills/engineering-task/SKILL.md @@ -21,7 +21,7 @@ At every pause or completed workflow checkpoint, emit exactly one status block w 8. `Next`: next legal graph action. 9. `User action`: exact user decision/action required, or `None`. -Use only these markers: `POLARIS_STARTED`, `REQUIREMENTS_NEEDED`, `WORK_ITEM_PREVIEW`, `WORK_ITEM_QUALIFIED`, `PLAN_DECISIONS_NEEDED`, `PLAN_READY`, `IMPLEMENTATION_HANDOFF_READY`, `IMPLEMENTATION_SESSION_STARTED`, `IMPLEMENTATION_PROGRESS`, `IMPLEMENTATION_FINISHED`, `DOCS_SYNCED`, `REVIEW_HANDOFF_READY`, `REVIEW_SESSION_STARTED`, `REVIEW_ACCEPTED`, `REVIEW_REJECTED`, `VALIDATION_PASS`, `VALIDATION_FAIL`, `TASK_BLOCKED`, `TASK_CANCELLED`, and `TASK_CLOSED`. +Use only these markers: `POLARIS_STARTED`, `REQUIREMENTS_NEEDED`, `WORK_ITEM_PREVIEW`, `WORK_ITEM_QUALIFIED`, `PLAN_DECISIONS_NEEDED`, `PLAN_READY`, `IMPLEMENTATION_HANDOFF_READY`, `IMPLEMENTATION_SESSION_STARTED`, `IMPLEMENTATION_PROGRESS`, `REVIEW_HANDOFF_READY`, `REVIEW_SESSION_STARTED`, `REVIEW_ACCEPTED`, `REVIEW_REJECTED`, `VALIDATION_PASS`, `VALIDATION_FAIL`, `TASK_BLOCKED`, `TASK_CANCELLED`, and `TASK_CLOSED`. Never report an anticipated state. Reload authority after every transition. A stage may append details after the nine fields but may not rename, reorder, or omit them. For bounded Human decisions, prefer `request_user_input` or equivalent structured choices. If the tool is unavailable, render identical text choices. Treat UI and text answers identically. Do not change host mode to obtain UI. @@ -43,19 +43,19 @@ Never report an anticipated state. Reload authority after every transition. A st ## Independent Implementation 7. Before Implementation, require frozen `implementation_dispatch` authority with `mode=auto_new_task`, `fallback=same_session`, `same_local_project=true`, and `authorized=true`. The same `Confirm and execute` answer authorizes every Implementer task for the revision, including rework attempts up to the graph limit. -8. Run `START_IMPLEMENTATION` after required R2 pre-approval. Run `build_implementation_handoff.py`, register the exact returned path with `DISPATCH_IMPLEMENTATION`, then emit `IMPLEMENTATION_HANDOFF_READY`. Never assemble task-relative paths independently of `task_layout.py`. +8. Run `build_implementation_handoff.py`, then use `START_IMPLEMENTATION` after required R2 pre-approval to atomically validate and register the exact returned handoff path. Reload state and emit `IMPLEMENTATION_HANDOFF_READY`. Never assemble task-relative paths independently of `task_layout.py`. 9. Use the exact worker title `Polaris Implement · · · attempt `. Dispatch a fresh isolated worker in the same local checkout through the host appendix. Never inherit the main conversation or use a separate worktree by default. 10. Before creation, first reuse a valid Implementation artifact bound to the registered handoff. Otherwise reuse or resume only one unambiguous worker identity as defined by the host appendix. Never create a duplicate for the same task, revision, and attempt; ambiguous identity requires the same-session fallback rather than guessing. 11. Give the Implementer only the task ID and registered handoff path. Use this exact prompt: `Use {{skill:implementation}} for . Load only and its package as task context; read state.json only to verify the registered handoff. Work in the shared local checkout, define and execute linear implementation_steps through update_implementation_progress.py, write the immutable Implementation JSON at output_path with matching step_results, and return its path. Do not run task transitions, Review, Validation, or close the task.` Do not include main-chat history or implementation advice. -12. Initialize the ignored live snapshot with the updater's `INITIALIZE` event, then emit `IMPLEMENTATION_SESSION_STARTED` with the nine fixed fields plus `Implementation task`, `Handoff`, `Progress`, and `Dispatch mode`; set `User action` to `None` while work is proceeding. -13. While `IMPLEMENTING`, answer status requests by validating the registered handoff's `progress_json_path` and formatting its fields directly in the conversation. Emit `IMPLEMENTATION_PROGRESS` with the latest phase; current step ID/title; completed or skipped prefix; pending suffix; checks; blocker; timestamp; and adapter-defined worker reference. Derive those views from the single ordered `implementation_steps` list. Do not persist a duplicate Markdown status file, invent percentages, infer progress from elapsed time, or reconstruct the path from prose. A status query must not cancel or duplicate the worker. -14. Wait for the Implementer result. Require live progress phase `CHECKPOINTING`, every step terminal, the same Implementer session, and exact equality between the live step projection and immutable `step_results`. Then register the Implementation, run `FINISH_IMPLEMENTATION`, and reload state. Continue the exact same Implementer worker through the host appendix with `{{skill:documentation-sync}}`; it writes Knowledge Delta and any documentation checkpoint without running transitions. Register that artifact, run `SYNC_DOCS`, reload state, and emit `DOCS_SYNCED`. Only then is the Implementer worker finished. -15. If worker management is unavailable or dispatch fails, use the registered handoff in this main task, invoke `{{skill:implementation}}` and `{{skill:documentation-sync}}` locally, and keep writing the same progress snapshots. Report `Dispatch mode: same_session_fallback` and that immediate status responses may be delayed; do not block solely because host automation is unavailable. +12. When the host supports live reporting, initialize the ignored snapshot with the updater's `INITIALIZE` event. Emit `IMPLEMENTATION_SESSION_STARTED` with the nine fixed fields plus `Implementation task`, `Handoff`, optional `Progress`, and `Dispatch mode`; set `User action` to `None` while work is proceeding. Snapshot absence is not a blocker. +13. While `IMPLEMENTING`, answer status requests from the validated live snapshot when it exists; otherwise report that only durable artifacts are available. When present, emit `IMPLEMENTATION_PROGRESS` with the latest phase; current step ID/title; completed or skipped prefix; pending suffix; checks; blocker; timestamp; and adapter-defined worker reference. Derive those views from the single ordered `implementation_steps` list. Do not persist a duplicate Markdown status file, invent percentages, infer progress from elapsed time, or reconstruct the path from prose. A status query must not cancel or duplicate the worker. +14. Wait for the Implementation artifact, then continue the exact same Implementer worker through the host appendix with `{{skill:documentation-sync}}`; it writes the Knowledge Delta and final subject checkpoint without running transitions. Live progress is optional telemetry: validate and report it when present, but never require it for a durable gate. Require the same Implementer session and terminal `step_results` in the immutable Implementation artifact. Only after both artifacts and the final subject are ready is the Implementer worker finished. +15. If worker management is unavailable or dispatch fails, use the registered handoff in this main task and invoke `{{skill:implementation}}` and `{{skill:documentation-sync}}` locally. Continue an existing progress snapshot when present, but do not create one merely to satisfy a gate. Report `Dispatch mode: same_session_fallback` and that immediate status responses may be delayed; do not block solely because host automation is unavailable. 16. If an Implementer requests permission or hits a blocker, show the exact Implementer task or agent ID and `User action`. The Implementer never advances the graph, reviews itself, validates acceptance, or closes the task. ## Independent Review -17. At `DOCS_SYNCED`, build and register an immutable Reviewer handoff, run `START_REVIEW`, and emit `REVIEW_HANDOFF_READY`. R0 performs an explicit isolated same-session pass. For R1/R2, the main task only dispatches Reviewers, waits, reloads authority, and applies transitions. Never fork the implementation conversation. +17. While still `IMPLEMENTING`, build the immutable Reviewer handoff from the final Implementation, Knowledge Delta, and subject. Run `START_REVIEW` once with all three artifact paths and the final subject base/head so the gate validates and registers them atomically, then emit `REVIEW_HANDOFF_READY`. R0 performs an explicit isolated same-session pass. For R1/R2, the main task only dispatches Reviewers, waits, reloads authority, and applies transitions. Never fork the implementation conversation. 18. Require frozen `review_dispatch` authority with `mode=auto_new_task`, `fallback=manual_handoff`, `same_local_project=true`, and `authorized=true`. Do not create Reviewer tasks without it. 19. Use the exact worker title `Polaris Review · · · attempt · reviewer `. Before creation, first accept a valid deterministic Review artifact. Otherwise dispatch a fresh isolated Reviewer and reuse only one unambiguous identity through the host appendix. Never reuse an Implementer or another Reviewer slot, inherit implementation chat, or create a duplicate; ambiguous identity uses manual fallback. 20. Give the Reviewer only the task ID, Reviewer slot, registered handoff path. Use this exact prompt: `Use {{skill:adversarial-review}} for , Reviewer slot . Load only and its package. Write the immutable Review JSON and return its verdict and path. Do not modify implementation or run task transitions.` Do not include implementation explanations, chat history, proposed findings, or expected verdicts. @@ -65,7 +65,7 @@ Never report an anticipated state. Reload authority after every transition. A st ## Completion and gates -24. Invoke `{{skill:validation}}` only at `VALIDATING`. Stop at Human or mechanical gates and record a blocker instead of guessing. +24. Invoke `{{skill:validation}}` only at `VALIDATING`. After Review acceptance the state is already `VALIDATING`: R0/R1 use `PASS_AND_CLOSE`; R2 uses `PASS_VALIDATION`, waits for final Human approval, then uses `CLOSE`. Stop at Human or mechanical gates and record a blocker instead of guessing. 25. Report `TASK_CLOSED` only after `transition_task.py` actually reaches `CLOSED`; otherwise report the current checkpoint. Give each conversation an opaque stable session ID. If the host exposes none, generate one once and reuse it only within that conversation. diff --git a/skills/implementation/SKILL.md b/skills/implementation/SKILL.md index ac7bec1..2b383f3 100644 --- a/skills/implementation/SKILL.md +++ b/skills/implementation/SKILL.md @@ -7,16 +7,16 @@ description: Internal Polaris worker stage for an explicitly started `{{skill:en 1. Require the task ID and registered Implementation handoff path returned by the main task. Load only that handoff and its package as task context; read `state.json` only to verify registration. Use paths carried by the handoff or resolved by `task_layout.py`; never reconstruct them from prose. Do not read the main conversation or infer unstated requirements. 2. Confirm state is `IMPLEMENTING`, the handoff hash matches `state.json`, and `artifact_attempt`, revision, base commit, output path, and progress paths are current. At the Implementation boundary invoke `{{skill:code-intelligence}}` before editing for optional handoff-scoped edit relationships, first using `status` or `sync-if-needed`. Use CodeGraph only with an existing `.codegraph/` directory: prefer `codegraph_explore`, with `codegraph explore` as the non-MCP fallback. A bounded `codegraph sync` is non-blocking. Missing or failing Provider output immediately falls back to direct source reading. Query again during Implementation only when a later declared step depends on relationships from newly changed code. -3. Generate one stable Implementer session ID for this conversation. Before changing code, use the initialized live snapshot's `DEFINE_STEPS` event to create a non-empty ordered `implementation_steps` list. Every step receives the next `STEP-NNN` ID and must reference one or more acceptance IDs from the frozen Work Item. -4. Execute steps linearly with `START_STEP`, then `COMPLETE_STEP`, `BLOCK_STEP`, or `RESUME_STEP`; use `SKIP_STEP` only with an explicit reason. Existing step identity, title, order, and acceptance bindings are immutable. Newly discovered work may only be added at the end with `APPEND_STEP`. Never edit `progress.json` directly. +3. Generate one stable Implementer session ID for this conversation. Before changing code, create a non-empty ordered `implementation_steps` list. Every step receives the next `STEP-NNN` ID and must reference one or more acceptance IDs from the frozen Work Item. If an ignored live snapshot was initialized, mirror the list through its `DEFINE_STEPS` event. +4. Execute steps linearly. When live telemetry exists, use `START_STEP`, then `COMPLETE_STEP`, `BLOCK_STEP`, or `RESUME_STEP`; use `SKIP_STEP` only with an explicit reason. Existing step identity, title, order, and acceptance bindings are immutable. Newly discovered work may only be added at the end, using `APPEND_STEP` when telemetry exists. Never edit `progress.json` directly or create it as a durable prerequisite. 5. Change only declared subject paths and protect unrelated user changes. Work in small build/test/fix loops. 6. Do not alter goal, scope, acceptance, or hard constraints. Return a blocker when any must change. 7. Record Plan deviations and reasons. After Review rejection, load the handoff's prior Review, answer every open Finding once in an immutable Review Response, and bind it to the new subject. 8. Run planned local checks and append reproducible evidence with `ADD_CHECK`. Never report a made-up percentage; derive completed, current, and remaining work from the ordered steps. 9. Complete or explicitly skip every step, then create a subject checkpoint commit containing scoped code, tests, build configuration, and relevant project docs only. -10. Finalize an immutable v2 Implementation Code Intelligence record, including `UNAVAILABLE`, `FAILED`, or `SKIPPED` when appropriate. Write the immutable Implementation JSON at the handoff's `output_path`, reference that record, bind the handoff, subject, session, deviations, and checks, and copy the exact terminal `id`, `status`, and `result` projection into `step_results`. Code Intelligence evidence is never a gate. -11. Use `SET_PHASE` to enter `CHECKPOINTING` only after every step is `COMPLETED` or `SKIPPED`, then return the artifact path, session ID, subject base/head, diff hash, step results, checks, deviations, Review Response path when present, and remaining Documentation Sync work. +10. If this stage actually performed a Provider status, sync, or explore operation, finalize an immutable v2 Implementation Code Intelligence record and reference it. If no Provider operation ran, omit the Code Intelligence record and its optional artifact reference. Write the immutable Implementation JSON at the handoff's `output_path`, bind the handoff, subject, session, deviations, and checks, and copy the exact terminal `id`, `status`, and `result` projection into `step_results`. Code Intelligence evidence is never a gate. +11. After every step is `COMPLETED` or `SKIPPED`, use `SET_PHASE` to enter `CHECKPOINTING` only when live telemetry exists. Return the artifact path, session ID, subject base/head, diff hash, step results, checks, deviations, Review Response path when present, and remaining Documentation Sync work. -Do not run `FINISH_IMPLEMENTATION`, Documentation Sync, Review, Validation, or any completion transition. Do not emit a Polaris checkpoint marker; the main `{{skill:engineering-task}}` validates the artifact, advances the graph, and continues this task for `{{skill:documentation-sync}}`. +Do not run workflow transitions, Review, Validation, or task closure. Do not emit a Polaris checkpoint marker; the main `{{skill:engineering-task}}` validates the artifact and continues this same task for `{{skill:documentation-sync}}` while authority remains `IMPLEMENTING`. CodeGraph fallback contract: never run `codegraph init` or manage the Provider. Save and classify each response; when `RESPONSE_BANNER` is present, persist its successful explore response hash as `freshness.response_sha256`. For `PARTIAL_STALE`, if a named path is a current confined regular file, directly read it and record `READ_SOURCE` with its current SHA-256; if a safe path is missing/deleted, inspect the registered subject Git diff and record `INSPECT_GIT_DIFF` with null observed SHA-256 and bound base/head/diff evidence; for unsafe paths, record `NOT_VERIFIED` and use source search. For `INDEX_STALE` or `NOT_VERIFIED`, use source search and Git evidence, then stop graph calls for this stage. Each `SEARCH_SOURCE` fallback records `result_paths`: zero or at most 100 unique POSIX paths, each a current confined regular file with its current SHA-256; non-`SEARCH_SOURCE` fallbacks use empty `result_paths`. Graph evidence never gates implementation. diff --git a/skills/validation/SKILL.md b/skills/validation/SKILL.md index 561e93d..f47f819 100644 --- a/skills/validation/SKILL.md +++ b/skills/validation/SKILL.md @@ -10,9 +10,9 @@ description: Internal Polaris stage for an explicitly started `{{skill:engineeri 3. Record command/check, working directory, environment summary, start time, exit code, result, and output path or hash. 4. Mark the overall verdict PASS only when every acceptance criterion passes. 5. Write a new immutable Validation JSON attempt. Do not create a duplicate Markdown artifact. -6. Use `PASS_VALIDATION`, `FAIL_IMPLEMENTATION`, or `FAIL_PLAN` according to the evidence. -7. Require final Human approval before `CLOSE` when rigor is R2. +6. On PASS, write the immutable Result JSON bound to the same revision and subject. For R0/R1, submit Validation and Result together with `PASS_AND_CLOSE`. For R2, submit Validation with `PASS_VALIDATION`, require final Human approval, then submit Result and approval with `CLOSE`. +7. On FAIL, use `FAIL_IMPLEMENTATION` or `FAIL_PLAN` according to the evidence. -After the transition succeeds, reload state and emit `[POLARIS:VALIDATION_PASS]` or `[POLARIS:VALIDATION_FAIL]` with the nine fixed `{{skill:engineering-task}}` status fields. Include one result per acceptance ID, its evidence path or hash, the overall Validation path, and the next legal transition. Emit `[POLARIS:TASK_CLOSED]` only after a separate successful `CLOSE` transition. +After the transition succeeds, reload state and emit `[POLARIS:VALIDATION_PASS]` or `[POLARIS:VALIDATION_FAIL]` with the nine fixed `{{skill:engineering-task}}` status fields. Include one result per acceptance ID, its evidence path or hash, the Validation path, Result path on PASS, and the next legal transition. For R0/R1, emit `[POLARIS:TASK_CLOSED]` only after `PASS_AND_CLOSE` succeeds. For R2, emit it only after the later `CLOSE` succeeds. Compilation alone is not completion unless it is the only explicit acceptance criterion. Never edit `state.json` or claim `CLOSED` directly. diff --git a/tests/test_codegraph.py b/tests/test_codegraph.py index 773e385..c4e0f3b 100644 --- a/tests/test_codegraph.py +++ b/tests/test_codegraph.py @@ -189,6 +189,18 @@ def test_readmes_keep_codegraph_operational_boundaries(self) -> None: with self.assertRaises(AssertionError): self.assertIn("codegraph sync", mutated, path.name) + def test_stage_surfaces_do_not_require_unused_provider_records(self) -> None: + """未执行 Provider 操作的阶段明确省略 record,不制造 UNAVAILABLE 噪声。""" + for relative in [ + "skills/architecture-planning/SKILL.md", + "skills/implementation/SKILL.md", + "skills/documentation-sync/SKILL.md", + "skills/adversarial-review/SKILL.md", + "skills/code-intelligence/SKILL.md", + ]: + text = (ROOT / relative).read_text(encoding="utf-8") + self.assertIn("omit the Code Intelligence record", text, relative) + def adapter_functions(self) -> tuple[object, object]: adapter_path = SCRIPTS / "internal" / "codegraph_adapter.py" self.assertTrue( diff --git a/tests/test_core.py b/tests/test_core.py index 2316f32..9d55ff1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -189,6 +189,35 @@ def test_workflow_013_contains_only_governance_states(self) -> None: self.assertEqual(events["ACCEPT_REVIEW"]["to"], "VALIDATING") self.assertEqual(events["PASS_AND_CLOSE"]["to"], "CLOSED") + def test_workflow_surfaces_use_simplified_events(self) -> None: + """执行 Skills 与恢复入口不再指示已删除的状态或事件。""" + surfaces = [ + ROOT / "skills" / "engineering-task" / "SKILL.md", + ROOT / "skills" / "implementation" / "SKILL.md", + ROOT / "skills" / "documentation-sync" / "SKILL.md", + ROOT / "skills" / "validation" / "SKILL.md", + ROOT / "scripts" / "internal" / "recovery_protocol.py", + ROOT / "scripts" / "recover_task.py", + ROOT / "hosts" / "claude-code" / "agents" / "polaris-implementer.md", + ] + retired = { + "DISPATCH_IMPLEMENTATION", + "FINISH_IMPLEMENTATION", + "SYNC_DOCS", + "START_VALIDATION", + '"IMPLEMENTED"', + '"DOCS_SYNCED"', + '"REVIEWED"', + } + for path in surfaces: + text = path.read_text(encoding="utf-8") + for value in retired: + self.assertNotIn(value, text, path.relative_to(ROOT).as_posix()) + validation = (ROOT / "skills" / "validation" / "SKILL.md").read_text( + encoding="utf-8" + ) + self.assertIn("PASS_AND_CLOSE", validation) + def freeze_work_item(self) -> None: path = self.task / "revisions" / "work-item-r001.json" value = read_json(path) @@ -354,6 +383,14 @@ def test_start_implementation_atomically_registers_handoff(self) -> None: None, ) + def test_recovery_allows_implementing_without_live_progress(self) -> None: + """Fresh-session 恢复把缺失进度视为无遥测,而不是流程 blocker。""" + self.enter_implementing() + self.assertFalse((self.task / "runtime" / "progress.json").exists()) + recovered = recover(self.repo, "TASK-0001") + self.assertIsNone(recovered["live_implementation_progress"]) + self.assertIn("START_REVIEW", recovered["recommended_next_action"]) + def implementation_value( self, base: str, head: str, session_id: str ) -> dict[str, object]: @@ -3141,8 +3178,7 @@ def test_skills_define_stable_conversation_checkpoints(self) -> None: "POLARIS_STARTED", "IMPLEMENTATION_SESSION_STARTED", "IMPLEMENTATION_PROGRESS", - "IMPLEMENTATION_FINISHED", - "DOCS_SYNCED", + "REVIEW_HANDOFF_READY", "REVIEW_SESSION_STARTED", "REVIEW_ACCEPTED", "REVIEW_REJECTED", From 070072b9d15ebad51373b73ab1e92fc6c63682cb Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 23:02:36 +0800 Subject: [PATCH 08/17] docs: publish Polaris 0.1.20 workflow 0.1.3 --- README.md | 14 ++++++----- README.zh-CN.md | 14 ++++++----- docs/USAGE.md | 51 ++++++++++++++++++++--------------------- plan.md | 40 +++++++++++++++----------------- tests/test_codegraph.py | 15 ++++++++++-- 5 files changed, 73 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 0e53b0e..6a12b19 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ English | [简体中文](README.zh-CN.md) -> Current protocol version: `0.1.19` (in development); workflow version: `0.1.2` +> Current protocol version: `0.1.20` (in development); workflow version: `0.1.3` Polaris is a repo-native engineering workflow for coding agent hosts. It stores requirements, plans, implementation results, independent reviews, validation evidence, and task state in Git, then uses deterministic gates to prevent requirement drift, stale evidence, and agents declaring their own work complete. @@ -11,17 +11,19 @@ Polaris currently supports Codex and Claude Code. [plan.md](plan.md) is the v0.1 ## Core workflow ```text -DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → IMPLEMENTED - → DOCS_SYNCED → REVIEWING → REVIEWED - → VALIDATING → VERIFIED → CLOSED +DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → REVIEWING → VALIDATING + ├─ R0/R1 → CLOSED + └─ R2 → VERIFIED → CLOSED ``` - A Work Item freezes the goal, scope, and acceptance criteria. -- An independent Implementer works from an immutable handoff and updates code, tests, and documentation. +- An independent Implementer works from an immutable handoff and updates code, tests, and documentation inside one `IMPLEMENTING` stage. - An independent Reviewer checks specification compliance and engineering quality. - Validation binds every acceptance criterion to reproducible evidence. - `events.jsonl` records state changes; `state.json` is a rebuildable projection. - Reviews and validations bind to the current revision, Git commits, and diff hash. Content changes invalidate stale evidence. +- R0/R1 atomically validate and close with `PASS_AND_CLOSE`; R2 retains `VERIFIED` for final Human approval. +- Live implementation progress is optional ignored telemetry and is never a durable gate. - Only `transition_task.py` can write `VERIFIED` or `CLOSED` after its gates pass. Tasks use `R1` by default. Low-risk mechanical changes may use `R0`; public APIs, persistent formats, architecture boundaries, concurrency, security, and resource-lifetime changes require `R2`. @@ -87,7 +89,7 @@ codegraph init polaris code-intelligence add codegraph --repo . ``` -Run these commands from the target repository as appropriate. `codegraph init` creates the `.codegraph/` marker; without it Polaris records `UNAVAILABLE` and uses source and Git directly. Polaris can only read CodeGraph status, explore indexed relationships, and perform one bounded `codegraph sync` at a declared stage boundary. It never installs, initializes, starts, configures, reconfigures, waits for, or manages CodeGraph or its watcher/daemon/MCP configuration. +Run these commands from the target repository as appropriate. `codegraph init` creates the `.codegraph/` marker; without it Polaris uses source and Git directly and creates no stage record. Polaris writes a Code Intelligence record only when it actually performs a Provider status, sync, or explore operation. Polaris can only read CodeGraph status, explore indexed relationships, and perform one bounded `codegraph sync` at a declared stage boundary. It never installs, initializes, starts, configures, reconfigures, waits for, or manages CodeGraph or its watcher/daemon/MCP configuration. CodeGraph's watcher and connection reconciliation are the primary freshness mechanisms. Polaris records a limited conclusion at the time it checks: `CURRENT_AT_CHECK`, `PARTIAL_STALE`, `INDEX_STALE`, `NOT_VERIFIED`, or `UNAVAILABLE`; it never claims commit-exact graph freshness. A `PARTIAL_STALE` response names specific files: read each current file directly (`READ_SOURCE`), or inspect the registered Git diff if it was deleted (`INSPECT_GIT_DIFF`). For `INDEX_STALE` or `NOT_VERIFIED`, treat the graph only as a lead and search the repository plus Git (`SEARCH_SOURCE`). Validation remains graph-free and relies on source, Git, builds, tests, static checks, and Human Checks. diff --git a/README.zh-CN.md b/README.zh-CN.md index 86e6cdf..871a53d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ [English](README.md) | 简体中文 -> 当前协议版本:`0.1.19`(开发中);Workflow 版本:`0.1.2` +> 当前协议版本:`0.1.20`(开发中);Workflow 版本:`0.1.3` Polaris 是运行在 Coding Agent 宿主上的仓库原生工程工作流。它把需求、计划、实现、独立审查、验证和任务状态保存在 Git 仓库中,并通过确定性门禁防止需求漂移、证据过期和 Agent 自行宣布完成。 @@ -11,17 +11,19 @@ Polaris 是运行在 Coding Agent 宿主上的仓库原生工程工作流。它 ## 核心流程 ```text -DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → IMPLEMENTED - → DOCS_SYNCED → REVIEWING → REVIEWED - → VALIDATING → VERIFIED → CLOSED +DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → REVIEWING → VALIDATING + ├─ R0/R1 → CLOSED + └─ R2 → VERIFIED → CLOSED ``` - Work Item 冻结目标、范围和验收标准。 -- 独立 Implementer 依据不可变 handoff 完成代码、测试和文档。 +- 独立 Implementer 在一个 `IMPLEMENTING` 阶段内依据不可变 handoff 完成代码、测试和文档。 - 独立 Reviewer 审查需求符合性与工程质量。 - Validation 将每项验收标准绑定到可复现证据。 - `events.jsonl` 保存状态变更,`state.json` 是可重建投影。 - Review 和 Validation 绑定当前 Revision、Git commit 与 diff hash;内容变化会使旧证据失效。 +- R0/R1 通过 `PASS_AND_CLOSE` 原子校验并关闭;R2 保留 `VERIFIED` 等待最终 Human approval。 +- 实时 Implementation 进度只是 ignored 的可选遥测,不参与耐久门禁。 - 只有 `transition_task.py` 能通过合法门禁写入 `VERIFIED` 或 `CLOSED`。 任务默认采用 `R1`。低风险机械修改可使用 `R0`;公共接口、持久化格式、架构边界、并发、安全或资源生命周期变更使用 `R2`。 @@ -87,7 +89,7 @@ codegraph init polaris code-intelligence add codegraph --repo . ``` -`codegraph init` 创建 `.codegraph/` marker。只有目标仓库已经有这个 marker 且项目策略允许时,Polaris 才会使用 CodeGraph;没有 marker 时,记录 `UNAVAILABLE` 并直接使用源码和 Git。Polaris 只会读取 `status`、查询 `explore`,以及只在声明的阶段边界至多执行一次有界 `codegraph sync`;它绝不安装、初始化、启动、配置、重新配置、等待或管理 CodeGraph、watcher、daemon 或 MCP 配置。 +`codegraph init` 创建 `.codegraph/` marker。只有目标仓库已经有这个 marker 且项目策略允许时,Polaris 才会使用 CodeGraph;没有 marker 时直接使用源码和 Git,不生成阶段 record。只有实际执行 Provider `status`、`sync` 或 `explore` 操作时才写 Code Intelligence record。Polaris 只会读取 `status`、查询 `explore`,以及只在声明的阶段边界至多执行一次有界 `codegraph sync`;它绝不安装、初始化、启动、配置、重新配置、等待或管理 CodeGraph、watcher、daemon 或 MCP 配置。 CodeGraph 的 watcher 和连接时 reconciliation 是正常情况下的实时更新机制。Polaris 只记录检查时的有限结论:`CURRENT_AT_CHECK`、`PARTIAL_STALE`、`INDEX_STALE`、`NOT_VERIFIED` 或 `UNAVAILABLE`,不会宣称与 Git commit 精确一致。`PARTIAL_STALE` 会精确列出待同步文件:当前普通文件必须直接读取并记录 `READ_SOURCE`;已删除文件必须检查注册 subject 的 Git diff 并记录 `INSPECT_GIT_DIFF`。`INDEX_STALE` 或 `NOT_VERIFIED` 时,图只能作为导航线索,Agent 必须通过仓库搜索和 Git 证据记录 `SEARCH_SOURCE`。Provider 不可用、status 不可读或 sync 失败都不阻断阶段;Validation 不调用 CodeGraph,仍以源码、Git、构建、测试、静态检查和 Human Check 为准。 diff --git a/docs/USAGE.md b/docs/USAGE.md index 194dd4e..2696648 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -2,7 +2,7 @@ 本文面向希望在受支持 Coding Agent 宿主中使用 Polaris 管理软件工程任务的项目成员。当前内置 Codex 与 Claude Code 适配器;本文从首次接入讲到日常提出需求、独立 Implementation、进度查询、Review、验证、恢复与升级。 -> 当前版本:v0.1.19。Polaris v0.1 是仓库原生的 Skills、宿主 worker 定义与 Python 脚本集合,并提供一个只分发到这些脚本的 `polaris` CLI;不提供后台服务或图形界面。 +> 当前协议版本:v0.1.20;Workflow 版本:v0.1.3。Polaris v0.1 是仓库原生的 Skills、宿主 worker 定义与 Python 脚本集合,并提供一个只分发到这些脚本的 `polaris` CLI;不提供后台服务或图形界面。 ## 1. 先理解 Polaris 保存什么 @@ -195,7 +195,7 @@ polaris code-intelligence add codegraph --repo . 前两个命令绝不会由 Polaris 执行;`codegraph init` 创建 `.codegraph/`,它是 Polaris 允许查询的前提。最后一个命令只创建或更新 `.polaris/code-intelligence.json`,将模式设为 `auto_optional`、将 CodeGraph 放到 Provider 优先级首位,并保留已有 `include` / `exclude` 规则。命令可幂等重跑,未知 Provider 或非法旧配置会在写入前拒绝。 -Python CLI 无法直接查看 Codex 或 Claude Code 当前会话中的 MCP 工具,因此成功只表示 Provider 已加入 Polaris;返回的 `runtime_status` 为 `checked_by_next_workflow`。下一次 Workflow 只有在仓库已经存在 `.codegraph/` 时才会检查实际能力;缺少 marker、工具、健康 status 或可解析响应时记录 `UNAVAILABLE` 或 `NOT_VERIFIED`,并继续原有的源码搜索、读取、构建、测试和 Review 流程。 +Python CLI 无法直接查看 Codex 或 Claude Code 当前会话中的 MCP 工具,因此成功只表示 Provider 已加入 Polaris;返回的 `runtime_status` 为 `checked_by_next_workflow`。下一次 Workflow 只有在仓库已经存在 `.codegraph/` 时才会检查实际能力;缺少 marker 或策略禁用时直接继续源码搜索、读取、构建、测试和 Review,不生成阶段 record。只有实际执行 `status`、`sync` 或 `explore` 后才写 record;操作失败时如实记录 `UNAVAILABLE` 或 `NOT_VERIFIED`,但不阻断阶段。 不执行该命令时仍保留默认自动发现。`.polaris/code-intelligence.json` 也可用于禁用 Provider、调整优先级或限制索引范围。例如: @@ -347,7 +347,7 @@ Polaris 每次暂停、等待用户决定或完成一个阶段时,都会在对 - `REQUIREMENTS_NEEDED`:需求仍有会影响方案或验收的未知项; - `WORK_ITEM_PREVIEW`:Work Item 已整理好,等待用户确认冻结; - `PLAN_DECISIONS_NEEDED`:Plan 已形成,但仍有必须由用户选择的方案边界; -- `WORK_ITEM_QUALIFIED`、`PLAN_READY`、`IMPLEMENTATION_FINISHED`、`DOCS_SYNCED`:阶段检查点; +- `WORK_ITEM_QUALIFIED`、`PLAN_READY`:需求与规划检查点; - `IMPLEMENTATION_HANDOFF_READY`:独立实现输入已冻结并注册; - `IMPLEMENTATION_SESSION_STARTED`:宿主已创建或复用独立 Implementer 任务; - `IMPLEMENTATION_PROGRESS`:展示最近有效的本机实现进度,不使用估算百分比; @@ -403,9 +403,9 @@ User action: 请选择“确认并执行”以冻结上述内容并授权自动 默认主路径是: ```text -DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → IMPLEMENTED - → DOCS_SYNCED → REVIEWING → REVIEWED - → VALIDATING → VERIFIED → CLOSED +DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → REVIEWING → VALIDATING + ├─ R0/R1 → CLOSED + └─ R2 → VERIFIED → CLOSED ``` 各阶段含义: @@ -413,14 +413,11 @@ DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → IMPLEMENTED 1. `DRAFT`:把自然语言需求整理成 Work Item。 2. `QUALIFIED`:目标、范围、约束、验收证据、风险和决策所有者已冻结。 3. `PLANNED`:形成结构化 `working-set.json`、变更计划和验收映射。 -4. `IMPLEMENTING`:在冻结范围内修改并运行局部检查。 -5. `IMPLEMENTED`:已有 subject checkpoint commit 和实现证据。 -6. `DOCS_SYNCED`:文档影响已分类,过时知识已处理。 -7. `REVIEWING`:Reviewer 仅依据冻结 handoff 独立审查。 -8. `REVIEWED`:Review 已接受。 -9. `VALIDATING`:逐条机械验证验收标准。 -10. `VERIFIED`:所有验收项 PASS。 -11. `CLOSED`:结果产物齐全,任务关闭。 +4. `IMPLEMENTING`:同一个 Implementer 在冻结范围内完成代码、测试、文档、Implementation 与 Knowledge Delta。 +5. `REVIEWING`:组合门禁已冻结最终 subject 和 Review handoff;Reviewer 仅依据 handoff 独立审查。 +6. `VALIDATING`:Review 已接受,逐条机械验证验收标准。 +7. `VERIFIED`:仅用于 R2;所有验收项 PASS,等待最终 Human approval。 +8. `CLOSED`:R0/R1 由 `PASS_AND_CLOSE` 原子关闭;R2 最终批准后由 `CLOSE` 关闭。 任务状态不得直接编辑。状态转换必须通过: @@ -453,12 +450,12 @@ Polaris 根据风险选择严谨度: 主任务在完成 Planning 和所需预批准后: -1. 执行 `START_IMPLEMENTATION`; -2. 生成不可变 `implementations/rNNN/handoff-NNN.json`; -3. 通过 `DISPATCH_IMPLEMENTATION` 注册 handoff; -4. 在同一本地项目和同一 checkout 创建独立 Implementer 任务; -5. 等待并验证 Implementation artifact,由主任务执行 `FINISH_IMPLEMENTATION`; -6. 续接同一个 Implementer 任务完成 Documentation Sync,再由主任务执行 `SYNC_DOCS`。 +1. 生成不可变 `implementations/rNNN/handoff-NNN.json`; +2. 通过一次 `START_IMPLEMENTATION` 原子校验并注册 handoff; +3. 在同一本地项目和同一 checkout 创建独立 Implementer 任务; +4. 等待并验证 Implementation artifact; +5. 续接同一个 Implementer 任务,在 `IMPLEMENTING` 内完成 Documentation Sync; +6. 主任务构建 Review handoff,并用一次 `START_REVIEW` 注册 Implementation、Knowledge Delta、handoff 和最终 subject。 自动 Implementer 标题固定为: @@ -476,9 +473,9 @@ Implementer 只接收 task ID 和已注册 handoff,不继承主任务聊天。 .polaris/tasks/TASK-0001/runtime/progress.json ``` -它保存当前 phase、有序 `implementation_steps`、最近检查、blocker、用户动作和更新时间,也是这份本机快照的机械权威。每个步骤都有稳定 `STEP-NNN`、标题、状态、关联的 Work Item 验收 ID 和终态结果。当前、已完成和剩余工作由这一个列表推导,不能跳步或回退;发现新工作时只能追加。Implementer 通过明确事件更新它,Polaris 不根据耗时猜测百分比,也不生成内容重复的 Markdown 文件。 +它是可选的本机遥测,保存当前 phase、有序 `implementation_steps`、最近检查、blocker、用户动作和更新时间。每个步骤都有稳定 `STEP-NNN`、标题、状态、关联的 Work Item 验收 ID 和终态结果。当前、已完成和剩余工作由这一个列表推导,不能跳步或回退;发现新工作时只能追加。Implementer 通过明确事件更新它,Polaris 不根据耗时猜测百分比,也不生成内容重复的 Markdown 文件。该文件缺失不会阻断任何耐久转换,最终证据以 Implementation JSON 为准。 -这个目录默认加入 `.gitignore`,因此不会污染工作树,也不会随 Git 在另一台电脑继续。换电脑后,耐久状态仍能恢复到最近 checkpoint;新 Implementer 会创建新的本机进度快照。 +这个目录默认加入 `.gitignore`,因此不会污染工作树,也不会随 Git 在另一台电脑继续。换电脑后,耐久状态仍能恢复;宿主需要实时报告时可创建新的本机进度快照。 ### 9.2 其他查看方式 @@ -605,8 +602,8 @@ polaris migrate --repo . 迁移协议具有以下固定规则: 1. `workflow/migrations.json` 是支持路径的唯一、append-only 注册表;历史步骤必须保留,以便校验已提交的迁移记录。一次命令只允许从当前项目版本迁移到 vendored 版本的一个显式相邻步骤,不推断、不跨级。 -2. 注册步骤同时绑定源/目标 `polaris_version` 与 `workflow_version`。v1 迁移策略不改变冻结 workflow;需要改变 workflow 时必须先升级迁移协议和策略实现。 -3. 项目版本由 `replace_version` 策略更新;每个现有任务由 `append_version_event` 策略追加同状态的 `MIGRATE_POLARIS` 事件,旧 `events.jsonl` 行不可修改。 +2. 注册步骤同时绑定源/目标 `polaris_version` 与 `workflow_version`。Migration protocol v2 支持仅更新版本,也支持显式替换冻结 workflow 并映射任务状态。 +3. `0.1.19 → 0.1.20` 使用 `replace_version_and_workflow` 与 `append_mapped_workflow_event`:冻结 workflow 更新到 `0.1.3`,旧 `IMPLEMENTED` / `DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`。迁移事件记录源/目标状态及旧版本;旧 `events.jsonl` 行不可修改。 4. `.polaris/migrations/MIG--to-.json` 先写为 `IN_PROGRESS`,全部投影更新后改为 `COMPLETED`。迁移锁会记录迁移/任务身份、主机名和 PID;若进程在中间终止,同一主机重新执行命令会接管已死亡的同迁移锁、验证并复用已经追加的事件,不会重复迁移。活跃进程、其他迁移或来源不明的锁不会被自动删除。 5. 迁移完成后脚本自动运行项目校验;`validate_project.py` 会拒绝未完成记录、缺失/伪造的任务迁移事件或版本不一致。 @@ -628,7 +625,9 @@ polaris validate-project --repo . v0.1.18 增加 `polaris code-intelligence add `,用于把已配置 Provider 显式加入 Polaris 流程;Workflow 版本仍为 v0.1.2。 -v0.1.19 将正式 Provider 固定为 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph),引入 watcher/connect reconciliation 下的检查时新鲜度、精确 stale point 和源码回退记录。已提交的 v1 Code Intelligence record 保持不可变,并在迁移中标为 `retired_provider_evidence`;新阶段必须生成 v2 record。Workflow 版本仍为 v0.1.2。 +v0.1.19 将正式 Provider 固定为 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph),引入 watcher/connect reconciliation 下的检查时新鲜度、精确 stale point 和源码回退记录。已提交的 v1 Code Intelligence record 保持不可变,并在迁移中标为 `retired_provider_evidence`。Workflow 版本仍为 v0.1.2。 + +v0.1.20 / Workflow v0.1.3 删除没有独立治理边界的中间状态和事件;`START_IMPLEMENTATION` 与 `START_REVIEW` 各自原子注册所需产物,Review 接受后直接进入 `VALIDATING`,R0/R1 使用 `PASS_AND_CLOSE`。本机进度改为可选遥测;未执行 Provider 操作时不再生成 Code Intelligence record。 ## 13. 失败探索与卡点 @@ -682,7 +681,7 @@ python tools/polaris/scripts/record_exploration.py TASK-0001 --repo . --promote ### 为什么已经实现,还不能说完成 -`IMPLEMENTED` 只表示实现 checkpoint 已形成。还需 Documentation Sync、Review、Validation 和 Result 门禁,状态机才能写入 `CLOSED`。 +实现产物只是 `IMPLEMENTING` 节点内的一部分。还需 Documentation Sync、独立 Review、Validation 和 Result 门禁;只有转换脚本通过完整候选投影校验后才能写入 `CLOSED`。 ### 自动化脚本如何被其他工具读取 diff --git a/plan.md b/plan.md index 4701347..553eb73 100644 --- a/plan.md +++ b/plan.md @@ -2,6 +2,7 @@ > 状态:Implementation underway > 目标版本:v0.1 +> 当前协议:`0.1.20`;Workflow:`0.1.3` > 产品形态:Repo-native Skill System > 宿主 Runtime:声明式可扩展;v0.1 内置 Codex、Claude Code > @@ -416,12 +417,12 @@ Work Item 确认与 Plan 决策是两个独立 Human gate。前者冻结目标 默认主路径: ```text -DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → IMPLEMENTED - → DOCS_SYNCED → REVIEWING → REVIEWED - → VALIDATING → VERIFIED → CLOSED +DRAFT → QUALIFIED → PLANNED → IMPLEMENTING → REVIEWING → VALIDATING + ├─ R0/R1 → CLOSED + └─ R2 → VERIFIED → CLOSED ``` -文档同步在独立 Review 之前完成,使 Reviewer 审查的 subject commit 同时包含代码、测试和项目文档,并让 Review Package 包含对应的 Knowledge Delta。Review 或 Validation 引发返工时,旧 Documentation Sync、Review 和 Validation 均失效,并按 Graph 回到相应节点重新执行。 +Implementation 与 Documentation Sync 在同一个 `IMPLEMENTING` 节点内完成。`START_REVIEW` 一次性校验并注册 Implementation、Knowledge Delta、Review handoff 和最终 subject,使 Reviewer 审查的 commit 同时包含代码、测试和项目文档。Review 或 Validation 引发返工时,下游 Review 和 Validation 证据失效,并按 Graph 回到相应治理节点重新执行。 必须支持以下治理回路: @@ -443,18 +444,15 @@ v0.1 不设置 `FAILED`:可修复失败通过治理回路处理,外部阻塞 |---|---|---| | `QUALIFIED` | 用户确认的冻结 Work Item revision | 必填字段通过;AC statement/evidence 非空且非 `TODO`;Human-owned 未决项为零 | | `PLANNED` | Plan + Plan Decision Register + Working Set | 每个 AC 有验证映射;风险与受影响文档已列出;Human-owned Plan 决策均绑定 CD 且无未决项 | -| `IMPLEMENTING` | `PLANNED`;随后注册 Implementation handoff | R2 已获得实施前 Human approval;`DISPATCH_IMPLEMENTATION` 校验 handoff 与当前 revision/attempt/Plan/Working Set 绑定 | -| `IMPLEMENTED` | 绑定 handoff 的 Implementation record + checkpoint commit | 实现者检查通过;session、handoff hash、所有偏离、subject commit 和 diff hash 已冻结;只有主任务执行转换 | -| `DOCS_SYNCED` | Knowledge Delta + docs checkpoint commit | 无未处置 STALE;必要 Decision/Exploration 已落盘;最终 Review subject 已冻结 | -| `REVIEWING` | 冻结 revision + subject base/head commit + subject diff hash + evidence | R1/R2 Reviewer session 与 implementer session 独立;R0 可使用隔离式同会话 Review | -| `REVIEWED` | 当前 revision/subject 对应的 Review JSON | verdict=`ACCEPT`;所需 Reviewer 数量满足;blocking findings 为零 | -| `VALIDATING` | Validation plan | Review 针对当前 revision、subject head commit 和 subject diff hash | -| `VERIFIED` | 当前 subject 对应的 Validation JSON | 所有 AC 为 PASS;验证命令退出码有效 | -| `CLOSED` | Result JSON | `validate_task.py` 全 PASS;R2 已获最终 Human approval | +| `IMPLEMENTING` | Plan + Working Set + Implementation handoff | `START_IMPLEMENTATION` 原子注册 handoff;R2 已获得实施前 Human approval;handoff 与当前 revision/attempt/Plan/Working Set 绑定 | +| `REVIEWING` | Implementation + Knowledge Delta + Review handoff + 冻结 subject | `START_REVIEW` 组合门禁校验实现、文档、handoff、session、commit/diff;无未处置 STALE | +| `VALIDATING` | 当前 revision/subject 对应的 accepted Review | 所需 Reviewer 数量满足;blocking findings 为零;Review 直接通过 `ACCEPT_REVIEW` 进入本状态 | +| `VERIFIED` | 当前 subject 对应的 PASS Validation | 仅 R2 使用;所有 AC 为 PASS,等待最终 Human approval | +| `CLOSED` | PASS Validation + Result | R0/R1 通过 `PASS_AND_CLOSE` 原子校验候选投影;R2 另需最终 Human approval 后 `CLOSE` | `.polaris/workflow.json` 保存当前项目实际使用且版本锁定的节点、边、依赖和门禁 ID;`tools/polaris/workflow/default-workflow.json` 只用于初始化。`transition_task.py` 只接受图中边并先运行对应 validators,Skill 不直接编辑 `state` 字段。v0.1 遇到 `polaris_version` 或 `workflow_version` 不匹配时拒绝正常执行,不做隐式迁移。 -版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。v1 支持 `replace_version` 项目策略和 `append_version_event` 任务策略:后者为每个任务追加保持原状态的 `MIGRATE_POLARIS` 事件,不改写 append-only 历史。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED` 和各任务前后 sequence;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、冻结 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。改变 workflow 或数据形态的新迁移,必须先增加新的声明式策略和针对性测试。 +版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。Migration protocol v2 保留 `replace_version` / `append_version_event`,并增加 `replace_version_and_workflow` / `append_mapped_workflow_event`。`0.1.19 → 0.1.20` 原子替换冻结 workflow 为 `0.1.3`,追加带源/目标状态及旧版本字段的迁移事件;旧 `IMPLEMENTED`、`DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`,其余治理状态保持含义。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED`、各任务 sequence 和状态映射;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、未声明的 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。 迁移占用任务转换锁时必须写入结构化 owner:迁移 ID、任务 ID、主机名、PID 和创建时间。重跑只允许接管同一迁移在同一主机上、且原 PID 已确认不存在的锁;活跃 PID、其他迁移、其他主机、空锁或损坏锁一律拒绝。这样既能从进程崩溃或机器重启恢复,又不把真实并发误判为遗留锁。 @@ -489,11 +487,11 @@ AGENTS.md ### 可选 Code Intelligence 协议 - v0.1 的唯一正式 Provider 是 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph)。`providers/code-intelligence/codegraph.json` 声明其 MCP `codegraph_explore` 和 CLI `status`、`explore`、`sync` 能力;核心 record 使用 Provider-neutral 的新鲜度和回退字段。 -- `.codegraph/` 由用户创建和维护。Polaris 允许用户显式运行 `polaris code-intelligence add codegraph --repo .`,但绝不安装、初始化、启动或配置 Provider、watcher、daemon、锁或 MCP;缺少 marker 时记录 `UNAVAILABLE` 并直接回退源码。 +- `.codegraph/` 由用户创建和维护。Polaris 允许用户显式运行 `polaris code-intelligence add codegraph --repo .`,但绝不安装、初始化、启动或配置 Provider、watcher、daemon、锁或 MCP;缺少 marker 或策略禁用时直接回退源码,不生成新的阶段 record。 - Provider 原生 watcher 与连接时 reconciliation 是保持索引接近工作树的主机制。Polaris 只在阶段入口、已知索引冻结或最终 Documentation Sync 的有界点读取 status;仅在 status 表示 pending 时至多执行一次 `codegraph sync`,随后至多复查一次,绝不等待或轮询。 - 记录的结论限定为检查时:`CURRENT_AT_CHECK`、`PARTIAL_STALE`、`INDEX_STALE`、`NOT_VERIFIED` 或 `UNAVAILABLE`,不得宣称与某个 Git commit 严格一致。逐文件 stale point 必须记录路径和原因;文件仍存在时 Agent 直接读取源码并记录 `READ_SOURCE`,已删除时检查注册 subject 的 Git diff 并记录 `INSPECT_GIT_DIFF`;索引级失效使用 `SEARCH_SOURCE` 和 Git 证据。 - Planning、Implementation 与 Reviewer 只在冻结范围内使用图关系;返回路径必须经源码确认才可进入 Working Set,Reviewer 必须独立查询。响应的局部 stale 不会丢弃其余图线索,但 stale 路径不能直接作为编辑或 Review 结论。 -- Provider 不可用、能力缺失、超时、错误响应或同步失败都必须记录准确的新鲜度并继续既有流程。图不扩展 scope,也不是 Workflow gate;Validation 完全不调用 CodeGraph,仍只依赖源码、Git、构建、测试、静态检查和 Human Check。 +- 只有阶段实际执行 Provider `status`、`sync` 或 `explore` 操作时才写耐久 record,并准确记录成功、失败和新鲜度;未执行操作时省略 artifact 引用。图不扩展 scope,也不是 Workflow gate;Validation 完全不调用 CodeGraph,仍只依赖源码、Git、构建、测试、静态检查和 Human Check。 - Git 只保存绑定 Provider、阶段、subject、目的、有限摘要、响应哈希、新鲜度、stale point 与源码回退证据;原始响应只进入 ignored runtime。已提交 v1 record 是不可变历史证据,迁移后标为 `retired_provider_evidence`,不能支持新的新鲜度结论。 ## 9. 确定性脚本 @@ -540,16 +538,16 @@ Validation evidence 至少记录 `acceptance_id / command_or_check / cwd / envir ### Independent Implementation 1. 主任务是唯一用户入口和状态机所有者;自动路径中不修改 subject,只负责生成/注册 handoff、派发或续接 Worker、等待、读取进度、校验产物和执行转换。 -2. `START_IMPLEMENTATION` 后生成 `implementations/rNNN/handoff-NNN.json`,再通过 `DISPATCH_IMPLEMENTATION` 自转换注册。handoff 冻结 Work Item、Plan、Working Set、项目规则、subject base、prior Review(返工时)、确定性输出路径和实时进度路径。 +2. 先生成 `implementations/rNNN/handoff-NNN.json`,再由 `START_IMPLEMENTATION` 原子校验和注册。handoff 冻结 Work Item、Plan、Working Set、项目规则、subject base、prior Review(返工时)、确定性输出路径和可选实时进度路径。 3. Work Item 的 `implementation_dispatch.authorized=true` 是“确认并执行”对当前 revision 全部 Implementer attempts 的显式授权。宿主按自身执行附录在同一本地项目和 checkout 创建隔离 worker;worker 不继承主聊天,也不默认使用 worktree。Codex 的 worker 是可见新任务,Claude Code 的 worker 是保留 agent ID 的非 fork `polaris-implementer` subagent。 4. Implementer 标题固定为 `Polaris Implement · · · attempt `。创建前先复用与 handoff 绑定的有效 Implementation artifact,其次只按适配器声明的稳定身份复用唯一 worker。多条或不明确记录时不得猜测,回退同会话执行。 5. Implementer 只接收 task ID 与已注册 handoff,不接收主聊天、实现建议或预期结果。它拥有本轮代码、测试、构建文件和项目文档的单写者权限,但不执行 Graph 转换、Review、Validation 或关闭。 -6. 主任务先用 `INITIALIZE` 创建空的 `QUEUED` 快照;Implementer 在改代码前用 `DEFINE_STEPS` 建立有序、非空且绑定 Work Item 验收 ID 的 `implementation_steps`。每步使用稳定 `STEP-NNN`,只能通过 `START_STEP / COMPLETE_STEP / BLOCK_STEP / RESUME_STEP / SKIP_STEP` 线性推进;新发现工作只能用 `APPEND_STEP` 加到末尾,不能重排、删除、改名或回退。测试证据用 `ADD_CHECK` 追加,阶段用 `SET_PHASE` 更新。 -7. `.polaris/tasks//runtime/progress.json` 只保留一份有序步骤权威;current、completed 和 remaining 均由步骤状态推导。所有步骤终态后才能进入 `CHECKPOINTING`,Implementation artifact 必须复制完全一致的 `step_results`。主任务按需格式化展示,不生成 Markdown 副本、Task DAG 或主观百分比。 +6. `implementation_steps` 在 Implementation artifact 中形成耐久终态证据。宿主需要实时报告时可用 `INITIALIZE / DEFINE_STEPS / START_STEP / COMPLETE_STEP / BLOCK_STEP / RESUME_STEP / SKIP_STEP / APPEND_STEP` 维护 ignored 快照;不能重排、删除、改名或回退。 +7. `.polaris/tasks//runtime/progress.json` 是可选本机遥测;存在时 current、completed 和 remaining 由步骤状态推导,主任务按需格式化展示。门禁不得要求该 ignored 文件存在,也不得要求它与耐久 `step_results` 完全相等;不生成 Markdown 副本、Task DAG 或主观百分比。 8. 每个任务的 `runtime/` 子目录默认 Git ignored,不影响工作树 checkpoint,也不承诺跨电脑恢复。正式 Implementation、Knowledge Delta、commit/diff 和 event 继续写入耐久 Authority。主任务可随时读取进度;若整个宿主停止运行,快照只代表最后一次成功更新。 -9. Implementation artifact 必须绑定 handoff path/hash、Implementer session 和终态 `step_results`。主任务验证后执行 `FINISH_IMPLEMENTATION`,再按适配器声明的稳定身份续接同一个 Implementer worker 执行渲染后的 `documentation-sync` Skill;Worker 写回 Knowledge Delta 和最终 subject checkpoint,主任务执行 `SYNC_DOCS`。 +9. Implementation artifact 必须绑定 handoff path/hash、Implementer session 和终态 `step_results`。同一个 Implementer worker 随后在 `IMPLEMENTING` 内执行 `documentation-sync`,写回 Knowledge Delta 和最终 subject checkpoint。主任务构建 Review handoff,并用一次 `START_REVIEW` 组合校验和注册全部产物。 10. Review 或 Validation 返工生成新 attempt、新 handoff 和新的 Implementer 任务;prior Review 通过 handoff 传递,Implementer 写 Review Response。不同 attempt 不复用 Implementer session。 -11. 宿主缺少创建、查找、等待或续接能力时,主任务使用同一 handoff 执行 `same_session` fallback,仍更新进度文件并明确提示即时状态响应可能延迟;不得仅因宿主能力不足把业务任务置为 `BLOCKED`。 +11. 宿主缺少创建、查找、等待或续接能力时,主任务使用同一 handoff 执行 `same_session` fallback;已有进度文件可继续更新,但不得为了门禁新建它。需明确提示即时状态响应可能延迟;不得仅因宿主能力不足把业务任务置为 `BLOCKED`。 ### Independent Review @@ -584,7 +582,7 @@ Validation evidence 至少记录 `acceptance_id / command_or_check / cwd / envir ### Durability checkpoint - Vendored Skills、`tools/polaris/`、`.polaris/` 的耐久状态和任务代码均纳入 Git;`.polaris/tasks//runtime/` 是明确忽略的本机瞬时例外。 -- `IMPLEMENTED`、`DOCS_SYNCED`、`REVIEWED`、`VERIFIED` 必须引用一个本地 checkpoint commit;Polaris 不自动 push、merge 或发布。 +- `REVIEWING`、`VALIDATING`、`VERIFIED` 和 `CLOSED` 的 subject 证据必须绑定本地 checkpoint commit;Polaris 不自动 push、merge 或发布。 - Fresh-session 可以继续当前工作树;Fresh-clone 只保证恢复到最近一次已提交的阶段边界,不承诺恢复尚未保存或尚未提交的编辑器内容。 - Review 和 Validation 只接受 Git commit SHA,不接受 working-tree marker。创建 checkpoint 前必须识别并保护用户已有的无关改动,不能把不属于 Task scope 的变化混入证据 commit。 diff --git a/tests/test_codegraph.py b/tests/test_codegraph.py index c4e0f3b..075f6fa 100644 --- a/tests/test_codegraph.py +++ b/tests/test_codegraph.py @@ -148,8 +148,19 @@ def test_managed_surfaces_only_name_the_official_codegraph(self) -> None: self.assertIn(official, path.read_text(encoding="utf-8"), path.relative_to(ROOT).as_posix()) for path in [ROOT / "README.md", ROOT / "README.zh-CN.md"]: text = path.read_text(encoding="utf-8") - self.assertIn("0.1.19", text, path.relative_to(ROOT).as_posix()) - self.assertIn("0.1.2", text, path.relative_to(ROOT).as_posix()) + self.assertIn("0.1.20", text, path.relative_to(ROOT).as_posix()) + self.assertIn("0.1.3", text, path.relative_to(ROOT).as_posix()) + + def test_authority_surfaces_publish_workflow_013(self) -> None: + for path in [ + ROOT / "README.md", + ROOT / "README.zh-CN.md", + ROOT / "docs" / "USAGE.md", + ROOT / "plan.md", + ]: + text = path.read_text(encoding="utf-8") + self.assertIn("0.1.20", text, path.relative_to(ROOT).as_posix()) + self.assertIn("0.1.3", text, path.relative_to(ROOT).as_posix()) def test_readmes_keep_codegraph_operational_boundaries(self) -> None: """User-facing authorities retain the source-fallback and ownership boundaries.""" From 5b60df4310445abda97c23b37397518870bd0621 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 23:11:53 +0800 Subject: [PATCH 09/17] fix: complete simplified workflow rework paths --- docs/USAGE.md | 2 + ...26-08-18-workflow-simplification-design.md | 2 +- plan.md | 4 +- pyproject.toml | 2 +- scripts/build_review_handoff.py | 17 + skills/engineering-task/SKILL.md | 4 +- tests/test_core.py | 588 ++++++------------ workflow/default-workflow.json | 3 +- 8 files changed, 200 insertions(+), 422 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 2696648..7853167 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -457,6 +457,8 @@ Polaris 根据风险选择严谨度: 5. 续接同一个 Implementer 任务,在 `IMPLEMENTING` 内完成 Documentation Sync; 6. 主任务构建 Review handoff,并用一次 `START_REVIEW` 注册 Implementation、Knowledge Delta、handoff 和最终 subject。 +首次执行时,`START_IMPLEMENTATION` 完成 `PLANNED → IMPLEMENTING`;Review 或 Validation 返工时,它作为 `IMPLEMENTING → IMPLEMENTING` 自转换注册下一 attempt 的新 handoff,避免恢复已失效的旧实施交接。 + 自动 Implementer 标题固定为: ```text diff --git a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md index fca7ec5..2a38cef 100644 --- a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md +++ b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md @@ -94,7 +94,7 @@ BLOCKED -- RESOLVE_BLOCK --> blocked_from ### 开始 Implementation -`START_IMPLEMENTATION` 执行 `PLANNED → IMPLEMENTING`,并要求在同一次转换中提交 Implementation handoff。门禁合并检查: +`START_IMPLEMENTATION` 首次执行 `PLANNED → IMPLEMENTING`;Review 或 Validation 返工时执行 `IMPLEMENTING → IMPLEMENTING` 自转换。两种情况都要求在同一次转换中提交当前 attempt 的 Implementation handoff。门禁合并检查: - R2 实施前批准; - handoff 的身份、revision、attempt、Plan、Working Set 和 package。 diff --git a/plan.md b/plan.md index 553eb73..0fb4da4 100644 --- a/plan.md +++ b/plan.md @@ -444,7 +444,7 @@ v0.1 不设置 `FAILED`:可修复失败通过治理回路处理,外部阻塞 |---|---|---| | `QUALIFIED` | 用户确认的冻结 Work Item revision | 必填字段通过;AC statement/evidence 非空且非 `TODO`;Human-owned 未决项为零 | | `PLANNED` | Plan + Plan Decision Register + Working Set | 每个 AC 有验证映射;风险与受影响文档已列出;Human-owned Plan 决策均绑定 CD 且无未决项 | -| `IMPLEMENTING` | Plan + Working Set + Implementation handoff | `START_IMPLEMENTATION` 原子注册 handoff;R2 已获得实施前 Human approval;handoff 与当前 revision/attempt/Plan/Working Set 绑定 | +| `IMPLEMENTING` | Plan + Working Set + Implementation handoff | `START_IMPLEMENTATION` 原子注册 handoff;首次执行为 `PLANNED → IMPLEMENTING`,Review/Validation 返工时以 `IMPLEMENTING → IMPLEMENTING` 自转换注册下一 attempt;R2 已获得实施前 Human approval;handoff 与当前 revision/attempt/Plan/Working Set 绑定 | | `REVIEWING` | Implementation + Knowledge Delta + Review handoff + 冻结 subject | `START_REVIEW` 组合门禁校验实现、文档、handoff、session、commit/diff;无未处置 STALE | | `VALIDATING` | 当前 revision/subject 对应的 accepted Review | 所需 Reviewer 数量满足;blocking findings 为零;Review 直接通过 `ACCEPT_REVIEW` 进入本状态 | | `VERIFIED` | 当前 subject 对应的 PASS Validation | 仅 R2 使用;所有 AC 为 PASS,等待最终 Human approval | @@ -538,7 +538,7 @@ Validation evidence 至少记录 `acceptance_id / command_or_check / cwd / envir ### Independent Implementation 1. 主任务是唯一用户入口和状态机所有者;自动路径中不修改 subject,只负责生成/注册 handoff、派发或续接 Worker、等待、读取进度、校验产物和执行转换。 -2. 先生成 `implementations/rNNN/handoff-NNN.json`,再由 `START_IMPLEMENTATION` 原子校验和注册。handoff 冻结 Work Item、Plan、Working Set、项目规则、subject base、prior Review(返工时)、确定性输出路径和可选实时进度路径。 +2. 先生成 `implementations/rNNN/handoff-NNN.json`,再由 `START_IMPLEMENTATION` 原子校验和注册。首次执行为 `PLANNED → IMPLEMENTING`;Review 或 Validation 返工时使用 `IMPLEMENTING → IMPLEMENTING` 自转换注册下一 attempt 的 handoff。handoff 冻结 Work Item、Plan、Working Set、项目规则、subject base、prior Review(返工时)、确定性输出路径和可选实时进度路径。 3. Work Item 的 `implementation_dispatch.authorized=true` 是“确认并执行”对当前 revision 全部 Implementer attempts 的显式授权。宿主按自身执行附录在同一本地项目和 checkout 创建隔离 worker;worker 不继承主聊天,也不默认使用 worktree。Codex 的 worker 是可见新任务,Claude Code 的 worker 是保留 agent ID 的非 fork `polaris-implementer` subagent。 4. Implementer 标题固定为 `Polaris Implement · · · attempt `。创建前先复用与 handoff 绑定的有效 Implementation artifact,其次只按适配器声明的稳定身份复用唯一 worker。多条或不明确记录时不得猜测,回退同会话执行。 5. Implementer 只接收 task ID 与已注册 handoff,不接收主聊天、实现建议或预期结果。它拥有本轮代码、测试、构建文件和项目文档的单写者权限,但不执行 Graph 转换、Review、Validation 或关闭。 diff --git a/pyproject.toml b/pyproject.toml index b71b7c9..d5c11c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "corona-polaris" -version = "0.1.19" +version = "0.1.20" description = "Repo-native AI engineering workflow command dispatcher" requires-python = ">=3.10" dependencies = [] diff --git a/scripts/build_review_handoff.py b/scripts/build_review_handoff.py index c63cfb8..3e9128f 100644 --- a/scripts/build_review_handoff.py +++ b/scripts/build_review_handoff.py @@ -93,6 +93,7 @@ def build( knowledge_path: Path | None = None, subject_base: str | None = None, subject_head: str | None = None, + review_response_path: Path | None = None, ) -> dict[str, Any]: root = protocol_root(repo) directory = task_dir(repo, task_id) @@ -134,6 +135,20 @@ def build( "head_commit": head, "diff_hash": subject_diff_hash(repo, base, head), } + if review_response_path is not None: + resolved_response = review_response_path.resolve() + try: + relative_response = resolved_response.relative_to(directory.resolve()) + except ValueError as exc: + raise RuleFailure( + f"review response must be inside the task directory: {resolved_response}" + ) from exc + if not resolved_response.is_file(): + raise RuleFailure(f"review response does not exist: {resolved_response}") + state["artifacts"]["review_response"] = { + "path": relative_response.as_posix(), + "sha256": file_sha256(resolved_response), + } implementation_reference = normalized_reference( directory, state["artifacts"].get("implementation") ) @@ -286,6 +301,7 @@ def main() -> int: parser.add_argument("--knowledge-delta", type=Path) parser.add_argument("--subject-base") parser.add_argument("--subject-head") + parser.add_argument("--review-response", type=Path) parser.add_argument("--repo", type=Path, default=Path.cwd()) parser.add_argument("--json", action="store_true") args = parser.parse_args() @@ -299,6 +315,7 @@ def main() -> int: args.knowledge_delta, args.subject_base, args.subject_head, + args.review_response, ), args.json, ) diff --git a/skills/engineering-task/SKILL.md b/skills/engineering-task/SKILL.md index b24e327..af9b255 100644 --- a/skills/engineering-task/SKILL.md +++ b/skills/engineering-task/SKILL.md @@ -43,7 +43,7 @@ Never report an anticipated state. Reload authority after every transition. A st ## Independent Implementation 7. Before Implementation, require frozen `implementation_dispatch` authority with `mode=auto_new_task`, `fallback=same_session`, `same_local_project=true`, and `authorized=true`. The same `Confirm and execute` answer authorizes every Implementer task for the revision, including rework attempts up to the graph limit. -8. Run `build_implementation_handoff.py`, then use `START_IMPLEMENTATION` after required R2 pre-approval to atomically validate and register the exact returned handoff path. Reload state and emit `IMPLEMENTATION_HANDOFF_READY`. Never assemble task-relative paths independently of `task_layout.py`. +8. Run `build_implementation_handoff.py`, then use `START_IMPLEMENTATION` after required R2 pre-approval to atomically validate and register the exact returned handoff path. The initial transition is `PLANNED → IMPLEMENTING`; after Review or Validation rework, use the same event as an `IMPLEMENTING → IMPLEMENTING` self-transition to register the next attempt's handoff. Reload state and emit `IMPLEMENTATION_HANDOFF_READY`. Never assemble task-relative paths independently of `task_layout.py`. 9. Use the exact worker title `Polaris Implement · · · attempt `. Dispatch a fresh isolated worker in the same local checkout through the host appendix. Never inherit the main conversation or use a separate worktree by default. 10. Before creation, first reuse a valid Implementation artifact bound to the registered handoff. Otherwise reuse or resume only one unambiguous worker identity as defined by the host appendix. Never create a duplicate for the same task, revision, and attempt; ambiguous identity requires the same-session fallback rather than guessing. 11. Give the Implementer only the task ID and registered handoff path. Use this exact prompt: `Use {{skill:implementation}} for . Load only and its package as task context; read state.json only to verify the registered handoff. Work in the shared local checkout, define and execute linear implementation_steps through update_implementation_progress.py, write the immutable Implementation JSON at output_path with matching step_results, and return its path. Do not run task transitions, Review, Validation, or close the task.` Do not include main-chat history or implementation advice. @@ -60,7 +60,7 @@ Never report an anticipated state. Reload authority after every transition. A st 19. Use the exact worker title `Polaris Review · · · attempt · reviewer `. Before creation, first accept a valid deterministic Review artifact. Otherwise dispatch a fresh isolated Reviewer and reuse only one unambiguous identity through the host appendix. Never reuse an Implementer or another Reviewer slot, inherit implementation chat, or create a duplicate; ambiguous identity uses manual fallback. 20. Give the Reviewer only the task ID, Reviewer slot, registered handoff path. Use this exact prompt: `Use {{skill:adversarial-review}} for , Reviewer slot . Load only and its package. Write the immutable Review JSON and return its verdict and path. Do not modify implementation or run task transitions.` Do not include implementation explanations, chat history, proposed findings, or expected verdicts. 21. After dispatch, emit `REVIEW_SESSION_STARTED` with the nine fields plus the adapter-defined `Review task` reference, `Reviewer slot`, `Handoff`, and `Dispatch mode`; set `User action` to `None` while the worker is running. If worker creation, lookup, or waiting fails, keep state `REVIEWING`, emit `REVIEW_HANDOFF_READY`, and provide the exact manual new-session prompt using the rendered Skill syntax; do not enter `BLOCKED` solely because host automation is unavailable. -22. Dispatch required Reviewers sequentially. On the first `REJECT`, register the artifact and run `REJECT_REVIEW`; on rework, generate a new handoff and a fresh Implementer attempt. When all required Reviewers `ACCEPT`, register slot 1 as `review` and slot 2 as `review_2` when required, then run `ACCEPT_REVIEW`. Reviewer session IDs must be distinct from each other and the Implementer. +22. Dispatch required Reviewers sequentially. On the first `REJECT`, register the artifact and run `REJECT_REVIEW`; on rework, generate a new handoff, register it with the `START_IMPLEMENTATION` self-transition, and dispatch a fresh Implementer attempt. When all required Reviewers `ACCEPT`, register slot 1 as `review` and slot 2 as `review_2` when required, then run `ACCEPT_REVIEW`. Reviewer session IDs must be distinct from each other and the Implementer. 23. Never write a Review verdict in the main or Implementer task. Only Reviewer tasks write `ACCEPT` or `REJECT`. ## Completion and gates diff --git a/tests/test_core.py b/tests/test_core.py index 9d55ff1..9b5f33d 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -185,6 +185,10 @@ def test_workflow_013_contains_only_governance_states(self) -> None: self.assertNotIn("FINISH_IMPLEMENTATION", events) self.assertNotIn("SYNC_DOCS", events) self.assertNotIn("START_VALIDATION", events) + self.assertEqual( + events["START_IMPLEMENTATION"]["from"], + ["PLANNED", "IMPLEMENTING"], + ) self.assertEqual(events["START_REVIEW"]["from"], ["IMPLEMENTING"]) self.assertEqual(events["ACCEPT_REVIEW"]["to"], "VALIDATING") self.assertEqual(events["PASS_AND_CLOSE"]["to"], "CLOSED") @@ -340,6 +344,10 @@ def enter_planned(self) -> None: def enter_implementing(self) -> None: self.enter_planned() + self.register_implementation_handoff() + + def register_implementation_handoff(self) -> dict[str, object]: + """Register the next deterministic handoff for initial work or rework.""" handoff = build_implementation_handoff(self.repo, "TASK-0001") handoff_path = Path(handoff["path"]) artifacts = [ @@ -362,6 +370,7 @@ def enter_implementing(self) -> None: None, None, ) + return handoff def test_start_implementation_atomically_registers_handoff(self) -> None: """START_IMPLEMENTATION 同时校验并注册不可变 Implementer 输入。""" @@ -567,21 +576,47 @@ def start_review( implementer_session_id: str = "impl-session", isolation: str = "fresh_session", ) -> tuple[dict[str, object], dict[str, object]]: - handoff_result = build_review_handoff( - self.repo, - "TASK-0001", - implementer_session_id, - isolation, + state = read_json(self.task / "state.json") + if state["status"] == "REVIEWING": + reference = state["artifacts"]["review_handoff"] + return read_json(self.task / reference["path"]), state + handoff_result, implementation_path, knowledge_path, base, head = ( + self.build_current_review_handoff(implementer_session_id, isolation) ) handoff_path = Path(handoff_result["path"]) transition( self.repo, "TASK-0001", "START_REVIEW", - [f"review_handoff={handoff_path.relative_to(self.task).as_posix()}"], - None, - None, + [ + "implementation=" + implementation_path.relative_to(self.task).as_posix(), + "knowledge_delta=" + knowledge_path.relative_to(self.task).as_posix(), + "review_handoff=" + handoff_path.relative_to(self.task).as_posix(), + ] + + ( + [ + "review_response=" + + ( + self.task + / "reviews" + / "r001" + / f"response-{read_json(implementation_path)['artifact_attempt']:03d}.json" + ).relative_to(self.task).as_posix() + ] + if ( + read_json(implementation_path)["artifact_attempt"] > 1 + and ( + self.task + / "reviews" + / "r001" + / f"response-{read_json(implementation_path)['artifact_attempt']:03d}.json" + ).is_file() + ) + else [] + ), None, + base, + head, None, None, None, @@ -589,6 +624,41 @@ def start_review( state = read_json(self.task / "state.json") return read_json(handoff_path), state + def build_current_review_handoff( + self, + implementer_session_id: str = "impl-session", + isolation: str = "fresh_session", + ) -> tuple[dict[str, object], Path, Path, str, str]: + state = read_json(self.task / "state.json") + handoff_reference = state["artifacts"]["implementation_handoff"] + implementation_handoff = read_json(self.task / handoff_reference["path"]) + attempt = implementation_handoff["artifact_attempt"] + implementation_path = ( + self.task / "implementations" / "r001" / f"attempt-{attempt:03d}.json" + ) + knowledge_path = ( + self.task / "knowledge" / "r001" / f"knowledge-delta-{attempt:03d}.json" + ) + implementation = read_json(implementation_path) + base = implementation["subject_base_commit"] + head = implementation["subject_head_commit"] + response_candidate = ( + self.task / "reviews" / "r001" / f"response-{attempt:03d}.json" + ) + response_path = response_candidate if response_candidate.is_file() else None + handoff_result = build_review_handoff( + self.repo, + "TASK-0001", + implementer_session_id, + isolation, + implementation_path, + knowledge_path, + base, + head, + response_path, + ) + return handoff_result, implementation_path, knowledge_path, base, head + def test_start_review_does_not_require_live_progress(self) -> None: """耐久 Review 门禁不依赖 ignored 的本机进度快照。""" self.enter_implementing() @@ -965,6 +1035,8 @@ def finish_and_reject_attempt( reviewer_session_id: str, finding: dict[str, object], ) -> dict[str, object]: + if "implementation_handoff" not in read_json(self.task / "state.json")["artifacts"]: + self.register_implementation_handoff() self.dispatch_implementation() (self.repo / "subject.txt").write_text( f"review attempt {attempt}\n", encoding="utf-8" @@ -978,9 +1050,6 @@ def finish_and_reject_attempt( ) implementation = self.implementation_value(base, head, implementer_session_id) write_json_atomic(implementation_path, implementation) - artifacts = [ - f"implementation=implementations/r001/attempt-{attempt:03d}.json" - ] if attempt > 1: prior_reference = read_json(self.task / "state.json")["artifacts"][ "prior_review" @@ -1009,19 +1078,6 @@ def finish_and_reject_attempt( } ) write_json_atomic(response_path, response) - artifacts.append(f"review_response=reviews/r001/response-{attempt:03d}.json") - transition( - self.repo, - "TASK-0001", - "FINISH_IMPLEMENTATION", - artifacts, - None, - base, - head, - None, - None, - None, - ) knowledge_path = ( self.task / "knowledge" @@ -1033,18 +1089,6 @@ def finish_and_reject_attempt( {"changed_paths": ["subject.txt"], "evidence": "No documentation impact"} ) write_json_atomic(knowledge_path, knowledge) - transition( - self.repo, - "TASK-0001", - "SYNC_DOCS", - [f"knowledge_delta=knowledge/r001/knowledge-delta-{attempt:03d}.json"], - None, - base, - head, - None, - None, - None, - ) handoff, state = self.start_review(implementer_session_id) current_finding = copy.deepcopy(finding) if attempt > 1: @@ -1644,11 +1688,15 @@ def test_pending_plan_decision_blocks_until_human_authority_is_bound(self) -> No with self.assertRaises(RuleFailure): validate(self.repo, "TASK-0001") + handoff = build_implementation_handoff(self.repo, "TASK-0001") transition( self.repo, "TASK-0001", "START_IMPLEMENTATION", - [], + [ + "implementation_handoff=" + + Path(handoff["path"]).relative_to(self.task).as_posix() + ], None, None, None, @@ -1656,7 +1704,6 @@ def test_pending_plan_decision_blocks_until_human_authority_is_bound(self) -> No None, None, ) - handoff = build_implementation_handoff(self.repo, "TASK-0001") package = read_json(Path(handoff["path"]))["package"] self.assertIn("plan_decisions", {entry["role"] for entry in package}) @@ -1949,7 +1996,7 @@ def test_every_normal_writer_uses_the_protocol_compatibility_gate(self) -> None: with self.assertRaisesRegex(RuleFailure, "frozen workflow"): require_protocol_compatible(self.repo) - workflow["workflow_version"] = "0.1.2" + workflow["workflow_version"] = "0.1.3" write_json_atomic(workflow_path, workflow) state = read_json(self.task / "state.json") state["polaris_version"] = "0.1.10" @@ -3397,12 +3444,12 @@ def test_implementer_receives_only_handoff_and_cannot_transition(self) -> None: self.assertIn("Give the Implementer only the task ID and registered handoff path", entry_text) self.assertIn("Load only and its package", entry_text) self.assertIn("Do not read the main conversation", implementation_text) - self.assertIn("Do not run `FINISH_IMPLEMENTATION`", implementation_text) - self.assertIn("Continue the exact same Implementer worker", entry_text) - self.assertIn("Do not run `SYNC_DOCS`", docs_text) + self.assertIn("Do not run workflow transitions", implementation_text) + self.assertIn("continue the exact same Implementer worker", entry_text) + self.assertIn("Do not run workflow transitions", docs_text) def test_implementation_handoff_and_result_are_mechanically_bound(self) -> None: - """DISPATCH_IMPLEMENTATION 注册不可变 handoff,未绑定该 handoff 的实现结果不能完成。""" + """START_REVIEW 拒绝未绑定已注册 handoff 的 Implementation。""" self.enter_implementing() handoff, reference = self.dispatch_implementation() state = read_json(self.task / "state.json") @@ -3422,12 +3469,34 @@ def test_implementation_handoff_and_result_are_mechanically_bound(self) -> None: implementation = self.implementation_value(base, head, "impl-bound-session") implementation["implementation_handoff_sha256"] = "0" * 64 write_json_atomic(path, implementation) + knowledge_path = self.task / "knowledge" / "r001" / "knowledge-delta-001.json" + knowledge = self.knowledge_value(1, base, head) + knowledge["entries"][0].update( + {"changed_paths": ["subject.txt"], "evidence": "No documentation impact"} + ) + write_json_atomic(knowledge_path, knowledge) + handoff_result = build_review_handoff( + self.repo, + "TASK-0001", + "impl-bound-session", + "fresh_session", + path, + knowledge_path, + base, + head, + ) + review_handoff_path = Path(handoff_result["path"]) with self.assertRaises(RuleFailure): transition( self.repo, "TASK-0001", - "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-001.json"], + "START_REVIEW", + [ + "implementation=implementations/r001/attempt-001.json", + "knowledge_delta=knowledge/r001/knowledge-delta-001.json", + "review_handoff=" + + review_handoff_path.relative_to(self.task).as_posix(), + ], None, base, head, @@ -3435,13 +3504,29 @@ def test_implementation_handoff_and_result_are_mechanically_bound(self) -> None: None, None, ) + review_handoff_path.unlink() implementation["implementation_handoff_sha256"] = reference["sha256"] write_json_atomic(path, implementation) + handoff_result = build_review_handoff( + self.repo, + "TASK-0001", + "impl-bound-session", + "fresh_session", + path, + knowledge_path, + base, + head, + ) + review_handoff_path = Path(handoff_result["path"]) result = transition( self.repo, "TASK-0001", - "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-001.json"], + "START_REVIEW", + [ + "implementation=implementations/r001/attempt-001.json", + "knowledge_delta=knowledge/r001/knowledge-delta-001.json", + "review_handoff=" + review_handoff_path.relative_to(self.task).as_posix(), + ], None, base, head, @@ -3449,7 +3534,7 @@ def test_implementation_handoff_and_result_are_mechanically_bound(self) -> None: None, None, ) - self.assertEqual(result["to"], "IMPLEMENTED") + self.assertEqual(result["to"], "REVIEWING") def test_frozen_task_references_survive_physical_root_relocation(self) -> None: """任务整体搬迁后,逻辑路径、handoff、进度、探索与恢复仍可解析。""" @@ -3974,47 +4059,17 @@ def test_new_revision_is_created_then_explicitly_activated(self) -> None: self.assertEqual(read_json(self.task / "state.json")["current_revision"], 2) def test_implementation_and_final_documentation_subjects_are_bound(self) -> None: - """Implementation 绑定实现 checkpoint,Knowledge Delta 绑定含文档的最终 subject。""" - self.freeze_work_item() - build_working_set(self.repo, "TASK-0001", True) - transition( - self.repo, "TASK-0001", "QUALIFY", [], None, None, None, None, None, None - ) - transition( - self.repo, - "TASK-0001", - "PLAN", - [ - "plan=PLAN.md", - "plan_decisions=plan-decisions.json", - "working_set=working-set.json", - ], - None, - None, - None, - None, - None, - None, - ) - transition( - self.repo, - "TASK-0001", - "START_IMPLEMENTATION", - [], - None, - None, - None, - None, - None, - None, - ) - self.dispatch_implementation() + """Implementation 与 Knowledge Delta 共同绑定含文档的最终 subject。""" + self.enter_implementing() base = run_git(self.repo, "rev-parse", "HEAD") (self.repo / "subject.txt").write_text("subject\n", encoding="utf-8") - run_git(self.repo, "add", "subject.txt") - run_git(self.repo, "commit", "-q", "-m", "subject") - head = run_git(self.repo, "rev-parse", "HEAD") - diff_hash = subject_diff_hash(self.repo, base, head) + docs_path = self.repo / "docs" / "subject.md" + docs_path.parent.mkdir() + docs_path.write_text("Subject behavior documentation\n", encoding="utf-8") + run_git(self.repo, "add", "subject.txt", "docs/subject.md") + run_git(self.repo, "commit", "-q", "-m", "subject and documentation") + final_head = run_git(self.repo, "rev-parse", "HEAD") + final_diff_hash = subject_diff_hash(self.repo, base, final_head) implementation_intelligence = read_json( ROOT / "templates" / "task-sources" / "code-intelligence-record.json" @@ -4025,8 +4080,8 @@ def test_implementation_and_final_documentation_subjects_are_bound(self) -> None "artifact_attempt": 1, "target": { "base_commit": base, - "head_commit": head, - "diff_hash": diff_hash, + "head_commit": final_head, + "diff_hash": final_diff_hash, }, } ) @@ -4037,32 +4092,12 @@ def test_implementation_and_final_documentation_subjects_are_bound(self) -> None implementation_intelligence_result["path"] ) implementation_path = self.task / "implementations" / "r001" / "attempt-001.json" - implementation = self.implementation_value(base, head, "impl-session") + implementation = self.implementation_value(base, final_head, "impl-session") implementation["code_intelligence"] = { "path": implementation_intelligence_path.relative_to(self.task).as_posix(), "sha256": file_sha256(implementation_intelligence_path), } write_json_atomic(implementation_path, implementation) - transition( - self.repo, - "TASK-0001", - "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-001.json"], - None, - base, - head, - None, - None, - None, - ) - - docs_path = self.repo / "docs" / "subject.md" - docs_path.parent.mkdir() - docs_path.write_text("Subject behavior documentation\n", encoding="utf-8") - run_git(self.repo, "add", "docs/subject.md") - run_git(self.repo, "commit", "-q", "-m", "sync subject documentation") - final_head = run_git(self.repo, "rev-parse", "HEAD") - final_diff_hash = subject_diff_hash(self.repo, base, final_head) documentation_intelligence = read_json( ROOT / "templates" / "task-sources" / "code-intelligence-record.json" ) @@ -4101,36 +4136,12 @@ def test_implementation_and_final_documentation_subjects_are_bound(self) -> None knowledge["subject_diff_hash"] = "0" * 64 write_json_atomic(knowledge_path, knowledge) with self.assertRaises(RuleFailure): - transition( - self.repo, - "TASK-0001", - "SYNC_DOCS", - ["knowledge_delta=knowledge/r001/knowledge-delta-001.json"], - None, - base, - final_head, - None, - None, - None, - ) + check_docs(self.repo, "TASK-0001", knowledge_path, base, final_head) knowledge["subject_diff_hash"] = correct_diff_hash write_json_atomic(knowledge_path, knowledge) result = check_docs(self.repo, "TASK-0001", knowledge_path, base, final_head) self.assertEqual(result["changed_paths"], 2) - synced = transition( - self.repo, - "TASK-0001", - "SYNC_DOCS", - ["knowledge_delta=knowledge/r001/knowledge-delta-001.json"], - None, - base, - final_head, - None, - None, - None, - ) - self.assertEqual(synced["to"], "DOCS_SYNCED") - self.assertEqual(validate(self.repo, "TASK-0001")["state"], "DOCS_SYNCED") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") def test_stale_documentation_is_rejected(self) -> None: """Knowledge Delta 存在未处置 STALE 时拒绝文档同步通过。""" @@ -4144,189 +4155,7 @@ def test_stale_documentation_is_rejected(self) -> None: def test_full_r1_flow_closes_only_after_review_and_validation(self) -> None: """R1 必须依次通过独立 Review、全部 AC Validation 和 Result 才能 CLOSED。""" - self.test_implementation_and_final_documentation_subjects_are_bound() - state = read_json(self.task / "state.json") - subject = state["subject"] - with self.assertRaises(RuleFailure): - transition( - self.repo, "TASK-0001", "CLOSE", [], None, None, None, None, None, None - ) - review_handoff_result = build_review_handoff( - self.repo, "TASK-0001", "impl-session", "fresh_session" - ) - review_handoff = read_json(Path(review_handoff_result["path"])) - self.assertTrue( - { - "implementation_code_intelligence", - "documentation_code_intelligence", - }.issubset({item["role"] for item in review_handoff["package"]}) - ) - transition( - self.repo, - "TASK-0001", - "START_REVIEW", - [ - "review_handoff=" - + Path(review_handoff_result["path"]).relative_to(self.task).as_posix() - ], - None, - None, - None, - None, - None, - None, - ) - review_path = self.task / "reviews" / "r001" / "review-001.json" - review = read_json(template_path(ROOT, "review")) - handoff_reference = read_json(self.task / "state.json")["artifacts"][ - "review_handoff" - ] - review.update( - { - "work_item_revision": 2, - "implementer_session_id": "impl-session", - "reviewer_session_id": "review-session", - "handoff_path": handoff_reference["path"], - "handoff_sha256": handoff_reference["sha256"], - "subject_base_commit": subject["base_commit"], - "subject_head_commit": subject["head_commit"], - "subject_diff_hash": subject["diff_hash"], - "reviewed_at": "2026-08-12T00:00:00Z", - "verdict": "ACCEPT", - } - ) - write_json_atomic(review_path, review) - with self.assertRaises(RuleFailure): - transition( - self.repo, - "TASK-0001", - "ACCEPT_REVIEW", - ["review=reviews/r001/review-001.json"], - None, - None, - None, - None, - None, - None, - ) - review["work_item_revision"] = 1 - review["subject_diff_hash"] = "0" * 64 - write_json_atomic(review_path, review) - with self.assertRaises(RuleFailure): - transition( - self.repo, - "TASK-0001", - "ACCEPT_REVIEW", - ["review=reviews/r001/review-001.json"], - None, - None, - None, - None, - None, - None, - ) - review["subject_diff_hash"] = subject["diff_hash"] - write_json_atomic(review_path, review) - transition( - self.repo, - "TASK-0001", - "ACCEPT_REVIEW", - ["review=reviews/r001/review-001.json"], - None, - None, - None, - None, - None, - None, - ) - transition( - self.repo, - "TASK-0001", - "START_VALIDATION", - [], - None, - None, - None, - None, - None, - None, - ) - validation_path = self.task / "validations" / "r001" / "validation-001.json" - validation = read_json(template_path(ROOT, "validation")) - validation.update( - { - "subject_base_commit": subject["base_commit"], - "subject_head_commit": subject["head_commit"], - "subject_diff_hash": subject["diff_hash"], - "validated_at": "2026-08-12T00:01:00Z", - "verdict": "PASS", - "acceptance_results": [], - } - ) - write_json_atomic(validation_path, validation) - with self.assertRaises(RuleFailure): - transition( - self.repo, - "TASK-0001", - "PASS_VALIDATION", - ["validation=validations/r001/validation-001.json"], - None, - None, - None, - None, - None, - None, - ) - validation["acceptance_results"] = [ - { - "acceptance_id": "AC-01", - "command_or_check": "state validation", - "cwd": ".", - "environment_summary": "test", - "started_at": "2026-08-12T00:01:00Z", - "exit_code": 0, - "result": "PASS", - "output_path_or_hash": "inline:test", - } - ] - write_json_atomic(validation_path, validation) - transition( - self.repo, - "TASK-0001", - "PASS_VALIDATION", - ["validation=validations/r001/validation-001.json"], - None, - None, - None, - None, - None, - None, - ) - result_path = self.task / "results" / "r001" / "result-001.json" - result = read_json(template_path(ROOT, "result")) - result.update( - { - "subject_base_commit": subject["base_commit"], - "subject_head_commit": subject["head_commit"], - "subject_diff_hash": subject["diff_hash"], - "summary": "Validated smoke task", - } - ) - write_json_atomic(result_path, result) - closed = transition( - self.repo, - "TASK-0001", - "CLOSE", - ["result=results/r001/result-001.json"], - None, - None, - None, - None, - None, - None, - ) - self.assertEqual(closed["to"], "CLOSED") - self.assertEqual(validate(self.repo, "TASK-0001")["state"], "CLOSED") + self.test_r1_pass_and_close_validates_candidate_projection() def test_r1_review_requires_a_bound_independent_session_attestation(self) -> None: """R1 Review 即使 session 字段不同,也必须绑定 handoff 且声明未继承实现聊天。""" @@ -4372,21 +4201,24 @@ def test_review_handoff_freezes_evidence_directory(self) -> None: self.test_implementation_and_final_documentation_subjects_are_bound() evidence = self.task / "evidence" / "r001" / "checks.txt" evidence.write_text("original evidence\n", encoding="utf-8") - handoff_path = Path( - build_review_handoff( - self.repo, "TASK-0001", "impl-session", "fresh_session" - )["path"] + handoff_result, implementation_path, knowledge_path, base, head = ( + self.build_current_review_handoff() ) + handoff_path = Path(handoff_result["path"]) evidence.write_text("mutated evidence\n", encoding="utf-8") with self.assertRaises(RuleFailure): transition( self.repo, "TASK-0001", "START_REVIEW", - [f"review_handoff={handoff_path.relative_to(self.task).as_posix()}"], - None, - None, + [ + "implementation=" + implementation_path.relative_to(self.task).as_posix(), + "knowledge_delta=" + knowledge_path.relative_to(self.task).as_posix(), + f"review_handoff={handoff_path.relative_to(self.task).as_posix()}", + ], None, + base, + head, None, None, None, @@ -4433,18 +4265,6 @@ def test_validation_rework_supersedes_the_accepted_review(self) -> None: None, None, ) - transition( - self.repo, - "TASK-0001", - "START_VALIDATION", - [], - None, - None, - None, - None, - None, - None, - ) subject_1 = read_json(self.task / "state.json")["subject"] validation_path = self.task / "validations" / "r001" / "validation-001.json" validation = read_json(template_path(ROOT, "validation")) @@ -4485,6 +4305,7 @@ def test_validation_rework_supersedes_the_accepted_review(self) -> None: self.assertEqual(failed["to"], "IMPLEMENTING") state = read_json(self.task / "state.json") self.assertEqual(state["artifacts"]["prior_review"]["path"], "reviews/r001/review-001.json") + self.register_implementation_handoff() base = subject_1["base_commit"] (self.repo / "subject.txt").write_text("validation fix\n", encoding="utf-8") @@ -4495,18 +4316,6 @@ def test_validation_rework_supersedes_the_accepted_review(self) -> None: implementation_path = self.task / "implementations" / "r001" / "attempt-002.json" implementation = self.implementation_value(base, head, "impl-session-2") write_json_atomic(implementation_path, implementation) - transition( - self.repo, - "TASK-0001", - "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-002.json"], - None, - base, - head, - None, - None, - None, - ) knowledge_path = self.task / "knowledge" / "r001" / "knowledge-delta-002.json" knowledge = self.knowledge_value(2, base, head) knowledge["entries"][0].update( @@ -4516,18 +4325,6 @@ def test_validation_rework_supersedes_the_accepted_review(self) -> None: } ) write_json_atomic(knowledge_path, knowledge) - transition( - self.repo, - "TASK-0001", - "SYNC_DOCS", - ["knowledge_delta=knowledge/r001/knowledge-delta-002.json"], - None, - base, - head, - None, - None, - None, - ) handoff_2, state = self.start_review("impl-session-2") self.assertEqual(handoff_2["artifact_attempt"], 2) self.assertEqual( @@ -4574,6 +4371,7 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: None, ) self.assertEqual(rejected["to"], "IMPLEMENTING") + self.register_implementation_handoff() base = handoff_1["subject_base_commit"] (self.repo / "subject.txt").write_text("subject fixed\n", encoding="utf-8") @@ -4584,19 +4382,17 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: implementation_path = self.task / "implementations" / "r001" / "attempt-002.json" implementation = self.implementation_value(base, head, "impl-session-2") write_json_atomic(implementation_path, implementation) + knowledge_path = self.task / "knowledge" / "r001" / "knowledge-delta-002.json" + knowledge = self.knowledge_value(2, base, head) + knowledge["entries"][0].update( + { + "changed_paths": ["docs/subject.md", "subject.txt"], + "evidence": "The final subject retains the synchronized documentation", + } + ) + write_json_atomic(knowledge_path, knowledge) with self.assertRaises(RuleFailure): - transition( - self.repo, - "TASK-0001", - "FINISH_IMPLEMENTATION", - ["implementation=implementations/r001/attempt-002.json"], - None, - base, - head, - None, - None, - None, - ) + self.start_review("impl-session-2") prior_reference = read_json(self.task / "state.json")["artifacts"]["prior_review"] response_path = self.task / "reviews" / "r001" / "response-002.json" @@ -4620,42 +4416,6 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: } ) write_json_atomic(response_path, response) - transition( - self.repo, - "TASK-0001", - "FINISH_IMPLEMENTATION", - [ - "implementation=implementations/r001/attempt-002.json", - "review_response=reviews/r001/response-002.json", - ], - None, - base, - head, - None, - None, - None, - ) - knowledge_path = self.task / "knowledge" / "r001" / "knowledge-delta-002.json" - knowledge = self.knowledge_value(2, base, head) - knowledge["entries"][0].update( - { - "changed_paths": ["docs/subject.md", "subject.txt"], - "evidence": "The final subject retains the synchronized documentation", - } - ) - write_json_atomic(knowledge_path, knowledge) - transition( - self.repo, - "TASK-0001", - "SYNC_DOCS", - ["knowledge_delta=knowledge/r001/knowledge-delta-002.json"], - None, - base, - head, - None, - None, - None, - ) handoff_2, state = self.start_review("impl-session-2") review_2_path = self.task / "reviews" / "r001" / "review-002.json" write_json_atomic( @@ -4702,8 +4462,8 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: None, None, ) - self.assertEqual(accepted["to"], "REVIEWED") - self.assertEqual(validate(self.repo, "TASK-0001")["state"], "REVIEWED") + self.assertEqual(accepted["to"], "VALIDATING") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "VALIDATING") def test_third_rejected_review_enters_human_blocked_state(self) -> None: """第三次 Review 仍 REJECT 时不再自动返工,而是进入 Human-owned BLOCKED。""" @@ -4726,9 +4486,7 @@ def test_third_rejected_review_enters_human_blocked_state(self) -> None: None, None, ) - transition( - self.repo, "TASK-0001", "START_IMPLEMENTATION", [], None, None, None, None, None, None - ) + self.register_implementation_handoff() base = run_git(self.repo, "rev-parse", "HEAD") finding = { "id": "F-001", diff --git a/workflow/default-workflow.json b/workflow/default-workflow.json index e6ab3f7..15d2691 100644 --- a/workflow/default-workflow.json +++ b/workflow/default-workflow.json @@ -39,7 +39,8 @@ { "event": "START_IMPLEMENTATION", "from": [ - "PLANNED" + "PLANNED", + "IMPLEMENTING" ], "to": "IMPLEMENTING", "gate": "implementation_start_ready" From 4c0df5ef3fb80d8a979e11bbb5b1b0334ab6ab87 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 23:25:35 +0800 Subject: [PATCH 10/17] fix: harden simplified workflow gates --- docs/USAGE.md | 2 +- ...26-08-18-workflow-simplification-design.md | 3 +- plan.md | 2 +- scripts/build_review_handoff.py | 6 +- scripts/internal/migration_protocol.py | 11 +- scripts/internal/transition_effects.py | 13 + scripts/internal/transition_gates.py | 10 +- scripts/internal/validation_protocol.py | 25 ++ scripts/validate_task.py | 10 +- skills/engineering-task/SKILL.md | 2 +- skills/implementation/SKILL.md | 2 +- tests/test_core.py | 295 ++++++++++++++++-- 12 files changed, 329 insertions(+), 52 deletions(-) create mode 100644 scripts/internal/validation_protocol.py diff --git a/docs/USAGE.md b/docs/USAGE.md index 7853167..9c8483d 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -605,7 +605,7 @@ polaris migrate --repo . 1. `workflow/migrations.json` 是支持路径的唯一、append-only 注册表;历史步骤必须保留,以便校验已提交的迁移记录。一次命令只允许从当前项目版本迁移到 vendored 版本的一个显式相邻步骤,不推断、不跨级。 2. 注册步骤同时绑定源/目标 `polaris_version` 与 `workflow_version`。Migration protocol v2 支持仅更新版本,也支持显式替换冻结 workflow 并映射任务状态。 -3. `0.1.19 → 0.1.20` 使用 `replace_version_and_workflow` 与 `append_mapped_workflow_event`:冻结 workflow 更新到 `0.1.3`,旧 `IMPLEMENTED` / `DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`。迁移事件记录源/目标状态及旧版本;旧 `events.jsonl` 行不可修改。 +3. `0.1.19 → 0.1.20` 使用 `replace_version_and_workflow` 与 `append_mapped_workflow_event`:冻结 workflow 更新到 `0.1.3`,旧 `IMPLEMENTED` / `DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`;旧 R0/R1 `VERIFIED` 也映射回 `VALIDATING`,以便通过 `PASS_AND_CLOSE` 重新提交关闭产物,R2 `VERIFIED` 保持不变。迁移事件记录源/目标状态及旧版本;旧 `events.jsonl` 行不可修改。 4. `.polaris/migrations/MIG--to-.json` 先写为 `IN_PROGRESS`,全部投影更新后改为 `COMPLETED`。迁移锁会记录迁移/任务身份、主机名和 PID;若进程在中间终止,同一主机重新执行命令会接管已死亡的同迁移锁、验证并复用已经追加的事件,不会重复迁移。活跃进程、其他迁移或来源不明的锁不会被自动删除。 5. 迁移完成后脚本自动运行项目校验;`validate_project.py` 会拒绝未完成记录、缺失/伪造的任务迁移事件或版本不一致。 diff --git a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md index 2a38cef..8cf2fea 100644 --- a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md +++ b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md @@ -183,7 +183,8 @@ Code Intelligence 继续保持可选、在 artifact 边界上 Provider-neutral | `REVIEWING` | `REVIEWING` | | `REVIEWED` | `VALIDATING` | | `VALIDATING` | `VALIDATING` | -| `VERIFIED` | `VERIFIED` | +| R0/R1 `VERIFIED` | `VALIDATING`,允许通过 `PASS_AND_CLOSE` 重新提交现有 Validation 与 Result | +| R2 `VERIFIED` | `VERIFIED` | | `BLOCKED` | `BLOCKED`,并按相同规则映射 `blocked_from` | | `CLOSED` | `CLOSED` | | `CANCELLED` | `CANCELLED` | diff --git a/plan.md b/plan.md index 0fb4da4..5bb93a9 100644 --- a/plan.md +++ b/plan.md @@ -452,7 +452,7 @@ v0.1 不设置 `FAILED`:可修复失败通过治理回路处理,外部阻塞 `.polaris/workflow.json` 保存当前项目实际使用且版本锁定的节点、边、依赖和门禁 ID;`tools/polaris/workflow/default-workflow.json` 只用于初始化。`transition_task.py` 只接受图中边并先运行对应 validators,Skill 不直接编辑 `state` 字段。v0.1 遇到 `polaris_version` 或 `workflow_version` 不匹配时拒绝正常执行,不做隐式迁移。 -版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。Migration protocol v2 保留 `replace_version` / `append_version_event`,并增加 `replace_version_and_workflow` / `append_mapped_workflow_event`。`0.1.19 → 0.1.20` 原子替换冻结 workflow 为 `0.1.3`,追加带源/目标状态及旧版本字段的迁移事件;旧 `IMPLEMENTED`、`DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`,其余治理状态保持含义。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED`、各任务 sequence 和状态映射;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、未声明的 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。 +版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。Migration protocol v2 保留 `replace_version` / `append_version_event`,并增加 `replace_version_and_workflow` / `append_mapped_workflow_event`。`0.1.19 → 0.1.20` 原子替换冻结 workflow 为 `0.1.3`,追加带源/目标状态及旧版本字段的迁移事件;旧 `IMPLEMENTED`、`DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`,旧 R0/R1 `VERIFIED` 映射到 `VALIDATING` 以重新提交 `PASS_AND_CLOSE`,仅 R2 保持 `VERIFIED`。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED`、各任务 sequence 和状态映射;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、未声明的 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。 迁移占用任务转换锁时必须写入结构化 owner:迁移 ID、任务 ID、主机名、PID 和创建时间。重跑只允许接管同一迁移在同一主机上、且原 PID 已确认不存在的锁;活跃 PID、其他迁移、其他主机、空锁或损坏锁一律拒绝。这样既能从进程崩溃或机器重启恢复,又不把真实并发误判为遗留锁。 diff --git a/scripts/build_review_handoff.py b/scripts/build_review_handoff.py index 3e9128f..76b407d 100644 --- a/scripts/build_review_handoff.py +++ b/scripts/build_review_handoff.py @@ -31,6 +31,7 @@ from internal.task_location_protocol import logical_repo_path, resolve_repo_reference from internal.review_protocol import MAX_REVIEW_ATTEMPTS from internal.task_layout import evidence_dir, review_handoff_path, state_path +from internal.transition_gates import check_gate from internal.working_set_protocol import validate_working_set @@ -149,6 +150,9 @@ def build( "path": relative_response.as_posix(), "sha256": file_sha256(resolved_response), } + workflow = read_json(repo / ".polaris" / "workflow.json") + check_gate(repo, root, directory, state, "implementation_ready", None, workflow) + check_gate(repo, root, directory, state, "docs_ready", None, workflow) implementation_reference = normalized_reference( directory, state["artifacts"].get("implementation") ) @@ -270,7 +274,7 @@ def build( "package": package, } path = review_handoff_path(directory, revision, attempt) - if path.exists(): + if path.exists() and stored_state["artifacts"].get("review_handoff") is not None: raise InputFailure(f"review handoff is immutable and already exists: {path}") write_json_atomic(path, handoff) validate_json_file(path, root / "schemas" / "review-handoff.schema.json") diff --git a/scripts/internal/migration_protocol.py b/scripts/internal/migration_protocol.py index 8800e77..deb7a7a 100644 --- a/scripts/internal/migration_protocol.py +++ b/scripts/internal/migration_protocol.py @@ -54,9 +54,11 @@ } -def _map_legacy_stage(status: str, artifacts: dict[str, Any]) -> str: +def _map_legacy_stage(status: str, artifacts: dict[str, Any], rigor: str) -> str: if status == "IMPLEMENTING": return "IMPLEMENTING" if "implementation_handoff" in artifacts else "PLANNED" + if status == "VERIFIED" and rigor != "R2": + return "VALIDATING" mapped = LEGACY_STATUS_MAP.get(status) if mapped is None: raise RuleFailure(f"cannot map legacy workflow state: {status}") @@ -68,15 +70,18 @@ def map_migrated_status(state: dict[str, Any]) -> tuple[str, str | None]: artifacts = state.get("artifacts") if not isinstance(artifacts, dict): raise RuleFailure("legacy task artifacts must be an object") + rigor = state.get("rigor") + if rigor not in {"R0", "R1", "R2"}: + raise RuleFailure("legacy task rigor must be R0, R1, or R2") status = state.get("status") if status == "BLOCKED": blocked_from = state.get("blocked_from") if not isinstance(blocked_from, str): raise RuleFailure("legacy BLOCKED task has no blocked_from state") - return "BLOCKED", _map_legacy_stage(blocked_from, artifacts) + return "BLOCKED", _map_legacy_stage(blocked_from, artifacts, rigor) if not isinstance(status, str): raise RuleFailure("legacy task status must be a string") - return _map_legacy_stage(status, artifacts), None + return _map_legacy_stage(status, artifacts, rigor), None def load_migration_protocol(protocol_root: Path) -> dict[str, Any]: diff --git a/scripts/internal/transition_effects.py b/scripts/internal/transition_effects.py index d317d22..f0fa22e 100644 --- a/scripts/internal/transition_effects.py +++ b/scripts/internal/transition_effects.py @@ -72,6 +72,19 @@ def prepare_next_state( ) -> tuple[dict[str, Any], dict[str, dict[str, str]], dict[str, str] | None]: next_state = copy.deepcopy(state) submitted_artifacts = parse_artifacts(artifact_values, directory) + if event_name == "START_IMPLEMENTATION": + if "implementation_handoff" not in submitted_artifacts: + raise RuleFailure( + "START_IMPLEMENTATION requires a new implementation_handoff artifact" + ) + if ( + state["status"] == "IMPLEMENTING" + and state["artifacts"].get("implementation_handoff") is not None + ): + raise RuleFailure( + "START_IMPLEMENTATION self-transition requires rework to clear the active " + "handoff before registering a new implementation_handoff" + ) next_state["artifacts"].update(submitted_artifacts) if revision is not None: diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index 893d34c..49223c8 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -19,6 +19,7 @@ from .review_protocol import validate_handoff, validate_review, validate_review_response from .plan_decision_protocol import validate_plan_decisions from .working_set_protocol import validate_working_set +from .validation_protocol import validate_acceptance_coverage def artifact_file(directory: Path, state: dict[str, Any], name: str) -> Path: @@ -242,14 +243,7 @@ def check_gate( validation = load_validation(root, directory, state) if validation["verdict"] != "PASS": raise RuleFailure("Validation verdict must be PASS") - expected = {item["id"] for item in work_item["acceptance"]} - actual = { - item["acceptance_id"] - for item in validation["acceptance_results"] - if item["result"] == "PASS" - } - if expected != actual: - raise RuleFailure("Validation must PASS every acceptance criterion") + validate_acceptance_coverage(work_item, validation) if validation["subject_diff_hash"] != state["subject"]["diff_hash"]: raise RuleFailure("Validation targets the wrong subject") if gate == "validation_passed_and_closure_ready": diff --git a/scripts/internal/validation_protocol.py b/scripts/internal/validation_protocol.py new file mode 100644 index 0000000..83a4c4e --- /dev/null +++ b/scripts/internal/validation_protocol.py @@ -0,0 +1,25 @@ +"""Shared mechanical rules for Validation artifacts.""" + +from __future__ import annotations + +from typing import Any + +from .polaris_core import RuleFailure + + +def validate_acceptance_coverage( + work_item: dict[str, Any], validation: dict[str, Any] +) -> None: + """Require one passing Validation result for every acceptance criterion.""" + expected = [item["id"] for item in work_item["acceptance"]] + results = validation["acceptance_results"] + actual = [item["acceptance_id"] for item in results] + if ( + len(actual) != len(expected) + or len(set(actual)) != len(actual) + or set(actual) != set(expected) + or any(item["result"] != "PASS" for item in results) + ): + raise RuleFailure( + "Validation must cover every acceptance criterion exactly once with PASS" + ) diff --git a/scripts/validate_task.py b/scripts/validate_task.py index 686daab..e436864 100644 --- a/scripts/validate_task.py +++ b/scripts/validate_task.py @@ -29,6 +29,7 @@ from internal.review_protocol import validate_handoff, validate_review, validate_review_response from internal.plan_decision_protocol import validate_plan_decisions from internal.working_set_protocol import validate_working_set +from internal.validation_protocol import validate_acceptance_coverage from internal.task_layout import events_path, explorations_dir from internal.task_layout import state_path as task_state_path @@ -248,14 +249,7 @@ def validate_projection( ) if validation["verdict"] != "PASS": raise RuleFailure("VERIFIED requires a PASS Validation") - expected = {item["id"] for item in work_item["acceptance"]} - actual = [ - item["acceptance_id"] - for item in validation["acceptance_results"] - if item["result"] == "PASS" - ] - if len(actual) != len(expected) or set(actual) != expected: - raise RuleFailure("Validation does not PASS every acceptance criterion exactly once") + validate_acceptance_coverage(work_item, validation) if ( validation["task_id"] != task_id or validation["work_item_revision"] != state["current_revision"] diff --git a/skills/engineering-task/SKILL.md b/skills/engineering-task/SKILL.md index af9b255..48f9cdf 100644 --- a/skills/engineering-task/SKILL.md +++ b/skills/engineering-task/SKILL.md @@ -46,7 +46,7 @@ Never report an anticipated state. Reload authority after every transition. A st 8. Run `build_implementation_handoff.py`, then use `START_IMPLEMENTATION` after required R2 pre-approval to atomically validate and register the exact returned handoff path. The initial transition is `PLANNED → IMPLEMENTING`; after Review or Validation rework, use the same event as an `IMPLEMENTING → IMPLEMENTING` self-transition to register the next attempt's handoff. Reload state and emit `IMPLEMENTATION_HANDOFF_READY`. Never assemble task-relative paths independently of `task_layout.py`. 9. Use the exact worker title `Polaris Implement · · · attempt `. Dispatch a fresh isolated worker in the same local checkout through the host appendix. Never inherit the main conversation or use a separate worktree by default. 10. Before creation, first reuse a valid Implementation artifact bound to the registered handoff. Otherwise reuse or resume only one unambiguous worker identity as defined by the host appendix. Never create a duplicate for the same task, revision, and attempt; ambiguous identity requires the same-session fallback rather than guessing. -11. Give the Implementer only the task ID and registered handoff path. Use this exact prompt: `Use {{skill:implementation}} for . Load only and its package as task context; read state.json only to verify the registered handoff. Work in the shared local checkout, define and execute linear implementation_steps through update_implementation_progress.py, write the immutable Implementation JSON at output_path with matching step_results, and return its path. Do not run task transitions, Review, Validation, or close the task.` Do not include main-chat history or implementation advice. +11. Give the Implementer only the task ID and registered handoff path. Use this exact prompt: `Use {{skill:implementation}} for . Load only and its package as task context; read state.json only to verify the registered handoff. Work in the shared local checkout and define and execute linear implementation_steps. When an initialized live snapshot exists, mirror steps and checks through update_implementation_progress.py; otherwise keep telemetry absent. Always write the immutable Implementation JSON at output_path with matching step_results and checks, then return its path. Do not run task transitions, Review, Validation, or close the task.` Do not include main-chat history or implementation advice. 12. When the host supports live reporting, initialize the ignored snapshot with the updater's `INITIALIZE` event. Emit `IMPLEMENTATION_SESSION_STARTED` with the nine fixed fields plus `Implementation task`, `Handoff`, optional `Progress`, and `Dispatch mode`; set `User action` to `None` while work is proceeding. Snapshot absence is not a blocker. 13. While `IMPLEMENTING`, answer status requests from the validated live snapshot when it exists; otherwise report that only durable artifacts are available. When present, emit `IMPLEMENTATION_PROGRESS` with the latest phase; current step ID/title; completed or skipped prefix; pending suffix; checks; blocker; timestamp; and adapter-defined worker reference. Derive those views from the single ordered `implementation_steps` list. Do not persist a duplicate Markdown status file, invent percentages, infer progress from elapsed time, or reconstruct the path from prose. A status query must not cancel or duplicate the worker. 14. Wait for the Implementation artifact, then continue the exact same Implementer worker through the host appendix with `{{skill:documentation-sync}}`; it writes the Knowledge Delta and final subject checkpoint without running transitions. Live progress is optional telemetry: validate and report it when present, but never require it for a durable gate. Require the same Implementer session and terminal `step_results` in the immutable Implementation artifact. Only after both artifacts and the final subject are ready is the Implementer worker finished. diff --git a/skills/implementation/SKILL.md b/skills/implementation/SKILL.md index 2b383f3..308b586 100644 --- a/skills/implementation/SKILL.md +++ b/skills/implementation/SKILL.md @@ -12,7 +12,7 @@ description: Internal Polaris worker stage for an explicitly started `{{skill:en 5. Change only declared subject paths and protect unrelated user changes. Work in small build/test/fix loops. 6. Do not alter goal, scope, acceptance, or hard constraints. Return a blocker when any must change. 7. Record Plan deviations and reasons. After Review rejection, load the handoff's prior Review, answer every open Finding once in an immutable Review Response, and bind it to the new subject. -8. Run planned local checks and append reproducible evidence with `ADD_CHECK`. Never report a made-up percentage; derive completed, current, and remaining work from the ordered steps. +8. Run planned local checks and record reproducible evidence in the immutable Implementation artifact. When live telemetry exists, append reproducible evidence to the snapshot with `ADD_CHECK`; without a snapshot, do not initialize one merely to report checks. Never report a made-up percentage; derive completed, current, and remaining work from the ordered steps. 9. Complete or explicitly skip every step, then create a subject checkpoint commit containing scoped code, tests, build configuration, and relevant project docs only. 10. If this stage actually performed a Provider status, sync, or explore operation, finalize an immutable v2 Implementation Code Intelligence record and reference it. If no Provider operation ran, omit the Code Intelligence record and its optional artifact reference. Write the immutable Implementation JSON at the handoff's `output_path`, bind the handoff, subject, session, deviations, and checks, and copy the exact terminal `id`, `status`, and `result` projection into `step_results`. Code Intelligence evidence is never a gate. 11. After every step is `COMPLETED` or `SKIPPED`, use `SET_PHASE` to enter `CHECKPOINTING` only when live telemetry exists. Return the artifact path, session ID, subject base/head, diff hash, step results, checks, deviations, Review Response path when present, and remaining Documentation Sync work. diff --git a/tests/test_core.py b/tests/test_core.py index 9b5f33d..e54ad0e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -873,6 +873,69 @@ def test_r1_pass_and_close_validates_candidate_projection(self) -> None: ) write_json_atomic(result_path, result) + passing_result = copy.deepcopy(validation["acceptance_results"][0]) + unknown_result = copy.deepcopy(passing_result) + unknown_result["acceptance_id"] = "AC-02" + failing_result = copy.deepcopy(passing_result) + failing_result["result"] = "FAIL" + failing_result["exit_code"] = 1 + for label, invalid_results in ( + ("duplicate", [passing_result, copy.deepcopy(passing_result)]), + ("unknown", [unknown_result]), + ("failure", [failing_result]), + ): + with self.subTest(invalid_validation=label): + validation["acceptance_results"] = invalid_results + write_json_atomic(validation_path, validation) + with self.assertRaisesRegex(RuleFailure, "exactly once.*PASS"): + transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + validation["acceptance_results"] = [passing_result] + validation["acceptance_results"].append( + { + "acceptance_id": "AC-01", + "command_or_check": "contradictory check", + "cwd": ".", + "environment_summary": "test", + "started_at": "2026-08-18T00:00:01Z", + "exit_code": 1, + "result": "FAIL", + "output_path_or_hash": "inline:failure", + } + ) + write_json_atomic(validation_path, validation) + with self.assertRaisesRegex(RuleFailure, "exactly once.*PASS"): + transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + validation["acceptance_results"].pop() + write_json_atomic(validation_path, validation) + implementation_path = ( self.task / "implementations" / "r001" / "attempt-001.json" ) @@ -980,6 +1043,34 @@ def test_r2_keeps_verified_and_final_approval_gate(self) -> None: None, None, ) + validation["acceptance_results"].append( + { + "acceptance_id": "AC-01", + "command_or_check": "contradictory check", + "cwd": ".", + "environment_summary": "test", + "started_at": "2026-08-18T00:00:01Z", + "exit_code": 1, + "result": "FAIL", + "output_path_or_hash": "inline:failure", + } + ) + write_json_atomic(validation_path, validation) + with self.assertRaisesRegex(RuleFailure, "exactly once.*PASS"): + transition( + self.repo, + "TASK-0001", + "PASS_VALIDATION", + ["validation=validations/r001/validation-001.json"], + None, + None, + None, + None, + None, + None, + ) + validation["acceptance_results"].pop() + write_json_atomic(validation_path, validation) verified = transition( self.repo, "TASK-0001", @@ -2046,7 +2137,6 @@ def test_workflow_migration_maps_every_legacy_state(self) -> None: "REVIEWING": "REVIEWING", "REVIEWED": "VALIDATING", "VALIDATING": "VALIDATING", - "VERIFIED": "VERIFIED", "CLOSED": "CLOSED", "CANCELLED": "CANCELLED", } @@ -2054,13 +2144,23 @@ def test_workflow_migration_maps_every_legacy_state(self) -> None: with self.subTest(legacy=legacy): self.assertEqual( map_migrated_status( - {"status": legacy, "blocked_from": None, "artifacts": {}} + { + "status": legacy, + "blocked_from": None, + "artifacts": {}, + "rigor": "R1", + } ), (expected, None), ) self.assertEqual( map_migrated_status( - {"status": "IMPLEMENTING", "blocked_from": None, "artifacts": {}} + { + "status": "IMPLEMENTING", + "blocked_from": None, + "artifacts": {}, + "rigor": "R1", + } ), ("PLANNED", None), ) @@ -2070,6 +2170,7 @@ def test_workflow_migration_maps_every_legacy_state(self) -> None: "status": "IMPLEMENTING", "blocked_from": None, "artifacts": {"implementation_handoff": {"path": "handoff.json"}}, + "rigor": "R1", } ), ("IMPLEMENTING", None), @@ -2080,10 +2181,44 @@ def test_workflow_migration_maps_every_legacy_state(self) -> None: "status": "BLOCKED", "blocked_from": "DOCS_SYNCED", "artifacts": {}, + "rigor": "R1", } ), ("BLOCKED", "IMPLEMENTING"), ) + self.assertEqual( + map_migrated_status( + { + "status": "VERIFIED", + "blocked_from": None, + "artifacts": {}, + "rigor": "R1", + } + ), + ("VALIDATING", None), + ) + self.assertEqual( + map_migrated_status( + { + "status": "VERIFIED", + "blocked_from": None, + "artifacts": {}, + "rigor": "R2", + } + ), + ("VERIFIED", None), + ) + self.assertEqual( + map_migrated_status( + { + "status": "BLOCKED", + "blocked_from": "VERIFIED", + "artifacts": {}, + "rigor": "R0", + } + ), + ("BLOCKED", "VALIDATING"), + ) def test_migration_replaces_frozen_workflow_and_maps_tasks(self) -> None: """0.1.19 迁移替换冻结 workflow,并用单个事件映射旧状态。""" @@ -2141,6 +2276,91 @@ def test_migration_replaces_frozen_workflow_and_maps_tasks(self) -> None: self.assertEqual(record["tasks"][0]["source_status"], "DOCS_SYNCED") self.assertEqual(record["tasks"][0]["target_status"], "IMPLEMENTING") + def test_migrated_r1_verified_task_can_close(self) -> None: + """旧 R1 VERIFIED 回到 VALIDATING 后可用原 Validation 原子关闭。""" + handoff, state = self.enter_reviewing_without_progress() + validating = self.accept_current_review(handoff, state) + subject = validating["subject"] + validation_path = self.task / "validations" / "r001" / "validation-001.json" + validation = read_json(template_path(ROOT, "validation")) + validation.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "validated_at": "2026-08-18T00:00:00Z", + "verdict": "PASS", + "acceptance_results": [ + { + "acceptance_id": "AC-01", + "command_or_check": "state validation", + "cwd": ".", + "environment_summary": "legacy fixture", + "started_at": "2026-08-18T00:00:00Z", + "exit_code": 0, + "result": "PASS", + "output_path_or_hash": "inline:test", + } + ], + } + ) + write_json_atomic(validation_path, validation) + validation_reference = { + "path": "validations/r001/validation-001.json", + "sha256": file_sha256(validation_path), + } + state_path = self.task / "state.json" + legacy_state = read_json(state_path) + legacy_state["status"] = "VERIFIED" + legacy_state["artifacts"]["validation"] = validation_reference + write_json_atomic(state_path, legacy_state) + event_path = self.task / "events.jsonl" + events = read_jsonl(event_path) + events[-1]["to"] = "VERIFIED" + events[-1]["artifacts"] = legacy_state["artifacts"] + write_text_atomic( + event_path, + "".join( + json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n" + for event in events + ), + ) + self.set_protocol_version("0.1.19") + self.set_workflow_version("0.1.2") + vendor(ROOT, self.repo, False) + + migrate_project(self.repo) + + migrated = read_json(state_path) + self.assertEqual(migrated["status"], "VALIDATING") + result_path = self.task / "results" / "r001" / "result-001.json" + result = read_json(template_path(ROOT, "result")) + result.update( + { + "subject_base_commit": subject["base_commit"], + "subject_head_commit": subject["head_commit"], + "subject_diff_hash": subject["diff_hash"], + "summary": "Closed after workflow migration", + } + ) + write_json_atomic(result_path, result) + closed = transition( + self.repo, + "TASK-0001", + "PASS_AND_CLOSE", + [ + "validation=validations/r001/validation-001.json", + "result=results/r001/result-001.json", + ], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual(closed["to"], "CLOSED") + def test_migration_resumes_after_event_append_without_duplication(self) -> None: """中断后重跑会采用已追加的迁移事件并完成投影,不重复写事件。""" self.set_protocol_version("0.1.19") @@ -3475,36 +3695,17 @@ def test_implementation_handoff_and_result_are_mechanically_bound(self) -> None: {"changed_paths": ["subject.txt"], "evidence": "No documentation impact"} ) write_json_atomic(knowledge_path, knowledge) - handoff_result = build_review_handoff( - self.repo, - "TASK-0001", - "impl-bound-session", - "fresh_session", - path, - knowledge_path, - base, - head, - ) - review_handoff_path = Path(handoff_result["path"]) with self.assertRaises(RuleFailure): - transition( + build_review_handoff( self.repo, "TASK-0001", - "START_REVIEW", - [ - "implementation=implementations/r001/attempt-001.json", - "knowledge_delta=knowledge/r001/knowledge-delta-001.json", - "review_handoff=" - + review_handoff_path.relative_to(self.task).as_posix(), - ], - None, + "impl-bound-session", + "fresh_session", + path, + knowledge_path, base, head, - None, - None, - None, ) - review_handoff_path.unlink() implementation["implementation_handoff_sha256"] = reference["sha256"] write_json_atomic(path, implementation) handoff_result = build_review_handoff( @@ -3841,6 +4042,14 @@ def test_implementation_status_contract_and_same_session_fallback(self) -> None: self.assertIn(detail, entry_text) self.assertIn("Dispatch mode: same_session_fallback", entry_text) self.assertIn("immediate status responses may be delayed", entry_text) + implementation_text = ( + ROOT / "skills" / "implementation" / "SKILL.md" + ).read_text(encoding="utf-8") + self.assertIn("When an initialized live snapshot exists", entry_text) + self.assertIn( + "When live telemetry exists, append reproducible evidence", + implementation_text, + ) def test_r1_dispatches_one_visible_fresh_local_task(self) -> None: """R1 按宿主创建隔离 worker,禁止 fork 和默认 worktree。""" @@ -4223,6 +4432,38 @@ def test_review_handoff_freezes_evidence_directory(self) -> None: None, None, ) + evidence.write_text("original evidence\n", encoding="utf-8") + rebuilt = build_review_handoff( + self.repo, + "TASK-0001", + "impl-session", + "fresh_session", + implementation_path, + knowledge_path, + base, + head, + ) + self.assertEqual(Path(rebuilt["path"]), handoff_path) + + def test_start_implementation_self_transition_requires_new_handoff(self) -> None: + """IMPLEMENTING 自转换只能在返工清除旧 handoff 后注册下一 attempt。""" + self.enter_implementing() + state = read_json(self.task / "state.json") + with self.assertRaisesRegex(RuleFailure, "new implementation_handoff"): + transition( + self.repo, + "TASK-0001", + "START_IMPLEMENTATION", + [], + None, + None, + None, + None, + None, + None, + ) + unchanged = read_json(self.task / "state.json") + self.assertEqual(unchanged["sequence"], state["sequence"]) def test_review_handoff_survives_physical_root_relocation(self) -> None: """Reviewer 冻结包使用逻辑任务路径,整体搬迁不改变其内容或校验结果。""" From 8b8e4c050fbc977284484fd9a19b130abe496ee2 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Tue, 18 Aug 2026 23:31:54 +0800 Subject: [PATCH 11/17] fix: protect handoff and acceptance authority --- scripts/build_review_handoff.py | 43 +++++++++++++- scripts/internal/transition_gates.py | 3 +- scripts/internal/validation_protocol.py | 8 +++ scripts/validate_task.py | 3 +- tests/test_core.py | 76 +++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/scripts/build_review_handoff.py b/scripts/build_review_handoff.py index 76b407d..6bd8686 100644 --- a/scripts/build_review_handoff.py +++ b/scripts/build_review_handoff.py @@ -10,6 +10,7 @@ from typing import Any from internal.polaris_core import ( + acquire_lock, InputFailure, RuleFailure, current_work_item_path, @@ -18,6 +19,8 @@ full_commit, protocol_root, read_json, + rebuild_state_value, + release_lock, require_protocol_compatible, run_main, task_dir, @@ -30,7 +33,7 @@ from internal.code_intelligence_protocol import record_reference from internal.task_location_protocol import logical_repo_path, resolve_repo_reference from internal.review_protocol import MAX_REVIEW_ATTEMPTS -from internal.task_layout import evidence_dir, review_handoff_path, state_path +from internal.task_layout import events_path, evidence_dir, review_handoff_path, state_path from internal.transition_gates import check_gate from internal.working_set_protocol import validate_working_set @@ -85,7 +88,7 @@ def _code_intelligence_entry( return _entry(repo, role, directory / reference["path"]) -def build( +def _build_locked( repo: Path, task_id: str, implementer_session_id: str, @@ -289,6 +292,42 @@ def build( } +def build( + repo: Path, + task_id: str, + implementer_session_id: str, + isolation_mode: str | None, + implementation_path: Path | None = None, + knowledge_path: Path | None = None, + subject_base: str | None = None, + subject_head: str | None = None, + review_response_path: Path | None = None, +) -> dict[str, Any]: + """Build while excluding task transitions and rejecting stale projections.""" + directory = task_dir(repo, task_id) + lock_path = directory / ".transition.lock" + descriptor = acquire_lock(lock_path) + try: + stored_state = read_json(state_path(directory)) + if rebuild_state_value(events_path(directory)) != stored_state: + raise RuleFailure( + "state.json differs from events.jsonl; rebuild before building Review handoff" + ) + return _build_locked( + repo, + task_id, + implementer_session_id, + isolation_mode, + implementation_path, + knowledge_path, + subject_base, + subject_head, + review_response_path, + ) + finally: + release_lock(lock_path, descriptor) + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("task_id") diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index 49223c8..09e9055 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -19,7 +19,7 @@ from .review_protocol import validate_handoff, validate_review, validate_review_response from .plan_decision_protocol import validate_plan_decisions from .working_set_protocol import validate_working_set -from .validation_protocol import validate_acceptance_coverage +from .validation_protocol import validate_acceptance_coverage, validate_acceptance_ids def artifact_file(directory: Path, state: dict[str, Any], name: str) -> Path: @@ -90,6 +90,7 @@ def check_gate( raise RuleFailure( f"Acceptance {criterion['id']} has unresolved {field}" ) + validate_acceptance_ids(work_item) if work_item["known_unknowns"]: raise RuleFailure("Work Item has unresolved known_unknowns") dispatch = work_item.get("review_dispatch") diff --git a/scripts/internal/validation_protocol.py b/scripts/internal/validation_protocol.py index 83a4c4e..e020580 100644 --- a/scripts/internal/validation_protocol.py +++ b/scripts/internal/validation_protocol.py @@ -7,10 +7,18 @@ from .polaris_core import RuleFailure +def validate_acceptance_ids(work_item: dict[str, Any]) -> None: + """Require stable unique IDs before a Work Item becomes authority.""" + identifiers = [item["id"] for item in work_item["acceptance"]] + if len(identifiers) != len(set(identifiers)): + raise RuleFailure("Work Item contains duplicate acceptance IDs") + + def validate_acceptance_coverage( work_item: dict[str, Any], validation: dict[str, Any] ) -> None: """Require one passing Validation result for every acceptance criterion.""" + validate_acceptance_ids(work_item) expected = [item["id"] for item in work_item["acceptance"]] results = validation["acceptance_results"] actual = [item["acceptance_id"] for item in results] diff --git a/scripts/validate_task.py b/scripts/validate_task.py index e436864..1c97740 100644 --- a/scripts/validate_task.py +++ b/scripts/validate_task.py @@ -29,7 +29,7 @@ from internal.review_protocol import validate_handoff, validate_review, validate_review_response from internal.plan_decision_protocol import validate_plan_decisions from internal.working_set_protocol import validate_working_set -from internal.validation_protocol import validate_acceptance_coverage +from internal.validation_protocol import validate_acceptance_coverage, validate_acceptance_ids from internal.task_layout import events_path, explorations_dir from internal.task_layout import state_path as task_state_path @@ -106,6 +106,7 @@ def validate_projection( raise RuleFailure("current Work Item identity or revision does not match state") if work_item["rigor"] != state["rigor"]: raise RuleFailure("Work Item rigor does not match task state") + validate_acceptance_ids(work_item) if any(work_item["risk_flags"].values()) and state["rigor"] != "R2": raise RuleFailure("a true risk flag requires rigor R2") full_commit(repo, work_item["base_commit"]) diff --git a/tests/test_core.py b/tests/test_core.py index e54ad0e..7b73feb 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3583,6 +3583,7 @@ def test_worker_dispatch_requires_confirm_and_execute_authorization(self) -> Non None, None, ) + write_json_atomic(path, value) with self.assertRaises(RuleFailure): transition( @@ -3616,6 +3617,35 @@ def test_worker_dispatch_requires_confirm_and_execute_authorization(self) -> Non "QUALIFIED", ) + def test_duplicate_acceptance_ids_are_rejected(self) -> None: + """不同内容不得复用同一个 Acceptance ID,否则 Validation 无法精确覆盖。""" + self.freeze_work_item() + work_item_path = self.task / "revisions" / "work-item-r001.json" + work_item = read_json(work_item_path) + work_item["acceptance"].append( + { + "id": "AC-01", + "statement": "A distinct criterion with a reused ID", + "evidence": "a different check", + } + ) + write_json_atomic(work_item_path, work_item) + with self.assertRaisesRegex(RuleFailure, "duplicate acceptance IDs"): + validate(self.repo, "TASK-0001") + with self.assertRaisesRegex(RuleFailure, "duplicate acceptance IDs"): + transition( + self.repo, + "TASK-0001", + "QUALIFY", + [], + None, + None, + None, + None, + None, + None, + ) + def test_r0_keeps_isolated_review_in_same_session(self) -> None: """R0 继续使用同会话隔离审查,不创建独立 Review 任务。""" entry_text = ( @@ -4433,6 +4463,20 @@ def test_review_handoff_freezes_evidence_directory(self) -> None: None, ) evidence.write_text("original evidence\n", encoding="utf-8") + lock_path = self.task / ".transition.lock" + lock_path.write_text("held", encoding="utf-8") + with self.assertRaises(InputFailure): + build_review_handoff( + self.repo, + "TASK-0001", + "impl-session", + "fresh_session", + implementation_path, + knowledge_path, + base, + head, + ) + lock_path.unlink() rebuilt = build_review_handoff( self.repo, "TASK-0001", @@ -4444,6 +4488,38 @@ def test_review_handoff_freezes_evidence_directory(self) -> None: head, ) self.assertEqual(Path(rebuilt["path"]), handoff_path) + stale_state = read_json(self.task / "state.json") + transition( + self.repo, + "TASK-0001", + "START_REVIEW", + [ + "implementation=" + implementation_path.relative_to(self.task).as_posix(), + "knowledge_delta=" + knowledge_path.relative_to(self.task).as_posix(), + "review_handoff=" + handoff_path.relative_to(self.task).as_posix(), + ], + None, + base, + head, + None, + None, + None, + ) + registered_hash = file_sha256(handoff_path) + write_json_atomic(self.task / "state.json", stale_state) + evidence.write_text("changed after registration\n", encoding="utf-8") + with self.assertRaisesRegex(RuleFailure, "differs from events"): + build_review_handoff( + self.repo, + "TASK-0001", + "impl-session", + "fresh_session", + implementation_path, + knowledge_path, + base, + head, + ) + self.assertEqual(file_sha256(handoff_path), registered_hash) def test_start_implementation_self_transition_requires_new_handoff(self) -> None: """IMPLEMENTING 自转换只能在返工清除旧 handoff 后注册下一 attempt。""" From 28c699fdc1ad7b8fcfb5c3d52564e95562c17199 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:31:14 +0800 Subject: [PATCH 12/17] fix: preserve valid workflow rework boundaries --- scripts/internal/recovery_protocol.py | 8 +++++++ scripts/internal/transition_effects.py | 11 +++++++-- scripts/validate_task.py | 32 ++++++++++++++++++++++++-- tests/test_core.py | 27 ++++++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/scripts/internal/recovery_protocol.py b/scripts/internal/recovery_protocol.py index 6d74633..50f9b22 100644 --- a/scripts/internal/recovery_protocol.py +++ b/scripts/internal/recovery_protocol.py @@ -24,6 +24,14 @@ def recommended_action(state: dict[str, Any]) -> str: + if ( + state["status"] == "IMPLEMENTING" + and "implementation_handoff" not in state["artifacts"] + ): + return ( + "build the next Implementer handoff and register it atomically with " + "START_IMPLEMENTATION" + ) return NEXT_ACTIONS.get(state["status"], "inspect the workflow before acting") diff --git a/scripts/internal/transition_effects.py b/scripts/internal/transition_effects.py index f0fa22e..861b7e4 100644 --- a/scripts/internal/transition_effects.py +++ b/scripts/internal/transition_effects.py @@ -152,14 +152,21 @@ def apply_event_effects( max_attempts = transition_rule.get("max_attempts", MAX_REVIEW_ATTEMPTS) if review["artifact_attempt"] >= max_attempts: destination = transition_rule.get("on_max_attempts_to", "BLOCKED") - next_state["blocked_from"] = state["status"] + next_state["blocked_from"] = transition_rule["to"] next_state["blocker"] = { "type": "review_dispute", "reason": f"Review remained rejected after {max_attempts} attempts", "decision_owner": "human", } - if event_name == "REJECT_REVIEW" and destination == "IMPLEMENTING": + review_rework_boundary = event_name == "REJECT_REVIEW" and ( + destination == "IMPLEMENTING" + or ( + destination == "BLOCKED" + and next_state.get("blocker", {}).get("type") == "review_dispute" + ) + ) + if review_rework_boundary: _discard_artifacts(next_state, IMPLEMENTATION_DOWNSTREAM_ARTIFACTS) next_state["subject"] = None elif event_name == "REJECT_REVIEW": diff --git a/scripts/validate_task.py b/scripts/validate_task.py index 1c97740..74e1cd4 100644 --- a/scripts/validate_task.py +++ b/scripts/validate_task.py @@ -119,13 +119,19 @@ def validate_projection( ): raise RuleFailure(f"invalid task exploration scope: {exploration_path}") + prior_review = None if "prior_review" in state["artifacts"]: prior_reference = normalized_reference( directory, state["artifacts"]["prior_review"] ) - validate_json_file( + prior_review = validate_json_file( directory / prior_reference["path"], root / "schemas" / "review.schema.json" ) + if ( + prior_review["task_id"] != task_id + or prior_review["work_item_revision"] != state["current_revision"] + ): + raise RuleFailure("prior Review targets the wrong task revision") projected_status = state["status"] status = authority_status(state) @@ -137,7 +143,29 @@ def validate_projection( require_artifact(state, directory, "plan_decisions") validate_plan_decisions(repo, root, directory, state, True) if at_least(status, "IMPLEMENTING"): - validate_implementation_handoff(repo, root, directory, state) + if "implementation_handoff" in state["artifacts"]: + validate_implementation_handoff(repo, root, directory, state) + else: + stale_names = { + "implementation", + "knowledge_delta", + "review", + "review_2", + "review_handoff", + "review_response", + "validation", + "result", + "final_approval", + } + if ( + status != "IMPLEMENTING" + or prior_review is None + or state["subject"] is not None + or stale_names.intersection(state["artifacts"]) + ): + raise RuleFailure( + f"state {projected_status} requires an Implementation handoff" + ) if state["rigor"] == "R2": require_artifact(state, directory, "pre_approval") if at_least(status, "REVIEWING"): diff --git a/tests/test_core.py b/tests/test_core.py index 7b73feb..bd76f1b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4622,6 +4622,10 @@ def test_validation_rework_supersedes_the_accepted_review(self) -> None: self.assertEqual(failed["to"], "IMPLEMENTING") state = read_json(self.task / "state.json") self.assertEqual(state["artifacts"]["prior_review"]["path"], "reviews/r001/review-001.json") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") + recovered = recover(self.repo, "TASK-0001") + self.assertEqual(recovered["state"]["status"], "IMPLEMENTING") + self.assertIn("START_IMPLEMENTATION", recovered["recommended_next_action"]) self.register_implementation_handoff() base = subject_1["base_commit"] @@ -4688,6 +4692,10 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: None, ) self.assertEqual(rejected["to"], "IMPLEMENTING") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") + recovered = recover(self.repo, "TASK-0001") + self.assertEqual(recovered["state"]["status"], "IMPLEMENTING") + self.assertIn("START_IMPLEMENTATION", recovered["recommended_next_action"]) self.register_implementation_handoff() base = handoff_1["subject_base_commit"] @@ -4838,6 +4846,25 @@ def test_third_rejected_review_enters_human_blocked_state(self) -> None: self.assertEqual(result["to"], "BLOCKED") state = read_json(self.task / "state.json") self.assertEqual(state["blocker"]["type"], "review_dispute") + self.assertEqual(state["blocked_from"], "IMPLEMENTING") + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "BLOCKED") + self.assertEqual(recover(self.repo, "TASK-0001")["state"]["status"], "BLOCKED") + + transition( + self.repo, + "TASK-0001", + "RESOLVE_BLOCK", + [], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") + recovered = recover(self.repo, "TASK-0001") + self.assertIn("START_IMPLEMENTATION", recovered["recommended_next_action"]) def test_recovery_working_set_and_exploration_promotion(self) -> None: """Fresh-session Recovery 只输出四类最小信息,并检索匹配模块的失败探索。""" From 9e67e036bebbe4ac7d5017a3c61e7f08736078ee Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:32:56 +0800 Subject: [PATCH 13/17] fix: bind validation before verified transitions --- scripts/internal/transition_gates.py | 32 +++++++++++-------- scripts/internal/validation_protocol.py | 16 ++++++++++ scripts/validate_task.py | 18 +++++------ tests/test_core.py | 41 +++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index 09e9055..b62f248 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -19,7 +19,11 @@ from .review_protocol import validate_handoff, validate_review, validate_review_response from .plan_decision_protocol import validate_plan_decisions from .working_set_protocol import validate_working_set -from .validation_protocol import validate_acceptance_coverage, validate_acceptance_ids +from .validation_protocol import ( + validate_acceptance_coverage, + validate_acceptance_ids, + validate_validation_identity, +) def artifact_file(directory: Path, state: dict[str, Any], name: str) -> Path: @@ -33,7 +37,7 @@ def artifact_file(directory: Path, state: dict[str, Any], name: str) -> Path: if not path.is_file(): raise RuleFailure(f"artifact does not exist: {path}") if isinstance(reference, dict) and reference.get("sha256") != file_sha256(path): - raise RuleFailure(f"artifact content changed after registration: {name}") + raise RuleFailure(f"artifact {name} changed after it was registered") return path @@ -245,8 +249,13 @@ def check_gate( if validation["verdict"] != "PASS": raise RuleFailure("Validation verdict must be PASS") validate_acceptance_coverage(work_item, validation) - if validation["subject_diff_hash"] != state["subject"]["diff_hash"]: - raise RuleFailure("Validation targets the wrong subject") + implementation = validate_json_file( + artifact_file(directory, state, "implementation"), + root / "schemas" / "implementation.schema.json", + ) + validate_validation_identity( + state, validation, implementation["artifact_attempt"] + ) if gate == "validation_passed_and_closure_ready": from validate_task import validate_projection @@ -257,14 +266,13 @@ def check_gate( validation = load_validation(root, directory, state) if validation["verdict"] != "FAIL": raise RuleFailure("failure transition requires a FAIL Validation") - if ( - validation["task_id"] != state["task_id"] - or validation["work_item_revision"] != revision - or validation["subject_base_commit"] != state["subject"]["base_commit"] - or validation["subject_head_commit"] != state["subject"]["head_commit"] - or validation["subject_diff_hash"] != state["subject"]["diff_hash"] - ): - raise RuleFailure("failed Validation targets the wrong revision or subject") + implementation = validate_json_file( + artifact_file(directory, state, "implementation"), + root / "schemas" / "implementation.schema.json", + ) + validate_validation_identity( + state, validation, implementation["artifact_attempt"] + ) elif gate == "closure_ready": if state["rigor"] != "R2": raise RuleFailure("only R2 closes from VERIFIED") diff --git a/scripts/internal/validation_protocol.py b/scripts/internal/validation_protocol.py index e020580..b9b740d 100644 --- a/scripts/internal/validation_protocol.py +++ b/scripts/internal/validation_protocol.py @@ -31,3 +31,19 @@ def validate_acceptance_coverage( raise RuleFailure( "Validation must cover every acceptance criterion exactly once with PASS" ) + + +def validate_validation_identity( + state: dict[str, Any], validation: dict[str, Any], artifact_attempt: int +) -> None: + """Bind Validation authority to the current task, attempt, and frozen subject.""" + subject = state.get("subject") + if not isinstance(subject, dict) or ( + validation["task_id"] != state["task_id"] + or validation["work_item_revision"] != state["current_revision"] + or validation["artifact_attempt"] != artifact_attempt + or validation["subject_base_commit"] != subject["base_commit"] + or validation["subject_head_commit"] != subject["head_commit"] + or validation["subject_diff_hash"] != subject["diff_hash"] + ): + raise RuleFailure("Validation targets the wrong revision, attempt, or subject") diff --git a/scripts/validate_task.py b/scripts/validate_task.py index 74e1cd4..2e68118 100644 --- a/scripts/validate_task.py +++ b/scripts/validate_task.py @@ -29,7 +29,11 @@ from internal.review_protocol import validate_handoff, validate_review, validate_review_response from internal.plan_decision_protocol import validate_plan_decisions from internal.working_set_protocol import validate_working_set -from internal.validation_protocol import validate_acceptance_coverage, validate_acceptance_ids +from internal.validation_protocol import ( + validate_acceptance_coverage, + validate_acceptance_ids, + validate_validation_identity, +) from internal.task_layout import events_path, explorations_dir from internal.task_layout import state_path as task_state_path @@ -279,15 +283,9 @@ def validate_projection( if validation["verdict"] != "PASS": raise RuleFailure("VERIFIED requires a PASS Validation") validate_acceptance_coverage(work_item, validation) - if ( - validation["task_id"] != task_id - or validation["work_item_revision"] != state["current_revision"] - or validation["artifact_attempt"] != implementation["artifact_attempt"] - or validation["subject_base_commit"] != state["subject"]["base_commit"] - or validation["subject_head_commit"] != state["subject"]["head_commit"] - or validation["subject_diff_hash"] != state["subject"]["diff_hash"] - ): - raise RuleFailure("Validation targets the wrong revision or subject") + validate_validation_identity( + state, validation, implementation["artifact_attempt"] + ) if status == "CLOSED": result_path = require_artifact(state, directory, "result") result = validate_json_file(result_path, root / "schemas" / "result.schema.json") diff --git a/tests/test_core.py b/tests/test_core.py index bd76f1b..5871762 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1071,6 +1071,47 @@ def test_r2_keeps_verified_and_final_approval_gate(self) -> None: ) validation["acceptance_results"].pop() write_json_atomic(validation_path, validation) + identity_mismatches = { + "task_id": "TASK-9999", + "work_item_revision": 2, + "artifact_attempt": 2, + "subject_base_commit": subject["head_commit"], + "subject_head_commit": subject["base_commit"], + "subject_diff_hash": "0" * 64, + } + for field, mismatched_value in identity_mismatches.items(): + with self.subTest(validation_identity=field): + original_value = validation[field] + validation[field] = mismatched_value + write_json_atomic(validation_path, validation) + state_bytes = (self.task / "state.json").read_bytes() + event_bytes = (self.task / "events.jsonl").read_bytes() + index_path = self.repo / ".polaris" / "project-index.json" + index_bytes = index_path.read_bytes() + try: + with self.assertRaisesRegex( + RuleFailure, "Validation targets the wrong" + ): + transition( + self.repo, + "TASK-0001", + "PASS_VALIDATION", + ["validation=validations/r001/validation-001.json"], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual((self.task / "state.json").read_bytes(), state_bytes) + self.assertEqual((self.task / "events.jsonl").read_bytes(), event_bytes) + finally: + (self.task / "state.json").write_bytes(state_bytes) + (self.task / "events.jsonl").write_bytes(event_bytes) + index_path.write_bytes(index_bytes) + validation[field] = original_value + write_json_atomic(validation_path, validation) verified = transition( self.repo, "TASK-0001", From a41acf277d3634baba562bd57b747d5bd38a92bb Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:34:36 +0800 Subject: [PATCH 14/17] fix: support second reviewer rejection --- scripts/internal/transition_effects.py | 23 +++++++-- scripts/internal/transition_gates.py | 35 ++++++++++++-- tests/test_core.py | 65 ++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/scripts/internal/transition_effects.py b/scripts/internal/transition_effects.py index 861b7e4..3aa19d4 100644 --- a/scripts/internal/transition_effects.py +++ b/scripts/internal/transition_effects.py @@ -133,6 +133,23 @@ def _discard_artifacts(state: dict[str, Any], names: frozenset[str]) -> None: state["artifacts"].pop(name, None) +def _rejecting_review( + root: Path, directory: Path, state: dict[str, Any] +) -> tuple[str, dict[str, Any]]: + matches: list[tuple[str, dict[str, Any]]] = [] + for name in ("review", "review_2"): + if name not in state["artifacts"]: + continue + review, _ = load_registered( + root, directory, state, name, "review.schema.json" + ) + if review["verdict"] == "REJECT": + matches.append((name, review)) + if len(matches) != 1: + raise RuleFailure("REJECT_REVIEW requires exactly one rejecting Review") + return matches[0] + + def apply_event_effects( root: Path, directory: Path, @@ -143,11 +160,9 @@ def apply_event_effects( transition_rule: dict[str, Any], ) -> str: if event_name == "REJECT_REVIEW": - review, _ = load_registered( - root, directory, next_state, "review", "review.schema.json" - ) + review_name, review = _rejecting_review(root, directory, next_state) next_state["artifacts"]["prior_review"] = normalized_reference( - directory, next_state["artifacts"]["review"] + directory, next_state["artifacts"][review_name] ) max_attempts = transition_rule.get("max_attempts", MAX_REVIEW_ATTEMPTS) if review["artifact_attempt"] >= max_attempts: diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index b62f248..0adf52a 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -211,10 +211,9 @@ def check_gate( artifact_file(directory, state, "knowledge_delta") check_subject(repo, state.get("subject")) validate_handoff(repo, root, directory, state) - elif gate in {"review_accepted", "review_rejected"}: - expected = "ACCEPT" if gate == "review_accepted" else "REJECT" + elif gate == "review_accepted": names = ["review"] - if expected == "ACCEPT" and any( + if any( work_item["risk_flags"].get(flag, False) for flag in workflow.get("two_reviewer_risk_flags", []) ): @@ -222,12 +221,38 @@ def check_gate( reviews = [load_review(root, directory, state, name) for name in names] reviewer_ids: set[str] = set() for review in reviews: - if review["verdict"] != expected: - raise RuleFailure(f"Review verdict must be {expected}") + if review["verdict"] != "ACCEPT": + raise RuleFailure("Review verdict must be ACCEPT") + validate_review(repo, root, directory, state, review, work_item) + if review["reviewer_session_id"] in reviewer_ids: + raise RuleFailure("required Reviews must come from distinct Reviewer sessions") + reviewer_ids.add(review["reviewer_session_id"]) + elif gate == "review_rejected": + requires_two = any( + work_item["risk_flags"].get(flag, False) + for flag in workflow.get("two_reviewer_risk_flags", []) + ) + names = ["review"] + if requires_two and "review_2" in state["artifacts"]: + names.append("review_2") + elif not requires_two and "review_2" in state["artifacts"]: + raise RuleFailure("review_2 is reserved for high-risk two-Reviewer flow") + reviews = [load_review(root, directory, state, name) for name in names] + reviewer_ids: set[str] = set() + for review in reviews: validate_review(repo, root, directory, state, review, work_item) if review["reviewer_session_id"] in reviewer_ids: raise RuleFailure("required Reviews must come from distinct Reviewer sessions") reviewer_ids.add(review["reviewer_session_id"]) + verdicts = [review["verdict"] for review in reviews] + allowed = [["REJECT"]] + if requires_two: + allowed.append(["ACCEPT", "REJECT"]) + if verdicts not in allowed: + raise RuleFailure( + "Review rejection requires slot 1 REJECT or slot 1 ACCEPT followed by " + "slot 2 REJECT" + ) elif gate == "validation_ready": names = ["review"] if any( diff --git a/tests/test_core.py b/tests/test_core.py index 5871762..9fff0f0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4831,6 +4831,71 @@ def test_rejected_finding_requires_response_and_stable_follow_up(self) -> None: self.assertEqual(accepted["to"], "VALIDATING") self.assertEqual(validate(self.repo, "TASK-0001")["state"], "VALIDATING") + def test_high_risk_second_reviewer_can_reject_into_rework(self) -> None: + """高风险 R2 的第二位 Reviewer 可独立拒绝并成为返工 Authority。""" + self.set_task_rigor("R2") + work_item_path = self.task / "revisions" / "work-item-r001.json" + work_item = read_json(work_item_path) + work_item["risk_flags"]["security"] = True + write_json_atomic(work_item_path, work_item) + handoff, state = self.enter_reviewing_without_progress() + + accepted_path = self.task / "reviews" / "r001" / "review-001.json" + write_json_atomic( + accepted_path, + self.review_value(handoff, state, "review-session-1", "ACCEPT"), + ) + finding = { + "id": "F-001", + "introduced_in_attempt": 1, + "category": "engineering", + "acceptance_id": None, + "scope_violation": False, + "blocking": True, + "severity": "high", + "location": "subject.txt:1", + "claim": "The security boundary remains unsafe", + "evidence": "The frozen subject admits a counterexample", + "required_action": "Harden the security boundary", + "status": "open", + "reviewer_resolution": None, + } + rejected_path = self.task / "reviews" / "r001" / "review-002.json" + write_json_atomic( + rejected_path, + self.review_value( + handoff, state, "review-session-2", "REJECT", [finding] + ), + ) + + rejected = transition( + self.repo, + "TASK-0001", + "REJECT_REVIEW", + [ + "review=reviews/r001/review-001.json", + "review_2=reviews/r001/review-002.json", + ], + None, + None, + None, + None, + None, + None, + ) + + self.assertEqual(rejected["to"], "IMPLEMENTING") + state = read_json(self.task / "state.json") + self.assertEqual( + state["artifacts"]["prior_review"]["path"], + "reviews/r001/review-002.json", + ) + self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") + self.assertIn( + "START_IMPLEMENTATION", + recover(self.repo, "TASK-0001")["recommended_next_action"], + ) + def test_third_rejected_review_enters_human_blocked_state(self) -> None: """第三次 Review 仍 REJECT 时不再自动返工,而是进入 Human-owned BLOCKED。""" self.freeze_work_item() From daa96183e40886c8051b2f1411c13d111e5417f2 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:35:01 +0800 Subject: [PATCH 15/17] docs: correct review handoff workflow --- docs/USAGE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/USAGE.md b/docs/USAGE.md index 9c8483d..9cc12c0 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -495,8 +495,8 @@ Implementer 只接收 task ID 和已注册 handoff,不继承主任务聊天。 实现和 Documentation Sync 完成后,由主任务根据 Implementer 的最终产物生成冻结 handoff: ```powershell -python tools/polaris/scripts/build_review_handoff.py TASK-0001 --repo . --implementer-session-id impl-20260813 --isolation fresh_session -python tools/polaris/scripts/transition_task.py TASK-0001 START_REVIEW --repo . --artifact review_handoff=reviews/r001/handoff-001.json +python tools/polaris/scripts/build_review_handoff.py TASK-0001 --repo . --implementer-session-id impl-20260813 --isolation fresh_session --implementation .polaris/tasks/TASK-0001/implementations/r001/attempt-001.json --knowledge-delta .polaris/tasks/TASK-0001/knowledge/r001/knowledge-delta-001.json --subject-base --subject-head +python tools/polaris/scripts/transition_task.py TASK-0001 START_REVIEW --repo . --artifact implementation=implementations/r001/attempt-001.json --artifact knowledge_delta=knowledge/r001/knowledge-delta-001.json --artifact review_handoff=reviews/r001/handoff-001.json --subject-base --subject-head ``` 然后按严谨度处理: From 4d227cebd0e4f687a23fee32b1ad3d9352143946 Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:51:35 +0800 Subject: [PATCH 16/17] fix: stop review retries at dispute limit --- ...26-08-18-workflow-simplification-design.md | 2 + plan.md | 2 +- scripts/internal/recovery_protocol.py | 5 +++ scripts/internal/transition_gates.py | 5 +++ tests/test_core.py | 39 +++++++++++-------- 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md index 8cf2fea..9d7005f 100644 --- a/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md +++ b/docs/superpowers/specs/2026-08-18-workflow-simplification-design.md @@ -90,6 +90,8 @@ BLOCKED -- RESOLVE_BLOCK --> blocked_from 任意非终态 -- CANCEL --> CANCELLED ``` +`review_dispute` 是 `RESOLVE_BLOCK` 的例外:同一 revision 的 Review 达到最大 attempt 后,任务保持 Human-owned `BLOCKED`,只能通过 `NEW_REVISION` 或 `CANCEL` 离开,不能开启第 4 次 attempt。 + ## 转换设计 ### 开始 Implementation diff --git a/plan.md b/plan.md index 5bb93a9..1abe8f9 100644 --- a/plan.md +++ b/plan.md @@ -436,7 +436,7 @@ BLOCKED -- RESOLVED --> blocked_from 任意非终态 -- HUMAN_CANCEL --> CANCELLED ``` -`BLOCKED` 保存 `blocked_from`、`blocker_type`、原因和所需 Decision Owner。R2 等待人工审批使用 `blocker_type=human_approval`,不增加单独的等待状态。条件解除后默认只返回 `blocked_from`;生成新 revision 时由 `NEW_REVISION` 特殊转换直接进入 `QUALIFIED`。 +`BLOCKED` 保存 `blocked_from`、`blocker_type`、原因和所需 Decision Owner。R2 等待人工审批使用 `blocker_type=human_approval`,不增加单独的等待状态。条件解除后默认只返回 `blocked_from`;但 Review 达到同一 revision 的最大 attempt 后使用 `blocker_type=review_dispute`,禁止 `RESOLVE_BLOCK` 继续当前 revision,只允许 Human 选择 `NEW_REVISION` 或 `CANCEL`。生成新 revision 时由 `NEW_REVISION` 特殊转换直接进入 `QUALIFIED`。 v0.1 不设置 `FAILED`:可修复失败通过治理回路处理,外部阻塞进入 `BLOCKED`,人工放弃进入 `CANCELLED`,脚本或环境异常以退出码 `2` 报告且不改变业务状态。 diff --git a/scripts/internal/recovery_protocol.py b/scripts/internal/recovery_protocol.py index 50f9b22..f480f87 100644 --- a/scripts/internal/recovery_protocol.py +++ b/scripts/internal/recovery_protocol.py @@ -24,6 +24,11 @@ def recommended_action(state: dict[str, Any]) -> str: + if ( + state["status"] == "BLOCKED" + and state.get("blocker", {}).get("type") == "review_dispute" + ): + return "have the Human Decision Owner choose NEW_REVISION or CANCEL" if ( state["status"] == "IMPLEMENTING" and "implementation_handoff" not in state["artifacts"] diff --git a/scripts/internal/transition_gates.py b/scripts/internal/transition_gates.py index 0adf52a..46a3521 100644 --- a/scripts/internal/transition_gates.py +++ b/scripts/internal/transition_gates.py @@ -315,5 +315,10 @@ def check_gate( elif gate == "blocker_resolved": if state.get("blocked_from") is None: raise RuleFailure("BLOCKED state has no blocked_from state") + if state.get("blocker", {}).get("type") == "review_dispute": + raise RuleFailure( + "review_dispute cannot be resolved in the same revision; " + "use NEW_REVISION or CANCEL" + ) elif gate == "human_cancelled": artifact_file(directory, state, "cancel_decision") diff --git a/tests/test_core.py b/tests/test_core.py index 9fff0f0..27a67fc 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4954,23 +4954,30 @@ def test_third_rejected_review_enters_human_blocked_state(self) -> None: self.assertEqual(state["blocker"]["type"], "review_dispute") self.assertEqual(state["blocked_from"], "IMPLEMENTING") self.assertEqual(validate(self.repo, "TASK-0001")["state"], "BLOCKED") - self.assertEqual(recover(self.repo, "TASK-0001")["state"]["status"], "BLOCKED") - - transition( - self.repo, - "TASK-0001", - "RESOLVE_BLOCK", - [], - None, - None, - None, - None, - None, - None, - ) - self.assertEqual(validate(self.repo, "TASK-0001")["state"], "IMPLEMENTING") recovered = recover(self.repo, "TASK-0001") - self.assertIn("START_IMPLEMENTATION", recovered["recommended_next_action"]) + self.assertEqual(recovered["state"]["status"], "BLOCKED") + self.assertIn("NEW_REVISION", recovered["recommended_next_action"]) + self.assertIn("CANCEL", recovered["recommended_next_action"]) + + state_before = (self.task / "state.json").read_bytes() + events_before = (self.task / "events.jsonl").read_bytes() + with self.assertRaisesRegex(RuleFailure, "NEW_REVISION or CANCEL"): + transition( + self.repo, + "TASK-0001", + "RESOLVE_BLOCK", + [], + None, + None, + None, + None, + None, + None, + ) + self.assertEqual((self.task / "state.json").read_bytes(), state_before) + self.assertEqual((self.task / "events.jsonl").read_bytes(), events_before) + state = read_json(self.task / "state.json") + self.assertEqual(state["status"], "BLOCKED") def test_recovery_working_set_and_exploration_promotion(self) -> None: """Fresh-session Recovery 只输出四类最小信息,并检索匹配模块的失败探索。""" From 393bd5fe1a97b2ca05fd0cf33fe9523cbfa07b3a Mon Sep 17 00:00:00 2001 From: GraphZLL Date: Wed, 19 Aug 2026 00:55:55 +0800 Subject: [PATCH 17/17] fix: clear blockers on new revisions --- scripts/internal/transition_effects.py | 2 ++ tests/test_core.py | 37 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/scripts/internal/transition_effects.py b/scripts/internal/transition_effects.py index 3aa19d4..5fae158 100644 --- a/scripts/internal/transition_effects.py +++ b/scripts/internal/transition_effects.py @@ -93,6 +93,8 @@ def prepare_next_state( next_state["current_revision"] = revision next_state["artifacts"] = {} next_state["subject"] = None + next_state["blocked_from"] = None + next_state["blocker"] = None work_item = current_work_item_path(directory, revision) next_state["rigor"] = read_json(work_item)["rigor"] diff --git a/tests/test_core.py b/tests/test_core.py index 27a67fc..e00dfd4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4979,6 +4979,43 @@ def test_third_rejected_review_enters_human_blocked_state(self) -> None: state = read_json(self.task / "state.json") self.assertEqual(state["status"], "BLOCKED") + created = new_revision(self.repo, "TASK-0001") + self.assertEqual(created["revision"], 2) + work_item_path = self.task / "revisions" / "work-item-r002.json" + work_item = read_json(work_item_path) + work_item["implementation_dispatch"]["authorized"] = True + work_item["review_dispatch"]["authorized"] = True + work_item["known_unknowns"] = [] + write_json_atomic(work_item_path, work_item) + transition( + self.repo, + "TASK-0001", + "NEW_REVISION", + [], + 2, + None, + None, + None, + None, + None, + ) + + state = read_json(self.task / "state.json") + self.assertEqual(state["status"], "QUALIFIED") + self.assertIsNone(state["blocked_from"]) + self.assertIsNone(state["blocker"]) + last_event = read_jsonl(self.task / "events.jsonl")[-1] + self.assertIsNone(last_event["blocked_from"]) + self.assertIsNone(last_event["blocker"]) + self.assertEqual(rebuild_state_value(self.task / "events.jsonl"), state) + recovered = recover(self.repo, "TASK-0001") + self.assertIsNone(recovered["state"]["blocker"]) + project_index = read_json(self.repo / ".polaris" / "project-index.json") + indexed_task = next( + item for item in project_index["tasks"] if item["task_id"] == "TASK-0001" + ) + self.assertIsNone(indexed_task["blocker"]) + def test_recovery_working_set_and_exploration_promotion(self) -> None: """Fresh-session Recovery 只输出四类最小信息,并检索匹配模块的失败探索。""" work_item_path = self.task / "revisions" / "work-item-r001.json"