cc-devflow 4.4.1 → 4.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/.claude/skills/cc-act/CHANGELOG.md +6 -0
- package/.claude/skills/cc-act/SKILL.md +9 -1
- package/.claude/skills/cc-act/assets/PR_BRIEF_TEMPLATE.md +4 -0
- package/.claude/skills/cc-act/assets/RELEASE_NOTE_TEMPLATE.md +4 -0
- package/.claude/skills/cc-act/scripts/cc-act-common.sh +5 -0
- package/.claude/skills/cc-act/scripts/render-pr-brief.sh +5 -0
- package/.claude/skills/cc-act/scripts/sync-act-docs.sh +14 -1
- package/.claude/skills/cc-check/CHANGELOG.md +5 -0
- package/.claude/skills/cc-check/SKILL.md +9 -1
- package/.claude/skills/cc-check/assets/REPORT_CARD_TEMPLATE.json +3 -0
- package/.claude/skills/cc-do/CHANGELOG.md +5 -0
- package/.claude/skills/cc-do/SKILL.md +9 -1
- package/.claude/skills/cc-investigate/CHANGELOG.md +5 -0
- package/.claude/skills/cc-investigate/SKILL.md +9 -1
- package/.claude/skills/cc-investigate/assets/ANALYSIS_TEMPLATE.md +1 -0
- package/.claude/skills/cc-investigate/assets/TASKS_TEMPLATE.md +1 -0
- package/.claude/skills/cc-investigate/assets/TASK_MANIFEST_TEMPLATE.json +3 -0
- package/.claude/skills/cc-plan/CHANGELOG.md +5 -0
- package/.claude/skills/cc-plan/SKILL.md +9 -1
- package/.claude/skills/cc-plan/assets/DESIGN_TEMPLATE.md +1 -0
- package/.claude/skills/cc-plan/assets/TASKS_TEMPLATE.md +1 -0
- package/.claude/skills/cc-plan/assets/TASK_MANIFEST_TEMPLATE.json +3 -0
- package/.claude/skills/cc-plan/assets/TINY_DESIGN_TEMPLATE.md +1 -0
- package/.claude/skills/cc-roadmap/CHANGELOG.md +5 -0
- package/.claude/skills/cc-roadmap/SKILL.md +9 -1
- package/.claude/skills/cc-roadmap/assets/BACKLOG_TEMPLATE.md +1 -0
- package/.claude/skills/cc-roadmap/assets/ROADMAP_TEMPLATE.md +1 -0
- package/.claude/skills/cc-roadmap/assets/TRACKING_TEMPLATE.json +4 -1
- package/.claude/skills/cc-spec-init/CHANGELOG.md +5 -0
- package/.claude/skills/cc-spec-init/SKILL.md +9 -1
- package/.claude/skills/cc-spec-init/assets/CAPABILITY_TEMPLATE.md +1 -0
- package/.claude/skills/cc-spec-init/assets/CHANGE_META_TEMPLATE.json +3 -0
- package/.claude/skills/cc-spec-init/assets/INDEX_TEMPLATE.md +1 -0
- package/CHANGELOG.md +19 -0
- package/README.md +43 -0
- package/README.zh-CN.md +43 -0
- package/bin/cc-devflow-cli.js +226 -0
- package/config/schema/cc-devflow-config.schema.json +45 -0
- package/config/user-config.template.yml +16 -0
- package/docs/examples/example-bindings.json +8 -8
- package/docs/examples/full-design-blocked/BACKLOG.md +1 -1
- package/docs/examples/full-design-blocked/README.md +1 -1
- package/docs/examples/full-design-blocked/ROADMAP.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/design.md +1 -1
- package/docs/examples/full-design-blocked/changes/REQ-002-bulk-invite-import/planning/tasks.md +1 -1
- package/docs/examples/full-design-blocked/roadmap-tracking.json +1 -1
- package/docs/examples/local-handoff/BACKLOG.md +1 -1
- package/docs/examples/local-handoff/README.md +1 -1
- package/docs/examples/local-handoff/ROADMAP.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/design.md +1 -1
- package/docs/examples/local-handoff/changes/REQ-003-audit-log-export/planning/tasks.md +1 -1
- package/docs/examples/local-handoff/roadmap-tracking.json +1 -1
- package/docs/examples/pdca-loop/BACKLOG.md +1 -1
- package/docs/examples/pdca-loop/README.md +1 -1
- package/docs/examples/pdca-loop/ROADMAP.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/design.md +1 -1
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/task-manifest.json +2 -2
- package/docs/examples/pdca-loop/changes/REQ-001-copy-invite-link/planning/tasks.md +1 -1
- package/docs/examples/pdca-loop/roadmap-tracking.json +1 -1
- package/lib/skill-runtime/__tests__/cli-bootstrap.integration.test.js +112 -2
- package/lib/skill-runtime/__tests__/config.test.js +161 -0
- package/lib/skill-runtime/__tests__/runtime.integration.test.js +2 -0
- package/lib/skill-runtime/config.js +379 -0
- package/lib/skill-runtime/index.js +2 -0
- package/package.json +1 -1
|
@@ -15,10 +15,14 @@ const REPO_ROOT = path.resolve(__dirname, '../../..');
|
|
|
15
15
|
const CLI_BIN = path.join(REPO_ROOT, 'bin/cc-devflow-cli.js');
|
|
16
16
|
const TEMPLATE_ROOT = path.join(REPO_ROOT, '.claude');
|
|
17
17
|
|
|
18
|
-
function runCli(args, cwd) {
|
|
18
|
+
function runCli(args, cwd, env = {}) {
|
|
19
19
|
const result = spawnSync(process.execPath, [CLI_BIN, ...args], {
|
|
20
20
|
cwd,
|
|
21
|
-
encoding: 'utf8'
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
env: {
|
|
23
|
+
...process.env,
|
|
24
|
+
...env
|
|
25
|
+
}
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
if (result.error) {
|
|
@@ -76,6 +80,84 @@ describe('cc-devflow cli distribution bootstrap', () => {
|
|
|
76
80
|
expect(fs.existsSync(path.join(repoRoot, '.claude', 'tsc-cache'))).toBe(false);
|
|
77
81
|
});
|
|
78
82
|
|
|
83
|
+
test('init does not bake project YAML output policy into managed Claude skills', () => {
|
|
84
|
+
const customSkillDir = path.join(repoRoot, '.claude', 'skills', 'custom-skill');
|
|
85
|
+
fs.mkdirSync(customSkillDir, { recursive: true });
|
|
86
|
+
fs.writeFileSync(path.join(customSkillDir, 'SKILL.md'), '# Custom Skill\n');
|
|
87
|
+
fs.mkdirSync(path.join(repoRoot, '.cc-devflow'), { recursive: true });
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
path.join(repoRoot, '.cc-devflow', 'config.yml'),
|
|
90
|
+
[
|
|
91
|
+
'version: 1',
|
|
92
|
+
'output:',
|
|
93
|
+
' document_language: zh-CN',
|
|
94
|
+
'agent_preferences:',
|
|
95
|
+
' general:',
|
|
96
|
+
' - 先给结论。',
|
|
97
|
+
''
|
|
98
|
+
].join('\n')
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = runCli(['init', '--dir', repoRoot], repoRoot);
|
|
102
|
+
expect(result.status).toBe(0);
|
|
103
|
+
|
|
104
|
+
const skillContent = fs.readFileSync(
|
|
105
|
+
path.join(repoRoot, '.claude', 'skills', 'cc-plan', 'SKILL.md'),
|
|
106
|
+
'utf8'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(skillContent).not.toContain('<!-- CC-DEVFLOW OUTPUT POLICY START -->');
|
|
110
|
+
expect(skillContent).not.toContain('文档语言: zh-CN');
|
|
111
|
+
expect(skillContent).toContain('cc-devflow config resolve --format policy');
|
|
112
|
+
expect(fs.readFileSync(path.join(customSkillDir, 'SKILL.md'), 'utf8')).toBe('# Custom Skill\n');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('config commands manage and diagnose the effective output policy', () => {
|
|
116
|
+
fs.mkdirSync(path.join(repoRoot, '.cc-devflow'), { recursive: true });
|
|
117
|
+
fs.writeFileSync(
|
|
118
|
+
path.join(repoRoot, '.cc-devflow', 'config.yml'),
|
|
119
|
+
[
|
|
120
|
+
'version: 1',
|
|
121
|
+
'output:',
|
|
122
|
+
' document_language: zh-CN',
|
|
123
|
+
''
|
|
124
|
+
].join('\n')
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const resolveResult = runCli(
|
|
128
|
+
['config', 'resolve', '--cwd', repoRoot, '--format', 'policy', '--trace'],
|
|
129
|
+
repoRoot
|
|
130
|
+
);
|
|
131
|
+
const getResult = runCli(['config', 'get', 'output.document_language', '--cwd', repoRoot], repoRoot);
|
|
132
|
+
const doctorResult = runCli(['config', 'doctor', '--cwd', repoRoot], repoRoot);
|
|
133
|
+
|
|
134
|
+
expect(resolveResult.status).toBe(0);
|
|
135
|
+
expect(resolveResult.stdout).toContain('CC-DevFlow Output Policy');
|
|
136
|
+
expect(resolveResult.stdout).toContain('Output language: zh-CN');
|
|
137
|
+
expect(resolveResult.stdout).toContain('Trace:');
|
|
138
|
+
expect(resolveResult.stdout).toContain('project');
|
|
139
|
+
expect(getResult.status).toBe(0);
|
|
140
|
+
expect(getResult.stdout.trim()).toBe('zh-CN');
|
|
141
|
+
expect(doctorResult.status).toBe(0);
|
|
142
|
+
expect(doctorResult.stdout).toContain('Config OK');
|
|
143
|
+
|
|
144
|
+
const setResult = runCli(
|
|
145
|
+
['config', 'set', 'output.document_language', 'en', '--cwd', repoRoot, '--project'],
|
|
146
|
+
repoRoot
|
|
147
|
+
);
|
|
148
|
+
expect(setResult.status).toBe(0);
|
|
149
|
+
expect(runCli(['config', 'get', 'output.document_language', '--cwd', repoRoot], repoRoot).stdout.trim()).toBe('en');
|
|
150
|
+
|
|
151
|
+
const isolatedHome = path.join(repoRoot, 'home');
|
|
152
|
+
const userInitResult = runCli(
|
|
153
|
+
['config', 'init', '--cwd', repoRoot, '--user', '--force'],
|
|
154
|
+
repoRoot,
|
|
155
|
+
{ HOME: isolatedHome }
|
|
156
|
+
);
|
|
157
|
+
expect(userInitResult.status).toBe(0);
|
|
158
|
+
expect(userInitResult.stdout).toContain(path.join(isolatedHome, '.cc-devflow', 'config.yml'));
|
|
159
|
+
});
|
|
160
|
+
|
|
79
161
|
test('init overwrites diverged .claude files with packaged content', () => {
|
|
80
162
|
expect(runCli(['init', '--dir', repoRoot], repoRoot).status).toBe(0);
|
|
81
163
|
|
|
@@ -161,6 +243,34 @@ describe('cc-devflow cli distribution bootstrap', () => {
|
|
|
161
243
|
);
|
|
162
244
|
});
|
|
163
245
|
|
|
246
|
+
test('adapt mirrors Codex skills without baking project YAML output policy', () => {
|
|
247
|
+
fs.mkdirSync(path.join(repoRoot, '.cc-devflow'), { recursive: true });
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(repoRoot, '.cc-devflow', 'config.yml'),
|
|
250
|
+
[
|
|
251
|
+
'version: 1',
|
|
252
|
+
'output:',
|
|
253
|
+
' document_language: zh-CN',
|
|
254
|
+
'agent_preferences:',
|
|
255
|
+
' documentation:',
|
|
256
|
+
' - 输出文档默认使用中文。',
|
|
257
|
+
''
|
|
258
|
+
].join('\n')
|
|
259
|
+
);
|
|
260
|
+
expect(runCli(['init', '--dir', repoRoot], repoRoot).status).toBe(0);
|
|
261
|
+
|
|
262
|
+
const result = runCli(['adapt', '--cwd', repoRoot, '--platform', 'codex'], repoRoot);
|
|
263
|
+
expect(result.status).toBe(0);
|
|
264
|
+
|
|
265
|
+
const codexPlanSkill = fs.readFileSync(
|
|
266
|
+
path.join(repoRoot, '.codex', 'skills', 'cc-plan', 'SKILL.md'),
|
|
267
|
+
'utf8'
|
|
268
|
+
);
|
|
269
|
+
expect(codexPlanSkill).not.toContain('<!-- CC-DEVFLOW OUTPUT POLICY START -->');
|
|
270
|
+
expect(codexPlanSkill).not.toContain('文档语言: zh-CN');
|
|
271
|
+
expect(codexPlanSkill).toContain('cc-devflow config resolve --format policy');
|
|
272
|
+
});
|
|
273
|
+
|
|
164
274
|
test('adapt preserves pre-existing non-public Codex skills and does not mirror new private ones', () => {
|
|
165
275
|
expect(runCli(['init', '--dir', repoRoot], repoRoot).status).toBe(0);
|
|
166
276
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 临时 home/repo 配置、env 覆盖与 CLI 等价写入操作。
|
|
3
|
+
* [OUTPUT]: 验证运行时个人配置解析、校验、trace、policy 与写入行为。
|
|
4
|
+
* [POS]: skill runtime 个人配置的单元测试。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
LOCAL_CONFIG_RELATIVE_PATH,
|
|
14
|
+
PROJECT_CONFIG_RELATIVE_PATH,
|
|
15
|
+
USER_CONFIG_RELATIVE_PATH,
|
|
16
|
+
getConfigValue,
|
|
17
|
+
resolveUserConfig,
|
|
18
|
+
setConfigValue,
|
|
19
|
+
writeConfigTemplate
|
|
20
|
+
} = require('../config.js');
|
|
21
|
+
|
|
22
|
+
describe('skill-runtime user config', () => {
|
|
23
|
+
let tmpRoot;
|
|
24
|
+
let homeDir;
|
|
25
|
+
let repoRoot;
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-runtime-config-'));
|
|
29
|
+
homeDir = path.join(tmpRoot, 'home');
|
|
30
|
+
repoRoot = path.join(tmpRoot, 'repo');
|
|
31
|
+
fs.mkdirSync(path.join(homeDir, '.cc-devflow'), { recursive: true });
|
|
32
|
+
fs.mkdirSync(path.join(repoRoot, '.cc-devflow'), { recursive: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('resolves document language with default, file, env, and cli precedence trace', () => {
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(homeDir, USER_CONFIG_RELATIVE_PATH),
|
|
42
|
+
[
|
|
43
|
+
'version: 1',
|
|
44
|
+
'output:',
|
|
45
|
+
' document_language: en',
|
|
46
|
+
'agent_preferences:',
|
|
47
|
+
' planning:',
|
|
48
|
+
' - Keep plans short.',
|
|
49
|
+
''
|
|
50
|
+
].join('\n')
|
|
51
|
+
);
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(repoRoot, PROJECT_CONFIG_RELATIVE_PATH),
|
|
54
|
+
[
|
|
55
|
+
'version: 1',
|
|
56
|
+
'output:',
|
|
57
|
+
' document_language: zh-CN',
|
|
58
|
+
''
|
|
59
|
+
].join('\n')
|
|
60
|
+
);
|
|
61
|
+
fs.writeFileSync(
|
|
62
|
+
path.join(repoRoot, LOCAL_CONFIG_RELATIVE_PATH),
|
|
63
|
+
[
|
|
64
|
+
'version: 1',
|
|
65
|
+
'agent_preferences:',
|
|
66
|
+
' review:',
|
|
67
|
+
' order: severe-first',
|
|
68
|
+
''
|
|
69
|
+
].join('\n')
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const result = resolveUserConfig({
|
|
73
|
+
cwd: repoRoot,
|
|
74
|
+
homeDir,
|
|
75
|
+
env: {
|
|
76
|
+
CC_DEVFLOW_DOCUMENT_LANGUAGE: 'en'
|
|
77
|
+
},
|
|
78
|
+
overrides: {
|
|
79
|
+
output: {
|
|
80
|
+
document_language: 'zh-CN'
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(result.config.output.document_language).toBe('zh-CN');
|
|
86
|
+
expect(result.config.agent_preferences.planning).toEqual(['Keep plans short.']);
|
|
87
|
+
expect(result.config.agent_preferences.review.order).toBe('severe-first');
|
|
88
|
+
expect(result.sources.map((source) => source.kind)).toEqual(['user', 'project', 'local', 'env', 'cli']);
|
|
89
|
+
expect(result.trace.map((entry) => `${entry.source}:${entry.key}:${entry.value}`)).toEqual([
|
|
90
|
+
'default:version:1',
|
|
91
|
+
'default:output.document_language:en',
|
|
92
|
+
'user:version:1',
|
|
93
|
+
'user:output.document_language:en',
|
|
94
|
+
'user:agent_preferences.planning:["Keep plans short."]',
|
|
95
|
+
'project:version:1',
|
|
96
|
+
'project:output.document_language:zh-CN',
|
|
97
|
+
'local:version:1',
|
|
98
|
+
'local:agent_preferences.review.order:severe-first',
|
|
99
|
+
'env:output.document_language:en',
|
|
100
|
+
'cli:output.document_language:zh-CN'
|
|
101
|
+
]);
|
|
102
|
+
expect(result.policy).toContain('Output language: zh-CN');
|
|
103
|
+
expect(result.policy).toContain('Machine-enforced fields: output.document_language');
|
|
104
|
+
expect(result.policy).toContain('Agent advisory preferences');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('rejects unsupported language and unknown top-level fields', () => {
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(repoRoot, PROJECT_CONFIG_RELATIVE_PATH),
|
|
110
|
+
[
|
|
111
|
+
'version: 1',
|
|
112
|
+
'output:',
|
|
113
|
+
' document_language: klingon',
|
|
114
|
+
''
|
|
115
|
+
].join('\n')
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(() => resolveUserConfig({ cwd: repoRoot, homeDir })).toThrow(/output.document_language/);
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(repoRoot, PROJECT_CONFIG_RELATIVE_PATH),
|
|
122
|
+
[
|
|
123
|
+
'version: 1',
|
|
124
|
+
'preferences:',
|
|
125
|
+
' general:',
|
|
126
|
+
' - non standard root',
|
|
127
|
+
''
|
|
128
|
+
].join('\n')
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(() => resolveUserConfig({ cwd: repoRoot, homeDir })).toThrow(/Unsupported config key/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('wraps invalid YAML errors with the config path', () => {
|
|
135
|
+
const configPath = path.join(repoRoot, PROJECT_CONFIG_RELATIVE_PATH);
|
|
136
|
+
fs.writeFileSync(configPath, 'version: 1\noutput:\n document_language: [\n');
|
|
137
|
+
|
|
138
|
+
expect(() => resolveUserConfig({ cwd: repoRoot, homeDir })).toThrow(configPath);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('reads and writes config values without losing advisory preferences', () => {
|
|
142
|
+
const configPath = writeConfigTemplate({ scope: 'project', cwd: repoRoot, homeDir });
|
|
143
|
+
expect(configPath).toBe(path.join(repoRoot, PROJECT_CONFIG_RELATIVE_PATH));
|
|
144
|
+
|
|
145
|
+
setConfigValue('output.document_language', 'en', { scope: 'project', cwd: repoRoot, homeDir });
|
|
146
|
+
setConfigValue('agent_preferences.documentation.voice', 'direct', {
|
|
147
|
+
scope: 'project',
|
|
148
|
+
cwd: repoRoot,
|
|
149
|
+
homeDir
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const resolved = resolveUserConfig({ cwd: repoRoot, homeDir });
|
|
153
|
+
|
|
154
|
+
expect(getConfigValue(resolved.config, 'output.document_language')).toBe('en');
|
|
155
|
+
expect(getConfigValue(resolved.config, 'agent_preferences.documentation.voice')).toBe('direct');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('rejects unknown config scopes instead of silently writing project config', () => {
|
|
159
|
+
expect(() => writeConfigTemplate({ scope: 'team', cwd: repoRoot, homeDir })).toThrow(/Unknown config scope/);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [INPUT]: 用户级、项目级、本地 YAML 配置,以及 env / CLI 覆盖。
|
|
3
|
+
* [OUTPUT]: 运行时输出策略、key 级来源 trace、配置读写与 doctor 结果。
|
|
4
|
+
* [POS]: skill runtime 的个人配置真相源,避免把个人偏好烘进 Skill 文件。
|
|
5
|
+
* [PROTOCOL]: 变更时更新此头部,然后检查 CLAUDE.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const yaml = require('js-yaml');
|
|
12
|
+
|
|
13
|
+
const USER_CONFIG_RELATIVE_PATH = path.join('.cc-devflow', 'config.yml');
|
|
14
|
+
const PROJECT_CONFIG_RELATIVE_PATH = path.join('.cc-devflow', 'config.yml');
|
|
15
|
+
const LOCAL_CONFIG_RELATIVE_PATH = path.join('.cc-devflow', 'config.local.yml');
|
|
16
|
+
const CONFIG_TEMPLATE_PATH = path.resolve(__dirname, '..', '..', 'config', 'user-config.template.yml');
|
|
17
|
+
const SUPPORTED_DOCUMENT_LANGUAGES = new Set(['en', 'zh-CN']);
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
version: 1,
|
|
21
|
+
output: {
|
|
22
|
+
document_language: 'en'
|
|
23
|
+
},
|
|
24
|
+
agent_preferences: {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function traceValue(value) {
|
|
32
|
+
if (Array.isArray(value) || isPlainObject(value)) {
|
|
33
|
+
return JSON.stringify(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readYamlConfig(filePath) {
|
|
40
|
+
if (!fs.existsSync(filePath)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
47
|
+
parsed = yaml.load(content) || {};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(`Failed to parse cc-devflow config ${filePath}: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isPlainObject(parsed)) {
|
|
53
|
+
throw new Error(`cc-devflow config must be a YAML object: ${filePath}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validateConfigShape(parsed, filePath);
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function validateConfigShape(config, source = 'config') {
|
|
61
|
+
const allowedRootKeys = new Set(['version', 'output', 'agent_preferences']);
|
|
62
|
+
|
|
63
|
+
for (const key of Object.keys(config || {})) {
|
|
64
|
+
if (!allowedRootKeys.has(key)) {
|
|
65
|
+
throw new Error(`Unsupported config key "${key}" in ${source}. Put custom fields under agent_preferences.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if ('version' in config && config.version !== 1) {
|
|
70
|
+
throw new Error(`Unsupported config version "${config.version}" in ${source}. Expected version: 1.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if ('output' in config) {
|
|
74
|
+
if (!isPlainObject(config.output)) {
|
|
75
|
+
throw new Error(`output must be an object in ${source}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const key of Object.keys(config.output)) {
|
|
79
|
+
if (key !== 'document_language') {
|
|
80
|
+
throw new Error(`Unsupported output key "output.${key}" in ${source}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
'document_language' in config.output
|
|
86
|
+
&& !SUPPORTED_DOCUMENT_LANGUAGES.has(config.output.document_language)
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Unsupported output.document_language "${config.output.document_language}" in ${source}. Use en or zh-CN.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('agent_preferences' in config && !isPlainObject(config.agent_preferences)) {
|
|
95
|
+
throw new Error(`agent_preferences must be an object in ${source}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pushTrace(trace, source, sourcePath, key, value) {
|
|
100
|
+
trace.push({
|
|
101
|
+
key,
|
|
102
|
+
value: traceValue(value),
|
|
103
|
+
source,
|
|
104
|
+
path: sourcePath || null
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function applyConfigLayer(base, layer, trace, source, sourcePath, prefix = '') {
|
|
109
|
+
const next = Array.isArray(base) ? [...base] : { ...(base || {}) };
|
|
110
|
+
|
|
111
|
+
for (const [key, value] of Object.entries(layer || {})) {
|
|
112
|
+
const traceKey = prefix ? `${prefix}.${key}` : key;
|
|
113
|
+
|
|
114
|
+
if (isPlainObject(value)) {
|
|
115
|
+
next[key] = applyConfigLayer(
|
|
116
|
+
isPlainObject(next[key]) ? next[key] : {},
|
|
117
|
+
value,
|
|
118
|
+
trace,
|
|
119
|
+
source,
|
|
120
|
+
sourcePath,
|
|
121
|
+
traceKey
|
|
122
|
+
);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
next[key] = Array.isArray(value) ? [...value] : value;
|
|
127
|
+
pushTrace(trace, source, sourcePath, traceKey, next[key]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function collectConfigSources(options = {}) {
|
|
134
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
135
|
+
const homeDir = path.resolve(options.homeDir || os.homedir());
|
|
136
|
+
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
kind: 'user',
|
|
140
|
+
path: path.join(homeDir, USER_CONFIG_RELATIVE_PATH)
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
kind: 'project',
|
|
144
|
+
path: path.join(cwd, PROJECT_CONFIG_RELATIVE_PATH)
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
kind: 'local',
|
|
148
|
+
path: path.join(cwd, LOCAL_CONFIG_RELATIVE_PATH)
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function envToConfig(env = process.env) {
|
|
154
|
+
const documentLanguage = env.CC_DEVFLOW_DOCUMENT_LANGUAGE
|
|
155
|
+
|| env.CC_DEVFLOW_OUTPUT_DOCUMENT_LANGUAGE;
|
|
156
|
+
|
|
157
|
+
if (!documentLanguage) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
output: {
|
|
163
|
+
document_language: documentLanguage
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function resolveUserConfig(options = {}) {
|
|
169
|
+
const trace = [];
|
|
170
|
+
const sources = [];
|
|
171
|
+
let config = applyConfigLayer({}, DEFAULT_CONFIG, trace, 'default', null);
|
|
172
|
+
|
|
173
|
+
for (const source of collectConfigSources(options)) {
|
|
174
|
+
const value = readYamlConfig(source.path);
|
|
175
|
+
if (!value) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
config = applyConfigLayer(config, value, trace, source.kind, source.path);
|
|
180
|
+
sources.push(source);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const envConfig = envToConfig(options.env || process.env);
|
|
184
|
+
if (envConfig) {
|
|
185
|
+
validateConfigShape(envConfig, 'environment');
|
|
186
|
+
config = applyConfigLayer(config, envConfig, trace, 'env', null);
|
|
187
|
+
sources.push({ kind: 'env', path: null });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (options.overrides && Object.keys(options.overrides).length > 0) {
|
|
191
|
+
validateConfigShape(options.overrides, 'CLI overrides');
|
|
192
|
+
config = applyConfigLayer(config, options.overrides, trace, 'cli', null);
|
|
193
|
+
sources.push({ kind: 'cli', path: null });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
validateConfigShape(config, 'resolved config');
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
enabled: sources.length > 0,
|
|
200
|
+
config,
|
|
201
|
+
sources,
|
|
202
|
+
trace,
|
|
203
|
+
policy: renderOutputPolicy(config)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function formatPreferenceLines(value, prefix = '') {
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
return value.map((item) => `- ${prefix}: ${traceValue(item)}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!isPlainObject(value)) {
|
|
213
|
+
return [`- ${prefix}: ${traceValue(value)}`];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return Object.entries(value).flatMap(([key, child]) => {
|
|
217
|
+
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
218
|
+
return formatPreferenceLines(child, nextPrefix);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderOutputPolicy(config = DEFAULT_CONFIG) {
|
|
223
|
+
const documentLanguage = config.output?.document_language || DEFAULT_CONFIG.output.document_language;
|
|
224
|
+
const lines = [
|
|
225
|
+
'## CC-DevFlow Output Policy',
|
|
226
|
+
'',
|
|
227
|
+
'Machine-enforced fields: output.document_language',
|
|
228
|
+
`- Output language: ${documentLanguage}`,
|
|
229
|
+
`- Durable Markdown artifacts must include \`Output language: ${documentLanguage}\` in their metadata or first screen.`,
|
|
230
|
+
'',
|
|
231
|
+
'Agent advisory preferences:'
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const preferences = config.agent_preferences || {};
|
|
235
|
+
const preferenceLines = formatPreferenceLines(preferences);
|
|
236
|
+
|
|
237
|
+
if (preferenceLines.length === 0) {
|
|
238
|
+
lines.push('- none');
|
|
239
|
+
} else {
|
|
240
|
+
lines.push(...preferenceLines);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `${lines.join('\n')}\n`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getConfigPath(scope, options = {}) {
|
|
247
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
248
|
+
const homeDir = path.resolve(options.homeDir || os.homedir());
|
|
249
|
+
|
|
250
|
+
if (scope === 'user') {
|
|
251
|
+
return path.join(homeDir, USER_CONFIG_RELATIVE_PATH);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (scope === 'local') {
|
|
255
|
+
return path.join(cwd, LOCAL_CONFIG_RELATIVE_PATH);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (scope !== 'project') {
|
|
259
|
+
throw new Error(`Unknown config scope "${scope}". Use user, project, or local.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return path.join(cwd, PROJECT_CONFIG_RELATIVE_PATH);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function getConfigValue(config, keyPath) {
|
|
266
|
+
return String(keyPath || '')
|
|
267
|
+
.split('.')
|
|
268
|
+
.filter(Boolean)
|
|
269
|
+
.reduce((value, key) => (value == null ? undefined : value[key]), config);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function setNestedValue(target, keyPath, value) {
|
|
273
|
+
const parts = String(keyPath || '').split('.').filter(Boolean);
|
|
274
|
+
if (parts.length === 0) {
|
|
275
|
+
throw new Error('Config key is required.');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let cursor = target;
|
|
279
|
+
for (const key of parts.slice(0, -1)) {
|
|
280
|
+
if (!isPlainObject(cursor[key])) {
|
|
281
|
+
cursor[key] = {};
|
|
282
|
+
}
|
|
283
|
+
cursor = cursor[key];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
cursor[parts[parts.length - 1]] = value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function coerceConfigValue(keyPath, value) {
|
|
290
|
+
if (keyPath === 'output.document_language') {
|
|
291
|
+
if (!SUPPORTED_DOCUMENT_LANGUAGES.has(value)) {
|
|
292
|
+
throw new Error(`Unsupported output.document_language "${value}". Use en or zh-CN.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function writeYamlConfig(filePath, config) {
|
|
300
|
+
validateConfigShape(config, filePath);
|
|
301
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
302
|
+
fs.writeFileSync(filePath, yaml.dump(config, { lineWidth: 100, noRefs: true }), 'utf8');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function setConfigValue(keyPath, value, options = {}) {
|
|
306
|
+
if (keyPath !== 'output.document_language' && !keyPath.startsWith('agent_preferences.')) {
|
|
307
|
+
throw new Error('Only output.document_language and agent_preferences.* can be set.');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const filePath = getConfigPath(options.scope || 'project', options);
|
|
311
|
+
const config = readYamlConfig(filePath) || { version: 1 };
|
|
312
|
+
setNestedValue(config, keyPath, coerceConfigValue(keyPath, value));
|
|
313
|
+
writeYamlConfig(filePath, config);
|
|
314
|
+
return filePath;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function writeConfigTemplate(options = {}) {
|
|
318
|
+
const scope = options.scope || 'project';
|
|
319
|
+
const filePath = getConfigPath(scope, options);
|
|
320
|
+
|
|
321
|
+
if (fs.existsSync(filePath) && !options.force) {
|
|
322
|
+
return filePath;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const template = readYamlConfig(CONFIG_TEMPLATE_PATH);
|
|
326
|
+
|
|
327
|
+
writeYamlConfig(filePath, template);
|
|
328
|
+
return filePath;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isLocalConfigIgnored(repoRoot) {
|
|
332
|
+
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
333
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const lines = fs.readFileSync(gitignorePath, 'utf8')
|
|
338
|
+
.split(/\r?\n/)
|
|
339
|
+
.map((line) => line.trim())
|
|
340
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
341
|
+
|
|
342
|
+
return lines.includes('.cc-devflow/config.local.yml')
|
|
343
|
+
|| lines.includes('/.cc-devflow/config.local.yml')
|
|
344
|
+
|| lines.includes('.cc-devflow/');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function doctorUserConfig(options = {}) {
|
|
348
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
349
|
+
const resolved = resolveUserConfig(options);
|
|
350
|
+
const warnings = [];
|
|
351
|
+
const localPath = path.join(cwd, LOCAL_CONFIG_RELATIVE_PATH);
|
|
352
|
+
|
|
353
|
+
if (fs.existsSync(localPath) && !isLocalConfigIgnored(cwd)) {
|
|
354
|
+
warnings.push('.cc-devflow/config.local.yml exists but is not ignored by .gitignore.');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
ok: warnings.length === 0,
|
|
359
|
+
warnings,
|
|
360
|
+
resolved
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = {
|
|
365
|
+
DEFAULT_CONFIG,
|
|
366
|
+
CONFIG_TEMPLATE_PATH,
|
|
367
|
+
LOCAL_CONFIG_RELATIVE_PATH,
|
|
368
|
+
PROJECT_CONFIG_RELATIVE_PATH,
|
|
369
|
+
SUPPORTED_DOCUMENT_LANGUAGES,
|
|
370
|
+
USER_CONFIG_RELATIVE_PATH,
|
|
371
|
+
doctorUserConfig,
|
|
372
|
+
getConfigPath,
|
|
373
|
+
getConfigValue,
|
|
374
|
+
readYamlConfig,
|
|
375
|
+
renderOutputPolicy,
|
|
376
|
+
resolveUserConfig,
|
|
377
|
+
setConfigValue,
|
|
378
|
+
writeConfigTemplate
|
|
379
|
+
};
|
|
@@ -15,6 +15,7 @@ const lifecycle = require('./lifecycle');
|
|
|
15
15
|
const teamState = require('./team-state');
|
|
16
16
|
const delegation = require('./delegation');
|
|
17
17
|
const paths = require('./paths');
|
|
18
|
+
const config = require('./config');
|
|
18
19
|
const worker = require('./operations/worker');
|
|
19
20
|
const workerRun = require('./operations/worker-run');
|
|
20
21
|
|
|
@@ -29,6 +30,7 @@ module.exports = {
|
|
|
29
30
|
...teamState,
|
|
30
31
|
...delegation,
|
|
31
32
|
...paths,
|
|
33
|
+
...config,
|
|
32
34
|
...worker,
|
|
33
35
|
...workerRun
|
|
34
36
|
};
|