ccgx-workflow 2.4.1 → 2.5.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.
Files changed (46) hide show
  1. package/README.md +134 -277
  2. package/README.zh-CN.md +134 -272
  3. package/dist/chunks/version-build.mjs +1 -1
  4. package/dist/cli.mjs +1 -1
  5. package/dist/index.d.mts +709 -16
  6. package/dist/index.d.ts +709 -16
  7. package/dist/index.mjs +1061 -30
  8. package/dist/shared/{ccgx-workflow.j1spUsik.mjs → ccgx-workflow.CdHnJLak.mjs} +106 -22
  9. package/package.json +1 -1
  10. package/templates/commands/agents/code-fixer.md +6 -6
  11. package/templates/commands/agents/phase-runner.md +46 -14
  12. package/templates/commands/agents/plan-checker.md +10 -0
  13. package/templates/commands/analyze.md +66 -25
  14. package/templates/commands/autonomous.md +428 -225
  15. package/templates/commands/cancel.md +9 -0
  16. package/templates/commands/codex-exec.md +12 -11
  17. package/templates/commands/context.md +14 -0
  18. package/templates/commands/debate.md +10 -6
  19. package/templates/commands/execute.md +76 -28
  20. package/templates/commands/optimize.md +53 -25
  21. package/templates/commands/plan.md +78 -28
  22. package/templates/commands/review.md +26 -19
  23. package/templates/commands/spec-impl.md +68 -127
  24. package/templates/commands/spec-plan.md +61 -82
  25. package/templates/commands/spec-research.md +35 -92
  26. package/templates/commands/spec-review.md +34 -119
  27. package/templates/commands/status.md +1 -0
  28. package/templates/commands/team-exec.md +45 -13
  29. package/templates/commands/team.md +64 -167
  30. package/templates/commands/test.md +56 -34
  31. package/templates/commands/verify-work.md +36 -13
  32. package/templates/commands/verify.md +35 -0
  33. package/templates/commands/workflow.md +22 -37
  34. package/templates/hooks/ccg-loop-detector.cjs +39 -8
  35. package/templates/hooks/ccg-statusline.js +142 -2
  36. package/templates/hooks/ccg-stop-gate.cjs +248 -19
  37. package/templates/hooks/ccg-subagent-context.cjs +505 -0
  38. package/templates/scripts/ccg-state-lock.cjs +510 -0
  39. package/templates/scripts/ccg-team-schedule.cjs +328 -0
  40. package/templates/scripts/ccgx-call-plugin.mjs +494 -141
  41. package/templates/scripts/invoke-model.mjs +28 -1
  42. package/templates/scripts/task-store.cjs +614 -0
  43. package/templates/skills/tools/verify-change/SKILL.md +7 -0
  44. package/templates/skills/tools/verify-module/SKILL.md +7 -0
  45. package/templates/skills/tools/verify-quality/SKILL.md +7 -0
  46. package/templates/skills/tools/verify-security/SKILL.md +8 -0
@@ -81,7 +81,7 @@ pending_checks:
81
81
 
82
82
  ## 工作流程(lifecycle)
83
83
 
84
- ### Step 0 — Resume 检测
84
+ ### Step 0 — Resume 检测 `[Gate: pre-flight]`
85
85
 
86
86
  ```bash
87
87
  TASK_ID="${1:-$(detect-task-id)}" # detect-task-id 见下
@@ -93,7 +93,8 @@ UAT_FILE="${UAT_DIR}/UAT.md"
93
93
  1. 命令参数 `$ARGUMENTS`(用户显式给)
94
94
  2. `.context/state.md` 当前 phase 字段
95
95
  3. `git rev-parse --abbrev-ref HEAD` 分支名(取最后一段)
96
- 4. `verify-work-$(date +%Y%m%d-%H%M)` 兜底
96
+ 4. **[Gate: pre-flight 阻断分支]** 优先级 1-3 全失败且 `git diff --name-only HEAD` 为空 → 输出阻断提示并**停止**,要求用户显式传 task-id(`/ccg:verify-work <task-id>`),不产生任何部分工作——与 GATE_REGISTRY `verify-work-entry` 的 failurePath("task-id 无法推断且无 git diff → 阻断")对齐
97
+ 5. 有 git diff 时才启用 `verify-work-$(date +%Y%m%d-%H%M)` 时间戳兜底
97
98
 
98
99
  ```bash
99
100
  if [ -f "$UAT_FILE" ]; then
@@ -136,15 +137,15 @@ fi
136
137
 
137
138
  按 git diff 性质开门(决策矩阵):
138
139
 
