@house365/comate 0.6.0 → 0.7.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 +15 -0
- package/bin/cli.mjs +6 -0
- package/bin/doctor.mjs +86 -2
- package/bin/usage.mjs +252 -0
- package/docs/COMATE_MANUAL.md +27 -2
- package/package.json +3 -2
- package/skills/comate/scripts/comate-lib.sh +27 -10
- package/skills/comate/tests/comate.bats +24 -12
- package/skills/comate-gate/SKILL.md +22 -2
- package/skills/comate-gate/scaffold/docs/knowledge-base/README.md +6 -6
package/README.md
CHANGED
|
@@ -61,6 +61,21 @@ cd your-project && comate init
|
|
|
61
61
|
|
|
62
62
|
输入 `/comate` 自动探测当前阶段并分派,或直接调用某个阶段 skill。状态记录在每个 change 的 `openspec/changes/<name>/.comate.yaml` 中,支持上下文压缩后断点恢复。
|
|
63
63
|
|
|
64
|
+
## Token 用量:`comate usage`
|
|
65
|
+
|
|
66
|
+
查看**当前项目**的 token 消耗与估算成本(封装 [ccusage](https://github.com/ryoppippi/ccusage),随 comate 一起装):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
comate usage # 本项目按会话(session)统计
|
|
70
|
+
comate usage daily # 本项目按天
|
|
71
|
+
comate usage --global # 全部项目(不做项目隔离)
|
|
72
|
+
comate usage --json -s 2026-08-01 # 其余参数原样透传给 ccusage
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
默认只统计当前工作目录这一个项目:Claude Code 数据按 `~/.claude/projects/<项目>/` 隔离,Codex 数据按会话记录里的 `cwd` 过滤,两者都不会混入其它项目的用量。加 `--global` 看全量,或用 `comate usage codex ...` 单看某个 agent。
|
|
76
|
+
|
|
77
|
+
> 粒度到项目/会话级;不做 open/design/build 阶段级归因(那需要在状态机打时间戳,属后续增强)。
|
|
78
|
+
|
|
64
79
|
## 目录结构
|
|
65
80
|
|
|
66
81
|
```
|
package/bin/cli.mjs
CHANGED
|
@@ -21,12 +21,18 @@ if (args[0] === 'doctor') {
|
|
|
21
21
|
process.exit(doctor(args.slice(1)));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
if (args[0] === 'usage') {
|
|
25
|
+
const { usage } = await import('./usage.mjs');
|
|
26
|
+
process.exit(await usage(args.slice(1), { pkgRoot }));
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
25
30
|
console.log(`comate — OpenSpec + Superpowers 规约驱动的开发编排器
|
|
26
31
|
|
|
27
32
|
用法:
|
|
28
33
|
comate init 安装 comate 和依赖到用户级,并在当前项目创建 openspec/
|
|
29
34
|
comate doctor 健康检查
|
|
35
|
+
comate usage 查看本项目 token 用量(--global 看全部;参数透传给 ccusage)
|
|
30
36
|
|
|
31
37
|
装好后在 agent 中输入 /comate 开始使用。`);
|
|
32
38
|
process.exit(0);
|
package/bin/doctor.mjs
CHANGED
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
// ponytail: comate.sh already implements every check. This only locates the
|
|
4
4
|
// installed copy so the user doesn't have to remember the path.
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import {
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
import { atLeast, capture, npmInvocation } from './init.mjs';
|
|
8
12
|
|
|
9
13
|
// Where the skills CLI puts things. `init` installs user-level, but a project
|
|
10
14
|
// copy shadows it for the agent, so check cwd first and report on the same one
|
|
@@ -67,5 +71,85 @@ export function doctor(argv) {
|
|
|
67
71
|
console.error(`无法执行 bash (${bash}) — comate 的脚本需要 bash。${error.message}`);
|
|
68
72
|
return 1;
|
|
69
73
|
}
|
|
74
|
+
|
|
75
|
+
// ccusage is a node dependency (for `comate usage`), not a bash-side tool, so
|
|
76
|
+
// it lives here rather than in comate.sh's doctor. A miss is a warning, never
|
|
77
|
+
// a failure: usage falls back to `npx ccusage@latest`, so it still works.
|
|
78
|
+
reportCcusage();
|
|
79
|
+
|
|
80
|
+
// Version check is the only place a user learns a newer comate exists: the
|
|
81
|
+
// npm package doesn't self-update and `~/.agents/skills` keeps the copied
|
|
82
|
+
// skills until `comate init` is re-run. Never blocks or fails doctor — a
|
|
83
|
+
// network miss / offline just skips the line (see checkForUpdate).
|
|
84
|
+
reportUpdate();
|
|
85
|
+
|
|
70
86
|
return status ?? 1;
|
|
71
87
|
}
|
|
88
|
+
|
|
89
|
+
// resolvable = shipped as comate's pinned dependency; the common, happy case
|
|
90
|
+
// after `npm i -g @house365/comate`. Otherwise point at the two ways out.
|
|
91
|
+
export function ccusageResolvable(pkgRoot = defaultPkgRoot()) {
|
|
92
|
+
try {
|
|
93
|
+
createRequire(join(pkgRoot, 'package.json')).resolve('ccusage/package.json');
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reportCcusage() {
|
|
101
|
+
if (ccusageResolvable()) {
|
|
102
|
+
console.log(' ✓ ccusage present (comate usage 可用)');
|
|
103
|
+
} else {
|
|
104
|
+
console.log(
|
|
105
|
+
' ⚠ ccusage not found — reinstall with npm i -g @house365/comate ' +
|
|
106
|
+
'(comate usage 会回退到 npx ccusage@latest)',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read {name, version} from the installed package.json. Both come from the same
|
|
112
|
+
// file so the check keys off whatever package this binary actually belongs to —
|
|
113
|
+
// no hardcoded name/version to drift when the package is renamed or bumped.
|
|
114
|
+
export function localPackage(pkgRoot = defaultPkgRoot()) {
|
|
115
|
+
try {
|
|
116
|
+
const { name, version } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'));
|
|
117
|
+
return name && version ? { name, version } : null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Compare the installed version against npm's latest. Returns null (silent) on
|
|
124
|
+
// anything uncertain — no package.json, no npm, offline, timeout, unparseable
|
|
125
|
+
// output — because a *diagnostic* command must never hang or error on a network
|
|
126
|
+
// miss. `fetchLatest` is injectable so this is unit-testable without the network.
|
|
127
|
+
export function checkForUpdate({
|
|
128
|
+
pkg = localPackage(),
|
|
129
|
+
fetchLatest = defaultFetchLatest,
|
|
130
|
+
} = {}) {
|
|
131
|
+
if (!pkg) return null;
|
|
132
|
+
const latest = fetchLatest(pkg.name);
|
|
133
|
+
if (!latest || !/^\d+\.\d+\.\d+/.test(latest)) return null;
|
|
134
|
+
// outdated when current is NOT at least latest (atLeast is >=).
|
|
135
|
+
return { name: pkg.name, current: pkg.version, latest, outdated: !atLeast(pkg.version, latest) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// `npm view <pkg> version` — capture() already caps at 15s and returns null on
|
|
139
|
+
// any non-zero exit, so offline/timeout falls through to a skipped check.
|
|
140
|
+
function defaultFetchLatest(name) {
|
|
141
|
+
const { command, args, shell } = npmInvocation(['view', name, 'version']);
|
|
142
|
+
return capture(command, args, { shell: Boolean(shell) });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function reportUpdate(info = checkForUpdate()) {
|
|
146
|
+
if (!info || !info.outdated) return; // up to date or couldn't tell → say nothing
|
|
147
|
+
console.log(
|
|
148
|
+
` ⬆ 有新版 comate:${info.current} → ${info.latest}。升级:` +
|
|
149
|
+
`npm i -g ${info.name}@latest 然后重跑 comate init`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function defaultPkgRoot() {
|
|
154
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
155
|
+
}
|
package/bin/usage.mjs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// `comate usage` — show this project's Claude Code / Codex token usage by
|
|
2
|
+
// wrapping ccusage.
|
|
3
|
+
//
|
|
4
|
+
// Why a wrapper and not just `npx ccusage`: ccusage aggregates every project
|
|
5
|
+
// globally and there is no "just this repo" flag. Scoping has to be done per
|
|
6
|
+
// agent, because the two store data differently:
|
|
7
|
+
//
|
|
8
|
+
// Claude Code — one dir per project: ~/.claude/projects/<encoded-cwd>/*.jsonl.
|
|
9
|
+
// Honours CLAUDE_CONFIG_DIR. So: a throwaway root whose
|
|
10
|
+
// projects/ holds a single symlink to this project's dir.
|
|
11
|
+
// Codex — flat by date: ~/.codex/sessions/YYYY/MM/DD/*.jsonl, project
|
|
12
|
+
// identified only by a `cwd` inside each file's session_meta.
|
|
13
|
+
// Honours CODEX_HOME. So: filter files by cwd, symlink the
|
|
14
|
+
// matches into a throwaway sessions/ dir.
|
|
15
|
+
//
|
|
16
|
+
// Both env vars are set to the scoped roots even when a root is empty — an empty
|
|
17
|
+
// scoped root reports zero for that agent, which is correct, whereas leaving the
|
|
18
|
+
// var unset would let ccusage fall back to the global dir and leak every other
|
|
19
|
+
// project's usage in. `--global` skips all scoping and forwards straight through.
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import {
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdtempSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
openSync,
|
|
26
|
+
readSync,
|
|
27
|
+
closeSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
rmSync,
|
|
30
|
+
linkSync,
|
|
31
|
+
copyFileSync,
|
|
32
|
+
} from 'node:fs';
|
|
33
|
+
import { createRequire } from 'node:module';
|
|
34
|
+
import { tmpdir } from 'node:os';
|
|
35
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
36
|
+
|
|
37
|
+
// Claude Code encodes a project's cwd into a directory name by replacing every
|
|
38
|
+
// path separator and the drive colon with a dash: D:\a\b -> D--a-b, /a/b -> -a-b.
|
|
39
|
+
// Verified against a real ~/.claude/projects entry on Windows.
|
|
40
|
+
export function encodeProjectDir(cwd) {
|
|
41
|
+
return cwd.replace(/[:\\/]/g, '-');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function home() {
|
|
45
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Where Claude Code keeps this project's transcripts. CLAUDE_CONFIG_DIR wins if
|
|
49
|
+
// the user has relocated the config dir, matching ccusage's own resolution.
|
|
50
|
+
export function claudeDataDir(cwd = process.cwd(), env = process.env) {
|
|
51
|
+
const configRoot = env.CLAUDE_CONFIG_DIR || join(home(), '.claude');
|
|
52
|
+
return join(configRoot, 'projects', encodeProjectDir(cwd));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Codex's sessions root. CODEX_HOME wins, else ~/.codex.
|
|
56
|
+
export function codexSessionsDir(env = process.env) {
|
|
57
|
+
const root = env.CODEX_HOME || join(home(), '.codex');
|
|
58
|
+
return join(root, 'sessions');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Read just the head of a file — enough to catch the session_meta record, which
|
|
62
|
+
// Codex writes first. Avoids slurping multi-MB transcripts to read one cwd.
|
|
63
|
+
function readHead(file, bytes = 8192) {
|
|
64
|
+
const fd = openSync(file, 'r');
|
|
65
|
+
try {
|
|
66
|
+
const buf = Buffer.alloc(bytes);
|
|
67
|
+
const n = readSync(fd, buf, 0, bytes, 0);
|
|
68
|
+
return buf.toString('utf8', 0, n);
|
|
69
|
+
} finally {
|
|
70
|
+
closeSync(fd);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The cwd recorded in a Codex session file, or null. session_meta is the first
|
|
75
|
+
// record; its payload.cwd (older: top-level cwd) is the project path.
|
|
76
|
+
export function codexFileCwd(file, read = readHead) {
|
|
77
|
+
const head = read(file);
|
|
78
|
+
const line = head.split('\n').find((l) => l.includes('session_meta'));
|
|
79
|
+
if (!line) return null;
|
|
80
|
+
try {
|
|
81
|
+
const o = JSON.parse(line);
|
|
82
|
+
return o.payload?.cwd ?? o.cwd ?? null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Every *.jsonl under a Codex sessions dir whose recorded cwd matches this
|
|
89
|
+
// project. Bounded head-reads only; returns [] if the sessions dir is absent.
|
|
90
|
+
export function codexFilesForCwd(sessionsDir, cwd, { read = readHead } = {}) {
|
|
91
|
+
if (!existsSync(sessionsDir)) return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
const walk = (dir) => {
|
|
94
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
95
|
+
const p = join(dir, e.name);
|
|
96
|
+
if (e.isDirectory()) walk(p);
|
|
97
|
+
else if (e.name.endsWith('.jsonl') && codexFileCwd(p, read) === cwd) out.push(p);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(sessionsDir);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Prefer the pinned ccusage dependency (offline, version-locked); fall back to
|
|
105
|
+
// npx when resolution fails (e.g. a hoisting layout we can't see). Mirrors
|
|
106
|
+
// init.mjs's skillsInvocation so the two behave the same across install shapes.
|
|
107
|
+
export function ccusageInvocation(pkgRoot, args) {
|
|
108
|
+
try {
|
|
109
|
+
const require = createRequire(join(pkgRoot, 'package.json'));
|
|
110
|
+
const manifest = require('ccusage/package.json');
|
|
111
|
+
const rel = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin.ccusage;
|
|
112
|
+
const entry = join(dirnameOf(require.resolve('ccusage/package.json')), rel);
|
|
113
|
+
return { command: process.execPath, args: [entry, ...args] };
|
|
114
|
+
} catch {
|
|
115
|
+
return {
|
|
116
|
+
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
117
|
+
args: ['--yes', 'ccusage@latest', ...args],
|
|
118
|
+
shell: process.platform === 'win32',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function dirnameOf(file) {
|
|
124
|
+
return file.slice(0, Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\')));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Materialise one source file into the scoped tree. Hardlink first — it is
|
|
128
|
+
// instant, uses no extra disk, and (unlike a symlink or junction) is a real
|
|
129
|
+
// directory entry that ccusage's globber traverses on Windows, where symlinked
|
|
130
|
+
// and junctioned dirs are silently skipped. Copy is the cross-volume fallback
|
|
131
|
+
// (hardlinks can't span drives). Creates parent dirs as needed.
|
|
132
|
+
function linkFile(src, dst) {
|
|
133
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
134
|
+
try {
|
|
135
|
+
linkSync(src, dst);
|
|
136
|
+
} catch {
|
|
137
|
+
copyFileSync(src, dst);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Every *.jsonl under a dir, as [absolutePath, relativePath] pairs so the scoped
|
|
142
|
+
// copy can preserve Claude's per-project subdir layout.
|
|
143
|
+
function jsonlFiles(dir, base = dir, out = []) {
|
|
144
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
145
|
+
const p = join(dir, e.name);
|
|
146
|
+
if (e.isDirectory()) jsonlFiles(p, base, out);
|
|
147
|
+
else if (e.name.endsWith('.jsonl')) out.push([p, relative(base, p)]);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Build a throwaway config root scoped to this project, for both agents. Always
|
|
153
|
+
// returns { root, env, cleanup }: env carries CLAUDE_CONFIG_DIR + CODEX_HOME
|
|
154
|
+
// pointing at the scoped roots, so unset agents report zero rather than leaking
|
|
155
|
+
// global data. `found` says whether any agent had data for this project.
|
|
156
|
+
//
|
|
157
|
+
// Files are hardlinked (see linkFile) rather than the dir symlinked, because
|
|
158
|
+
// ccusage does not traverse symlinked/junctioned directories on Windows.
|
|
159
|
+
export function buildScopedEnv(
|
|
160
|
+
{ claudeDir, codexSessionsDir: codexDir, cwd },
|
|
161
|
+
{ mkdtemp = defaultMkdtemp, linkFn = linkFile, codexFiles = codexFilesForCwd } = {},
|
|
162
|
+
) {
|
|
163
|
+
const root = mkdtemp();
|
|
164
|
+
|
|
165
|
+
// Claude: hardlink this project's transcripts under projects/proj/, keeping
|
|
166
|
+
// any subdir structure so ccusage sees them exactly as it would in place.
|
|
167
|
+
const claudeConfig = join(root, 'claude');
|
|
168
|
+
const claudeProj = join(claudeConfig, 'projects', 'proj');
|
|
169
|
+
mkdirSync(claudeProj, { recursive: true });
|
|
170
|
+
let found = false;
|
|
171
|
+
if (claudeDir && existsSync(claudeDir)) {
|
|
172
|
+
for (const [abs, rel] of jsonlFiles(claudeDir)) {
|
|
173
|
+
linkFn(abs, join(claudeProj, rel));
|
|
174
|
+
found = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Codex: hardlink only the session files whose cwd matches this project,
|
|
179
|
+
// flattened under sessions/ (ccusage globs recursively, so layout is moot).
|
|
180
|
+
const codexHome = join(root, 'codex');
|
|
181
|
+
const codexSessions = join(codexHome, 'sessions');
|
|
182
|
+
mkdirSync(codexSessions, { recursive: true });
|
|
183
|
+
const matches = codexDir ? codexFiles(codexDir, cwd) : [];
|
|
184
|
+
matches.forEach((f, i) => linkFn(f, join(codexSessions, `${i}-${basename(f)}`)));
|
|
185
|
+
if (matches.length > 0) found = true;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
root,
|
|
189
|
+
found,
|
|
190
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeConfig, CODEX_HOME: codexHome },
|
|
191
|
+
cleanup: () => rmSync(root, { recursive: true, force: true }),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function defaultMkdtemp() {
|
|
196
|
+
return mkdtempSync(join(tmpdir(), 'comate-usage-'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Split comate's own flags (--global) from everything else, which is forwarded
|
|
200
|
+
// to ccusage untouched. Default subcommand is `session` — the per-conversation
|
|
201
|
+
// view is what "how much did this project cost" usually means.
|
|
202
|
+
export function parseArgs(argv) {
|
|
203
|
+
const global = argv.includes('--global');
|
|
204
|
+
const rest = argv.filter((a) => a !== '--global');
|
|
205
|
+
const hasCommand = rest.length > 0 && !rest[0].startsWith('-');
|
|
206
|
+
const forwarded = hasCommand ? rest : ['session', ...rest];
|
|
207
|
+
return { global, forwarded };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function usage(argv, { pkgRoot = defaultPkgRoot() } = {}) {
|
|
211
|
+
const { global, forwarded } = parseArgs(argv);
|
|
212
|
+
|
|
213
|
+
let scoped = null;
|
|
214
|
+
let env = process.env;
|
|
215
|
+
if (!global) {
|
|
216
|
+
const cwd = process.cwd();
|
|
217
|
+
scoped = buildScopedEnv({
|
|
218
|
+
claudeDir: claudeDataDir(cwd),
|
|
219
|
+
codexSessionsDir: codexSessionsDir(),
|
|
220
|
+
cwd,
|
|
221
|
+
});
|
|
222
|
+
if (!scoped.found) {
|
|
223
|
+
scoped.cleanup();
|
|
224
|
+
console.error(
|
|
225
|
+
'本项目暂无 Claude Code / Codex 会话记录。\n' +
|
|
226
|
+
'用 comate usage --global 查看全部项目的用量。',
|
|
227
|
+
);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
env = scoped.env;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const { command, args, shell } = ccusageInvocation(pkgRoot, forwarded);
|
|
235
|
+
const { status, error } = spawnSync(command, args, {
|
|
236
|
+
stdio: 'inherit',
|
|
237
|
+
env,
|
|
238
|
+
...(shell ? { shell: true } : {}),
|
|
239
|
+
});
|
|
240
|
+
if (error) {
|
|
241
|
+
console.error(`无法运行 ccusage:${error.message}\n试试 npx ccusage@latest`);
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
return status ?? 1;
|
|
245
|
+
} finally {
|
|
246
|
+
scoped?.cleanup();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function defaultPkgRoot() {
|
|
251
|
+
return dirnameOf(dirnameOf(import.meta.url.replace(/^file:\/\/\/?/, '')));
|
|
252
|
+
}
|
package/docs/COMATE_MANUAL.md
CHANGED
|
@@ -43,7 +43,7 @@ comate 是一个规约驱动、AI 辅助的开发编排器。它将 **OpenSpec**
|
|
|
43
43
|
|
|
44
44
|
流程链:(gate ->) open -> design -> build -> verify -> archive
|
|
45
45
|
|
|
46
|
-
**阶段 0(可选,PRD 门禁与拆分)**:需求来自一份**原始 PRD**时,先过 `/comate-gate`:审阅 PRD 质量、百分制评分、建阻断台账,门禁通过前禁止建任何 change;通过后按依赖/整体性/独立性/规模把 PRD 拆成 1 或 N 个 change(粒度由用户定),逐个交给 `/comate-open`。门禁状态钉在 PRD 同级
|
|
46
|
+
**阶段 0(可选,PRD 门禁与拆分)**:需求来自一份**原始 PRD**时,先过 `/comate-gate`:审阅 PRD 质量、百分制评分、建阻断台账,门禁通过前禁止建任何 change;通过后按依赖/整体性/独立性/规模把 PRD 拆成 1 或 N 个 change(粒度由用户定),逐个交给 `/comate-open`。门禁状态钉在 PRD 同级 `PRD-<NNN>.gate.yaml`(如 `PRD-001.gate.yaml`,机器真相源),评审报告为 `PRD-<NNN>-REVIEW.md`(如 `PRD-001-REVIEW.md`,人类可读)。PRD 通过门禁后被改会因哈希漂移自动失效(stale),需重跑 gate。需求来自对话时无需此阶段。
|
|
47
47
|
|
|
48
48
|
单次 `/comate` 调用从检测到的阶段开始,当退出条件满足时推进到下一阶段。智能体会自动推进后续阶段,但必须在决策阻塞点暂停,直到收到用户确认。
|
|
49
49
|
|
|
@@ -72,6 +72,8 @@ comate 是一个规约驱动、AI 辅助的开发编排器。它将 **OpenSpec**
|
|
|
72
72
|
| `/comate-grill` | 工具 | Claude | 决策台账(逐决策点澄清,附推荐 + 用户答案) |
|
|
73
73
|
| `/comate-kb-integration` | 工具 | Claude | 第三方对接档案(从官方文档 URL 生成 integrations 目录档案 + 索引行) |
|
|
74
74
|
|
|
75
|
+
此外,终端命令 `comate usage` 查看当前项目的 token 用量与估算成本(见 §2.7)。
|
|
76
|
+
|
|
75
77
|
---
|
|
76
78
|
|
|
77
79
|
## 2. 安装与前置条件
|
|
@@ -172,6 +174,29 @@ skills/
|
|
|
172
174
|
|
|
173
175
|
---
|
|
174
176
|
|
|
177
|
+
### 2.7 Token 用量:`comate usage`
|
|
178
|
+
|
|
179
|
+
终端命令,查看**当前项目**的 token 消耗与估算成本。封装 [ccusage](https://github.com/ryoppippi/ccusage)——它作为 comate 的依赖随包安装,`comate doctor` 会检测其可用性(缺失时回退 `npx ccusage@latest`)。
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
comate usage # 本项目按会话(session)统计(默认)
|
|
183
|
+
comate usage daily # 本项目按天
|
|
184
|
+
comate usage --global # 全部项目,不做项目隔离
|
|
185
|
+
comate usage codex daily # 单看某个 agent
|
|
186
|
+
comate usage --json -s 2026-08-01 # 其余参数原样透传给 ccusage
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**项目隔离原理**:ccusage 默认全局聚合、无项目过滤。comate 在临时目录里为当前项目重建一个 scoped 配置根,用 `CLAUDE_CONFIG_DIR` + `CODEX_HOME` 指过去,命令结束即清理:
|
|
190
|
+
|
|
191
|
+
- **Claude Code**:数据本就按 `~/.claude/projects/<编码 cwd>/` 分目录,硬链接本项目那一个目录的 `*.jsonl` 进 scoped 根。
|
|
192
|
+
- **Codex**:数据按日期平铺(`~/.codex/sessions/YYYY/MM/DD/`),项目路径藏在每个文件的 `session_meta.cwd`,故按 `cwd` 过滤后硬链接命中的文件。
|
|
193
|
+
|
|
194
|
+
用硬链接而非目录 symlink/junction:Windows 下 ccusage 的文件扫描不遍历 symlink/junction 目录,硬链接则表现为真实文件。硬链接跨盘失败时回退复制。两个 agent 的 scoped 根都会设,未命中的 agent 报 0 而非泄漏全局数据。
|
|
195
|
+
|
|
196
|
+
**边界**:粒度到项目/会话级,不做 open/design/build 阶段级归因(需在状态机流转处打时间戳,属后续增强)。
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
175
200
|
## 3. 快速开始
|
|
176
201
|
|
|
177
202
|
### 3.1 基本用法
|
|
@@ -561,7 +586,7 @@ bash "$COMATE" doctor
|
|
|
561
586
|
|
|
562
587
|
### gate - PRD 门禁(阶段 0)
|
|
563
588
|
|
|
564
|
-
对**原始 PRD** 做质量门禁,通过前禁止从该 PRD 建 change。状态钉在 PRD 同级
|
|
589
|
+
对**原始 PRD** 做质量门禁,通过前禁止从该 PRD 建 change。状态钉在 PRD 同级 `PRD-<NNN>.gate.yaml`(如 `PRD-001.gate.yaml`,独立于 change 的 `.comate.yaml`),评审报告为 `PRD-<NNN>-REVIEW.md`(如 `PRD-001-REVIEW.md`)。由 `/comate-gate` 驱动。
|
|
565
590
|
|
|
566
591
|
| 命令 | 描述 |
|
|
567
592
|
|---------|-------------|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@house365/comate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "comate —— OpenSpec + Superpowers 规约驱动的 AI 开发编排器(Agent Skills)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,11 +41,12 @@
|
|
|
41
41
|
"node": ">=20"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"ccusage": "^20.0.19",
|
|
44
45
|
"skills": "^1.5.21"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"postinstall": "node bin/postinstall.mjs",
|
|
48
|
-
"test": "node bin/init.test.mjs && bash skills/comate/tests/run.sh",
|
|
49
|
+
"test": "node bin/init.test.mjs && node bin/usage.test.mjs && bash skills/comate/tests/run.sh",
|
|
49
50
|
"doctor": "cd skills && bash comate/scripts/comate.sh doctor",
|
|
50
51
|
"prepublish-check": "node scripts/prepublish-check.mjs",
|
|
51
52
|
"pack-check": "node scripts/pack-check.mjs",
|
|
@@ -460,23 +460,40 @@ declare -A GATE_ENUMS=(
|
|
|
460
460
|
[gate_status]="reviewing blocked passed passed-with-tbd stale"
|
|
461
461
|
)
|
|
462
462
|
|
|
463
|
+
# gate_stem_for <prd> — derive the short stem used to name a PRD's gate.yaml
|
|
464
|
+
# and review report. A conforming PRD file is `PRD-<NNN>-<title>.md`, whose
|
|
465
|
+
# gate/report drop the title and key off `PRD-<NNN>` (so one dir with
|
|
466
|
+
# PRD-001/PRD-002 gets PRD-001.gate.yaml / PRD-002.gate.yaml). Non-conforming
|
|
467
|
+
# names (no PRD-NNN prefix) fall back to the full basename so callers still get
|
|
468
|
+
# a stable, collision-free stem instead of an error.
|
|
469
|
+
gate_stem_for() {
|
|
470
|
+
local base
|
|
471
|
+
base=$(basename "$1")
|
|
472
|
+
base="${base%.markdown}"
|
|
473
|
+
base="${base%.md}"
|
|
474
|
+
if [[ "$base" =~ ^([Pp][Rr][Dd]-[0-9]+) ]]; then
|
|
475
|
+
echo "${BASH_REMATCH[1]}"
|
|
476
|
+
else
|
|
477
|
+
echo "$base"
|
|
478
|
+
fi
|
|
479
|
+
}
|
|
480
|
+
|
|
463
481
|
# gate_path_for <prd> — echo the gate.yaml path adjacent to the PRD.
|
|
464
|
-
#
|
|
482
|
+
# Named `<stem>.gate.yaml` where stem is PRD-<NNN> (see gate_stem_for).
|
|
465
483
|
gate_path_for() {
|
|
466
|
-
local prd="$1"
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
echo "${
|
|
484
|
+
local prd="$1" dir stem
|
|
485
|
+
dir=$(dirname "$prd")
|
|
486
|
+
stem=$(gate_stem_for "$prd")
|
|
487
|
+
echo "${dir}/${stem}.gate.yaml"
|
|
470
488
|
}
|
|
471
489
|
|
|
472
490
|
# gate_report_for <prd> — echo the review report path (PRD same dir).
|
|
491
|
+
# Named `<stem>-REVIEW.md` where stem is PRD-<NNN> (see gate_stem_for).
|
|
473
492
|
gate_report_for() {
|
|
474
|
-
local prd="$1" dir
|
|
493
|
+
local prd="$1" dir stem
|
|
475
494
|
dir=$(dirname "$prd")
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
base="${base%.md}"
|
|
479
|
-
echo "${dir}/PRD-REVIEW-${base}.md"
|
|
495
|
+
stem=$(gate_stem_for "$prd")
|
|
496
|
+
echo "${dir}/${stem}-REVIEW.md"
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
# gate_yaml_set <file> <key> <value> — gate-file analogue of yaml_set: validate
|
|
@@ -454,13 +454,25 @@ seed_prd() {
|
|
|
454
454
|
local prd; prd=$(seed_prd)
|
|
455
455
|
run comate gate "$prd" review
|
|
456
456
|
[ "$status" -eq 0 ]
|
|
457
|
-
[ -f "docs/knowledge-base/requirements/test/PRD-001
|
|
457
|
+
[ -f "docs/knowledge-base/requirements/test/PRD-001.gate.yaml" ]
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
@test "gate names key off PRD-NNN stem, dropping title" {
|
|
461
|
+
local prd; prd=$(seed_prd)
|
|
462
|
+
run comate gate "$prd" review
|
|
463
|
+
[ "$status" -eq 0 ]
|
|
464
|
+
# gate.yaml and review report drop the -demo title, key off PRD-001
|
|
465
|
+
[ -f "docs/knowledge-base/requirements/test/PRD-001.gate.yaml" ]
|
|
466
|
+
[ ! -f "docs/knowledge-base/requirements/test/PRD-001-demo.gate.yaml" ]
|
|
467
|
+
[[ "$output" == *"PRD-001-REVIEW.md"* ]]
|
|
468
|
+
grep -q "review_report: docs/knowledge-base/requirements/test/PRD-001-REVIEW.md" \
|
|
469
|
+
"docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
|
|
458
470
|
}
|
|
459
471
|
|
|
460
472
|
@test "gate review writes reviewing status and pins prd_hash" {
|
|
461
473
|
local prd; prd=$(seed_prd)
|
|
462
474
|
comate gate "$prd" review >/dev/null
|
|
463
|
-
local gate="docs/knowledge-base/requirements/test/PRD-001
|
|
475
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
|
|
464
476
|
run comate gate "$prd" status
|
|
465
477
|
[ "$status" -eq 0 ]
|
|
466
478
|
[[ "$output" == *"reviewing"* ]]
|
|
@@ -502,7 +514,7 @@ seed_prd() {
|
|
|
502
514
|
comate gate "$prd" set score 90 >/dev/null
|
|
503
515
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
504
516
|
printf '# 评审报告\n' > \
|
|
505
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
517
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
506
518
|
run comate gate "$prd" pass
|
|
507
519
|
[ "$status" -eq 0 ]
|
|
508
520
|
run comate gate "$prd" check
|
|
@@ -515,7 +527,7 @@ seed_prd() {
|
|
|
515
527
|
comate gate "$prd" set score 65 >/dev/null
|
|
516
528
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
517
529
|
printf '# 评审报告\n' > \
|
|
518
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
530
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
519
531
|
run comate gate "$prd" pass --tbd
|
|
520
532
|
[ "$status" -eq 0 ]
|
|
521
533
|
run comate gate "$prd" status
|
|
@@ -528,7 +540,7 @@ seed_prd() {
|
|
|
528
540
|
comate gate "$prd" set score 90 >/dev/null
|
|
529
541
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
530
542
|
printf '# 评审报告\n' > \
|
|
531
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
543
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
532
544
|
comate gate "$prd" pass >/dev/null
|
|
533
545
|
# mutate PRD after passing
|
|
534
546
|
printf '\n额外内容\n' >> "$prd"
|
|
@@ -543,14 +555,14 @@ seed_prd() {
|
|
|
543
555
|
comate gate "$prd" set score 90 >/dev/null
|
|
544
556
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
545
557
|
printf '# 评审报告\n' > \
|
|
546
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
558
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
547
559
|
comate gate "$prd" pass >/dev/null
|
|
548
560
|
printf '\n改动\n' >> "$prd"
|
|
549
561
|
run comate gate "$prd" status
|
|
550
562
|
[ "$status" -eq 0 ]
|
|
551
563
|
[[ "$output" == *"stale"* ]]
|
|
552
564
|
# gate.yaml should still say passed (status is computed, not written by status cmd)
|
|
553
|
-
local gate="docs/knowledge-base/requirements/test/PRD-001
|
|
565
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
|
|
554
566
|
local stored; stored=$(grep '^gate_status:' "$gate" | awk '{print $2}')
|
|
555
567
|
[ "$stored" = "passed" ]
|
|
556
568
|
}
|
|
@@ -561,11 +573,11 @@ seed_prd() {
|
|
|
561
573
|
comate gate "$prd" set score 90 >/dev/null
|
|
562
574
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
563
575
|
printf '# 报告\n' > \
|
|
564
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
576
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
565
577
|
comate gate "$prd" pass >/dev/null
|
|
566
578
|
run comate gate "$prd" spawned login-flow
|
|
567
579
|
[ "$status" -eq 0 ]
|
|
568
|
-
local gate="docs/knowledge-base/requirements/test/PRD-001
|
|
580
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
|
|
569
581
|
local sc; sc=$(grep '^spawned_changes:' "$gate" | sed 's/spawned_changes: //')
|
|
570
582
|
[[ "$sc" == *"login-flow"* ]]
|
|
571
583
|
}
|
|
@@ -576,11 +588,11 @@ seed_prd() {
|
|
|
576
588
|
comate gate "$prd" set score 90 >/dev/null
|
|
577
589
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
578
590
|
printf '# 报告\n' > \
|
|
579
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
591
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
580
592
|
comate gate "$prd" pass >/dev/null
|
|
581
593
|
comate gate "$prd" spawned login-flow >/dev/null
|
|
582
594
|
comate gate "$prd" spawned logout-flow >/dev/null
|
|
583
|
-
local gate="docs/knowledge-base/requirements/test/PRD-001
|
|
595
|
+
local gate="docs/knowledge-base/requirements/test/PRD-001.gate.yaml"
|
|
584
596
|
local sc; sc=$(grep '^spawned_changes:' "$gate" | sed 's/spawned_changes: //')
|
|
585
597
|
[[ "$sc" == *"login-flow"* ]]
|
|
586
598
|
[[ "$sc" == *"logout-flow"* ]]
|
|
@@ -605,7 +617,7 @@ seed_prd() {
|
|
|
605
617
|
comate gate "$prd" set score 90 >/dev/null
|
|
606
618
|
comate gate "$prd" set blocking_open 0 >/dev/null
|
|
607
619
|
printf '# 报告\n' > \
|
|
608
|
-
"docs/knowledge-base/requirements/test/PRD-
|
|
620
|
+
"docs/knowledge-base/requirements/test/PRD-001-REVIEW.md"
|
|
609
621
|
comate gate "$prd" pass >/dev/null
|
|
610
622
|
printf '\n改动\n' >> "$prd"
|
|
611
623
|
# re-run review after PRD change — should reset to reviewing
|
|
@@ -17,7 +17,7 @@ description: "comate 阶段 0:PRD 门禁与拆分。用 /comate-gate 调用。
|
|
|
17
17
|
|
|
18
18
|
## 核心铁律
|
|
19
19
|
|
|
20
|
-
- **门禁状态是可验证的文件真相源**:钉在 PRD 同级 `<PRD>.gate.yaml
|
|
20
|
+
- **门禁状态是可验证的文件真相源**:钉在 PRD 同级 `<PRD-NNN>.gate.yaml`(如 `PRD-001.gate.yaml`)。评审报告 `<PRD-NNN>-REVIEW.md`(如 `PRD-001-REVIEW.md`)是给人看的派生物,**不是**状态源。
|
|
21
21
|
- **AskUserQuestion 是真阻塞点**:文字提示、推荐方案、默认值、历史偏好、用户未反对,都**不能**替代明确回答。
|
|
22
22
|
- **只审不改**:门禁只暴露 PRD 问题,不替 PRD 补内容、不替产品拍板。
|
|
23
23
|
- **门禁未通过禁止建 change**:`gate_status ∈ {passed, passed-with-tbd}` 且 PRD 哈希未漂移,才允许 comate-open 建 change(由 `gate check` 硬强制)。
|
|
@@ -59,6 +59,26 @@ bash "$COMATE" gate <prd> check # comate-open 建 change 前的
|
|
|
59
59
|
- 读取后重点提取:**功能清单**(各功能项 + 优先级 P0/P1/P2)、**范围(In/Out)**、**第三方依赖**、**验收标准**、文档状态。
|
|
60
60
|
- 若 PRD 明显未定稿(状态字段非「已定稿」等),用 **AskUserQuestion** 提示可能未冻结并暂停确认;确认前只审阅,不建 gate。
|
|
61
61
|
|
|
62
|
+
### 1.5 命名规范化(前置,定位到 PRD 即检测)
|
|
63
|
+
|
|
64
|
+
用户一旦指到 `docs/knowledge-base/requirements/` 下的文件,**先检测命名是否规范,再进门禁**——因为 gate.yaml 与评审报告的名字都从 PRD 的 `PRD-<NNN>` 前缀派生,名字不规范会连带派生错。comate 只有 PRD 一层(无 MRD/FRD、无固定负责人),只查这三样:
|
|
65
|
+
|
|
66
|
+
| 对象 | 规范 | 反例 → 规范名 |
|
|
67
|
+
|------|------|--------------|
|
|
68
|
+
| 功能目录 | `<YYYYMMDD-功能名>/` | `图片素材管理/` → `20260614-图片素材管理/` |
|
|
69
|
+
| PRD 文件 | 目录内 `PRD-<三位序号>-<功能名>.md`(序号紧跟类型在前) | `图片素材管理_PRD_V1.md` → `PRD-001-图片素材管理.md`;`PRD-1.md` → `PRD-001-图片素材管理.md` |
|
|
70
|
+
| 派生文件 | `PRD-<NNN>.gate.yaml`、`PRD-<NNN>-REVIEW.md`(脚本自动按 PRD-NNN 前缀生成,无需手动命名) | 旧式 `PRD-001-图片素材管理.gate.yaml`、`PRD-REVIEW-PRD-001-图片素材管理.md` → 迁移到新名 |
|
|
71
|
+
|
|
72
|
+
**检测到不规范时**(顺序很重要,避免断链):
|
|
73
|
+
|
|
74
|
+
1. 先 **Grep** 全仓搜旧文件名/旧路径的所有引用(正文、其它文档、`.comate.yaml` 的 `source_manifest`、gate.yaml 的 `prd_path`/`review_report`)。
|
|
75
|
+
2. 用 **AskUserQuestion** 给出「旧名 → 建议新名」让用户确认(改名影响引用,不自作主张;功能名/日期推断不出时一并问)。
|
|
76
|
+
3. 确认后 **`git mv`** 改名(目录、PRD 文件;已存在的旧 gate.yaml/报告一并 `git mv` 到新名),保留 git 历史,**不要** Write 新文件+删旧。
|
|
77
|
+
4. **Edit** 把第 1 步搜到的引用全部改成新名/新路径。
|
|
78
|
+
5. 再 **Grep** 一次旧名确认零残留。
|
|
79
|
+
|
|
80
|
+
已规范或用户确认改名后,才进第 2 步门禁——此时 `gate review` 生成的 gate.yaml/报告自然是新名。若 PRD 名已规范,本步无操作,直接进门禁。
|
|
81
|
+
|
|
62
82
|
### 2. 门禁:质量审阅 + 评分(不可跳过)
|
|
63
83
|
|
|
64
84
|
先初始化 gate(钉当前 PRD 哈希、拿到报告路径):
|
|
@@ -88,7 +108,7 @@ bash "$COMATE" gate <prd> review
|
|
|
88
108
|
- **仓库事实**(已有架构约定、字段、接口、对接档案):先用 Read/Glob/rg 核实并引用来源,不丢给用户决定。
|
|
89
109
|
- **产品决策**(范围、优先级、用户可见行为、状态流转、权限、数据口径、失败处理、验收规则):必须用 **AskUserQuestion** 问用户,不得用推荐方案/历史偏好/「待确认」代替确认。
|
|
90
110
|
|
|
91
|
-
**写评审报告(必生成)** 到 `gate review` 回显的 `review_report` 路径(PRD 同级 `PRD-REVIEW
|
|
111
|
+
**写评审报告(必生成)** 到 `gate review` 回显的 `review_report` 路径(PRD 同级 `<PRD-NNN>-REVIEW.md`,如 `PRD-001-REVIEW.md`)。内容含:评分 + 评级 + 一句话总评、问题台账表(`ID` / 维度 / 严重程度 / 原文依据 / 影响章节 / 是否阻断 / 建议 / 产品回填列)、逐条问题。若无问题,明确写「审阅通过,未发现阻断项」。
|
|
92
112
|
|
|
93
113
|
**阻断规则**:影响范围、用户行为、状态流转、数据契约、权限、异常处理或验收标准的问题标「阻断」。把当前未关闭的阻断问题数写入 gate:
|
|
94
114
|
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
docs/knowledge-base/requirements/
|
|
9
9
|
├─ _templates/ PRD 模板
|
|
10
10
|
└─ <YYYYMMDD-功能名>/ 一个功能一个目录,日期=该功能起始日 ──┐
|
|
11
|
-
├─ PRD-001-<功能名>.md
|
|
12
|
-
├─ PRD-001
|
|
13
|
-
├─ PRD-REVIEW
|
|
11
|
+
├─ PRD-001-<功能名>.md 产品需求文档 │
|
|
12
|
+
├─ PRD-001.gate.yaml 门禁状态(机器真相源) │
|
|
13
|
+
├─ PRD-001-REVIEW.md 门禁评审报告(人类可读) │
|
|
14
14
|
└─ CHANGES-MAP.md (多 change 时)功能项→change 映射 │
|
|
15
15
|
├─→ /comate-gate (门禁+拆分)
|
|
16
16
|
docs/knowledge-base/integrations/ │ ↓
|
|
@@ -24,7 +24,7 @@ comate 采用**简化需求流**:只保留 **PRD**(产品需求文档),通过 `/
|
|
|
24
24
|
1. **门禁**:审阅 PRD 质量、百分制评分、建阻断台账。阻断问题未清零,禁止建任何 change。
|
|
25
25
|
2. **拆分**:按依赖/整体性/独立性/规模判定该 PRD 变成 1 或 N 个 comate change(粒度由用户定),逐个交给 `/comate-open`。
|
|
26
26
|
|
|
27
|
-
PRD 门禁状态钉在同级
|
|
27
|
+
PRD 门禁状态钉在同级 `PRD-<三位序号>.gate.yaml`(机器真相源,含 `gate_status` / `score` / `blocking_open` / `prd_hash` / `spawned_changes`)。评审报告为 `PRD-<三位序号>-REVIEW.md`(人类可读,含问题台账)。PRD 通过门禁后被改会因哈希漂移自动失效(`stale`),需重跑 gate。
|
|
28
28
|
|
|
29
29
|
扇出的 comate change 内部用 proposal / design / tasks 承接 PRD 的功能项内容——**不再产独立 FRD 文档**,PRD 直接作为 change 的源文档被 `sources track` 钉住。
|
|
30
30
|
|
|
@@ -32,8 +32,8 @@ PRD 门禁状态钉在同级 `<PRD名>.gate.yaml`(机器真相源,含 `gate_stat
|
|
|
32
32
|
|
|
33
33
|
- 功能目录: `<YYYYMMDD-功能名>/`,日期为该功能需求起始日,功能名用中文短名,如 `20260805-积分商城/`
|
|
34
34
|
- PRD: 目录内 `PRD-<三位序号>-<功能名>.md`,功能名用目录功能名,如 `PRD-001-积分商城.md`
|
|
35
|
-
- gate: 自动生成,`PRD
|
|
36
|
-
- 评审报告: 自动生成,`PRD
|
|
35
|
+
- gate: 自动生成,`PRD-<三位序号>.gate.yaml`(去 title,只用 PRD-NNN),与 PRD 同级,如 `PRD-001.gate.yaml`
|
|
36
|
+
- 评审报告: 自动生成,`PRD-<三位序号>-REVIEW.md`,与 PRD 同级,如 `PRD-001-REVIEW.md`
|
|
37
37
|
|
|
38
38
|
## 横向 (integrations/)
|
|
39
39
|
|