ccg-workflow 2.1.15 → 3.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 +120 -270
- package/README.zh-CN.md +119 -269
- package/dist/cli.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/shared/{ccg-workflow.C2MaYeHU.mjs → ccg-workflow.81OoN8XX.mjs} +629 -297
- package/package.json +6 -30
- package/templates/commands/go.md +199 -0
- package/templates/commands-legacy/team.md +475 -0
- package/templates/engine/model-router.md +117 -0
- package/templates/engine/phase-guide.md +95 -0
- package/templates/engine/strategies/debug-investigate.md +156 -0
- package/templates/engine/strategies/deep-research.md +141 -0
- package/templates/engine/strategies/direct-fix.md +108 -0
- package/templates/engine/strategies/full-collaborate.md +291 -0
- package/templates/engine/strategies/git-action.md +43 -0
- package/templates/engine/strategies/guided-develop.md +208 -0
- package/templates/engine/strategies/optimize-measure.md +103 -0
- package/templates/engine/strategies/quick-implement.md +96 -0
- package/templates/engine/strategies/refactor-safely.md +157 -0
- package/templates/engine/strategies/review-audit.md +116 -0
- package/templates/hooks/session-start.js +100 -0
- package/templates/hooks/skill-router.js +144 -0
- package/templates/hooks/subagent-context.js +118 -0
- package/templates/hooks/task-utils.js +113 -0
- package/templates/hooks/workflow-state.js +39 -0
- package/templates/spec/backend/index.md +31 -0
- package/templates/spec/frontend/index.md +31 -0
- package/templates/spec/guides/index.md +30 -0
- /package/templates/{commands → commands-legacy}/analyze.md +0 -0
- /package/templates/{commands → commands-legacy}/backend.md +0 -0
- /package/templates/{commands → commands-legacy}/codex-exec.md +0 -0
- /package/templates/{commands → commands-legacy}/debug.md +0 -0
- /package/templates/{commands → commands-legacy}/enhance.md +0 -0
- /package/templates/{commands → commands-legacy}/execute.md +0 -0
- /package/templates/{commands → commands-legacy}/feat.md +0 -0
- /package/templates/{commands → commands-legacy}/frontend.md +0 -0
- /package/templates/{commands → commands-legacy}/optimize.md +0 -0
- /package/templates/{commands → commands-legacy}/plan.md +0 -0
- /package/templates/{commands → commands-legacy}/review.md +0 -0
- /package/templates/{commands → commands-legacy}/team-exec.md +0 -0
- /package/templates/{commands → commands-legacy}/team-plan.md +0 -0
- /package/templates/{commands → commands-legacy}/team-research.md +0 -0
- /package/templates/{commands → commands-legacy}/team-review.md +0 -0
- /package/templates/{commands → commands-legacy}/test.md +0 -0
- /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,113 @@
|
|
|
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
|
+
try {
|
|
29
|
+
return fs.statSync(path.join(tasksDir, d)).isDirectory()
|
|
30
|
+
&& fs.existsSync(path.join(tasksDir, d, 'task.json'));
|
|
31
|
+
} catch { return false; }
|
|
32
|
+
})
|
|
33
|
+
.sort()
|
|
34
|
+
.reverse();
|
|
35
|
+
|
|
36
|
+
for (const dir of dirs) {
|
|
37
|
+
try {
|
|
38
|
+
const raw = fs.readFileSync(path.join(tasksDir, dir, 'task.json'), 'utf-8');
|
|
39
|
+
const task = JSON.parse(raw);
|
|
40
|
+
if (task.status !== 'completed' && task.status !== 'archived') {
|
|
41
|
+
return { dir: path.join(tasksDir, dir), ...task };
|
|
42
|
+
}
|
|
43
|
+
} catch { /* skip malformed */ }
|
|
44
|
+
}
|
|
45
|
+
} catch { /* silent */ }
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readFileSafe(filePath) {
|
|
50
|
+
try { return fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readJsonSafe(filePath) {
|
|
54
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readContextJsonl(taskDir) {
|
|
58
|
+
const jsonlPath = path.join(taskDir, 'context.jsonl');
|
|
59
|
+
if (!fs.existsSync(jsonlPath)) return [];
|
|
60
|
+
try {
|
|
61
|
+
return fs.readFileSync(jsonlPath, 'utf-8')
|
|
62
|
+
.split('\n')
|
|
63
|
+
.filter(line => line.trim())
|
|
64
|
+
.map(line => { try { return JSON.parse(line); } catch { return null; } })
|
|
65
|
+
.filter(entry => entry && entry.file);
|
|
66
|
+
} catch { return []; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectTechStack(projectRoot) {
|
|
70
|
+
const indicators = [
|
|
71
|
+
{ file: 'package.json', stack: 'Node.js' },
|
|
72
|
+
{ file: 'go.mod', stack: 'Go' },
|
|
73
|
+
{ file: 'pyproject.toml', stack: 'Python' },
|
|
74
|
+
{ file: 'Cargo.toml', stack: 'Rust' },
|
|
75
|
+
{ file: 'pom.xml', stack: 'Java' },
|
|
76
|
+
{ file: 'build.gradle', stack: 'Java/Kotlin' },
|
|
77
|
+
];
|
|
78
|
+
const found = [];
|
|
79
|
+
for (const { file, stack } of indicators) {
|
|
80
|
+
if (fs.existsSync(path.join(projectRoot, file))) found.push(stack);
|
|
81
|
+
}
|
|
82
|
+
return found.length > 0 ? found.join(' + ') : 'Unknown';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getGitInfo(projectRoot) {
|
|
86
|
+
try {
|
|
87
|
+
const { execSync } = require('child_process');
|
|
88
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: projectRoot, stdio: 'pipe' }).toString().trim();
|
|
89
|
+
const status = execSync('git status --porcelain', { cwd: projectRoot, stdio: 'pipe' }).toString().trim();
|
|
90
|
+
const dirtyCount = status ? status.split('\n').length : 0;
|
|
91
|
+
return { branch, dirtyCount };
|
|
92
|
+
} catch { return { branch: 'unknown', dirtyCount: 0 }; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outputHook(eventName, additionalContext) {
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
hookSpecificOutput: {
|
|
98
|
+
hookEventName: eventName,
|
|
99
|
+
additionalContext
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
findProjectRoot,
|
|
106
|
+
getActiveTask,
|
|
107
|
+
readFileSafe,
|
|
108
|
+
readJsonSafe,
|
|
109
|
+
readContextJsonl,
|
|
110
|
+
detectTechStack,
|
|
111
|
+
getGitInfo,
|
|
112
|
+
outputHook
|
|
113
|
+
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Backend Spec — 后端编码规范
|
|
2
|
+
|
|
3
|
+
> 本文件定义后端代码的编码规范。子 Agent 在写代码前会自动读取此文件。
|
|
4
|
+
> 按项目实际情况修改内容。
|
|
5
|
+
|
|
6
|
+
## API 规范
|
|
7
|
+
|
|
8
|
+
- 路由命名: RESTful, kebab-case
|
|
9
|
+
- 请求/响应格式: JSON, camelCase fields
|
|
10
|
+
- 错误格式: `{ "error": { "code": "...", "message": "..." } }`
|
|
11
|
+
- 认证: Bearer token in Authorization header
|
|
12
|
+
|
|
13
|
+
## 错误处理
|
|
14
|
+
|
|
15
|
+
- 所有 API 端点必须返回结构化错误
|
|
16
|
+
- 4xx: 客户端错误(验证失败、未授权等)
|
|
17
|
+
- 5xx: 服务端错误(包装内部异常,不暴露堆栈)
|
|
18
|
+
- 超时: 设置合理超时,超时后返回 504
|
|
19
|
+
|
|
20
|
+
## 测试要求
|
|
21
|
+
|
|
22
|
+
- 核心逻辑覆盖率 > 80%
|
|
23
|
+
- API 端点必须有集成测试
|
|
24
|
+
- 测试命名: `describe('模块') → it('should 行为')`
|
|
25
|
+
|
|
26
|
+
## 安全清单
|
|
27
|
+
|
|
28
|
+
- [ ] 输入验证(不信任任何用户输入)
|
|
29
|
+
- [ ] SQL 参数化(禁止字符串拼接)
|
|
30
|
+
- [ ] 密钥不硬编码(使用环境变量)
|
|
31
|
+
- [ ] 敏感数据日志脱敏
|