ccg-workflow 2.1.16 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +119 -250
  2. package/README.zh-CN.md +125 -258
  3. package/dist/cli.mjs +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/shared/{ccg-workflow.CewMlBCj.mjs → ccg-workflow.2vO_S-e7.mjs} +160 -34
  6. package/package.json +6 -30
  7. package/templates/commands/go.md +205 -0
  8. package/templates/commands-legacy/team.md +475 -0
  9. package/templates/engine/model-router.md +117 -0
  10. package/templates/engine/phase-guide.md +95 -0
  11. package/templates/engine/strategies/debug-investigate.md +162 -0
  12. package/templates/engine/strategies/deep-research.md +141 -0
  13. package/templates/engine/strategies/direct-fix.md +108 -0
  14. package/templates/engine/strategies/full-collaborate.md +305 -0
  15. package/templates/engine/strategies/git-action.md +43 -0
  16. package/templates/engine/strategies/guided-develop.md +214 -0
  17. package/templates/engine/strategies/optimize-measure.md +103 -0
  18. package/templates/engine/strategies/quick-implement.md +96 -0
  19. package/templates/engine/strategies/refactor-safely.md +163 -0
  20. package/templates/engine/strategies/review-audit.md +116 -0
  21. package/templates/hooks/session-start.js +100 -0
  22. package/templates/hooks/skill-router.js +144 -0
  23. package/templates/hooks/subagent-context.js +118 -0
  24. package/templates/hooks/task-utils.js +167 -0
  25. package/templates/hooks/workflow-state.js +39 -0
  26. package/templates/spec/backend/index.md +31 -0
  27. package/templates/spec/frontend/index.md +31 -0
  28. package/templates/spec/guides/index.md +30 -0
  29. /package/templates/{commands → commands-legacy}/analyze.md +0 -0
  30. /package/templates/{commands → commands-legacy}/backend.md +0 -0
  31. /package/templates/{commands → commands-legacy}/codex-exec.md +0 -0
  32. /package/templates/{commands → commands-legacy}/debug.md +0 -0
  33. /package/templates/{commands → commands-legacy}/enhance.md +0 -0
  34. /package/templates/{commands → commands-legacy}/execute.md +0 -0
  35. /package/templates/{commands → commands-legacy}/feat.md +0 -0
  36. /package/templates/{commands → commands-legacy}/frontend.md +0 -0
  37. /package/templates/{commands → commands-legacy}/optimize.md +0 -0
  38. /package/templates/{commands → commands-legacy}/plan.md +0 -0
  39. /package/templates/{commands → commands-legacy}/review.md +0 -0
  40. /package/templates/{commands → commands-legacy}/team-exec.md +0 -0
  41. /package/templates/{commands → commands-legacy}/team-plan.md +0 -0
  42. /package/templates/{commands → commands-legacy}/team-research.md +0 -0
  43. /package/templates/{commands → commands-legacy}/team-review.md +0 -0
  44. /package/templates/{commands → commands-legacy}/test.md +0 -0
  45. /package/templates/{commands → commands-legacy}/workflow.md +0 -0
