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.
- package/README.md +134 -277
- package/README.zh-CN.md +134 -272
- package/dist/chunks/version-build.mjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +709 -16
- package/dist/index.d.ts +709 -16
- package/dist/index.mjs +1061 -30
- package/dist/shared/{ccgx-workflow.j1spUsik.mjs → ccgx-workflow.CdHnJLak.mjs} +106 -22
- package/package.json +1 -1
- package/templates/commands/agents/code-fixer.md +6 -6
- package/templates/commands/agents/phase-runner.md +46 -14
- package/templates/commands/agents/plan-checker.md +10 -0
- package/templates/commands/analyze.md +66 -25
- package/templates/commands/autonomous.md +428 -225
- package/templates/commands/cancel.md +9 -0
- package/templates/commands/codex-exec.md +12 -11
- package/templates/commands/context.md +14 -0
- package/templates/commands/debate.md +10 -6
- package/templates/commands/execute.md +76 -28
- package/templates/commands/optimize.md +53 -25
- package/templates/commands/plan.md +78 -28
- package/templates/commands/review.md +26 -19
- package/templates/commands/spec-impl.md +68 -127
- package/templates/commands/spec-plan.md +61 -82
- package/templates/commands/spec-research.md +35 -92
- package/templates/commands/spec-review.md +34 -119
- package/templates/commands/status.md +1 -0
- package/templates/commands/team-exec.md +45 -13
- package/templates/commands/team.md +64 -167
- package/templates/commands/test.md +56 -34
- package/templates/commands/verify-work.md +36 -13
- package/templates/commands/verify.md +35 -0
- package/templates/commands/workflow.md +22 -37
- package/templates/hooks/ccg-loop-detector.cjs +39 -8
- package/templates/hooks/ccg-statusline.js +142 -2
- package/templates/hooks/ccg-stop-gate.cjs +248 -19
- package/templates/hooks/ccg-subagent-context.cjs +505 -0
- package/templates/scripts/ccg-state-lock.cjs +510 -0
- package/templates/scripts/ccg-team-schedule.cjs +328 -0
- package/templates/scripts/ccgx-call-plugin.mjs +494 -141
- package/templates/scripts/invoke-model.mjs +28 -1
- package/templates/scripts/task-store.cjs +614 -0
- package/templates/skills/tools/verify-change/SKILL.md +7 -0
- package/templates/skills/tools/verify-module/SKILL.md +7 -0
- package/templates/skills/tools/verify-quality/SKILL.md +7 -0
- 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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
51
|
+
**通道 A — Bash 直调 plugin 快路径(默认)**:
|
|
52
52
|
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
上下文:<前序阶段关键文件路径 + 结论,不塞大段源码>
|
|
65
|
+
<期望输出格式 / JSON schema>` })
|
|
70
66
|
|
|
71
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: <
|
|
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
|
-
//
|
|
178
|
-
//
|
|
179
|
-
const
|
|
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();
|