139
- | 变更性质 | 触发判据 | 门组 |
140
- |---------|---------|------|
141
- | 新模块 | `git status` 显示新增目录 | verify-module → verify-security → verify-quality |
142
- | 小改动 | diff ≤ 30 行 | verify-quality → verify-change |
143
- | 常规改动 | diff 30-200 行 | verify-change → verify-quality → verifier agent |
144
- | 安全敏感 | 触及 auth/crypto/input/secret/sql 关键字 | verify-security → verify-change → verifier agent |
145
- | 重构 | 含 `refactor:` commit | verify-change → verify-quality → verify-security |
140
+ | 变更性质 | 触发判据 | 门组 | Gate 类型 |
141
+ |---------|---------|------|----------|
142
+ | 新模块 | `git status` 显示新增目录 | verify-module → verify-security → verify-quality | revision-loop(verify-security critical → abort 覆盖) |
143
+ | 小改动 | diff ≤ 30 行 | verify-quality → verify-change | revision-loop |
144
+ | 常规改动 | diff 30-200 行 | verify-change → verify-quality → verifier agent | revision-loop |
145
+ | 安全敏感 | 触及 auth/crypto/input/secret/sql 关键字 | verify-security → verify-change → verifier agent | revision-loop(verify-security critical → abort 覆盖) |
146
+ | 重构 | 含 `refactor:` commit | verify-change → verify-quality → verify-security | revision-loop(verify-security critical → abort 覆盖) |
146
147
 
147
- 每门返回结构化 `{ gate, status, counts, top_findings, artifacts }`,**FAIL with critical** 立即 short-circuit 用户先修,进 Step 5 前不继续问 UAT
148
+ 每门返回结构化 `{ gate, status, counts, top_findings, artifacts }`,**FAIL with critical** 立即 short-circuit 用户先修,进 Step 5 前不继续问 UAT。`[Gate: abort——停 UAT、UAT.md 状态保留、修复后 resume]`
148
149
 
149
150
  #### 2b. Cold-start smoke 自动注入
150
151
 
@@ -231,11 +232,30 @@ gaps:
231
232
 
232
233
  #### 5b. 并行 spawn diagnose
233
234
 
235
+ 按 debug.md 阶段 2 的 spawn 契约调度 `debug-session-manager`(fresh context,只找根因不修):
236
+
234
237
  ```
235
- Agent(subagent_type="ccg:debug", task=<symptom>, run_in_background=true)
238
+ Agent({
239
+ subagent_type: "debug-session-manager",
240
+ run_in_background: true,
241
+ prompt: `你是 debug-session-manager。完整执行多轮 hypothesis 循环,最终返回 ≤ 200 token 紧凑摘要。
242
+
243
+ ## 输入
244
+
245
+ slug: uat-<task-id>-G-<n>
246
+ mode: find_root_cause_only
247
+ workdir: <Bash pwd 取的项目绝对路径>
248
+ symptoms: |
249
+ <gap.symptom 用户原话 + 复现上下文>
250
+
251
+ ## 工作要求
252
+
253
+ (同 debug.md 阶段 2 prompt 模板:.context/debug/<slug>.md 持久 hypothesis 链、
254
+ 每个 hypothesis 必须 falsifiable、cap 3 refuted 升级、摘要严格三选一)`
255
+ })
236
256
  ```
237
257
 
238
- debug agent 找根因,输出 `.context/uat/<task-id>/diagnose-G-<n>.md`。
258
+ manager 返回 `ROOT_CAUSE_FOUND` / `CHECKPOINT_REACHED` 摘要后,主线把根因写入 `.context/uat/<task-id>/diagnose-G-<n>.md`。
239
259
 
240
260
  #### 5c. spawn planner --gaps
241
261
 
@@ -253,7 +273,7 @@ Agent(subagent_type="plan-checker", plan=<fix-plan>)
253
273
 
254
274
  返回 `{ findings, hasBlocker, counts }`。
255
275
 
256
- #### 5e. max-3-loop 收敛
276
+ #### 5e. max-3-loop 收敛 `[Gate: revision-loop max3 → escalation]`
257
277
 