@@ -0,0 +1,116 @@
1
+ # Strategy: Review Audit — 代码审查
2
+
3
+ > 适用于代码审查需求,双模型交叉验证,结果分级输出。
4
+
5
+ ## 适用条件
6
+ - 用户请求代码审查
7
+ - 任何复杂度级别
8
+ - 自动检测 git diff 作为审查范围
9
+
10
+ ## 前置加载
11
+
12
+ ```
13
+ Read("~/.claude/.ccg/engine/model-router.md")
14
+ ```
15
+
16
+ ---
17
+
18
+ ## 工作流状态机
19
+
20
+ [phase-state:1-scope]
21
+ 当前阶段:确定审查范围
22
+ 📍 Next: 范围确定后启动双模型审查
23
+ [/phase-state:1-scope]
24
+
25
+ [phase-state:2-review]
26
+ 当前阶段:双模型审查
27
+ Gate: 审查范围已确定 ✓
28
+ 📍 Next: 双模型审查返回后综合报告
29
+ [/phase-state:2-review]
30
+
31
+ [phase-state:3-report]
32
+ 当前阶段:综合报告
33
+ Gate: 双模型审查已返回 ✓
34
+ 📍 Next: 报告输出后等待用户决定
35
+ [/phase-state:3-report]
36
+
37
+ ---
38
+
39
+ ## 阶段详情
40
+
41
+ ### Phase 1: 确定审查范围 [required]
42
+
43
+ 1. 如果用户指定了文件/范围 → 使用指定范围
44
+ 2. 如果未指定 → 自动获取:
45
+ - `git diff HEAD` — 未提交的变更
46
+ - 如果无 diff → `git diff HEAD~1` — 最近一次提交
47
+ - 如果仍无 diff → 询问用户要审查什么
48
+ 3. 读取变更涉及的完整文件(不只是 diff,需要上下文)
49
+
50
+ 输出审查范围:
51
+ ```
52
+ 📋 审查范围
53
+ 变更: [N] 文件,[+M/-K] 行
54
+ 文件: [文件列表]
55
+ ```
56
+
57
+ ### Phase 2: 双模型审查 [required]
58
+
59
+ **Gate check**: 审查范围已确定
60
+
61
+ **并行调用**(`run_in_background: true`):
62
+ - **backend 模型**:reviewer 角色
63
+ ```
64
+ <TASK>
65
+ 需求:审查以下代码变更
66
+ 上下文:[git diff + 完整文件上下文]
67
+ </TASK>
68
+ OUTPUT: 审查发现(按严重度分级:Critical/Warning/Info,每条含:位置、问题、建议)
69
+ ```
70
+ - **frontend 模型**:reviewer 角色(相同格式)
71
+
72
+ 等待双模型返回。
73
+
74
+ ### Phase 3: 综合报告 + 质量关卡
75
+
76
+ **Gate check**: 双模型审查已返回
77
+
78
+ #### 3a. 质量关卡
79
+
80
+ **⛔ 必须逐个调用 Skill,不可跳过:**
81
+ - 调用 Skill `verify-security` — 等待报告
82
+ - 调用 Skill `verify-quality` — 等待报告
83
+
84
+ #### 3b. 综合报告
85
+
86
+ 合并双模型发现 + 质量关卡结果,去重,按严重度分级:
87
+
88
+ ```
89
+ 📋 代码审查报告
90
+
91
+ ## Critical(必须修复)
92
+ 1. [file:line] — [问题描述]
93
+ 建议: [具体修复建议]
94
+ 来源: [backend/frontend/质量关卡]
95
+
96
+ ## Warning(建议修复)
97
+ 1. [file:line] — [问题描述]
98
+ 建议: [具体修复建议]
99
+
100
+ ## Info(供参考)
101
+ 1. [file:line] — [观察/建议]
102
+
103
+ ---
104
+ 总计: [N] Critical, [M] Warning, [K] Info
105
+ ```
106
+
107
+ 如果有 Critical 发现,询问用户是否立即修复(可切换到 `direct-fix` 策略)。
108
+
109
+ ---
110
+
111
+ ## 铁律
112
+
113
+ - **审查结果必须分级** — 不可笼统说"代码看起来没问题"
114
+ - **双模型必须独立审查** — 交叉验证的价值在于独立性
115
+ - **Critical 必须明确标出** — 不可淡化严重问题
116
+ - **如无发现,明确说明** — "经双模型审查,未发现问题" 优于沉默
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ // CCG Session Start Hook — SessionStart
3
+ // Injects full project context when session starts, clears, or compacts.
4
+
5
+ 'use strict';
6
+
7
+ try {
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const {
11
+ findProjectRoot, getActiveTask, readFileSafe,
12
+ detectTechStack, getGitInfo, outputHook
13
+ } = require('./task-utils.js');
14
+
15
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
16
+ const root = findProjectRoot(cwd);
17
+
18
+ if (!root) process.exit(0);
19
+
20
+ const sections = [];
21
+
22
+ // Project info
23
+ const techStack = detectTechStack(root);
24
+ const git = getGitInfo(root);
25
+ sections.push(`<project>
26
+ Tech: ${techStack}
27
+ Branch: ${git.branch}
28
+ Dirty files: ${git.dirtyCount}
29
+ Root: ${root}
30
+ </project>`);
31
+
32
+ // Model routing config
33
+ const configPath = path.join(root, '.ccg', 'config.toml');
34
+ if (fs.existsSync(configPath)) {
35
+ const configRaw = readFileSafe(configPath);
36
+ if (configRaw) {
37
+ const frontendMatch = configRaw.match(/primary\s*=\s*"(\w+)"/);
38
+ const models = frontendMatch ? `Configured (see .ccg/config.toml)` : 'Default (frontend=gemini, backend=codex)';
39
+ sections.push(`<models>${models}</models>`);
40
+ }
41
+ } else {
42
+ sections.push('<models>Default (frontend=gemini, backend=codex)</models>');
43
+ }
44
+
45
+ // Active task
46
+ const task = getActiveTask(root);
47
+ if (task) {
48
+ const taskLines = [
49
+ `<active-task>`,
50
+ `Task: ${task.title || task.id} (${task.status})`,
51
+ `Strategy: ${task.strategy}`,
52
+ `Phase: ${task.currentPhase}`,
53
+ ];
54
+
55
+ if (task.gate) taskLines.push(`⛔ GATE: ${task.gate}`);
56
+ taskLines.push(`Next: ${task.nextAction || 'Continue'}`);
57
+ taskLines.push(`Dir: ${task.dir}`);
58
+
59
+ // Check for plan/prd
60
+ const planPath = path.join(task.dir, 'plan.md');
61
+ const prdPath = path.join(task.dir, 'requirements.md');
62
+ if (fs.existsSync(planPath)) taskLines.push(`Plan: ${planPath}`);
63
+ if (fs.existsSync(prdPath)) taskLines.push(`PRD: ${prdPath}`);
64
+
65
+ taskLines.push('</active-task>');
66
+ sections.push(taskLines.join('\n'));
67
+ } else {
68
+ sections.push('<active-task>No active task. Use /ccg:go to start.</active-task>');
69
+ }
70
+
71
+ // Spec availability
72
+ const specDir = path.join(root, '.ccg', 'spec');
73
+ if (fs.existsSync(specDir)) {
74
+ try {
75
+ const specPaths = [];
76
+ const walk = (dir, prefix) => {
77
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
78
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
79
+ if (entry.isDirectory()) walk(path.join(dir, entry.name), rel);
80
+ else if (entry.name.endsWith('.md')) specPaths.push(rel);
81
+ }
82
+ };
83
+ walk(specDir, '');
84
+ if (specPaths.length > 0) {
85
+ sections.push(`<specs>\nAvailable specs in .ccg/spec/:\n${specPaths.map(p => ` - ${p}`).join('\n')}\n</specs>`);
86
+ }
87
+ } catch { /* silent */ }
88
+ }
89
+
90
+ // Available commands hint
91
+ sections.push(`<commands>
92
+ Key commands: /ccg:go (smart entry), /ccg:commit, /ccg:review
93
+ All /ccg:* commands available. Use /ccg:go for intelligent routing.
94
+ </commands>`);
95
+
96
+ const context = `<ccg-session>\n${sections.join('\n\n')}\n</ccg-session>`;
97
+ outputHook('SessionStart', context);
98
+ } catch {
99
+ process.exit(0);
100
+ }
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // CCG Skill Router Hook — UserPromptSubmit
3
+ // Detects domain keywords in user message and injects relevant skill content.
4
+ // Fires alongside workflow-state.js on every user prompt.
5
+
6
+ 'use strict';
7
+
8
+ try {
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { findProjectRoot, outputHook } = require('./task-utils.js');
12
+
13
+ // Read hook input (contains user's message)
14
+ let inputData = '';
15
+ if (!process.stdin.isTTY) {
16
+ inputData = fs.readFileSync(0, 'utf-8');
17
+ }
18
+
19
+ // Extract user message from hook input
20
+ let userMessage = '';
21
+ try {
22
+ const parsed = JSON.parse(inputData);
23
+ userMessage = parsed.message || parsed.content || parsed.prompt || '';
24
+ if (typeof userMessage === 'object') userMessage = JSON.stringify(userMessage);
25
+ } catch {
26
+ userMessage = inputData;
27
+ }
28
+
29
+ if (!userMessage || userMessage.length < 5) process.exit(0);
30
+
31
+ const msgLower = userMessage.toLowerCase();
32
+
33
+ // Keyword → skill file routing table
34
+ const ROUTES = [
35
+ { keywords: ['渗透', '红队', 'pentest', 'exploit', 'c2', '横向', '提权', 'bypass', 'red team'], skill: 'domains/security/red-team.md', name: '红队渗透' },
36
+ { keywords: ['蓝队', '告警', 'ioc', '应急', '取证', 'siem', 'edr', 'blue team', 'incident'], skill: 'domains/security/blue-team.md', name: '蓝队防御' },
37
+ { keywords: ['sqli', 'xss', 'ssrf', 'rce', 'injection', 'owasp', 'web渗透', 'api安全'], skill: 'domains/security/pentest.md', name: 'Web渗透' },
38
+ { keywords: ['代码审计', '污点分析', 'sink', 'source', '危险函数', 'code audit'], skill: 'domains/security/code-audit.md', name: '代码审计' },
39
+ { keywords: ['逆向', 'pwn', 'fuzzing', '栈溢出', '堆溢出', 'rop', 'binary', 'reversing'], skill: 'domains/security/vuln-research.md', name: '漏洞研究' },
40
+ { keywords: ['osint', '威胁情报', '威胁建模', 'att&ck', 'threat', 'threat hunting'], skill: 'domains/security/threat-intel.md', name: '威胁情报' },
41
+ { keywords: ['api设计', 'rest', 'graphql', 'grpc', 'endpoint', 'versioning', 'api design'], skill: 'domains/architecture/api-design.md', name: 'API设计' },
42
+ { keywords: ['缓存', 'redis', 'memcached', 'cache', 'cdn', 'invalidation'], skill: 'domains/architecture/caching.md', name: '缓存架构' },
43
+ { keywords: ['kubernetes', 'docker', 'k8s', '微服务', 'service mesh', 'cloud native'], skill: 'domains/architecture/cloud-native.md', name: '云原生' },
44
+ { keywords: ['kafka', 'rabbitmq', '消息队列', 'event driven', 'pub/sub', 'message queue'], skill: 'domains/architecture/message-queue.md', name: '消息队列' },
45
+ { keywords: ['rag', 'retrieval', '向量', 'embedding', 'chunking', 'vector'], skill: 'domains/ai/rag-system.md', name: 'RAG系统' },
46
+ { keywords: ['ai agent', 'tool use', 'function calling', 'agent框架', 'orchestration'], skill: 'domains/ai/agent-dev.md', name: 'Agent开发' },
47
+ { keywords: ['prompt injection', 'jailbreak', 'guardrail', 'llm安全'], skill: 'domains/ai/llm-security.md', name: 'LLM安全' },
48
+ ];
49
+
50
+ // Find matching skills
51
+ const matched = ROUTES.filter(route =>
52
+ route.keywords.some(kw => msgLower.includes(kw))
53
+ );
54
+
55
+ // ── Model action triggers ──
56
+ // Detect when user wants to use a specific model for a task
57
+ const MODEL_ACTIONS = [
58
+ { keywords: ['codex审查', 'codex 审查', 'codex review', '用codex看', '让codex检查', 'codex检查'], model: 'codex', role: 'reviewer', action: '审查当前代码变更(git diff)' },
59
+ { keywords: ['codex分析', 'codex 分析', 'codex analyze', '用codex分析'], model: 'codex', role: 'analyzer', action: '分析当前项目/代码' },
60
+ { keywords: ['codex调试', 'codex 调试', 'codex debug', '用codex调试'], model: 'codex', role: 'debugger', action: '诊断问题' },
61
+ { keywords: ['codex测试', 'codex 测试', 'codex test', '用codex写测试'], model: 'codex', role: 'tester', action: '生成测试用例' },
62
+ { keywords: ['gemini审查', 'gemini 审查', 'gemini review', '用gemini看', '让gemini检查'], model: 'gemini', role: 'reviewer', action: '审查当前代码变更(git diff)' },
63
+ { keywords: ['gemini分析', 'gemini 分析', 'gemini analyze', '用gemini分析'], model: 'gemini', role: 'analyzer', action: '分析当前项目/代码' },
64
+ { keywords: ['gemini前端', 'gemini 前端', '用gemini做前端'], model: 'gemini', role: 'frontend', action: '前端开发分析' },
65
+ { keywords: ['双模型审查', '双模型 审查', '两个模型审查', 'dual review'], model: 'both', role: 'reviewer', action: '双模型交叉审查代码变更' },
66
+ { keywords: ['双模型分析', '双模型 分析', '两个模型分析', 'dual analyze'], model: 'both', role: 'analyzer', action: '双模型并行分析' },
67
+ ];
68
+
69
+ const modelAction = MODEL_ACTIONS.find(a => a.keywords.some(kw => msgLower.includes(kw)));
70
+ if (modelAction) {
71
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
72
+ const wrapperPath = path.join(homeDir, '.claude', 'bin', 'codeagent-wrapper');
73
+
74
+ let actionInstructions;
75
+ if (modelAction.model === 'both') {
76
+ actionInstructions = `<ccg-model-action>
77
+ 用户请求双模型${modelAction.role === 'reviewer' ? '审查' : '分析'}。请立即执行:
78
+
79
+ 1. 获取工作目录: WORKDIR=$(pwd)
80
+ 2. 并行调用两个模型 (run_in_background: true):
81
+
82
+ Backend (codex):
83
+ ${wrapperPath} --progress --backend codex - "$WORKDIR" <<'EOF'
84
+ ROLE_FILE: ${path.join(homeDir, '.claude', '.ccg', 'prompts', 'codex', modelAction.role + '.md')}
85
+ <TASK>${modelAction.action}</TASK>
86
+ EOF
87
+
88
+ Frontend (gemini):
89
+ ${wrapperPath} --progress --backend gemini - "$WORKDIR" <<'EOF'
90
+ ROLE_FILE: ${path.join(homeDir, '.claude', '.ccg', 'prompts', 'gemini', modelAction.role + '.md')}
91
+ <TASK>${modelAction.action}</TASK>
92
+ EOF
93
+
94
+ 3. 等待结果,综合输出
95
+ </ccg-model-action>`;
96
+ } else {
97
+ actionInstructions = `<ccg-model-action>
98
+ 用户请求使用 ${modelAction.model} 执行${modelAction.action}。请立即执行:
99
+
100
+ 1. 获取工作目录: WORKDIR=$(pwd)
101
+ 2. 调用模型:
102
+
103
+ ${wrapperPath} --progress --backend ${modelAction.model} - "$WORKDIR" <<'EOF'
104
+ ROLE_FILE: ${path.join(homeDir, '.claude', '.ccg', 'prompts', modelAction.model, modelAction.role + '.md')}
105
+ <TASK>${modelAction.action}</TASK>
106
+ EOF
107
+
108
+ 3. 等待结果并输出
109
+ </ccg-model-action>`;
110
+ }
111
+
112
+ outputHook('UserPromptSubmit', actionInstructions);
113
+ process.exit(0);
114
+ }
115
+
116
+ // ── Domain knowledge injection ──
117
+ if (matched.length === 0) process.exit(0);
118
+
119
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
120
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
121
+ const skillsBase = path.join(homeDir, '.claude', 'skills', 'ccg');
122
+
123
+ if (!fs.existsSync(skillsBase)) process.exit(0);
124
+
125
+ const injections = [];
126
+ for (const match of matched.slice(0, 2)) {
127
+ const skillPath = path.join(skillsBase, match.skill);
128
+ if (!fs.existsSync(skillPath)) continue;
129
+
130
+ try {
131
+ const content = fs.readFileSync(skillPath, 'utf-8');
132
+ const lines = content.split('\n');
133
+ const excerpt = lines.slice(0, 120).join('\n');
134
+ injections.push(`## ${match.name} (auto-injected)\n${excerpt}${lines.length > 120 ? '\n...(truncated, full: ' + match.skill + ')' : ''}`);
135
+ } catch { /* silent */ }
136
+ }
137
+
138
+ if (injections.length === 0) process.exit(0);
139
+
140
+ const context = `<ccg-domain-knowledge>\n${injections.join('\n\n---\n\n')}\n</ccg-domain-knowledge>`;
141
+ outputHook('UserPromptSubmit', context);
142
+ } catch {
143
+ process.exit(0);
144
+ }
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // CCG SubAgent Context Hook — PreToolUse (Bash|Agent matcher)
3
+ // Injects spec + task context when:
4
+ // 1. codeagent-wrapper is about to be called (Bash)
5
+ // 2. Agent Team member is about to be spawned (Agent)
6
+
7
+ 'use strict';
8
+
9
+ try {
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const {
13
+ findProjectRoot, getActiveTask, readFileSafe,
14
+ readContextJsonl, outputHook
15
+ } = require('./task-utils.js');
16
+
17
+ let inputData = '';
18
+ if (!process.stdin.isTTY) {
19
+ inputData = fs.readFileSync(0, 'utf-8');
20
+ }
21
+
22
+ let toolInput = {};
23
+ try {
24
+ const parsed = JSON.parse(inputData);
25
+ toolInput = parsed.tool_input || parsed.input || parsed;
26
+ } catch { /* not JSON */ }
27
+
28
+ // Determine trigger type
29
+ const command = toolInput.command || '';
30
+ const teamName = toolInput.team_name || '';
31
+ const agentPrompt = toolInput.prompt || '';
32
+
33
+ const isCodeagentCall = command.includes('codeagent-wrapper');
34
+ const isTeamSpawn = !!teamName;
35
+
36
+ // Only activate for codeagent-wrapper calls or Agent Team spawns
37
+ if (!isCodeagentCall && !isTeamSpawn) {
38
+ process.exit(0);
39
+ }
40
+
41
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
42
+ const root = findProjectRoot(cwd);
43
+ if (!root) process.exit(0);
44
+
45
+ const task = getActiveTask(root);
46
+ if (!task) process.exit(0);
47
+
48
+ const contextParts = [];
49
+
50
+ // Inject active task info for team members
51
+ if (isTeamSpawn) {
52
+ contextParts.push(`<ccg-active-task>
53
+ Active task: ${task.dir}
54
+ Task: ${task.title || task.id} (${task.status})
55
+ Strategy: ${task.strategy}
56
+ Phase: ${task.currentPhase}
57
+ </ccg-active-task>`);
58
+ }
59
+
60
+ // Read context.jsonl entries (specs + research refs)
61
+ const entries = readContextJsonl(task.dir);
62
+ if (entries.length > 0) {
63
+ const specContents = [];
64
+ for (const entry of entries) {
65
+ const filePath = path.isAbsolute(entry.file)
66
+ ? entry.file
67
+ : path.join(root, entry.file);
68
+ const content = readFileSafe(filePath);
69
+ if (content) {
70
+ specContents.push(`--- ${entry.file} (${entry.reason || 'context'}) ---\n${content}`);
71
+ }
72
+ }
73
+ if (specContents.length > 0) {
74
+ contextParts.push(`<ccg-specs>\n${specContents.join('\n\n')}\n</ccg-specs>`);
75
+ }
76
+ }
77
+
78
+ // Read PRD and plan
79
+ const prd = readFileSafe(path.join(task.dir, 'requirements.md'));
80
+ const plan = readFileSafe(path.join(task.dir, 'plan.md'));
81
+
82
+ if (prd || plan) {
83
+ const taskContext = ['<ccg-task-context>'];
84
+ if (prd) {
85
+ const prdSummary = prd.length > 2000 ? prd.substring(0, 2000) + '\n...(truncated)' : prd;
86
+ taskContext.push(`## Requirements\n${prdSummary}`);
87
+ }
88
+ if (plan) {
89
+ const planSummary = plan.length > 3000 ? plan.substring(0, 3000) + '\n...(truncated)' : plan;
90
+ taskContext.push(`## Plan\n${planSummary}`);
91
+ }
92
+ taskContext.push('</ccg-task-context>');
93
+ contextParts.push(taskContext.join('\n'));
94
+ }
95
+
96
+ // Read research files
97
+ const researchDir = path.join(task.dir, 'research');
98
+ if (fs.existsSync(researchDir)) {
99
+ try {
100
+ const researchFiles = fs.readdirSync(researchDir).filter(f => f.endsWith('.md'));
101
+ if (researchFiles.length > 0) {
102
+ const researchContents = researchFiles.map(f => {
103
+ const content = readFileSafe(path.join(researchDir, f));
104
+ return content ? `--- research/${f} ---\n${content.substring(0, 1500)}` : null;
105
+ }).filter(Boolean);
106
+ if (researchContents.length > 0) {
107
+ contextParts.push(`<ccg-research>\n${researchContents.join('\n\n')}\n</ccg-research>`);
108
+ }
109
+ }
110
+ } catch { /* silent */ }
111
+ }
112
+
113
+ if (contextParts.length === 0) process.exit(0);
114
+
115
+ outputHook('PreToolUse', contextParts.join('\n\n'));
116
+ } catch {
117
+ process.exit(0);
118
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // CCG Hook Shared Utilities
3
+ // Pure Node.js, zero external dependencies
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ function findProjectRoot(startDir) {
9
+ let dir = startDir || process.cwd();
10
+ for (let i = 0; i < 20; i++) {
11
+ if (fs.existsSync(path.join(dir, '.ccg', 'tasks'))) return dir;
12
+ if (fs.existsSync(path.join(dir, '.ccg'))) return dir;
13
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
14
+ const parent = path.dirname(dir);
15
+ if (parent === dir) break;
16
+ dir = parent;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function getActiveTask(projectRoot) {
22
+ const tasksDir = path.join(projectRoot, '.ccg', 'tasks');
23
+ if (!fs.existsSync(tasksDir)) return null;
24
+
25
+ try {
26
+ const dirs = fs.readdirSync(tasksDir)
27
+ .filter(d => {
28
+ if (d === 'archive') return false;
29
+ try {
30
+ const full = path.join(tasksDir, d);
31
+ return fs.statSync(full).isDirectory() && fs.existsSync(path.join(full, 'task.json'));
32
+ } catch { return false; }
33
+ })
34
+ .sort()
35
+ .reverse();
36
+
37
+ for (const dir of dirs) {
38
+ try {
39
+ const taskPath = path.join(tasksDir, dir, 'task.json');
40
+ if (!fs.existsSync(taskPath)) continue; // stale pointer detection
41
+ const raw = fs.readFileSync(taskPath, 'utf-8');
42
+ const task = JSON.parse(raw);
43
+ if (task.status !== 'completed' && task.status !== 'archived') {
44
+ return { dir: path.join(tasksDir, dir), ...task, _stale: false };
45
+ }
46
+ } catch { /* skip malformed */ }
47
+ }
48
+ } catch { /* silent */ }
49
+ return null;
50
+ }
51
+
52
+ function readFileSafe(filePath) {
53
+ try { return fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
54
+ }
55
+
56
+ function readJsonSafe(filePath) {
57
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
58
+ }
59
+
60
+ function readContextJsonl(taskDir) {
61
+ const jsonlPath = path.join(taskDir, 'context.jsonl');
62
+ if (!fs.existsSync(jsonlPath)) return [];
63
+ try {
64
+ return fs.readFileSync(jsonlPath, 'utf-8')
65
+ .split('\n')
66
+ .filter(line => line.trim())
67
+ .map(line => { try { return JSON.parse(line); } catch { return null; } })
68
+ .filter(entry => entry && entry.file);
69
+ } catch { return []; }
70
+ }
71
+
72
+ function detectTechStack(projectRoot) {
73
+ const indicators = [
74
+ { file: 'package.json', stack: 'Node.js' },
75
+ { file: 'go.mod', stack: 'Go' },
76
+ { file: 'pyproject.toml', stack: 'Python' },
77
+ { file: 'Cargo.toml', stack: 'Rust' },
78
+ { file: 'pom.xml', stack: 'Java' },
79
+ { file: 'build.gradle', stack: 'Java/Kotlin' },
80
+ ];
81
+ const found = [];
82
+ for (const { file, stack } of indicators) {
83
+ if (fs.existsSync(path.join(projectRoot, file))) found.push(stack);
84
+ }
85
+ return found.length > 0 ? found.join(' + ') : 'Unknown';
86
+ }
87
+
88
+ function getGitInfo(projectRoot) {
89
+ try {
90
+ const { execSync } = require('child_process');
91
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: projectRoot, stdio: 'pipe' }).toString().trim();
92
+ const status = execSync('git status --porcelain', { cwd: projectRoot, stdio: 'pipe' }).toString().trim();
93
+ const dirtyCount = status ? status.split('\n').length : 0;
94
+ return { branch, dirtyCount };
95
+ } catch { return { branch: 'unknown', dirtyCount: 0 }; }
96
+ }
97
+
98
+ function outputHook(eventName, additionalContext) {
99
+ console.log(JSON.stringify({
100
+ hookSpecificOutput: {
101
+ hookEventName: eventName,
102
+ additionalContext
103
+ }
104
+ }));
105
+ }
106
+
107
+ function archiveTask(taskDir, projectRoot) {
108
+ try {
109
+ const now = new Date();
110
+ const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
111
+ const archiveDir = path.join(projectRoot, '.ccg', 'tasks', 'archive', month);
112
+ if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
113
+ const name = path.basename(taskDir);
114
+ const dest = path.join(archiveDir, name);
115
+ fs.renameSync(taskDir, dest);
116
+ return dest;
117
+ } catch { return null; }
118
+ }
119
+
120
+ function autoCommitTask(projectRoot, message) {
121
+ try {
122
+ const { execSync } = require('child_process');
123
+ execSync('git add .ccg/tasks/', { cwd: projectRoot, stdio: 'pipe' });
124
+ const diff = execSync('git diff --cached --quiet', { cwd: projectRoot, stdio: 'pipe' }).toString();
125
+ return false; // nothing to commit
126
+ } catch {
127
+ try {
128
+ const { execSync } = require('child_process');
129
+ execSync(`git commit -m "${message || 'chore: archive ccg task'}"`, { cwd: projectRoot, stdio: 'pipe' });
130
+ return true;
131
+ } catch { return false; }
132
+ }
133
+ }
134
+
135
+ function seedContextJsonl(taskDir, projectRoot) {
136
+ const jsonlPath = path.join(taskDir, 'context.jsonl');
137
+ if (fs.existsSync(jsonlPath)) return;
138
+ const specDir = path.join(projectRoot, '.ccg', 'spec');
139
+ const lines = ['{"_example": "Fill with {\\\"file\\\": \\\"path\\\", \\\"reason\\\": \\\"why\\\"}. One entry per line. Seed rows (with _example key) are skipped."}'];
140
+ if (fs.existsSync(specDir)) {
141
+ try {
142
+ const walk = (dir, prefix) => {
143
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
144
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
145
+ if (e.isDirectory()) walk(path.join(dir, e.name), rel);
146
+ else if (e.name.endsWith('.md')) lines.push(JSON.stringify({ file: `.ccg/spec/${rel}`, reason: 'project spec' }));
147
+ }
148
+ };
149
+ walk(specDir, '');
150
+ } catch { /* silent */ }
151
+ }
152
+ try { fs.writeFileSync(jsonlPath, lines.join('\n') + '\n', 'utf-8'); } catch { /* silent */ }
153
+ }
154
+
155
+ module.exports = {
156
+ findProjectRoot,
157
+ getActiveTask,
158
+ readFileSafe,
159
+ readJsonSafe,
160
+ readContextJsonl,
161
+ detectTechStack,
162
+ getGitInfo,
163
+ outputHook,
164
+ archiveTask,
165
+ autoCommitTask,
166
+ seedContextJsonl
167
+ };
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ // CCG Workflow State Hook — UserPromptSubmit
3
+ // Injects per-turn breadcrumb based on active task state.
4
+ // Runs on EVERY user message. Must be fast (<1s) and never crash.
5
+
6
+ 'use strict';
7
+
8
+ try {
9
+ const { findProjectRoot, getActiveTask, outputHook } = require('./task-utils.js');
10
+
11
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
12
+ const root = findProjectRoot(cwd);
13
+
14
+ if (!root) process.exit(0);
15
+
16
+ const task = getActiveTask(root);
17
+
18
+ if (!task) {
19
+ process.exit(0);
20
+ }
21
+
22
+ const lines = [
23
+ '<ccg-state>',
24
+ `Task: ${task.title || task.id} (${task.status})`,
25
+ `Strategy: ${task.strategy}`,
26
+ `Phase: ${task.currentPhase}`,
27
+ ];
28
+
29
+ if (task.gate) {
30
+ lines.push(`⛔ GATE: ${task.gate}`);
31
+ }
32
+
33
+ lines.push(`Next: ${task.nextAction || 'Continue current phase'}`);
34
+ lines.push('</ccg-state>');
35
+
36
+ outputHook('UserPromptSubmit', lines.join('\n'));
37
+ } catch {
38
+ process.exit(0);
39
+ }