cowork-flow 1.7.3 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/docs/direction.md +15 -1
- package/package.json +1 -1
- package/presets/claude-code/.claude-plugin/plugin.json +1 -1
- package/presets/codex/.codex-plugin/plugin.json +1 -1
- package/presets/kimi-code/.kimi-plugin/plugin.json +1 -1
- package/presets/opencode/cowork-flow/plugin-core.js +51 -3
- package/presets/qoder/.qoder-plugin/plugin.json +1 -1
- package/presets/zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/adapters/cli/spec_check.py +10 -1
- package/template/.cowork-flow/scripts/adapters/cli/task.py +2 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_lifecycle_commands.py +48 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_next_runner.py +57 -0
- package/template/.cowork-flow/scripts/adapters/cli/task_parser.py +15 -0
- package/template/.cowork-flow/scripts/adapters/host/workflow_state_hook.py +21 -0
- package/template/.cowork-flow/scripts/services/ac_evidence.py +107 -0
- package/template/.cowork-flow/scripts/services/fact_view.py +16 -2
- package/template/.cowork-flow/scripts/services/lifecycle_checks.py +84 -0
- package/template/.cowork-flow/scripts/services/lifecycle_policy.py +5 -0
- package/template/.cowork-flow/scripts/services/spec_check.py +11 -1
- package/template/.cowork-flow/scripts/services/task_lifecycle.py +189 -2
- package/template/.cowork-flow/spec/contracts/context-injection.md +5 -3
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +1 -0
- package/template/.cowork-flow/spec/contracts/evidence-completion.md +48 -0
- package/template/.cowork-flow/spec/contracts/index.md +2 -0
- package/template/.cowork-flow/spec/contracts/spec-checks.md +49 -82
- package/template/.cowork-flow/spec/contracts/task-adopt.md +64 -0
- package/template/.cowork-flow/spec/runtime/contract-registry.json +13 -0
- package/template/.opencode/cowork-flow/plugin-core.js +51 -3
- package/template/skills/spec-sync/SKILL.md +1 -0
- package/template/skills/task-review/SKILL.md +3 -2
- package/template/skills/test-first/SKILL.md +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@
|
|
|
5
5
|
### Added
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
## [1.8.1] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **任务收养 `task next --run --adopt`(1.9.0 多执行者协作前置)**:为孤儿/滞留任务提供受支持的接管路径,替代手改 task.json meta。收养是独立动作(不与 review/complete/archive 链式同发):把 `in_progress`/`review` 状态、由另一执行者占有的任务重绑到收养者(`executor`),把评审基线**重置**到收养时 HEAD(原值记入 `meta.previousBaseline`),保留状态并写审计字段 `meta.previousExecutor` / `meta.previousBaseline` / `meta.adoptedAt`,结果 code `LIFECYCLE-EXECUTOR-ADOPTED`。误用 fail-closed(`LIFECYCLE-ADOPT-001`):planning/completed、无 executor、自我收养、process-fallback 身份、delegated 上下文、携带 create/plan 或批启动旗标全部拒绝且不改状态。start 路径"基线永不覆盖"不变量原样保留(既有测试零改动),收养是唯一经审计的显式重置路径;成功后 CLI 把会话绑定到该任务。契约见 `.cowork-flow/spec/contracts/task-adopt.md`。
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`--allow-missing-evidence` 恢复独立解析**(1.8.0 遗留缺陷):该旗标此前被误注册为 `--allow-unchecked` 的解析别名(同一 `add_argument`、dest 相同),造成两个后果——AC 证据豁免(`LIFECYCLE-AC-002` 的可豁免路径)从 CLI 不可达(`cmd_complete` 读取的属性 argparse 从不创建,恒为 False);且传入 `--allow-missing-evidence` 会静默获得 spec-check 未检查豁免(语义错误、方向危险:本意只豁免 AC 证据,却放开了缺失/超时的 spec-check 命令)。现在两个旗标各自独立解析,parser 级与命令贯通级回归测试锁定(既有测试手工构造 Namespace 绕过 parser,正是此缺陷漏网原因)。
|
|
17
|
+
|
|
18
|
+
## [1.8.0] - 2026-10-08
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **spec-check 采纳闭环(1.8.x 主线)**:让 spec-check 机制从"存在但沉默"变为可遵循。① 零声明可见性:仓库无生效声明时 `spec-check` 文本模式输出显式事实行 `spec-check: no checks declared (<N> spec files scanned)`(与"有声明且全过"可区分),`--json` 顶层新增 `specFiles`、summary 新增 `declarations`——"没挂声明"不再与"干净通过"同形(零声明保持退出码 0:不挂命令是合法状态,但必须被看见)。② 注入契约 `SPEC_CHECKS_V1`:`contract-registry.json` 新增第 10 条(digest 2 条 + readWhen 3 条),python/zcode/opencode 三线渲染在 digest 块输出 `- SPEC_CHECKS_V1: .cowork-flow/spec/contracts/spec-checks.md`。③ `spec-checks.md` 重排为使用者核心区(何时挂 / 语法含 UTF-8 无 BOM 注意点 / 三态与门禁含零声明语义 / 时机与输出 / 归属)+ 维护者附录(宿主能力矩阵 / delegated 缺口与补偿 / digest 注入 / 开放决策),零信息丢失由独立复核逐项清点确认。④ AI 挂命令职责三处一致:契约归属段、`task-review`(User specs 逐条问"可机检吗、已挂吗" + `user_spec_review` 列「条款 → 命令」清单)、`spec-sync`(同次维护挂/刷新声明);质量纪律四条(干净树跑出 pass / 反例能报错 / `files` 限定 / 挂后跑 spec-check 确认解析)获测试锁(`test_review_contract.py` markers)。不新增硬门禁——放权语义:主会话 AI 主动挂声明,review 报告留痕。
|
|
23
|
+
- **证据化完成(1.8.0 主线)**:任务完成从"流程走完"升级为"证据齐全"。decision-anchor 的验收标准(AC)成为机器可查询事实——`services/ac_evidence.py` 解析 `## 验收标准` 的勾选状态并读取任务目录 `evidence.jsonl`(追加式,`{ac, kind: test|command|manual, ref, note?, recordedAt, by}`);完成动作新增两条稳定门禁 code:存在未勾选 AC 阻断(`LIFECYCLE-AC-001`)、已声明 AC 缺有效证据记录阻断(`LIFECYCLE-AC-002`),新旗标 `--allow-missing-evidence` 豁免后者并把 `meta.evidenceExempt`(含 `at` 与 `missing`)留在任务记录;decision-anchor 注入块在 `Acceptance:` 行后追加 `AC evidence: n/m`(缺失时附 `missing=` 列表),三线(Python / zcode shim / opencode 插件)逐字节同格式并由矩阵测试锁定;`run state --json` 与 MCP 事实视图新增 `evidenceCoverage`。无 AC 的存量任务零影响(兼容路径有测试锁定),契约见 `.cowork-flow/spec/contracts/evidence-completion.md`。
|
|
24
|
+
|
|
8
25
|
## [1.7.3] - 2026-10-06
|
|
9
26
|
|
|
10
27
|
### Fixed
|
|
@@ -289,7 +306,9 @@
|
|
|
289
306
|
|
|
290
307
|
此版本未保留本地条目正文,详情见 [GitHub Release](https://github.com/fishlikewater/cowork-flow/releases/tag/v0.0.47)。
|
|
291
308
|
|
|
292
|
-
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.
|
|
309
|
+
[Unreleased]: https://github.com/fishlikewater/cowork-flow/compare/v1.8.1...HEAD
|
|
310
|
+
[1.8.1]: https://github.com/fishlikewater/cowork-flow/compare/v1.8.0...v1.8.1
|
|
311
|
+
[1.8.0]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.3...v1.8.0
|
|
293
312
|
[1.7.3]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.2...v1.7.3
|
|
294
313
|
[1.7.2]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.1...v1.7.2
|
|
295
314
|
[1.7.1]: https://github.com/fishlikewater/cowork-flow/compare/v1.7.0...v1.7.1
|
package/docs/direction.md
CHANGED
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
| 本规划文档落库 | ✅ 完成 | 08-27-direction-doc | — |
|
|
17
17
|
| 1.5→1.7 宿主广度 + 发布链(七宿主) | ✅ 完成 | CHANGELOG 1.5.0-1.7.2 | — |
|
|
18
18
|
| 1.7.3 维护:ZCode shim .mjs + 项目根查找修复 | ✅ 完成 | 10-04-zcode-shim-mjs(d0716a9) | — |
|
|
19
|
-
| 1.8.0 立项:证据化完成 |
|
|
19
|
+
| 1.8.0 立项:证据化完成 | ✅ 完成 | 10-05-direction-1-8-0 / 10-05-evidence-completion | — |
|
|
20
|
+
| 1.8.0 主线实现:证据化完成 | ✅ 完成 | 10-05-evidence-completion(baa386d,CI test + windows-core 全绿);已随 1.8.0 发布(cfe1ac1) | — |
|
|
21
|
+
| 1.8.x 收尾:Skill 证据补课 + spec-check 采纳闭环 | ✅ 完成 | 10-06-skill-evidence-training(09937d0)、10-08 spec-check 闭环(08f9d9b) | — |
|
|
22
|
+
| 1.9.0 前置:task adopt 命令 | ✅ 完成 | 10-06-task-adopt-command(e6e5132):`--adopt` 重绑执行者 + 基线重置 + 审计;契约 task-adopt.md | 1.9.0 主线(多执行者协作)立项 |
|
|
23
|
+
| 1.8.1 补丁:`--allow-missing-evidence` 解析修复 + adopt | 🔄 本次发布 | 10-08-fix-evidence-flag-parser(dc594a4);tag v1.8.1 → CI 发布 npm | 确认 CI 全绿与 npm latest=1.8.1 |
|
|
20
24
|
|
|
21
25
|
**当前结论**:方向路线图阶段 0-3 全部落地。项目当前形态:「多主机 Agent 协作的运行时上下文与协作事实层」——事实视图(CLI + MCP)、结构化注入、跨宿主一致性治理、执行者归属与证据位均已就绪并有测试锁定。后续演进见「不做什么」边界与观察期事项。
|
|
22
26
|
|
|
@@ -156,6 +160,14 @@
|
|
|
156
160
|
|
|
157
161
|
5. **采用**:npm 周下载与激活项目数——1.8.0 之后用数字验证"深度路线"是否兑现。
|
|
158
162
|
|
|
163
|
+
### 10.5 2026-10-06 增补:1.8.x 收尾与 1.9.0 排期
|
|
164
|
+
|
|
165
|
+
- **1.8.0 主线已实现**(10-05-evidence-completion,baa386d):AC 事实化、`evidence.jsonl`、完成门禁 `LIFECYCLE-AC-001/002`、三线注入覆盖行、`evidenceCoverage` 事实视图;主线任务自身以新门禁完成(四 AC 证据齐全,无豁免)。
|
|
166
|
+
- **1.8.0 发布判据**:CI 双平台(已绿)+ 真实宿主冒烟(维护者:带 AC 任务在 ZCode/Qoder 实跑一轮,观察覆盖行与门禁体验)+ Skill 证据补课合入。
|
|
167
|
+
- **1.9.0 主线候选:agent-teams 就绪**,按 10.3 排期;前置小任务 = **task adopt 命令**(孤儿任务收养:显式重绑执行者 + 陈旧基线降级 + 审计留痕)。需求证据:2026-10-06 归档两个滞留任务时,takeover 只重绑执行者,陈旧基线需手改 task.json meta 才能走降级归档——该操作目前无运行时命令承载。10.5 增补时立项(10-06-task-adopt-command),设计注意"基线永不覆盖"不变量与显式收养语义的边界。
|
|
168
|
+
- **Skill 证据补课**(10-06-skill-evidence-training):1.8.0 只改了运行时与契约,教 agent 写证据的目前只有门禁报错文案;task-review 等 Skill 正文需补 evidence.jsonl 约定。
|
|
169
|
+
- **维持**:文档英文化待维护者拍板(与 AGENTS.md 文档语言约定冲突);缓行清单不变。**观察项**:1.8.0 的机器可解析 AC 是与 Spec Kit / OpenSpec 互操作的第一块砖(spec 制品 ↔ decision-anchor 映射),符合"SDD 工具之下的状态层"定位,待 agent-teams 排期落定后议。
|
|
170
|
+
|
|
159
171
|
## 11. 修订记录
|
|
160
172
|
|
|
161
173
|
- 2026-08-27:创建。基于三路只读探查(注入层/状态模型/治理层)+ 大模型趋势分析;用户拍板方案 A;落库任务 08-27-direction-doc。
|
|
@@ -165,3 +177,5 @@
|
|
|
165
177
|
- 2026-08-29:阶段 3 完成并更新状态追踪——路线图阶段 0-3 全部落地。`run mcp-state` 无依赖 MCP stdio 只读服务(task_state / task_list)、`spec/contracts/fact-layer-access.md` 接入契约(只读保证 + 薄 adapter 立场)(a88474e)。观察期事项:AGNTCY 等跨 agent 协议收敛后按薄壳接入;MCP 写工具默认拒绝,提案须先重开 fact-layer-access 契约。
|
|
166
178
|
- 2026-08-29:MCP 全局入口补充——npm CLI 新增 `cowork-flow mcp-state` 透传命令(cwd 向上定位项目运行时、stdio inherit、退出码透传),MCP 客户端全局注册一次即可服务所有项目,消除逐项目 `.mcp.json` 配置(9a1fa86)。
|
|
167
179
|
- 2026-08-29:后续方向 P0-P3 执行完毕。**P0**:1.1.0 发布——dev→master 合并、CHANGELOG 1.1.0 段(含 1.0.0 载体错位注明)、tag v1.1.0 与 GitHub Release 创建;publish CI 首跑曝光 node:full 存量问题(dsh-hook 依赖 gitignored 运行时、Windows 无法 exec shebang 脚本、test runner IPC 崩溃)并全部修复——双平台门禁现已全绿。**发布确认:npm registry `cowork-flow@1.1.0` 已上线(dist-tag latest),AC-004 闭合,发版任务归档。** **P1**:`docs/mcp-client-setup.md`(codex/opencode/claude-code/zcode 真实注册格式)+ `test/mcp-client-matrix.test.js` 客户端等效启动验证。**P1'**:publish.yml 链路实测核对并写入 README 发布节。**P2**:flaky 组合复跑 5 轮未复现(记录为单次现象);doctor 会话卫生无残留。**P3**:观察期边界维持——MCP 写工具默认拒绝,AGNTCY 等协议收敛后按薄壳接入。- 2026-10-05:2026-10 修订落库(任务 10-05-direction-1-8-0)。1.7.3 维护完成(10-04-zcode-shim-mjs,d0716a9:ZCode shim 改 .mjs、findWorkflowRoot off-by-one 修复);1.8.0 主线定为**证据化完成**,主线任务 10-05-evidence-completion 立项(planning 就绪待启动);候选 B(agent-teams 就绪)列为 1.9.0 候选主线;文档英文化列为待决项,待维护者拍板。方向文档自本次起恢复版本控制(087ef72 untrack 后首次入库)。
|
|
180
|
+
- 2026-10-06:10.5 增补落库(任务 10-06-direction-post-1-8-0)。1.8.0 主线实现完成(10-05-evidence-completion,baa386d,CI 双平台全绿);立项 10-06-skill-evidence-training(1.8.0 收尾)与 10-06-task-adopt-command(1.9.0 前置);1.8.0 发布判据 = CI + 真实宿主冒烟 + Skill 补课。
|
|
181
|
+
- 2026-10-08:1.8.0 已发布(cfe1ac1,tag v1.8.0,CI publish 全绿,npm latest=1.8.0)。发布后排查发现 `--allow-missing-evidence` 被误注册为 `--allow-unchecked` 解析别名的遗留缺陷(AC 证据豁免从 CLI 不可达 + 误授 spec-check 豁免),修复入库(10-08-fix-evidence-flag-parser,dc594a4,parser 级与命令贯通级回归测试锁定)。1.9.0 前置 task adopt 完成(10-06-task-adopt-command,e6e5132):`task next --run --adopt` 独立动作——重绑执行者、评审基线重置到收养时 HEAD(审计保留原值)、状态不变、误用 fail-closed(`LIFECYCLE-ADOPT-001`);契约 `spec/contracts/task-adopt.md`,含与 `--takeover` 的边界。本次发布 1.8.1 同时携带两项交付(CHANGELOG 分列 Added/Fixed);1.9.0 主线(多执行者协作就绪)待立项。
|
package/package.json
CHANGED
|
@@ -574,8 +574,14 @@ function parseDecisionAnchor(text) {
|
|
|
574
574
|
if (section === "目标") {
|
|
575
575
|
if (line) goalLines.push(line)
|
|
576
576
|
} else if (section === "验收标准") {
|
|
577
|
-
const match = raw.match(/^\s*-\s
|
|
578
|
-
if (match)
|
|
577
|
+
const match = raw.match(/^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$/)
|
|
578
|
+
if (match) {
|
|
579
|
+
result.acceptanceCriteria.push({
|
|
580
|
+
id: match[2],
|
|
581
|
+
text: match[3],
|
|
582
|
+
checked: match[1] === 'x' || match[1] === 'X',
|
|
583
|
+
})
|
|
584
|
+
}
|
|
579
585
|
} else if (section === "被拒方案") {
|
|
580
586
|
const match = raw.match(/^\s*-\s*\*\*(.+?)\*\*/)
|
|
581
587
|
if (match) result.rejectedOptions.push(match[1].trim())
|
|
@@ -836,6 +842,36 @@ function withStageFacts(block, root, taskPath, status, readonly = false) {
|
|
|
836
842
|
return parts.length > 0 ? `${parts.join("\n\n")}\n\n${block}` : block
|
|
837
843
|
}
|
|
838
844
|
|
|
845
|
+
// Evidence coverage for the anchor block: one read of the task's
|
|
846
|
+
// evidence.jsonl, tolerating noise exactly like the python line. Returns
|
|
847
|
+
// null when the file is absent so the coverage line degrades silently.
|
|
848
|
+
function readEvidenceCoverage(root, taskPath) {
|
|
849
|
+
let text = ''
|
|
850
|
+
try {
|
|
851
|
+
text = readFileSync(resolve(root, taskPath, 'evidence.jsonl'), 'utf8')
|
|
852
|
+
} catch {
|
|
853
|
+
return null
|
|
854
|
+
}
|
|
855
|
+
const covered = new Set()
|
|
856
|
+
for (const raw of text.split('\n')) {
|
|
857
|
+
const line = raw.trim()
|
|
858
|
+
if (!line) continue
|
|
859
|
+
let record
|
|
860
|
+
try {
|
|
861
|
+
record = JSON.parse(line)
|
|
862
|
+
} catch {
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) continue
|
|
866
|
+
const ac = typeof record.ac === 'string' ? record.ac.trim() : ''
|
|
867
|
+
const ref = typeof record.ref === 'string' ? record.ref.trim() : ''
|
|
868
|
+
if (ac && ref && ['test', 'command', 'manual'].includes(record.kind)) {
|
|
869
|
+
covered.add(ac)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return covered
|
|
873
|
+
}
|
|
874
|
+
|
|
839
875
|
function decisionAnchorBlock(root, taskPath, status) {
|
|
840
876
|
if (!taskPath) {
|
|
841
877
|
return null
|
|
@@ -868,6 +904,17 @@ function decisionAnchorBlock(root, taskPath, status) {
|
|
|
868
904
|
lines.push(
|
|
869
905
|
`Acceptance: ${parsed.acceptanceCriteria.slice(0, 8).map((item) => `${item.id} ${item.text.slice(0, 80)}`).join("; ")}`
|
|
870
906
|
)
|
|
907
|
+
const covered = readEvidenceCoverage(root, taskPath)
|
|
908
|
+
if (covered) {
|
|
909
|
+
const total = parsed.acceptanceCriteria.length
|
|
910
|
+
const missing = parsed.acceptanceCriteria
|
|
911
|
+
.map((item) => item.id)
|
|
912
|
+
.filter((id) => !covered.has(id))
|
|
913
|
+
const detail = missing.length
|
|
914
|
+
? ` missing=${missing.slice(0, 5).join(',')}`
|
|
915
|
+
: ''
|
|
916
|
+
lines.push(`AC evidence: ${total - missing.length}/${total}${detail}`)
|
|
917
|
+
}
|
|
871
918
|
}
|
|
872
919
|
if (parsed.rejectedOptions.length > 0) {
|
|
873
920
|
lines.push(`Rejected: ${parsed.rejectedOptions.slice(0, 6).join("; ")}`)
|
|
@@ -1073,5 +1120,6 @@ export {
|
|
|
1073
1120
|
runEditSpecCheck,
|
|
1074
1121
|
contractFingerprint,
|
|
1075
1122
|
stableStringify,
|
|
1076
|
-
stageContractBlock
|
|
1123
|
+
stageContractBlock,
|
|
1124
|
+
decisionAnchorBlock
|
|
1077
1125
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.8.1
|
|
@@ -108,13 +108,22 @@ def _print_text_report(report: dict, *, verbose: bool) -> None:
|
|
|
108
108
|
print(_parse_error_line(parse_errors))
|
|
109
109
|
return
|
|
110
110
|
|
|
111
|
+
summary = report.get("summary", {})
|
|
112
|
+
if not parse_errors and summary.get("declarations") == 0:
|
|
113
|
+
# Zero declarations is a fact, not a pass: name it so a missing or
|
|
114
|
+
# silently ignored declaration block cannot look like a clean run.
|
|
115
|
+
print(
|
|
116
|
+
"spec-check: no checks declared "
|
|
117
|
+
f"({report.get('specFiles', 0)} spec files scanned)"
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
111
121
|
from services.spec_check import normalized_one_line
|
|
112
122
|
|
|
113
123
|
line = normalized_one_line(report)
|
|
114
124
|
if line:
|
|
115
125
|
print(line)
|
|
116
126
|
return
|
|
117
|
-
summary = report.get("summary", {})
|
|
118
127
|
print(
|
|
119
128
|
"spec-check: {pass} passed, {violation} violations, "
|
|
120
129
|
"{unchecked} unchecked".format(
|
|
@@ -38,6 +38,7 @@ from adapters.cli.task_context_commands import (
|
|
|
38
38
|
from adapters.cli.task_create_command import cmd_create, ensure_tasks_dir
|
|
39
39
|
from adapters.cli import task_navigation
|
|
40
40
|
from adapters.cli.task_lifecycle_commands import (
|
|
41
|
+
cmd_adopt,
|
|
41
42
|
cmd_complete,
|
|
42
43
|
cmd_current,
|
|
43
44
|
cmd_finish,
|
|
@@ -152,6 +153,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
152
153
|
review=cmd_review,
|
|
153
154
|
complete=cmd_complete,
|
|
154
155
|
archive=cmd_archive,
|
|
156
|
+
adopt=cmd_adopt,
|
|
155
157
|
),
|
|
156
158
|
)
|
|
157
159
|
return task_navigation.cmd_next(args)
|
|
@@ -30,6 +30,7 @@ from runtime.session_state import (
|
|
|
30
30
|
PROVENANCE_PROCESS_FALLBACK,
|
|
31
31
|
clear_active_task,
|
|
32
32
|
get_active_task,
|
|
33
|
+
set_active_task,
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
|
|
@@ -284,6 +285,50 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
284
285
|
return 0
|
|
285
286
|
|
|
286
287
|
|
|
288
|
+
def cmd_adopt(args: argparse.Namespace) -> int:
|
|
289
|
+
"""Adopt an active task from another executor."""
|
|
290
|
+
repo_root = get_repo_root()
|
|
291
|
+
execution_context = execution_context_from_namespace(args)
|
|
292
|
+
task_input = getattr(args, "dir", None)
|
|
293
|
+
if not task_input:
|
|
294
|
+
print(
|
|
295
|
+
colored("Error: task directory or name required", Colors.RED),
|
|
296
|
+
file=sys.stderr,
|
|
297
|
+
)
|
|
298
|
+
return 1
|
|
299
|
+
|
|
300
|
+
full_path = _resolve_start_task(task_input, repo_root)
|
|
301
|
+
if full_path is None:
|
|
302
|
+
return 1
|
|
303
|
+
|
|
304
|
+
service = TaskLifecycleService(repo_root)
|
|
305
|
+
result = service.adopt(
|
|
306
|
+
full_path,
|
|
307
|
+
executor=getattr(args, "executor", None),
|
|
308
|
+
execution_context=execution_context,
|
|
309
|
+
)
|
|
310
|
+
if not result.ok:
|
|
311
|
+
if result.code == "LIFECYCLE-ADOPT-001":
|
|
312
|
+
print(
|
|
313
|
+
colored("Error: task adoption blocked", Colors.RED),
|
|
314
|
+
file=sys.stderr,
|
|
315
|
+
)
|
|
316
|
+
for blocker in result.blockers:
|
|
317
|
+
print(f" - {blocker}", file=sys.stderr)
|
|
318
|
+
return 1
|
|
319
|
+
if result.title:
|
|
320
|
+
return _report_lifecycle_preflight(result)
|
|
321
|
+
if result.repository_error is not None:
|
|
322
|
+
return _report_lifecycle_repository_error(result)
|
|
323
|
+
return 1
|
|
324
|
+
|
|
325
|
+
task_path = _display_task_path(repo_root, full_path)
|
|
326
|
+
set_active_task(repo_root, task_path)
|
|
327
|
+
print(colored(f"[OK] Task adopted: {task_path}", Colors.GREEN))
|
|
328
|
+
print(f"Next: ./.cowork-flow/run task next {task_path} --run")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
|
|
287
332
|
def cmd_review(args: argparse.Namespace) -> int:
|
|
288
333
|
"""Mark a task ready for check."""
|
|
289
334
|
repo_root = get_repo_root()
|
|
@@ -336,6 +381,9 @@ def cmd_complete(args: argparse.Namespace) -> int:
|
|
|
336
381
|
task_dir,
|
|
337
382
|
execution_context=execution_context,
|
|
338
383
|
allow_unchecked_specs=bool(getattr(args, "allow_unchecked", False)),
|
|
384
|
+
allow_missing_evidence=bool(
|
|
385
|
+
getattr(args, "allow_missing_evidence", False)
|
|
386
|
+
),
|
|
339
387
|
)
|
|
340
388
|
if not result.ok:
|
|
341
389
|
if result.code == "LIFECYCLE-TRANSITION-001":
|
|
@@ -18,6 +18,7 @@ from runtime.session_state import (
|
|
|
18
18
|
get_active_task,
|
|
19
19
|
)
|
|
20
20
|
from services.plan_binding import PlanBindingError, bind_task_plan
|
|
21
|
+
from services.task_lifecycle import ADOPTABLE_STATUSES
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
LifecycleCommand = Callable[[argparse.Namespace], int]
|
|
@@ -30,6 +31,7 @@ class NextActionHandlers:
|
|
|
30
31
|
review: LifecycleCommand
|
|
31
32
|
complete: LifecycleCommand
|
|
32
33
|
archive: LifecycleCommand
|
|
34
|
+
adopt: LifecycleCommand
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
def namespace_with(args: argparse.Namespace, **overrides) -> argparse.Namespace:
|
|
@@ -270,7 +272,62 @@ def _run_plan_bind_action(args: argparse.Namespace, task_path: str) -> int:
|
|
|
270
272
|
return 0
|
|
271
273
|
|
|
272
274
|
|
|
275
|
+
def _run_adopt_action(
|
|
276
|
+
args: argparse.Namespace,
|
|
277
|
+
handlers: NextActionHandlers,
|
|
278
|
+
) -> int:
|
|
279
|
+
"""`--adopt` is its own action: it repairs ownership and the review
|
|
280
|
+
baseline without running any lifecycle transition."""
|
|
281
|
+
repo_root = get_repo_root()
|
|
282
|
+
if _create_input_names(args) or getattr(args, "from_plan", None):
|
|
283
|
+
print(
|
|
284
|
+
colored(
|
|
285
|
+
"Error: --adopt takes over an existing task; create/plan "
|
|
286
|
+
"inputs cannot run with it",
|
|
287
|
+
Colors.RED,
|
|
288
|
+
),
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
291
|
+
return 1
|
|
292
|
+
if getattr(args, "auto", False) or getattr(args, "approved", False):
|
|
293
|
+
print(
|
|
294
|
+
colored(
|
|
295
|
+
"Error: --adopt does not run batch start; adopt first, then "
|
|
296
|
+
"re-run with --auto --approved",
|
|
297
|
+
Colors.RED,
|
|
298
|
+
),
|
|
299
|
+
file=sys.stderr,
|
|
300
|
+
)
|
|
301
|
+
return 1
|
|
302
|
+
task_dir, task_path, _active_target = _next_target_for_run(args, repo_root)
|
|
303
|
+
if task_dir is None or task_path is None:
|
|
304
|
+
blockers = _fallback_binding_blockers(args, repo_root)
|
|
305
|
+
for blocker in blockers or ["no task target for --adopt; pass a task dir"]:
|
|
306
|
+
print(colored(f"Error: {blocker}", Colors.RED), file=sys.stderr)
|
|
307
|
+
return 1
|
|
308
|
+
if not task_dir.is_dir():
|
|
309
|
+
print(
|
|
310
|
+
colored(f"Error: task directory not found: {task_path}", Colors.RED),
|
|
311
|
+
file=sys.stderr,
|
|
312
|
+
)
|
|
313
|
+
return 1
|
|
314
|
+
status = task_navigation._status(repo_root, task_dir)
|
|
315
|
+
if status not in ADOPTABLE_STATUSES:
|
|
316
|
+
print(
|
|
317
|
+
colored(
|
|
318
|
+
"Error: --adopt only applies to an active task "
|
|
319
|
+
f"({'/'.join(ADOPTABLE_STATUSES)}); current status: {status}",
|
|
320
|
+
Colors.RED,
|
|
321
|
+
),
|
|
322
|
+
file=sys.stderr,
|
|
323
|
+
)
|
|
324
|
+
return 1
|
|
325
|
+
return handlers.adopt(namespace_with(args, dir=task_path))
|
|
326
|
+
|
|
327
|
+
|
|
273
328
|
def run_next_action(args: argparse.Namespace, handlers: NextActionHandlers) -> int:
|
|
329
|
+
if bool(getattr(args, "adopt", False)):
|
|
330
|
+
return _run_adopt_action(args, handlers)
|
|
274
331
|
action, error_code = _validated_next_action(args)
|
|
275
332
|
if action is None:
|
|
276
333
|
return error_code
|
|
@@ -31,6 +31,8 @@ Action inputs for `next --run`:
|
|
|
31
31
|
--parent <dir> Optional parent task directory for create_task
|
|
32
32
|
--from-plan <path> Plan path for create_task or planning-phase rebind
|
|
33
33
|
--auto --approved Optional batch start flags for start_task
|
|
34
|
+
--adopt Adopt an active task from another executor (rebind
|
|
35
|
+
executor + reset review baseline; status unchanged)
|
|
34
36
|
--commit Optional archive auto-commit flag for archive_task
|
|
35
37
|
|
|
36
38
|
Examples:
|
|
@@ -91,6 +93,13 @@ def _add_next_output_options(parser: argparse.ArgumentParser) -> None:
|
|
|
91
93
|
help="With task completion: allow unchecked spec-check commands "
|
|
92
94
|
"(missing/timeout) and record the exemption in task.json meta",
|
|
93
95
|
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--allow-missing-evidence",
|
|
98
|
+
dest="allow_missing_evidence",
|
|
99
|
+
action="store_true",
|
|
100
|
+
help="With task completion: allow checked acceptance criteria "
|
|
101
|
+
"without evidence and record the exemption in task.json meta",
|
|
102
|
+
)
|
|
94
103
|
parser.add_argument(
|
|
95
104
|
"--list",
|
|
96
105
|
dest="list_tasks",
|
|
@@ -156,6 +165,12 @@ def _add_next_runtime_inputs(parser: argparse.ArgumentParser) -> None:
|
|
|
156
165
|
action="store_true",
|
|
157
166
|
help="Take the task over from its current executor on start_task",
|
|
158
167
|
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--adopt",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="Adopt an active task from another executor: rebind executor, "
|
|
172
|
+
"reset the review baseline to HEAD, keep status, record audit fields",
|
|
173
|
+
)
|
|
159
174
|
|
|
160
175
|
|
|
161
176
|
def _add_next_command(subparsers: argparse._SubParsersAction) -> None:
|
|
@@ -382,12 +382,33 @@ def _decision_anchor_block(
|
|
|
382
382
|
for item in parsed["acceptanceCriteria"][:8]
|
|
383
383
|
)
|
|
384
384
|
lines.append(f"Acceptance: {items}")
|
|
385
|
+
coverage = _acceptance_evidence_coverage(root, task_path)
|
|
386
|
+
if coverage:
|
|
387
|
+
detail = ""
|
|
388
|
+
if coverage["missing"]:
|
|
389
|
+
detail = f" missing={','.join(coverage['missing'][:5])}"
|
|
390
|
+
lines.append(
|
|
391
|
+
f"AC evidence: {coverage['withEvidence']}/{coverage['total']}{detail}"
|
|
392
|
+
)
|
|
385
393
|
if parsed["rejectedOptions"]:
|
|
386
394
|
lines.append("Rejected: " + "; ".join(parsed["rejectedOptions"][:6]))
|
|
387
395
|
lines.append("</decision-anchor>")
|
|
388
396
|
return "\n".join(lines)
|
|
389
397
|
|
|
390
398
|
|
|
399
|
+
def _acceptance_evidence_coverage(root: Path, task_path: str | None):
|
|
400
|
+
"""AC evidence coverage facts for the anchor block; a supplementary fact
|
|
401
|
+
that degrades to absence on any failure instead of degrading injection."""
|
|
402
|
+
if not task_path:
|
|
403
|
+
return None
|
|
404
|
+
try:
|
|
405
|
+
from services.ac_evidence import coverage_summary
|
|
406
|
+
|
|
407
|
+
return coverage_summary(root / task_path)
|
|
408
|
+
except Exception:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
391
412
|
def _stage_contract_block(
|
|
392
413
|
root: Path, task_path: str | None, status: str
|
|
393
414
|
) -> str | None:
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Acceptance-criteria evidence facts for task completion.
|
|
4
|
+
|
|
5
|
+
Single read entry for the completion gate, the fact view, and injection:
|
|
6
|
+
which AC ids the decision anchor declares, which are checked, and which
|
|
7
|
+
have at least one evidence record in the task's evidence.jsonl. Judgment
|
|
8
|
+
about whether a record really proves an AC stays with review; the machine
|
|
9
|
+
only decides existence facts.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from services.fact_view import parse_decision_anchor
|
|
19
|
+
|
|
20
|
+
EVIDENCE_FILE = "evidence.jsonl"
|
|
21
|
+
EVIDENCE_KINDS = ("test", "command", "manual")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_decision_anchor(task_dir: Path) -> str | None:
|
|
25
|
+
path = Path(task_dir) / "decision-anchor.md"
|
|
26
|
+
try:
|
|
27
|
+
text = path.read_text(encoding="utf-8")
|
|
28
|
+
except (OSError, UnicodeDecodeError):
|
|
29
|
+
return None
|
|
30
|
+
return text or None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_acceptance_criteria(task_dir: Path) -> list[dict[str, Any]]:
|
|
34
|
+
"""AC rows (id / text / checked) declared by the task's decision anchor.
|
|
35
|
+
|
|
36
|
+
Empty list when the anchor is missing or declares no AC rows — the
|
|
37
|
+
compatibility path that keeps pre-existing tasks exempt from the gate."""
|
|
38
|
+
text = read_decision_anchor(task_dir)
|
|
39
|
+
if not text:
|
|
40
|
+
return []
|
|
41
|
+
return list(parse_decision_anchor(text)["acceptanceCriteria"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_evidence(task_dir: Path) -> list[dict[str, Any]]:
|
|
45
|
+
"""Valid evidence records appended by the implementer.
|
|
46
|
+
|
|
47
|
+
Bad lines and malformed records are skipped, never fatal: the gate
|
|
48
|
+
blocks on missing coverage, not on file noise."""
|
|
49
|
+
path = Path(task_dir) / EVIDENCE_FILE
|
|
50
|
+
try:
|
|
51
|
+
text = path.read_text(encoding="utf-8")
|
|
52
|
+
except (OSError, UnicodeDecodeError):
|
|
53
|
+
return []
|
|
54
|
+
records: list[dict[str, Any]] = []
|
|
55
|
+
for line in text.splitlines():
|
|
56
|
+
line = line.strip()
|
|
57
|
+
if not line:
|
|
58
|
+
continue
|
|
59
|
+
try:
|
|
60
|
+
record = json.loads(line)
|
|
61
|
+
except json.JSONDecodeError:
|
|
62
|
+
continue
|
|
63
|
+
if isinstance(record, dict) and _valid_record(record):
|
|
64
|
+
records.append(record)
|
|
65
|
+
return records
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _valid_record(record: dict[str, Any]) -> bool:
|
|
69
|
+
ac = record.get("ac")
|
|
70
|
+
ref = record.get("ref")
|
|
71
|
+
kind = record.get("kind")
|
|
72
|
+
return (
|
|
73
|
+
isinstance(ac, str)
|
|
74
|
+
and bool(ac.strip())
|
|
75
|
+
and isinstance(ref, str)
|
|
76
|
+
and bool(ref.strip())
|
|
77
|
+
and kind in EVIDENCE_KINDS
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def evidence_coverage(
|
|
82
|
+
acceptance: list[dict[str, Any]],
|
|
83
|
+
evidence: list[dict[str, Any]],
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
"""Coverage facts over declared AC ids: total, covered, missing ids."""
|
|
86
|
+
covered = {
|
|
87
|
+
record["ac"].strip()
|
|
88
|
+
for record in evidence
|
|
89
|
+
}
|
|
90
|
+
ids = [item["id"] for item in acceptance]
|
|
91
|
+
missing = [ac_id for ac_id in ids if ac_id not in covered]
|
|
92
|
+
return {
|
|
93
|
+
"total": len(ids),
|
|
94
|
+
"withEvidence": len(ids) - len(missing),
|
|
95
|
+
"missing": missing,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def coverage_summary(task_dir: Path) -> dict[str, Any] | None:
|
|
100
|
+
"""End-to-end coverage facts for one task.
|
|
101
|
+
|
|
102
|
+
None when the task declares no acceptance criteria — consumers treat
|
|
103
|
+
that as "gate not applicable" instead of a fake zero."""
|
|
104
|
+
acceptance = load_acceptance_criteria(task_dir)
|
|
105
|
+
if not acceptance:
|
|
106
|
+
return None
|
|
107
|
+
return evidence_coverage(acceptance, read_evidence(task_dir))
|
|
@@ -23,7 +23,7 @@ VERIFY_COMMAND_MAX = 5
|
|
|
23
23
|
SCOPE_BOUNDARY_TRUNCATE = 160
|
|
24
24
|
ANCHOR_SECTIONS = ("目标", "验收标准", "被拒方案", "验证命令", "范围边界")
|
|
25
25
|
ACCEPTANCE_RE = re.compile(
|
|
26
|
-
r"^\s*-\s
|
|
26
|
+
r"^\s*-\s*(?:\[([ xX])\]?\s*)?(AC-[A-Za-z0-9-]+)\s*[::]\s*(.+?)\s*$"
|
|
27
27
|
)
|
|
28
28
|
REJECTED_RE = re.compile(r"^\s*-\s*\*\*(.+?)\*\*")
|
|
29
29
|
_HEADING_RE = re.compile(r"^##\s*(.+?)\s*$")
|
|
@@ -53,7 +53,11 @@ def parse_decision_anchor(text: str) -> dict[str, Any]:
|
|
|
53
53
|
match = ACCEPTANCE_RE.match(raw)
|
|
54
54
|
if match:
|
|
55
55
|
acceptance.append(
|
|
56
|
-
{
|
|
56
|
+
{
|
|
57
|
+
"id": match.group(2),
|
|
58
|
+
"text": match.group(3),
|
|
59
|
+
"checked": match.group(1) in ("x", "X"),
|
|
60
|
+
}
|
|
57
61
|
)
|
|
58
62
|
elif section == "被拒方案":
|
|
59
63
|
match = REJECTED_RE.match(raw)
|
|
@@ -488,6 +492,15 @@ def build_fact_view(repo_root: Path, task_dir: Path) -> dict[str, Any]:
|
|
|
488
492
|
else:
|
|
489
493
|
anchor = {"exists": False}
|
|
490
494
|
|
|
495
|
+
evidence_coverage: dict[str, Any] | None = None
|
|
496
|
+
if anchor.get("acceptanceCriteria"):
|
|
497
|
+
try:
|
|
498
|
+
from services.ac_evidence import coverage_summary
|
|
499
|
+
|
|
500
|
+
evidence_coverage = coverage_summary(task_dir)
|
|
501
|
+
except Exception:
|
|
502
|
+
evidence_coverage = None
|
|
503
|
+
|
|
491
504
|
meta = task.get("meta") if isinstance(task, dict) else None
|
|
492
505
|
plan_file = meta.get("planFile") if isinstance(meta, dict) else None
|
|
493
506
|
plan: dict[str, Any] = {"bound": bool(plan_file)}
|
|
@@ -501,6 +514,7 @@ def build_fact_view(repo_root: Path, task_dir: Path) -> dict[str, Any]:
|
|
|
501
514
|
"taskPath": rel_task,
|
|
502
515
|
"task": task,
|
|
503
516
|
"decisionAnchor": anchor,
|
|
517
|
+
"evidenceCoverage": evidence_coverage,
|
|
504
518
|
"plan": plan,
|
|
505
519
|
"whitelist": file_scope_whitelist(repo_root, task_dir),
|
|
506
520
|
"sessions": _bound_sessions(repo_root, rel_task),
|