258
278
  ```
259
279
  loop_count += 1
@@ -273,6 +293,8 @@ else:
273
293
 
274
294
  **3 轮上限是硬规约**——与 plan-review-convergence / code-review-fix 一致。
275
295
 
296
+ **stall detection(提前升级)**:本轮 critical/BLOCKER 数 ≥ 上一轮 → 不等 3 轮直接升级(`resolveVerifyGateAction` 语义:连续两轮 issue 数不下降说明收敛环已失效)。
297
+
276
298
  ### Step 6 — 终态报告
277
299
 
278
300
  所有 check 已答(含 skip)+ 所有 gap 状态 ∈ {fixed, deferred}:
@@ -327,6 +349,7 @@ else:
327
349
  | `src/utils/uat-session.ts:inferIssueSeverity` | Step 5a 严重度推断 |
328
350
  | `src/utils/uat-session.ts:decideConvergence` | Step 5e max-3-loop 判定 |
329
351
  | `src/utils/plan-checker.ts:runPlanChecker` | Step 5d 修复计划静态校验 |
352
+ | `src/utils/verify-orchestrator.ts:resolveVerifyGateAction` | Step 5e 类型化失败路径(advance/revise/escalate + stall detection) |
330
353
 
331
354
  ## 硬性约束
332
355
 
@@ -35,6 +35,7 @@ $ARGUMENTS
35
35
  - 提取首个 `--gate=<value>` 或 `--gate <value>`
36
36
  - 剩余非 flag 参数作为 `<path>`
37
37
  - 未指定 gate → 默认 `all`
38
+ - **[Gate: pre-flight]** `--gate` 值不在 `{change, quality, security, module, all}` 内 → 报错并打印枚举("--gate 必须是 change | quality | security | module | all 之一"),不进入路由分发
38
39
 
39
40
  ## 路由规则
40
41
 
@@ -46,6 +47,39 @@ $ARGUMENTS
46
47
  | `module` | 读取并执行 `~/.claude/skills/ccg/tools/verify-module/SKILL.md` | 沿用旧 verify-module skill 的脚本 |
47
48
  | `all` | 调用 `/ccg:verify-work` 编排器 | 按 git diff 变更类型自动选门 |
48
49
 
50
+ ## Gates Taxonomy(4 类关卡与失败路径)
51
+
52
+ 全工作流的每个检查点都归属唯一的 gate 类型,失败时按该类型的确定性路径处理:
53
+
54
+ | 类型 | 语义 | 失败行为 | 恢复方式 |
55
+ |------|------|---------|---------|
56
+ | **pre-flight** | 操作开始前验前置条件 | 阻断入口,**不产生任何部分工作** | 补前置条件后重试 |
57
+ | **revision-loop** | 评估产出质量,不足则带具体反馈回喂 producer | max 3 轮迭代;**stall detection:连续两轮 issue 数不下降 → 提前升级**;轮次耗尽无条件升级 | 反馈修复后重跑 |
58
+ | **escalation** | 浮出无法自动解决的问题给人决策 | 暂停工作流、给选项(force / guide / abort)、等人选路 | 人选定路径后继续 |
59
+ | **abort** | 为止损立即终止 | **保留状态**、报告原因 | 人查根因修复后从 checkpoint 重启 |
60
+
61
+ 选型启发:先 pre-flight;检查发生在产出之后 = revision-loop;revision 解不了 = escalation;继续有害 = abort。
62
+
63
+ ### 每门分类表
64
+
65
+ | Gate | 类型 | critical 覆盖 | maxLoops | 失败路径 |
66
+ |------|------|--------------|----------|---------|
67
+ | `execute-entry` | pre-flight | — | — | `.claude/plan/<name>.md` 缺失 → 阻断入口,提示先跑 `/ccg:plan`;不产生部分工作 |
68
+ | `spec-impl-entry` | pre-flight | — | — | OPSX artifacts 缺失 → 阻断入口,提示先跑 `/ccg:spec-plan` |
69
+ | `verify-work-entry` | pre-flight | — | — | task-id 无法推断且无 git diff → 阻断,要求显式传 task-id |
70
+ | `verify-change` | revision-loop | — | 3 | findings 回喂修复→重跑;3 轮或 stall → escalation 三选 |
71
+ | `verify-quality` | revision-loop | — | 3 | 同 verify-change |
72
+ | `verify-module` | revision-loop | — | 3 | 缺文档/结构 → 补齐重跑;3 轮 → escalation |
73
+ | `verify-security` | revision-loop | **abort** | 3 | critical 漏洞 → abort(停止交付、保留 UAT/job 状态、报原因、修复后 resume);非 critical → revision-loop |
74
+ | `plan-checker` | revision-loop | — | 3 | BLOCKER 回喂 planner(仅修 BLOCKER);3 轮或 stall → escalation force/guide/abort |
75
+ | `requirements-coverage` | escalation | — | — | 未覆盖 REQ-ID → 阻断 planning 完成 + AskUserQuestion 三选:re-plan 补覆盖(推荐)/移到下一 phase/force 接受并记录 |
76
+ | `verify-wave` | revision-loop | — | 3 | critical → 修订轮(仅修 critical);stall 或 3 轮 → escalation |
77
+ | `uat-convergence` | revision-loop | — | 3 | diagnose→planner --gaps→plan-checker 收敛环;3 轮 → escalation 三选 |
78
+
79
+ **单一真相源**:`src/utils/verify-orchestrator.ts` 的 `GATE_REGISTRY`(含 `getGateClassification` / `resolveVerifyGateAction` helper)。本表是其人类可读镜像,分类变更只在 helper 改。
80
+
81
+ > **脚注**:本表是**全工作流 gate taxonomy 镜像**,不是 `--gate` 可路由集合——`--gate` 仅接受 `{change, quality, security, module, all}`。表内其余 gate 由各自命令在其工作流内触发(如 `requirements-coverage` 由 `/ccg:plan` 与 `/ccg:spec-plan` 触发,`execute-entry` 由 `/ccg:execute` 触发)。
82
+
49
83
  ## 兼容性
50
84
 
51
85
  旧的 `/ccg:verify-change` / `/ccg:verify-quality` / `/ccg:verify-security` / `/ccg:verify-module` 仍可工作(由 Skill Registry 自动生成),但 SKILL.md 已标记 `deprecated_in: 1.0.0`、`replaced_by: /ccg:verify --gate=<name>`。建议新工作流使用本统一命令。
@@ -60,6 +94,7 @@ $ARGUMENTS
60
94
  ## 注意事项
61
95
 
62
96
  - 本命令不**重新实现**校验逻辑,纯路由层。所有逻辑仍在 4 个 verify-* SKILL.md 中维护。
97
+ - **[Gate: pre-flight]** `--gate=module` 未提供 `<path>` → 阻断并提示用法(`/ccg:verify --gate=module <模块路径>`),不进入子门执行。
63
98
  - `--gate=all` 是 `/ccg:verify-work` 的别名,未来可能合并。
64
99
  - 如脚本需要传 `--mode`(如 verify-change 的 `--mode staged`),通过 `path` 之后的额外参数透传:`/ccg:verify --gate=change -- --mode staged`。
65
100
 
@@ -41,52 +41,37 @@ subagent_freshness: required
41
41
 
42
42
  **调用通道路由(CCG codeagent 退役,v2.2.0+)**
43
43
 
44
- 1. **优先 plugin spawn**(默认):plugin 已装 → `Agent(subagent_type="<codex:codex-rescue|gemini:gemini-rescue>")`。session 复用通过 prompt `--resume` flag 表达(subagent 映射为 codex/gemini `--resume-last`,**注意是 last,不是 by-id**)。
44
+ 1. **优先 Bash 直调 plugin 快路径**(默认):plugin 已装 → `{{CODEX_BASH_TASK}}` / `{{GEMINI_BASH_TASK}}` 占位符 + `--prompt-file` Bash 直调(review.md 同款),绕开 sonnet wrapper silent-fallback;**仅 `gemini-ccgx` fork 走 gemini-batch 29s 快路径,上游 + repatch 仍走 companion(ACP) 慢路径**(Windows 实测 5+min 卡死风险仍在)。session 复用加 `--resume-last`(**注意是 last,不是 by-id**)。
45
45
  2. **降级 codeagent-wrapper**(BC fallback):plugin 未装 → Bash 调用,保留 `resume <SESSION_ID>` 显式会话管理。
46
46
 
47
- **判定**:preflight `Bash` 跑 `node ~/.claude/.ccg/scripts/check-plugins.cjs`(解析 Claude Code 权威 `installed_plugins.json`)。exit `0` + stdout `{"codex":"<ver>","gemini":"<ver>"}` → 通道 A(plugin 默认);非 `0` → 通道 B(wrapper BC fallback)。
47
+ **判定**:preflight `Bash` 跑 `node ~/.claude/.ccg/scripts/check-plugins.cjs`(解析 Claude Code 权威 `installed_plugins.json`)。exit `0` + stdout `{"codex":"<ver>","gemini":"<ver>"}` → 通道 A(Bash 直调快路径,默认);非 `0` → 通道 B(wrapper BC fallback)。
48
48
 
49
49
  ---
50
50
 
51
- **通道 A — plugin spawn(默认)**:
51
+ **通道 A — Bash 直调 plugin 快路径(默认)**:
52
52
 
53
- **预备动作**:spawn 前主线先 Read 对应阶段的角色提示词(见下方表),把内容拼入 `<role>` 块。
53
+ > 占位符 `{{CODEX_BASH_TASK}}` / `{{GEMINI_BASH_TASK}}` 由 install 渲染为 `node <ccgx-call-plugin.mjs 绝对路径> <vendor> --json`。LLM 只需 Write tmpfile + 追加 `--prompt-file <tmpfile>` 运行,**不写 `Agent(...)`、不内联 role 全文**。
54
54
 
55
55
  ```
