@michaelxwb/code-flow 0.6.5 → 1.0.0
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/README.md +75 -110
- package/bin/code-flow.js +12 -0
- package/docs/README.md +19 -0
- package/docs/USAGE.md +268 -0
- package/docs/v1-design.md +1858 -0
- package/docs/v1-implementation.md +186 -0
- package/lib/adapters.js +126 -0
- package/lib/cli.js +258 -0
- package/lib/config.js +85 -0
- package/lib/doctor.js +283 -0
- package/lib/frontmatter.js +42 -0
- package/lib/fs.js +141 -0
- package/lib/glob.js +23 -0
- package/lib/hook.js +99 -0
- package/lib/init.js +132 -0
- package/lib/learn.js +170 -0
- package/lib/locks.js +52 -0
- package/lib/markdown.js +13 -0
- package/lib/process.js +116 -0
- package/lib/render.js +115 -0
- package/lib/repo.js +48 -0
- package/lib/runs.js +155 -0
- package/lib/session.js +40 -0
- package/lib/spec.js +80 -0
- package/lib/task.js +258 -0
- package/lib/taskdoc.js +308 -0
- package/lib/toml.js +140 -0
- package/lib/verify.js +168 -0
- package/lib/worktree.js +930 -0
- package/package.json +21 -9
- package/templates/adapters/opencode.mjs +65 -0
- package/templates/config.json +39 -0
- package/templates/instructions.md +13 -0
- package/templates/launcher/bridge.cjs +295 -0
- package/templates/launcher/hook.cmd +15 -0
- package/templates/launcher/hook.ps1 +10 -0
- package/templates/launcher/hook.sh +20 -0
- package/templates/skills/cf-check.md +19 -0
- package/templates/skills/cf-init.md +16 -0
- package/templates/skills/cf-spec.md +20 -0
- package/templates/skills/cf-task-align.md +19 -0
- package/templates/skills/cf-task-archive.md +20 -0
- package/templates/skills/cf-task-plan.md +31 -0
- package/templates/skills/cf-task-prd.md +14 -0
- package/templates/skills/cf-task-start.md +41 -0
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
- package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
- package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
- package/templates/workflow/project-guidelines.md +48 -0
- package/templates/workflow/scenarios.md +67 -0
- package/templates/workflow/task.md +56 -0
- package/docs/migrations/spec-workflow-0.6.0.md +0 -42
- package/src/adapters/claude/CLAUDE.md +0 -45
- package/src/adapters/claude/commands/cf-init.md +0 -264
- package/src/adapters/claude/commands/cf-learn.md +0 -291
- package/src/adapters/claude/commands/cf-spec.md +0 -67
- package/src/adapters/claude/commands/cf-stats.md +0 -86
- package/src/adapters/claude/commands/cf-sync.md +0 -25
- package/src/adapters/claude/commands/cf-task/align.md +0 -309
- package/src/adapters/claude/commands/cf-task/archive.md +0 -161
- package/src/adapters/claude/commands/cf-task/block.md +0 -36
- package/src/adapters/claude/commands/cf-task/graph.md +0 -80
- package/src/adapters/claude/commands/cf-task/note.md +0 -236
- package/src/adapters/claude/commands/cf-task/plan.md +0 -363
- package/src/adapters/claude/commands/cf-task/prd.md +0 -162
- package/src/adapters/claude/commands/cf-task/start.md +0 -304
- package/src/adapters/claude/commands/cf-task/status.md +0 -59
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/claude/commands/cf-validate.md +0 -75
- package/src/adapters/claude/settings.local.json +0 -50
- package/src/adapters/codex/AGENTS.md +0 -45
- package/src/adapters/codex/config.toml +0 -3
- package/src/adapters/codex/hooks.json +0 -50
- package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
- package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
- package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
- package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
- package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
- package/src/adapters/costrict/CLAUDE.md +0 -45
- package/src/adapters/costrict/commands/cf-init.md +0 -264
- package/src/adapters/costrict/commands/cf-learn.md +0 -291
- package/src/adapters/costrict/commands/cf-spec.md +0 -67
- package/src/adapters/costrict/commands/cf-stats.md +0 -86
- package/src/adapters/costrict/commands/cf-sync.md +0 -25
- package/src/adapters/costrict/commands/cf-task/align.md +0 -309
- package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
- package/src/adapters/costrict/commands/cf-task/block.md +0 -36
- package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
- package/src/adapters/costrict/commands/cf-task/note.md +0 -236
- package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
- package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
- package/src/adapters/costrict/commands/cf-task/start.md +0 -304
- package/src/adapters/costrict/commands/cf-task/status.md +0 -59
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/costrict/commands/cf-validate.md +0 -75
- package/src/adapters/costrict/settings.local.json +0 -50
- package/src/adapters/opencode/AGENTS.md +0 -45
- package/src/adapters/opencode/commands/cf-init.md +0 -281
- package/src/adapters/opencode/commands/cf-learn.md +0 -295
- package/src/adapters/opencode/commands/cf-spec.md +0 -59
- package/src/adapters/opencode/commands/cf-stats.md +0 -90
- package/src/adapters/opencode/commands/cf-sync.md +0 -29
- package/src/adapters/opencode/commands/cf-task/align.md +0 -314
- package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
- package/src/adapters/opencode/commands/cf-task/block.md +0 -40
- package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
- package/src/adapters/opencode/commands/cf-task/note.md +0 -240
- package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
- package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
- package/src/adapters/opencode/commands/cf-task/start.md +0 -309
- package/src/adapters/opencode/commands/cf-task/status.md +0 -63
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
- package/src/adapters/opencode/commands/cf-validate.md +0 -79
- package/src/adapters/opencode/opencode.json +0 -3
- package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
- package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
- package/src/cli.js +0 -1023
- package/src/core/code-flow/config.yml +0 -158
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_checks.py +0 -336
- package/src/core/code-flow/scripts/cf_core.py +0 -787
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_feedback.py +0 -56
- package/src/core/code-flow/scripts/cf_log.py +0 -154
- package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
- package/src/core/code-flow/scripts/cf_scan.py +0 -295
- package/src/core/code-flow/scripts/cf_session_state.py +0 -37
- package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
- package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
- package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
- package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
- package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
- package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
- package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
- package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
- package/src/core/code-flow/scripts/cf_stats.py +0 -506
- package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
- package/src/core/code-flow/scripts/cf_sync.py +0 -140
- package/src/core/code-flow/scripts/cf_task_index.py +0 -148
- package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
- package/src/core/code-flow/scripts/cf_validation.py +0 -81
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
- package/src/core/code-flow/specs/backend/_map.md +0 -56
- package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
- package/src/core/code-flow/specs/backend/database.md +0 -51
- package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
- package/src/core/code-flow/specs/backend/logging.md +0 -50
- package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
- package/src/core/code-flow/specs/frontend/_map.md +0 -52
- package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
- package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
- package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
- package/src/core/code-flow/specs/shared/_map.md +0 -57
- package/src/core/code-flow/tasks/.gitkeep +0 -0
- package/src/core/code-flow/validation.yml +0 -53
- package/src/migrate/spec-workflow.js +0 -238
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# v1 实施与验收记录
|
|
2
|
+
|
|
3
|
+
设计来源:[v1-design.md](v1-design.md)。
|
|
4
|
+
|
|
5
|
+
旧版 Git 基线:`eb0e4c0676680ac987ea48373f47a34c92dca881`。
|
|
6
|
+
|
|
7
|
+
实施任务位于 `.code-flow/tasks/2026-11-10/v1-runtime/v1-runtime.md`,已迁移为一份多 TASK 任务文档。当前仓库也已用 `init --platform=all` 安装 v1 模板、启动器和四平台入口。
|
|
8
|
+
|
|
9
|
+
## 当前状态
|
|
10
|
+
|
|
11
|
+
- **设计**:已修订本轮 review:段锁/短写锁、三方归属合并、合并后相关测试、仓库 group 锁、统一 bridge、collect 越界检查、ledger 恢复、模板交接与只读 doctor 契约;验收扩展至 AC-59。
|
|
12
|
+
- **代码**:已完成多 TASK 文档、Depends 批次、`--task` CLI、session/Delta 身份、段锁+短文档写锁;已安装 PRD/design/scenarios/task 模板、8 个用户命令、项目 launcher/bridge、`init --platform=all`、只读 `doctor`,以及 worktree prepare/collect/merge/cleanup、baseline/自动 init、三方归属合并、合并后集成验证、提交图恢复、仓库/group 锁和协调工作区写权限守卫。
|
|
13
|
+
- **未完成**:真实 AI 协作式 prd→align→plan 生成由使用者在实际项目中验证;worker 的实际派发数量由宿主/skill 执行,CI 已用模拟器验证 `workers/jobs` 边界。
|
|
14
|
+
- 当前仓库实现已覆盖主要运行时闭环,但不能宣称已完成全部 v1 设计与跨平台验收。
|
|
15
|
+
|
|
16
|
+
## 旧版产物处置
|
|
17
|
+
|
|
18
|
+
旧版 v0.8.1 的运行时、Specs、Tasks、归档和验收资料保留在 Git 标签 `v0.8.1`(提交 `eb0e4c0`)中,可按需恢复查看,不自动转换到 v1。
|
|
19
|
+
|
|
20
|
+
- 当前工作树只保留 v1 数据:`.code-flow/config.json`、v1 Specs、v1 多 TASK 文档、模板和 launcher。
|
|
21
|
+
- `v1-runtime` 的旧单文件 TASK 已人工迁移为 `.code-flow/tasks/2026-11-10/v1-runtime/v1-runtime.md`。
|
|
22
|
+
- 旧归档需求、旧 `spec-context.yml`、acceptance manifest 和验收证据不迁移;按设计只作为历史资料保留。
|
|
23
|
+
- 旧 `.code-flow/worktrees` 仍原地保留(gitignored)。它们可能是真实 Git worktree,不能按普通目录删除或移动;`doctor` 对其中的旧 ledger 报诊断,需人工决定清理。
|
|
24
|
+
- 仍活跃的旧需求必须人工用 v1 模板重建 PRD/design/scenarios 和任务声明;包内不提供迁移、转换或旧版清理脚本。
|
|
25
|
+
|
|
26
|
+
## 实现范围(目标)
|
|
27
|
+
|
|
28
|
+
- 全局 Node CLI、JSON 数据、路径身份、严格校验与原子写入。
|
|
29
|
+
- 稳定启动器:各平台共用项目 bridge,校验 PATH 包/协议后优先使用,回退固定 Node/JS;外层守卫保留控制权,错误输出被丢弃且退出 0。
|
|
30
|
+
- 显式任务/会话绑定、最新规则、完整块预算和原生 prompt 协议。
|
|
31
|
+
- 任务状态、真实测试执行、并发/互斥/屏障、超时/中断回收。
|
|
32
|
+
- Delta add/update/remove、稳定编号、部分成功和移动失败重试。
|
|
33
|
+
- 文档主流程:`cf-task:prd` → `cf-task:align` → `cf-task:plan`;产出 PRD、前后端 design、scenarios 和一份多 TASK 任务文档。
|
|
34
|
+
- 任务文档:`.code-flow/tasks/<date>/<requirement>/<requirement>.md`,内部多个 `## TASK-NNN` 段。
|
|
35
|
+
- Depends:按依赖拓扑分层批次;循环/缺失依赖报错。
|
|
36
|
+
- 模板资产:`.code-flow/templates/` 由 init 统一覆盖,用户 specs/tasks 不覆盖。
|
|
37
|
+
- 模板保留旧版分析深度,首次交付即改写退役 CLI/门禁字段与新 Task 格式;技能保留需求澄清、全栈交接、场景映射、Source 读取和未决问题处理。
|
|
38
|
+
- 最小用户命令面:`cf-init`、`cf-check`、`cf-spec`、`cf-task:{prd,align,plan,start,archive}`。
|
|
39
|
+
- worktree:精确 baseline、init 增量隔离、完整分支归属检查与收集 SHA;Task 三方归属合并,合并后聚合重跑相关测试,通过才 cleanup;ledger 写前意图和 Git 提交图恢复。
|
|
40
|
+
- 锁与环境:工作区段锁/短文档锁保护 Task;仓库 common dir 身份协调 group/编排;每次 runner 重建 ledger 隔离变量并控制 worker/jobs 总并发。
|
|
41
|
+
- `cf-check` / `code-flow doctor`:检查 Hook、模板、config、Specs、Tasks、worktree run ledger。
|
|
42
|
+
- 旧 Python/迁移/runtime 及旧测试退役,不保留兼容或转换代码。
|
|
43
|
+
|
|
44
|
+
## 设计 ↔ 实现差距
|
|
45
|
+
|
|
46
|
+
| 能力 | 设计 | 当前代码 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 文档主流程 | prd / align / plan 独立入口 | 已安装独立 skill;端到端产物验证待补 |
|
|
49
|
+
| 模板 | init 覆盖 `.code-flow/templates/` | 已覆盖安装;文档流程模板已移除旧 manifest/Stage Gate 指令 |
|
|
50
|
+
| 用户命令面 | init/check/spec + task:{prd,align,plan,start,archive} | 已安装 8 个入口 |
|
|
51
|
+
| 四平台命令路径 | Claude/Costrict/OpenCode 目录式命令;Codex 独立 skill | 已按旧版布局安装 |
|
|
52
|
+
| `init --platform=all` | 一次安装四平台 | 已实现 |
|
|
53
|
+
| 任务文档结构 | 一需求一份 `<requirement>.md`,多 TASK 段 | 已实现解析/写入/--task/session/Delta 身份;样本待迁移 |
|
|
54
|
+
| Depends 批次 | 拓扑分层,循环/缺失依赖报错 | 已实现解析/批次;prepare 只允许当前第一批 |
|
|
55
|
+
| 稳定启动器 | bridge 校验 PATH/固定入口、限时及输出;能力探测 | 已有 launcher/bridge/capabilities;完整异常矩阵待补 |
|
|
56
|
+
| OS 跨平台 | macOS/Linux/Windows + Node 矩阵 | 未在真实 Windows CI 验证 |
|
|
57
|
+
| worktree baseline | specs/需求/config 进入 baseline;dirty 业务代码拒绝 | 已实现 baseline、specs 哈希和自动提交范围检查 |
|
|
58
|
+
| worktree 自动 init | prepare 检测平台并写工具资产 | 已实现;失败保留现场并可重试 |
|
|
59
|
+
| worktree 编排 | 一任务一 worktree,自动合并 | 已实现 `lib/worktree.js` 与内部 `task parallel` |
|
|
60
|
+
| worktree 写权限 | Git 协调身份守卫 archive/learn 与未完成 run | 已实现 linked worktree 身份和未完成 run 守卫 |
|
|
61
|
+
| 多 TASK 并发写入 | 段操作锁 + 短文档写锁,重读后只补所属段 | 已实现段锁、短文档写锁和 Task 三方归属合并 |
|
|
62
|
+
| 跨 worktree 资源锁 | repository-key 的跨调用 group 锁与 ledger 环境 | 已实现 common-dir 仓库锁与 group 文件锁 |
|
|
63
|
+
| 集成验证/恢复 | 相关测试聚合、tested_head、写前意图与提交图恢复 | 已实现聚合/去重、tested_head、merge 意图与提交图恢复 |
|
|
64
|
+
| `source/scenarios` | Task 追溯 design/scenarios | 解析/list 已支持;计划技能待接入 |
|
|
65
|
+
| 健康检查 | `cf-check` / `doctor` | 已实现稳定 schema、模板/启动器/Spec/Task/worktree 检查 |
|
|
66
|
+
| 新运行时测试矩阵 | AC-31 ~ AC-59 | AC-31~AC-59 已有测试;AC-42 Windows CI 已通过;AC-55 已由 CI 模拟宿主派发 |
|
|
67
|
+
|
|
68
|
+
## 旧版 worktree 参考
|
|
69
|
+
|
|
70
|
+
参考旧版 Git 基线中的:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py
|
|
74
|
+
src/core/code-flow/scripts/codeflow/cli/cf_task_index.py
|
|
75
|
+
src/core/code-flow/scripts/codeflow/task/cf_task_merge.py
|
|
76
|
+
src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py
|
|
77
|
+
.claude/commands/cf-task/start.md
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
复用:
|
|
81
|
+
|
|
82
|
+
- prepare/collect/merge/cleanup 流程与分支/worktree 命名;
|
|
83
|
+
- 多 TASK 解析、`Depends` 批次;
|
|
84
|
+
- 任务文档按 TASK 归属三方合并的思路;不采用 done 优先或任意并集;
|
|
85
|
+
- 环境变量、port/db suffix、shared lock;
|
|
86
|
+
- run ledger 基础字段。
|
|
87
|
+
|
|
88
|
+
替换/删除:
|
|
89
|
+
|
|
90
|
+
- manifest/受管文件复制 → 稳定启动器 + worktree `init --platform`;
|
|
91
|
+
- Spec Context/manifest/evidence 合并 → 删除;
|
|
92
|
+
- spec refresh/Stage Gate/旧 batch smoke → 删除;集成验证直接复用本 run Task.tests,不恢复旧登记与证据系统;
|
|
93
|
+
- Python 实现 → Node 重写。
|
|
94
|
+
|
|
95
|
+
## 验收覆盖(现有)
|
|
96
|
+
|
|
97
|
+
“已有”表示薄内核已有相关测试样本;多 TASK 基础已补测试,启动器、worktree 和其余新契约仍需补测,不表示对应设计 AC 已整体验收通过。
|
|
98
|
+
|
|
99
|
+
| 场景 | 测试文件 | 状态 |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| AC-01、AC-09、AC-11、AC-12、AC-27、AC-30 | test/init.test.js、test/package.test.js | 已有 |
|
|
102
|
+
| AC-02、AC-03、AC-26 | test/package.test.js、test/init.test.js、test/cli.test.js | 已有 |
|
|
103
|
+
| AC-04、AC-05、AC-06、AC-28 | test/task.test.js、test/verify.test.js、test/cli.test.js | 已有 |
|
|
104
|
+
| AC-07、AC-19、AC-20、AC-21、AC-22、AC-29 | test/learn.test.js、test/task.test.js | 已有 |
|
|
105
|
+
| AC-08、AC-13、AC-14、AC-15、AC-16、AC-17、AC-18 | test/injection.test.js、test/data.test.js、test/package.test.js | 已有 |
|
|
106
|
+
| AC-10、AC-24 | test/performance.test.js、test/verify.test.js | 已有 |
|
|
107
|
+
| AC-23 | test/verify.test.js | 已有 |
|
|
108
|
+
| AC-25 | test/init.test.js | 已有 |
|
|
109
|
+
|
|
110
|
+
## 待补测试矩阵
|
|
111
|
+
|
|
112
|
+
| 场景 | 设计验收点 | 计划测试文件 | 状态 |
|
|
113
|
+
|---|---|---|---|
|
|
114
|
+
| AC-31 | prd → align → plan 产出 PRD、前后端 design、scenarios、一份多 TASK 任务文档 | test/workflow.test.js | 已有 |
|
|
115
|
+
| AC-32 | init 覆盖 `.code-flow/templates/`;用户 specs/tasks 不覆盖 | test/init.test.js | 已有基础 |
|
|
116
|
+
| AC-33 | scenarios 可被 design/task 引用,不生成 manifest、不记录结果、不阻断 finish/archive | test/workflow.test.js | 已有 |
|
|
117
|
+
| AC-34 | 一任务一 worktree;三方归属合并与集成验证通过后清理 | test/worktree.test.js | 已有 |
|
|
118
|
+
| AC-35 | worktree 只记录 Delta,主 worktree archive 统一应用 | test/worktree.test.js、test/learn.test.js | 已有 |
|
|
119
|
+
| AC-36 | 四平台只安装最小用户命令;路径与旧版一致 | test/init.test.js | 已有基础 |
|
|
120
|
+
| AC-37 | `cf-check` / `doctor` 报告 Hook、模板、config、Specs、Tasks、worktree 问题 | test/doctor.test.js、test/cli.test.js | 已有 |
|
|
121
|
+
| AC-38 | worker 移动目录后仍拒绝 archive/learn,按 Git 身份守卫 | test/worktree.test.js | 已有 |
|
|
122
|
+
| AC-39 | 一份 `<requirement>.md` 含多个 TASK 段;TASK ID/标题唯一,字段按段解析 | test/task.test.js | 已有基础 |
|
|
123
|
+
| AC-40 | Depends 拓扑分层;循环/缺失依赖报错;批次一=第 1 层 | test/task.test.js | 已有基础 |
|
|
124
|
+
| AC-41 | 稳定启动器优先 PATH,回退绝对入口;都失败不阻断 | test/init.test.js、test/package.test.js | 已有基础 |
|
|
125
|
+
| AC-42 | macOS/Linux/Windows 启动器、路径、引号、进程树回收 | test/launcher-windows.test.js + CI matrix | 已有基础/待 CI |
|
|
126
|
+
| AC-43 | baseline 含 specs/需求/config;dirty 业务代码拒绝;worktree specs 哈希一致 | test/worktree.test.js | 已有 |
|
|
127
|
+
| AC-44 | prepare 默认不 init worktree;`--init` 时检测平台并初始化,失败中止 | test/worktree.test.js、test/init.test.js | 已有 |
|
|
128
|
+
| AC-45 | 合并后聚合相关测试;失败不 cleanup;准确 tested_head | test/worktree.test.js、test/verify.test.js | 已有 |
|
|
129
|
+
| AC-46 | 同文档并发 finish 两段结果均保留,修改本段冲突 | test/task.test.js | 已有基础 |
|
|
130
|
+
| AC-47 | 三方归属保留 reopen;同段语义字段不无条件拼接 | test/worktree.test.js | 已有 |
|
|
131
|
+
| AC-48 | 跨 CLI/worktree group 锁、锁等待取消与资源释放 | test/verify.test.js、test/worktree.test.js | 已有基础 |
|
|
132
|
+
| AC-49 | 全分支 diff 审计、collect 后 SHA 变化重收集 | test/worktree.test.js | 已有 |
|
|
133
|
+
| AC-50 | merge 成功但 ledger 未更新,按 collected_head 祖先关系恢复 | test/worktree.test.js | 已有 |
|
|
134
|
+
| AC-51 | cleanup 中断后幂等续做,保护用户/外来资源 | test/worktree.test.js | 已有 |
|
|
135
|
+
| AC-52 | 模板首次改写与 prd/全栈 align/plan/start 的内容交接 | test/workflow.test.js | 已有基础 |
|
|
136
|
+
| AC-53 | doctor schema/退出码/只读;宿主未知;禁止执行用户代码 | test/doctor.test.js、test/cli.test.js | 已有 |
|
|
137
|
+
| AC-54 | PATH 包/协议校验、固定回退、错误 stdout 与 stdin 单次消费 | test/init.test.js、test/package.test.js | 已有 |
|
|
138
|
+
| AC-55 | runner 重建 ledger env 与 worker/jobs 总预算 | test/worktree.test.js、test/verify.test.js、test/worker-budget.test.js | 已有(CI 模拟宿主派发) |
|
|
139
|
+
| AC-56 | prepare/init 部分失败与同名外来资源恢复 | test/worktree.test.js | 已有 |
|
|
140
|
+
| AC-57 | 精确 init 增量识别、不提交/合并、混合用户内容保护 | test/worktree.test.js、test/init.test.js | 已有 |
|
|
141
|
+
| AC-58 | catalog 只读头无计数;spec list 准确统计 | test/injection.test.js | 已有 |
|
|
142
|
+
| AC-59 | 两个协调进程跨 run 互斥;会话仍工作区隔离 | test/worktree.test.js、test/injection.test.js | 已有 |
|
|
143
|
+
|
|
144
|
+
补充要求:
|
|
145
|
+
|
|
146
|
+
- 任务文档测试必须断言:一份 `<requirement>.md` 解析多个 `## TASK-NNN` 段;TASK ID 与标题一致、文档内唯一;worktree 只改所属段。
|
|
147
|
+
- Depends 测试必须断言:拓扑分层、批次一、循环依赖、缺失依赖、merge 顺序。
|
|
148
|
+
- 启动器测试必须覆盖包/协议校验、拒绝项目 bin、假程序/旧协议回退、选定后失败不再消费 stdin、损坏 stdout、总截止时间与外层退出守卫;三种 shell 与 OpenCode 策略相同。
|
|
149
|
+
- 模板覆盖测试必须断言:模板内容被更新,`specs/`、`tasks/`、`config.json` 未被覆盖。
|
|
150
|
+
- 四平台命令测试必须断言:旧路径布局仍可发现 `/cf-task:prd` 等入口;`finish/status/block/reopen/parallel` 不生成用户命令。
|
|
151
|
+
- worktree 测试必须使用真实临时 git 仓库和真实 worktree,覆盖 baseline(specs/需求/config)、dirty 业务代码拒绝、specs 哈希校验、自动 init、prepare、start、finish、自动 merge、Task 元数据冲突、代码冲突、cleanup、run ledger 恢复。
|
|
152
|
+
- scenarios 测试必须断言:scenarios 存在、可被 Task 引用、但 `finish`/`archive` 不解析、不阻断。
|
|
153
|
+
- doctor 测试必须断言稳定 schema/code/severity/退出码与宿主未知状态;运行前后文件、锁、worktree、ledger 不变,恶意 Hook/插件/Tests 没有被执行。
|
|
154
|
+
- 当前 `test/design.test.js` 只校验设计中的 JSON/Task/Delta/ledger/doctor 示例、章节/AC 引用,并执行 POSIX 外层退出守卫片段。它不替代上表的运行时测试。
|
|
155
|
+
|
|
156
|
+
## 本轮设计修订验证
|
|
157
|
+
|
|
158
|
+
- `node --test test/design.test.js`:9 项通过,校验 JSON/Delta/Task/ledger/doctor 示例、章节与验收引用,并执行真实 POSIX shell 守卫子进程。
|
|
159
|
+
- `npm test`:143 项,142 通过、1 项 Windows-only 跳过;包含多 TASK/Depends/并发写入、模板/文档流程契约、scenarios 不阻断、四平台命令布局、launcher/bridge PATH 选择/回退/慢探测/超时与输出上限、doctor schema、worktree 编排/自动 init/集成验证/冲突/范围审计/恢复/协调身份/跨进程仓库锁与 group 锁、PRD/design/scenarios/多 TASK 文档流契约、CI worker 并发预算模拟测试;环境为 macOS + Node v24.17.0。
|
|
160
|
+
- 修复开发测试入口:`node --test test` 在当前 Node 下无法加载目录;改由 `test/run.cjs` 枚举本测试目录的 `*.test.js` 并传给 Node,兼顾跨平台且不递归进入历史 worktree。
|
|
161
|
+
- GitHub Actions 保留 macOS/Linux/Windows 的 Node 18/22/24 矩阵,覆盖 CMD、PowerShell、Git Bash launcher、worktree 和 worker-budget 测试。本轮补丁在 macOS 本地验证,其他系统和 Node 版本的实际结果由 CI 确认。
|
|
162
|
+
- 已修复 P1 边界:Task `$` 写回、retry 混合文件用户内容(含未跟踪/再次重试/移除恢复)、Codex hooks/config 用户设置(含引号/点号键、Unicode 转义、inline 表拒绝、嵌套数组、子表 inline 表、多行字符串、注释/表位置、无计划混合文件、多行 features 值和 CRLF)、Git 边界、协调分支、staged rename/baseline、非 ASCII diff;并完成 P2 的 Specs 快照、partial init 重试、合法 rename、autocrlf、Delta target、启动器降级、bridge 截止/协议/额外字段、stdin 释放、OpenCode bridge/续轮/失败清理、模板 frontmatter、批次收尾、ledger env、doctor fallback 和冲突现场保留。
|
|
163
|
+
- 本轮收紧 Codex config 支持边界:只自动维护规范 `[features]` 表和根级单行 `features.hooks`;引号键/引号表、inline table、`[[features]]`、多行值和重复表明确拒绝,避免继续扩展 TOML 边角解析。
|
|
164
|
+
- 本轮全量复查增加 19 项回归测试:修复 TOML 注释括号误判、bridge 跨块 UTF-8 解码和诊断累积上限;统一 Markdown 围栏判定,保证 Task 字段/子节精确读写,拒绝重复字段和多行测试 ID,防止多行日志破坏文档;在写入前验证 Spec Delta 生成的规则;限定主 worktree 协调身份,合并前检查协调区变更,规范化 Task 路径身份,并在取得仓库锁后重查 learn/archive 的运行中批次约束。
|
|
165
|
+
- `npm run test:performance`:2 项通过;500 份 Specs、5000 个历史 Tasks,12 次进程样本的注入 p50 为 26.2 ms、p95 为 28.9 ms,目录扫描 0 次;1000 条测试声明规划耗时约 20.0 ms。数据来自本轮 macOS + Node v24.17.0 本地测量。
|
|
166
|
+
- 独立历史复现验证:517 组合法 TOML 输入均通过语法、保留内容和幂等性检查(其中 4 组不支持的形式安全拒绝),12 组 worktree 正向流程/用户内容保护案例及额外边界复现通过。复现脚本仅在临时目录执行,未加入产品代码。
|
|
167
|
+
|
|
168
|
+
## 验证命令
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm test
|
|
172
|
+
node --test test/worker-budget.test.js
|
|
173
|
+
npm run test:performance
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
包测试只在临时全局 prefix 安装,不替换机器实际全局 code-flow,不执行发布。性能数据由测试 diagnostic 输出,包括样本、环境和目录扫描次数;首次进程耗时单列,不宣称清空系统文件缓存。
|
|
177
|
+
|
|
178
|
+
## 历史资料
|
|
179
|
+
|
|
180
|
+
手工退役前的工作区资料保存在:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
/Users/jahan/workspace/code-flow-v0.8-preserved-sg_m8jm9
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
两个已有 git worktree 保留在原路径。备份与手工清理操作未加入运行时代码。
|
package/lib/adapters.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const f = require('./fs');
|
|
5
|
+
const toml = require('./toml');
|
|
6
|
+
const MARKER = 'code-flow:v1';
|
|
7
|
+
const PLATFORMS = {
|
|
8
|
+
claude: { hooks: '.claude/settings.local.json', instructions: 'CLAUDE.md', skills: '.claude/commands' },
|
|
9
|
+
costrict: { hooks: '.costrict/settings.local.json', instructions: 'CLAUDE.md', skills: '.costrict/commands' },
|
|
10
|
+
codex: { hooks: '.codex/hooks.json', instructions: 'AGENTS.md', skills: '.agents/skills' },
|
|
11
|
+
opencode: { hooks: '.opencode/plugins/code-flow/index.js', instructions: 'AGENTS.md', skills: '.opencode/commands' }
|
|
12
|
+
};
|
|
13
|
+
function shellQuote(text) { return "'" + text.replace(/'/g, "'\\''") + "'"; }
|
|
14
|
+
function psQuote(text) { return "'" + text.replace(/'/g, "''") + "'"; }
|
|
15
|
+
function posix(node, entry, format) {
|
|
16
|
+
return '# ' + MARKER + '\nif [ -x ' + shellQuote(node) + ' ] && [ -f ' + shellQuote(entry) + ' ]; then\n' +
|
|
17
|
+
' ' + shellQuote(node) + ' ' + shellQuote(entry) + ' hook prompt --format=' + format + '\n' +
|
|
18
|
+
' cf_hook_status=$?\n if [ "$cf_hook_status" -ne 0 ]; then printf "code-flow Hook failed\\n" >&2; fi\n' +
|
|
19
|
+
'else printf "code-flow global entry missing; run init again\\n" >&2; fi\nexit 0';
|
|
20
|
+
}
|
|
21
|
+
function powershell(node, entry, format) {
|
|
22
|
+
return '# ' + MARKER + '\ntry { if ((Test-Path -LiteralPath ' + psQuote(node) + ') -and ' +
|
|
23
|
+
'(Test-Path -LiteralPath ' + psQuote(entry) + ')) { & ' + psQuote(node) + ' ' + psQuote(entry) +
|
|
24
|
+
' hook prompt --format=' + format + ' } else { [Console]::Error.WriteLine("code-flow global entry missing") } } ' +
|
|
25
|
+
'catch { [Console]::Error.WriteLine($_.Exception.Message) }; exit 0';
|
|
26
|
+
}
|
|
27
|
+
function windows(node, entry, format) {
|
|
28
|
+
const quote = value => '"' + value.replace(/%/g, '%%') + '"';
|
|
29
|
+
return 'rem ' + MARKER + '\r\nif not exist ' + quote(node) + ' exit /b 0\r\n' +
|
|
30
|
+
'if not exist ' + quote(entry) + ' exit /b 0\r\n' + quote(node) + ' ' + quote(entry) +
|
|
31
|
+
' hook prompt --format=' + format + '\r\nexit /b 0';
|
|
32
|
+
}
|
|
33
|
+
function managed(hook) {
|
|
34
|
+
if (typeof hook?.command !== 'string') return false;
|
|
35
|
+
return hook.command.startsWith('# ' + MARKER + '\n') ||
|
|
36
|
+
hook.command.startsWith('rem ' + MARKER + '\r\n') ||
|
|
37
|
+
/(?:^|[\s/\\'"])\.code-flow[\\/]launcher[\\/]hook\.(?:sh|cmd|ps1)(?:$|[\s'"])/.test(hook.command) ||
|
|
38
|
+
/\.code-flow[\\/]launcher[\\/]hook\.(?:cmd|ps1)/.test(String(hook.commandWindows || ''));
|
|
39
|
+
}
|
|
40
|
+
function owned(text) {
|
|
41
|
+
if (text === null) return false;
|
|
42
|
+
if (/^\/\/ code-flow:v1\r?\n/.test(text)) return true;
|
|
43
|
+
if (/^---\r?\n[\s\S]*?\r?\n---\r?\n<!-- code-flow:v1 -->\r?\n/.test(text)) return true;
|
|
44
|
+
if (text.trimStart().startsWith('{')) {
|
|
45
|
+
try { return JSON.parse(text).managed === MARKER; } catch (error) {
|
|
46
|
+
throw new f.FlowError('config_format', 'Invalid managed JSON: ' + error.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function launcherCommand(platform, operatingSystem = process.platform) {
|
|
52
|
+
const format = platform;
|
|
53
|
+
if (operatingSystem === 'win32') {
|
|
54
|
+
return {
|
|
55
|
+
command: 'powershell -NoProfile -Command "if (Test-Path -LiteralPath \'.code-flow\\launcher\\hook.ps1\') ' +
|
|
56
|
+
'{ & \'.code-flow\\launcher\\hook.ps1\' -Format ' + format + ' }; exit 0"',
|
|
57
|
+
shell: 'powershell'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { command: "if [ -f '.code-flow/launcher/hook.sh' ]; then sh '.code-flow/launcher/hook.sh' " +
|
|
61
|
+
format + '; fi; exit 0' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hook(platform, _node, _entry, operatingSystem = process.platform) {
|
|
65
|
+
const result = { type: 'command', command: launcherCommand(platform, operatingSystem).command, timeout: 5 };
|
|
66
|
+
if (operatingSystem === 'win32' && !result.shell) result.shell = 'powershell';
|
|
67
|
+
if (platform === 'codex') result.commandWindows =
|
|
68
|
+
'cmd /c if exist ".code-flow\\launcher\\hook.cmd" (call ".code-flow\\launcher\\hook.cmd" ' +
|
|
69
|
+
platform + ') & exit /b 0';
|
|
70
|
+
if (platform === 'claude') result.onFailure = 'continue';
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function mergeHooks(raw, platform, node, entry) {
|
|
74
|
+
const data = raw === null ? {} : f.object(JSON.parse(raw), 'platform configuration');
|
|
75
|
+
const hooks = data.hooks === undefined ? {} : f.object(data.hooks, 'hooks');
|
|
76
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
77
|
+
f.demand(Array.isArray(groups), 'hooks_invalid', 'Hook groups must be arrays');
|
|
78
|
+
hooks[event] = groups.map(group => {
|
|
79
|
+
f.object(group, 'Hook group');
|
|
80
|
+
f.demand(Array.isArray(group.hooks), 'hooks_invalid', 'group.hooks must be an array');
|
|
81
|
+
return { ...group, hooks: group.hooks.filter(item => !managed(item)) };
|
|
82
|
+
}).filter(group => group.hooks.length > 0);
|
|
83
|
+
if (!hooks[event].length) delete hooks[event];
|
|
84
|
+
}
|
|
85
|
+
hooks.UserPromptSubmit = [...(hooks.UserPromptSubmit || []), { hooks: [hook(platform, node, entry)] }];
|
|
86
|
+
return JSON.stringify({ ...data, hooks }, null, 2) + '\n';
|
|
87
|
+
}
|
|
88
|
+
function instructions(raw, content) {
|
|
89
|
+
const start = '<!-- ' + MARKER + ' start -->', end = '<!-- ' + MARKER + ' end -->';
|
|
90
|
+
const block = start + '\n' + content.trim() + '\n' + end;
|
|
91
|
+
const text = raw ?? '';
|
|
92
|
+
const a = text.indexOf(start), b = text.indexOf(end);
|
|
93
|
+
f.demand((a < 0 && b < 0) || (a >= 0 && b > a && text.indexOf(start, a + 1) < 0 && text.indexOf(end, b + 1) < 0),
|
|
94
|
+
'instructions_invalid', 'Invalid code-flow managed instruction markers');
|
|
95
|
+
return a < 0 ? text.trimEnd() + (text ? '\n\n' : '') + block + '\n' :
|
|
96
|
+
text.slice(0, a) + block + text.slice(b + end.length);
|
|
97
|
+
}
|
|
98
|
+
function codexConfig(raw) {
|
|
99
|
+
const text = raw ?? '';
|
|
100
|
+
const found = toml.featuresHooks(text);
|
|
101
|
+
f.demand(!found.unsupported, 'config_format',
|
|
102
|
+
'Unsupported Codex features.hooks form (' + found.unsupported + '); use a [features] table with hooks = true');
|
|
103
|
+
const lines = text.split('\n');
|
|
104
|
+
if (found.header < 0 && found.key < 0) {
|
|
105
|
+
if (found.rootFeatures >= 0) {
|
|
106
|
+
lines.splice(found.rootFeatures + 1, 0, 'features.hooks = true');
|
|
107
|
+
return lines.join('\n').replace(/\n*$/, '\n');
|
|
108
|
+
}
|
|
109
|
+
return text.trimEnd() + (text ? '\n\n' : '') + '[features]\nhooks = true\n';
|
|
110
|
+
}
|
|
111
|
+
if (found.header >= 0 && found.key < 0) {
|
|
112
|
+
lines.splice(found.header + 1, 0, 'hooks = true');
|
|
113
|
+
} else if (found.key >= 0) {
|
|
114
|
+
const line = lines[found.key];
|
|
115
|
+
const clean = line.replace(/\r$/, '');
|
|
116
|
+
const match = /^(\s*[^=]*=\s*)(true|false)(\s*(?:#.*)?)(\r?)$/.exec(clean);
|
|
117
|
+
f.demand(match, 'config_format', 'Invalid features.hooks');
|
|
118
|
+
lines[found.key] = match[1] + 'true' + match[3] + (line.endsWith('\r') ? '\r' : '');
|
|
119
|
+
}
|
|
120
|
+
return lines.join('\n').replace(/\n*$/, '\n');
|
|
121
|
+
}
|
|
122
|
+
function plugin(node, entry) {
|
|
123
|
+
return fs.readFileSync(path.join(__dirname, '../templates/adapters/opencode.mjs'), 'utf8')
|
|
124
|
+
.replace('__CODE_FLOW_NODE__', JSON.stringify(node)).replace('__CODE_FLOW_ENTRY__', JSON.stringify(entry));
|
|
125
|
+
}
|
|
126
|
+
module.exports = { PLATFORMS, MARKER, managed, owned, hook, mergeHooks, instructions, codexConfig, plugin, posix, windows, powershell };
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const f = require('./fs');
|
|
5
|
+
const config = require('./config');
|
|
6
|
+
const task = require('./task');
|
|
7
|
+
const spec = require('./spec');
|
|
8
|
+
const render = require('./render');
|
|
9
|
+
const hook = require('./hook');
|
|
10
|
+
const COMMANDS = {
|
|
11
|
+
init: { count: 0, flags: { platform: 'string', 'dry-run': 'boolean' } },
|
|
12
|
+
doctor: { count: 0, flags: {} },
|
|
13
|
+
'hook prompt': { count: 0, flags: { format: 'string' } },
|
|
14
|
+
'hook capabilities': { count: 0, flags: {} },
|
|
15
|
+
'task start': { count: 1, flags: { task: 'string', session: 'string' } },
|
|
16
|
+
'task block': { count: 1, flags: { task: 'string', reason: 'string' } },
|
|
17
|
+
'task reopen': { count: 1, flags: { task: 'string', reason: 'string' } },
|
|
18
|
+
'task unbind': { count: 0, flags: { session: 'string' } },
|
|
19
|
+
'task finish': { count: 1, flags: { task: 'string', jobs: 'number' } },
|
|
20
|
+
'task archive': { count: 1, flags: { 'dry-run': 'boolean', 'no-learn': 'boolean' } },
|
|
21
|
+
'task status': { count: -1, flags: {} },
|
|
22
|
+
'process collect': { count: 0, flags: { out: 'string', transcript: 'string' } },
|
|
23
|
+
'task parallel prepare': { count: 0, flags: { 'task-file': 'string', tasks: 'string', 'run-id': 'string', workers: 'number', 'no-auto-baseline': 'boolean', init: 'boolean' } },
|
|
24
|
+
'task parallel status': { count: 0, flags: { 'run-id': 'string' } },
|
|
25
|
+
'task parallel collect': { count: 0, flags: { 'run-id': 'string' } },
|
|
26
|
+
'task parallel merge': { count: 0, flags: { 'run-id': 'string' } },
|
|
27
|
+
'task parallel cleanup': { count: 0, flags: { 'run-id': 'string', force: 'boolean' } },
|
|
28
|
+
'spec list': { count: 0, flags: {} },
|
|
29
|
+
'spec inject': { count: 0, flags: { 'task-file': 'string', task: 'string', path: 'multiple', full: 'boolean' } },
|
|
30
|
+
'spec learn': { count: 0, flags: { 'from-task': 'string', task: 'string', 'dry-run': 'boolean' } }
|
|
31
|
+
};
|
|
32
|
+
function usage(command = null) {
|
|
33
|
+
const names = command ? Object.keys(COMMANDS).filter(name => name === command || name.startsWith(command + ' ')) : Object.keys(COMMANDS);
|
|
34
|
+
f.demand(names.length > 0, 'usage', 'Unknown command: ' + command);
|
|
35
|
+
return 'code-flow v' + require('../package.json').version + '\nGlobal Node-only runtime\n\n' + names.map(name => {
|
|
36
|
+
const definition = COMMANDS[name];
|
|
37
|
+
const target = definition.count === 1 ? (name === 'task archive' ? ' <requirement-dir>' : ' <task-file>') :
|
|
38
|
+
definition.count === -1 ? ' [requirement-dir]' : '';
|
|
39
|
+
return ' code-flow ' + name + target + Object.entries(definition.flags).map(([key, type]) =>
|
|
40
|
+
' [--' + key + (type === 'boolean' ? '' : ' <value>') + ']').join('') + (name === 'hook prompt' ? '' : ' [--json]');
|
|
41
|
+
}).join('\n') + '\n';
|
|
42
|
+
}
|
|
43
|
+
function commandFor(argv) {
|
|
44
|
+
if (['init', 'doctor'].includes(argv[0])) return argv[0];
|
|
45
|
+
const three = argv.slice(0, 3).join(' ');
|
|
46
|
+
return Object.hasOwn(COMMANDS, three) ? three : argv.slice(0, 2).join(' ');
|
|
47
|
+
}
|
|
48
|
+
function parse(argv) {
|
|
49
|
+
const command = commandFor(argv);
|
|
50
|
+
f.demand(Object.hasOwn(COMMANDS, command), 'usage', 'Unknown command: ' + command);
|
|
51
|
+
const definition = COMMANDS[command], flags = { ...definition.flags, ...(command === 'hook prompt' ? {} : { json: 'boolean' }) };
|
|
52
|
+
const options = {}, positional = [], offset = command.split(' ').length;
|
|
53
|
+
for (let i = offset; i < argv.length; i++) {
|
|
54
|
+
const arg = argv[i];
|
|
55
|
+
if (!arg.startsWith('--')) { positional.push(arg); continue; }
|
|
56
|
+
const equal = arg.indexOf('='), key = arg.slice(2, equal < 0 ? undefined : equal);
|
|
57
|
+
f.demand(Object.hasOwn(flags, key), 'usage', 'Unknown option: --' + key);
|
|
58
|
+
const type = flags[key];
|
|
59
|
+
f.demand(type === 'multiple' || !Object.hasOwn(options, key), 'usage', 'Duplicate option: --' + key);
|
|
60
|
+
if (type === 'boolean') { f.demand(equal < 0, 'usage', '--' + key + ' takes no value'); options[key] = true; continue; }
|
|
61
|
+
const value = equal < 0 ? argv[++i] : arg.slice(equal + 1);
|
|
62
|
+
f.demand(typeof value === 'string' && value.length > 0 && !value.startsWith('--'), 'usage', 'Missing value: --' + key);
|
|
63
|
+
if (type === 'number') options[key] = f.positive(Number(value), key, true);
|
|
64
|
+
else if (type === 'multiple') (options[key] ??= []).push(value);
|
|
65
|
+
else options[key] = value;
|
|
66
|
+
}
|
|
67
|
+
f.demand(definition.count === -1 ? positional.length <= 1 : positional.length === definition.count,
|
|
68
|
+
'usage', 'Wrong arguments; use code-flow ' + command + ' --help');
|
|
69
|
+
return { command, positional, options };
|
|
70
|
+
}
|
|
71
|
+
function input(stream) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const chunks = []; let size = 0;
|
|
74
|
+
const finish = (error, raw) => {
|
|
75
|
+
clearTimeout(timer); stream.removeListener('data', data); stream.removeListener('end', end);
|
|
76
|
+
stream.removeListener('error', fail);
|
|
77
|
+
if (error) { stream.destroy(); reject(error); } else resolve(raw);
|
|
78
|
+
};
|
|
79
|
+
const data = chunk => {
|
|
80
|
+
size += Buffer.byteLength(chunk);
|
|
81
|
+
if (size > 1048576) finish(new f.FlowError('hook_input_large', 'Hook input exceeds 1 MiB'));
|
|
82
|
+
else chunks.push(Buffer.from(chunk));
|
|
83
|
+
};
|
|
84
|
+
const end = () => finish(null, Buffer.concat(chunks).toString('utf8'));
|
|
85
|
+
const fail = error => finish(error);
|
|
86
|
+
const timer = setTimeout(() => finish(new f.FlowError('hook_timeout', 'Hook input timed out')), 2000);
|
|
87
|
+
stream.on('data', data); stream.once('end', end); stream.once('error', fail);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function diagnostics(items, stderr) {
|
|
91
|
+
for (const item of items ?? []) stderr.write('code-flow [' + item.code + '] ' + item.message.replace(/[\r\n]+/g, ' ') + '\n');
|
|
92
|
+
}
|
|
93
|
+
function eventPath(cwd) {
|
|
94
|
+
if (process.env.CF_EVENT_LOG) return process.env.CF_EVENT_LOG;
|
|
95
|
+
try {
|
|
96
|
+
const root = config.findRoot(cwd, false);
|
|
97
|
+
if (!root) return null;
|
|
98
|
+
const settings = config.load(root);
|
|
99
|
+
if (!settings.capture.enabled) return null;
|
|
100
|
+
return f.inside(root, settings.capture.path + '/events.jsonl');
|
|
101
|
+
} catch (_) { return null; }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function counters(streams, file) {
|
|
105
|
+
if (!file) return streams;
|
|
106
|
+
const state = { stdout: 0, stderr: 0 };
|
|
107
|
+
const wrap = (stream, key) => ({ write(text) {
|
|
108
|
+
state[key] += Buffer.byteLength(String(text)); return stream.write(text);
|
|
109
|
+
} });
|
|
110
|
+
return { stdin: streams.stdin, stdout: wrap(streams.stdout, 'stdout'),
|
|
111
|
+
stderr: wrap(streams.stderr, 'stderr'), state };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function summarize(command, result) {
|
|
115
|
+
if (!result || typeof result !== 'object') return null;
|
|
116
|
+
if (command === 'task finish') return { ok: result.ok, executed: result.executed,
|
|
117
|
+
deduped: result.deduped, results: (result.results || []).slice(0, 50).map(item => ({
|
|
118
|
+
id: item.id, status: item.status, exit_code: item.exit_code, duration_ms: item.duration_ms })) };
|
|
119
|
+
if (command === 'task start' || command === 'spec inject') return { task: result.task ?? null,
|
|
120
|
+
emitted: (result.emitted || []).length, omitted: (result.omitted || []).length,
|
|
121
|
+
diagnostics: (result.diagnostics || []).map(item => item.code) };
|
|
122
|
+
if (command === 'spec learn') return { results: (result.results || []).slice(0, 50).map(item => ({
|
|
123
|
+
source: item.source, target: item.target, rule: item.rule, status: item.status })) };
|
|
124
|
+
if (command === 'spec list') return { specs: (result.items || []).length };
|
|
125
|
+
if (command === 'task status') return { tasks: (result.items || []).length,
|
|
126
|
+
done: (result.items || []).filter(item => item.status === 'done').length };
|
|
127
|
+
if (command === 'doctor') return { ok: result.ok, summary: result.summary };
|
|
128
|
+
return { ok: result.ok ?? null };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeEvent(argv, command, started, io, code, summary, cwd, file) {
|
|
132
|
+
if (!file) return;
|
|
133
|
+
try {
|
|
134
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
135
|
+
fs.appendFileSync(file, JSON.stringify({ version: 1, at: new Date().toISOString(), command,
|
|
136
|
+
args: argv, cwd, exit: code, ms: Date.now() - started,
|
|
137
|
+
stdout_bytes: io.state?.stdout ?? 0, stderr_bytes: io.state?.stderr ?? 0,
|
|
138
|
+
summary: summary ?? null }) + '\n');
|
|
139
|
+
} catch (_) { /* diagnostics only */ }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function runHook(options, streams) {
|
|
143
|
+
const format = options.format ?? 'claude';
|
|
144
|
+
let result;
|
|
145
|
+
try {
|
|
146
|
+
f.demand(['claude', 'codex', 'costrict', 'opencode'].includes(format),
|
|
147
|
+
'hook_format', 'Expected --format=claude|codex|costrict|opencode');
|
|
148
|
+
const raw = await input(streams.stdin);
|
|
149
|
+
if (!raw.trim()) return { code: 0, summary: { format, blank: true } };
|
|
150
|
+
result = hook.build(JSON.parse(raw), format);
|
|
151
|
+
} catch (error) { result = { payload: hook.payload(format, ''), diagnostics: [{ code: error.code || 'hook_input', message: error.message }] }; }
|
|
152
|
+
diagnostics(result.diagnostics, streams.stderr);
|
|
153
|
+
if (result.payload) streams.stdout.write(JSON.stringify(result.payload) + '\n');
|
|
154
|
+
return { code: 0, summary: { format, emitted: (result.emitted || []).length,
|
|
155
|
+
omitted: (result.omitted || []).length,
|
|
156
|
+
diagnostics: (result.diagnostics || []).map(item => item.code) } };
|
|
157
|
+
}
|
|
158
|
+
async function dispatch(parsed, cwd) {
|
|
159
|
+
const { command, positional: args, options } = parsed;
|
|
160
|
+
if (command === 'init') return require('./init').init(f.realpath(cwd),
|
|
161
|
+
{ platform: options.platform, dryRun: options['dry-run'] });
|
|
162
|
+
if (command === 'doctor') {
|
|
163
|
+
const root = config.findRoot(cwd, false) || f.realpath(cwd);
|
|
164
|
+
return require('./doctor').run(root);
|
|
165
|
+
}
|
|
166
|
+
if (command === 'hook capabilities') {
|
|
167
|
+
return { package: require('../package.json').name, version: require('../package.json').version,
|
|
168
|
+
hook_protocol: 1, node: process.execPath, entry: path.resolve(__dirname, '../bin/code-flow.js') };
|
|
169
|
+
}
|
|
170
|
+
const root = config.findRoot(cwd); config.load(root);
|
|
171
|
+
switch (command) {
|
|
172
|
+
case 'task start': return task.start(root, args[0], f.string(options.task, '--task'), options.session);
|
|
173
|
+
case 'task block': return task.transition(root, args[0], f.string(options.task, '--task'), 'block', options.reason);
|
|
174
|
+
case 'task reopen': return task.transition(root, args[0], f.string(options.task, '--task'), 'reopen', options.reason);
|
|
175
|
+
case 'task unbind': return require('./session').unbind(root, options.session);
|
|
176
|
+
case 'task finish': return task.finish(root, args[0], f.string(options.task, '--task'), options);
|
|
177
|
+
case 'task archive': return task.archive(root, args[0], { dryRun: options['dry-run'], noLearn: options['no-learn'] });
|
|
178
|
+
case 'task status': return task.list(root, args[0]);
|
|
179
|
+
case 'process collect': return require('./process').collect(root,
|
|
180
|
+
{ out: options.out, transcript: options.transcript });
|
|
181
|
+
case 'task parallel prepare': {
|
|
182
|
+
const runId = options['run-id'] || new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14);
|
|
183
|
+
return require('./worktree').prepare(root, { taskFile: f.string(options['task-file'], '--task-file'),
|
|
184
|
+
tasks: f.string(options.tasks, '--tasks').split(',').map(item => item.trim()).filter(Boolean),
|
|
185
|
+
runId, noAutoBaseline: Boolean(options['no-auto-baseline']), noInit: !options.init,
|
|
186
|
+
workers: options.workers });
|
|
187
|
+
}
|
|
188
|
+
case 'task parallel status': return require('./worktree').status(root, f.string(options['run-id'], '--run-id'));
|
|
189
|
+
case 'task parallel collect': return require('./worktree').collect(root, f.string(options['run-id'], '--run-id'));
|
|
190
|
+
case 'task parallel merge': return require('./worktree').merge(root, f.string(options['run-id'], '--run-id'));
|
|
191
|
+
case 'task parallel cleanup': return require('./worktree').cleanup(root,
|
|
192
|
+
f.string(options['run-id'], '--run-id'), Boolean(options.force));
|
|
193
|
+
case 'spec list': {
|
|
194
|
+
const result = spec.list(root); return { ok: result.diagnostics.length === 0, ...result };
|
|
195
|
+
}
|
|
196
|
+
case 'spec inject': {
|
|
197
|
+
const taskFile = options['task-file'];
|
|
198
|
+
f.demand(!taskFile || options.task, 'usage', '--task is required with --task-file');
|
|
199
|
+
return { ok: true, ...render.inject(root, { taskFile, taskId: options.task,
|
|
200
|
+
paths: options.path ?? [], full: options.full, strictTask: Boolean(taskFile) }) };
|
|
201
|
+
}
|
|
202
|
+
case 'spec learn': return task.learn(root, f.string(options['from-task'], '--from-task'),
|
|
203
|
+
f.string(options.task, '--task'), options['dry-run']);
|
|
204
|
+
default: throw new f.FlowError('usage', 'Unsupported command');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function display(result, json, stdout) {
|
|
208
|
+
if (json) {
|
|
209
|
+
const value = result.schema_version === 1 && Object.hasOwn(result, 'text')
|
|
210
|
+
? Object.fromEntries(Object.entries(result).filter(([key]) => key !== 'text')) : result;
|
|
211
|
+
stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
212
|
+
}
|
|
213
|
+
else if (result.text) stdout.write(result.text + (result.text.endsWith('\n') ? '' : '\n'));
|
|
214
|
+
else if (result.items) {
|
|
215
|
+
stdout.write(result.items.map(item => item.task ? item.task + ' ' + item.status + ' tests=' + item.tests :
|
|
216
|
+
item.id + ' rules=' + item.rules + ' ' + item.description).join('\n') + '\n');
|
|
217
|
+
} else stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
218
|
+
}
|
|
219
|
+
async function main(argv, streams = { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }, cwd = process.cwd()) {
|
|
220
|
+
const isHook = argv[0] === 'hook';
|
|
221
|
+
const started = Date.now();
|
|
222
|
+
const eventFile = eventPath(cwd);
|
|
223
|
+
const io = counters(streams, eventFile);
|
|
224
|
+
try {
|
|
225
|
+
if (!isHook && argv.length === 1 && ['--version', '-v'].includes(argv[0])) {
|
|
226
|
+
io.stdout.write(require('../package.json').version + '\n'); return 0;
|
|
227
|
+
}
|
|
228
|
+
if (!isHook && argv.includes('--help')) {
|
|
229
|
+
io.stdout.write(usage(argv.filter(arg => arg !== '--help').slice(0, 2).join(' ') || null)); return 0;
|
|
230
|
+
}
|
|
231
|
+
const parsed = parse(argv);
|
|
232
|
+
if (parsed.command === 'hook prompt') {
|
|
233
|
+
const hookResult = await runHook(parsed.options, io);
|
|
234
|
+
writeEvent(argv, 'hook prompt', started, io, hookResult.code, hookResult.summary, cwd, eventFile);
|
|
235
|
+
return hookResult.code;
|
|
236
|
+
}
|
|
237
|
+
const result = await dispatch(parsed, cwd);
|
|
238
|
+
diagnostics(result.diagnostics, io.stderr);
|
|
239
|
+
display(result, parsed.options.json, io.stdout);
|
|
240
|
+
const code = result.interrupted ? (result.interrupted === 'SIGINT' ? 130 : 143) : result.ok === false ? 1 : 0;
|
|
241
|
+
writeEvent(argv, parsed.command, started, io, code, summarize(parsed.command, result), cwd, eventFile);
|
|
242
|
+
return code;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
io.stderr.write('code-flow [' + (error.code || 'operation_failed') + '] ' + error.message + '\n');
|
|
245
|
+
if (isHook) {
|
|
246
|
+
const value = argv.find(arg => arg.startsWith('--format='));
|
|
247
|
+
const format = value ? value.slice(9) : argv[argv.indexOf('--format') + 1];
|
|
248
|
+
io.stdout.write(JSON.stringify(hook.payload(format, '')) + '\n');
|
|
249
|
+
writeEvent(argv, 'hook prompt', started, io, 0, { error: error.code || 'hook_error' }, cwd, eventFile);
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
if (argv.includes('--json')) io.stdout.write(JSON.stringify({ ok: false, code: error.code || 'operation_failed', message: error.message }) + '\n');
|
|
253
|
+
const code = error.exitCode ?? (error instanceof SyntaxError ? 2 : 1);
|
|
254
|
+
writeEvent(argv, argv.slice(0, 2).join(' '), started, io, code, { error: error.code || 'operation_failed' }, cwd, eventFile);
|
|
255
|
+
return code;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
module.exports = { main, parse, usage, dispatch, runHook };
|