56
- # 新会话调用
57
- Agent({
58
- subagent_type: "<codex:codex-rescue|gemini:gemini-rescue>",
59
- description: "简短描述",
60
- prompt: `<role>
61
- ${roleContent} // 主线 Read 后的角色提示词内容
62
- </role>
56
+ # 新阶段调用:Write tmpfile(≤2KB,让模型自己 Read 角色文件)+ Bash 直调
57
+ Write({ file_path: ".context/tmp/ccg-workflow-<phase>-<vendor>-$JOB.txt", content: `# 任务:<阶段名>
63
58
 
64
- <workdir>{{WORKDIR}}</workdir>
59
+ 工作目录:{{WORKDIR}}(你已在此,可直接用 Bash/Read)
60
+ 角色定义见 <对应阶段角色提示词路径,见下方表>,**先用 Read 工具读取再开始**。
61
+ **必须前台执行,禁止 --background / --wait**(ccgx 编排层已用 Agent run_in_background 包裹,插件内再 background = 双重后台,stop-gate / status / cancel 全盲)。
65
62
 
66
- <task>
67
63
  需求:<增强后的需求(如未增强则用 $ARGUMENTS)>
68
- 上下文:<前序阶段收集的项目上下文、分析结果等>
69
- </task>
64
+ 上下文:<前序阶段关键文件路径 + 结论,不塞大段源码>
65
+ <期望输出格式 / JSON schema>` })
70
66
 
71
- <structured_output_contract>
72
- <期望输出格式 / JSON schema>
73
- Return ≤200 token structured summary.
74
- </structured_output_contract>`
75
- })
76
-
77
- # 复用会话(同一 Claude session 内的连续阶段)
78
- Agent({
79
- subagent_type: "<codex:codex-rescue|gemini:gemini-rescue>",
80
- description: "简短描述",
81
- prompt: `--resume
67
+ Bash({ command: `{{CODEX_BASH_TASK 或 GEMINI_BASH_TASK}} --prompt-file .context/tmp/ccg-workflow-<phase>-<vendor>-$JOB.txt`, description: "简短描述", run_in_background: true })
82
68
 
83
- <task>
84
- <delta 指令(仅本阶段变化,不重述前序上下文,由 codex/gemini thread 自带历史)>
85
- </task>`
86
- })
69
+ # 复用会话(同任务连续阶段,只发 delta):加 --resume-last
70
+ Bash({ command: `{{CODEX_BASH_TASK 或 GEMINI_BASH_TASK}} --resume-last --prompt-file .context/tmp/ccg-workflow-<phase>-<vendor>-delta-$JOB.txt`, description: "简短描述 (resume)", run_in_background: true })
71
+ # delta tmpfile 只写本阶段变化指令,不重述前序上下文(codex/gemini thread 自带历史)。
87
72
  ```
88
73
 
89
- **通道 B — wrapper BC fallback**(plugin 未装时):
74
+ **通道 B — wrapper BC fallback**(plugin 未装时;wrapper 的 `ROLE_FILE:` 由 `invoke-model.mjs` 解析故保留):
90
75
 
91
76
  ```
92
77
  # 新会话
@@ -106,7 +91,7 @@ Bash({
106
91
  })
107
92
  ```
108
93
 
109
- **角色提示词路径**(通道 A 主线 Read 后拼入 `<role>` 块;通道 B 仍用 `ROLE_FILE:` 写在 EOF 内):
94
+ **角色提示词路径**(通道 A 写入 tmpfile 让模型自己 Read;通道 B 仍用 `ROLE_FILE:` 写在 EOF 内由 invoke-model.mjs 解析):
110
95
 
111
96
  | 阶段 | 后端 | 前端 |
112
97
  |------|-------|--------|
@@ -115,7 +100,7 @@ Bash({
115
100
  | 审查 | `~/.claude/.ccg/prompts/{{BACKEND_PRIMARY}}/reviewer.md` | `~/.claude/.ccg/prompts/{{FRONTEND_PRIMARY}}/reviewer.md` |
116
101
 
117
102
  **会话复用模型差异**:
118
- - **通道 A(plugin)**:同 Claude session 内连续阶段用 `--resume`,自动接上一次同 backend 的 thread(codex/gemini 各自独立的 `--resume-last` 行为)。**不支持跨任务跳点 resume by ID**——若中间插了其他 codex 调用会串。
103
+ - **通道 A(Bash 直调快路径)**:同任务连续阶段加 `--resume-last`,自动接上一次同 vendor 的 thread(codex 即刻可用;gemini gemini-ccgx plugin ≥1.3.0——helper 有版本闸门,过旧会显式报错)。**通道 A 仅支持 `--resume-last`,不支持按 id resume**——若中间插了其他同 vendor 调用会串;`JSON.parse(stdout).threadId` 仅作审计记录,按 id resume 只有通道 B wrapper(invoke-model)可用。
119
104
  - **通道 B(wrapper)**:每次 spawn 返回 `SESSION_ID: xxx`,后续 `resume xxx` 任意复用(注意:是 `resume`,不是 `--resume`)。
120
105
 
121
106
  **并行调用 + 事件驱动等待**:
@@ -123,7 +108,7 @@ Bash({
123
108
  1. 同 message 内 spawn 多个 `Bash(run_in_background: true)` 并行任务
124
109
  2. spawn 完后主线说明已启动 task-id,**直接 turn end**,**不调 TaskOutput**
125
110
  3. Claude Code 引擎在每个 task 完成时自动发 `<task-notification>` system-reminder 触发主线新 turn
126
- 4. 主线在新 turn 处理:从 `<output-file>` 路径 read stdout,按通道 schema parse 结果
111
+ 4. 主线在新 turn 处理:从 `<output-file>` 路径 read stdout,`JSON.parse(stdout).text`(通道 A)/ `--progress` 行(通道 B)
127
112
  5. **必须等所有相关 task 都收到通知**才进入下一阶段(按 task-id 计数已收齐)
128
113
 
129
114
  ⛔ **禁止**:
@@ -167,7 +152,7 @@ Bash({
167
152
  - {{BACKEND_PRIMARY}}:使用分析提示词,输出技术可行性、方案、风险
168
153
  - {{FRONTEND_PRIMARY}}:使用分析提示词,输出 UI 可行性、方案、体验
169
154
 
170
- `TaskOutput` 等待结果。**📌 保存 SESSION_ID**(`CODEX_SESSION` 和 `GEMINI_SESSION`)。
155
+ 事件驱动等待 task-notification(**不调 TaskOutput**),Read 各自 stdout 取结果。**📌 保存 SESSION_ID**(`CODEX_SESSION` 和 `GEMINI_SESSION`)。
171
156
 
172
157
  **务必遵循上方 `多模型调用规范` 的 `重要` 指示**
173
158
 
@@ -181,7 +166,7 @@ Bash({
181
166
  - {{BACKEND_PRIMARY}}:使用规划提示词 + `resume $CODEX_SESSION`,输出后端架构
182
167
  - {{FRONTEND_PRIMARY}}:使用规划提示词 + `resume $GEMINI_SESSION`,输出前端架构
183
168
 
184
- `TaskOutput` 等待结果。
169
+ 事件驱动等待 task-notification(**不调 TaskOutput**),Read 各自 stdout 取结果。
185
170
 
186
171
  **务必遵循上方 `多模型调用规范` 的 `重要` 指示**
187
172
 
@@ -203,7 +188,7 @@ Bash({
203
188
  - {{BACKEND_PRIMARY}}:使用审查提示词,关注安全、性能、错误处理
204
189
  - {{FRONTEND_PRIMARY}}:使用审查提示词,关注可访问性、设计一致性
205
190
 
206
- `TaskOutput` 等待结果。整合审查意见,用户确认后执行优化。
191
+ 事件驱动等待 task-notification(**不调 TaskOutput**)。整合审查意见,用户确认后执行优化。
207
192
 
208
193
  **务必遵循上方 `多模型调用规范` 的 `重要` 指示**
209
194
 
@@ -16,7 +16,16 @@
16
16
  // Repeated near-identical prompts within a short window are a strong loop
17
17
  // indicator regardless of what the orchestrator is doing internally.
18
18
  //
19
- // State file: <workdir>/.ccg/.turns.json (kept tiny: last 5 turns only)
19
+ // State file: <os.tmpdir()>/ccg-turns-<session_id>.json (kept tiny: last 5
20
+ // turns only). Keyed by session_id — DELIBERATELY NOT written into the
21
+ // project tree. Rationale: the turn window is a per-session, sub-10-minute
22
+ // signal with zero coupling to git / workspace / project identity, so it
23
+ // belongs in the OS temp dir — the same model already used by
24
+ // ccg-statusline.js and ccg-context-monitor.js. An earlier version wrote
25
+ // <cwd>/.ccg/.turns.json, whose unconditional mkdirSync seeded a stray .ccg
26
+ // directory in EVERY cwd (subdirs, monorepo packages, non-CCG projects) on
27
+ // every prompt — that pollution (and the project start-up failures it caused)
28
+ // is the bug this version fixes.
20
29
  //
21
30
  // Performance budget: <50 ms hot path. Hook fires every UserPromptSubmit; if
22
31
  // anything throws we exit 0 silently so the user message is never blocked.
@@ -25,8 +34,9 @@
25
34
 
26
35
  const fs = require('node:fs');
27
36
  const path = require('node:path');
37
+ const os = require('node:os');
38
+ const crypto = require('node:crypto');
28
39
 
29
- const STATE_FILE = path.join('.ccg', '.turns.json');
30
40
  const MAX_TURNS = 5; // sliding window
31
41
  const LOOP_THRESHOLD = 3; // 3 similar in a row → loop
32
42
  // Bound the state-file read. .turns.json holds at most MAX_TURNS rows of
@@ -143,9 +153,29 @@ function composeWarning(loop) {
143
153
  ].join('\n');
144
154
  }
145
155
 
156
+ // Derive a filesystem-safe key for the per-session state file.
157
+ // - Prefer Claude Code's session_id (UserPromptSubmit payloads supply it —
158
+ // see ccg-context-monitor.js `data.session_id` / ccg-session-state.cjs).
159
+ // - Reject any session_id containing a path separator or `..` so it can never
160
+ // escape os.tmpdir() (mirrors ccg-statusline.js / ccg-context-monitor.js).
161
+ // - Fall back to a stable sha256 of cwd when session_id is absent/unsafe, so
162
+ // loop detection keeps working without ever touching the project tree.
163
+ function resolveStateKey(input) {
164
+ const sid = input && typeof input.session_id === 'string' ? input.session_id : '';
165
+ if (sid && !/[/\\]|\.\./.test(sid)) return sid;
166
+ const cwd = input && typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
167
+ return `cwd-${crypto.createHash('sha256').update(cwd).digest('hex').slice(0, 16)}`;
168
+ }
169
+
170
+ // Absolute path to the session-scoped turn-history file, always under the OS
171
+ // temp dir — never under the project working directory.
172
+ function resolveStatePath(input) {
173
+ return path.join(os.tmpdir(), `ccg-turns-${resolveStateKey(input)}.json`);
174
+ }
175
+
146
176
  function readInput() {
147
- // SessionStart / UserPromptSubmit pass JSON on stdin. We only need .prompt
148
- // and .cwd. Failing to read is fine — we exit 0 silently.
177
+ // SessionStart / UserPromptSubmit pass JSON on stdin. We only need .prompt,
178
+ // .session_id and .cwd. Failing to read is fine — we exit 0 silently.
149
179
  let raw = '';
150
180
  try {
151
181
  raw = fs.readFileSync(0, 'utf-8');
@@ -174,10 +204,9 @@ function main() {
174
204
  return;
175
205
  }
176
206
 
177
- // CCG state lives under the project root. Use the cwd Claude Code passed in;
178
- // fall back to process.cwd() (which Claude Code sets for the hook anyway).
179
- const cwd = typeof input.cwd === 'string' && input.cwd ? input.cwd : process.cwd();
180
- const statePath = path.join(cwd, STATE_FILE);
207
+ // Loop state is a per-session sliding window. It lives in os.tmpdir() keyed by
208
+ // session_id, NOT in the project tree see resolveStatePath + file header.
209
+ const statePath = resolveStatePath(input);
181
210
 
182
211
  const fp = fingerprint(prompt);
183
212
  if (!fp) {
@@ -224,6 +253,8 @@ module.exports = {
224
253
  jaccard,
225
254
  detectLoop,
226
255
  composeWarning,
256
+ resolveStateKey,
257
+ resolveStatePath,
227
258
  // Constants exported so tests can document the contract.
228
259
  LOOP_THRESHOLD,
229
260
  WINDOW_MS,
@@ -41,6 +41,141 @@ function readGitBranch(dir) {
41
41
  }
42
42
  }
43
43
 
44
+ // --- project state (.context/STATE.json, P1-4 task-store projection) --------
45
+
46
+ // Hard bound on STATE.json size — the projection is a handful of fields, so
47
+ // anything bigger than 16KB is corrupt/abusive and gets ignored.
48
+ const STATE_MAX_BYTES = 16384;
49
+ // Staleness guard: a crashed orchestrator can leave active_phase behind.
50
+ // After 24h without an update the segment silently disappears.
51
+ const STATE_STALE_MS = 24 * 60 * 60 * 1000;
52
+ // Lifecycle words worth showing next to "Phase N" (in-flight scenes only).
53
+ const STATE_LIFECYCLE_WORDS = new Set([
54
+ 'discussing', 'planning', 'executing', 'verifying', 'fixing', 'reviewing',
55
+ ]);
56
+
57
+ /**
58
+ * Path-normalized directory equality. Windows paths are case-insensitive
59
+ * (C:\Users\X vs c:\users\x), so the $HOME stop guard must not compare raw
60
+ * strings.
61
+ */
62
+ function isSameDir(a, b) {
63
+ if (!a || !b) return false;
64
+ try {
65
+ const na = path.resolve(a);
66
+ const nb = path.resolve(b);
67
+ if (process.platform === 'win32') return na.toLowerCase() === nb.toLowerCase();
68
+ return na === nb;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Walk upward from `dir` (max 10 levels, stopping at home / fs root) looking
76
+ * for `.context/STATE.json`. Returns the absolute path or null.
77
+ */
78
+ function findProjectStateFile(dir) {
79
+ if (!dir || typeof dir !== 'string') return null;
80
+ try {
81
+ let cur = path.resolve(dir);
82
+ const home = os.homedir();
83
+ for (let i = 0; i < 10; i++) {
84
+ const candidate = path.join(cur, '.context', 'STATE.json');
85
+ if (fs.existsSync(candidate)) return candidate;
86
+ if (isSameDir(cur, home)) return null;
87
+ const parent = path.dirname(cur);
88
+ if (parent === cur) return null; // fs root
89
+ cur = parent;
90
+ }
91
+ return null;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Read + parse the nearest STATE.json. Returns the parsed object or null on
99
+ * any failure (missing, oversized, corrupt JSON, non-object). Never throws.
100
+ */
101
+ function readProjectState(dir) {
102
+ try {
103
+ const file = findProjectStateFile(dir);
104
+ if (!file) return null;
105
+ const st = fs.statSync(file);
106
+ if (!st.isFile() || st.size > STATE_MAX_BYTES) return null;
107
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
108
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
109
+ // Frozen projection schema v1 — a foreign/newer schema version degrades
110
+ // cleanly to "no segment" rather than misrendering unknown semantics.
111
+ if (parsed.schema_version !== undefined && parsed.schema_version !== 1) return null;
112
+ return parsed;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function isValidProgress(p) {
119
+ return !!p && typeof p === 'object'
120
+ && typeof p.total_phases === 'number' && p.total_phases > 0
121
+ && typeof p.completed_phases === 'number' && p.completed_phases >= 0;
122
+ }
123
+
124
+ function truncateAction(s, max) {
125
+ return s.length <= max ? s : s.slice(0, max) + '…';
126
+ }
127
+
128
+ /**
129
+ * Render the STATE.json segment ('' when nothing to show). Scene priority
130
+ * (GSD statusline contract):
131
+ * 1. active_phase set → "Phase <n>[ <lifecycle-word>]" (in-flight wins)
132
+ * 2. next_action set → "next <cmd ≤40 chars>" (idle recommendation)
133
+ * 3. progress 100% complete → "✓ phases done"
134
+ * 4. otherwise → '' (output stays byte-identical to pre-P1-4)
135
+ * Scenes 1-2 get a "[done/total] " prefix when progress is valid.
136
+ * Staleness: updated_at missing/unparseable/older than 24h → ''.
137
+ */
138
+ function formatStateSegment(state, nowMs) {
139
+ if (!state || typeof state !== 'object') return '';
140
+ try {
141
+ const ts = typeof state.updated_at === 'string' ? Date.parse(state.updated_at) : NaN;
142
+ if (!Number.isFinite(ts)) return '';
143
+ if (nowMs - ts > STATE_STALE_MS) return '';
144
+
145
+ const progressOk = isValidProgress(state.progress);
146
+ const prefix = progressOk
147
+ ? `[${state.progress.completed_phases}/${state.progress.total_phases}] `
148
+ : '';
149
+
150
+ // Scene 1: orchestrator in flight — a "next" recommendation would be
151
+ // misleading mid-run, so this scene always wins.
152
+ if (typeof state.active_phase === 'string' && state.active_phase.trim()) {
153
+ let seg = `Phase ${state.active_phase.trim()}`;
154
+ if (typeof state.status === 'string' && STATE_LIFECYCLE_WORDS.has(state.status.trim())) {
155
+ seg += ` ${state.status.trim()}`;
156
+ }
157
+ return prefix + seg;
158
+ }
159
+
160
+ // Scene 2: idle with a recommended next command.
161
+ if (typeof state.next_action === 'string' && state.next_action.trim()) {
162
+ return `${prefix}next ${truncateAction(state.next_action.trim(), 40)}`;
163
+ }
164
+
165
+ // Scene 3: everything done.
166
+ if (progressOk) {
167
+ const p = state.progress;
168
+ if (p.percent === 100 || p.completed_phases === p.total_phases) {
169
+ return '✓ phases done';
170
+ }
171
+ }
172
+
173
+ return '';
174
+ } catch {
175
+ return '';
176
+ }
177
+ }
178
+
44
179
  // --- core renderer ----------------------------------------------------------
45
180
 
46
181
  function runStatusline() {
@@ -120,13 +255,18 @@ function runStatusline() {
120
255
  }
121
256
 
122
257
  // Compose CCG-style status line:
123
- // <model> | <ctx> | <branch> | <sid4>
258
+ // <model> | <ctx> | <branch> | <state> | <sid4>
124
259
  const branch = readGitBranch(dir);
260
+ // P1-4: project lifecycle segment from .context/STATE.json (task-store
261
+ // projection). '' when no STATE.json — output then stays byte-identical
262
+ // to the pre-P1-4 statusline (backward-compat contract, test-locked).
263
+ const stateSeg = formatStateSegment(readProjectState(dir), Date.now());
125
264
  const sid4 = session && session.length >= 4 ? session.slice(-4) : '';
126
265
 
127
266
  const segments = [`\x1b[2m${model}\x1b[0m`];
128
267
  if (ctx) segments.push(ctx.trim());
129
268
  if (branch) segments.push(`\x1b[36m${branch}\x1b[0m`);
269
+ if (stateSeg) segments.push(`\x1b[35m${stateSeg}\x1b[0m`);
130
270
  if (sid4) segments.push(`\x1b[2m#${sid4}\x1b[0m`);
131
271
 
132
272
  process.stdout.write(segments.join(' │ '));
@@ -137,6 +277,6 @@ function runStatusline() {
137
277
  }
138
278
 
139
279
  // Export helpers for unit tests. Harmless when run as a script.
140
- module.exports = { readGitBranch };
280
+ module.exports = { readGitBranch, findProjectStateFile, readProjectState, formatStateSegment };
141
281
 
142
282
  if (require.main === module) runStatusline();