@house365/comate 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jian Liang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # comate
2
+
3
+ OpenSpec + Superpowers 规约驱动的 AI 开发编排器。
4
+
5
+ comate 把 **OpenSpec**(WHAT:提案、规约生命周期、归档)和 **Superpowers**(HOW:设计、规划、执行、收尾)整合成统一的五阶段工作流。核心原则:每个变更都要经过深度设计(hotfix / tweak 预设除外)。
6
+
7
+ ## 五阶段
8
+
9
+ ```
10
+ /comate ──自动探测阶段──┐
11
+ ▼
12
+ open → design → build → verify → archive
13
+ ```
14
+
15
+ | 阶段 | skill | 职责 |
16
+ |------|-------|------|
17
+ | open | `/comate-open` | 探索想法,创建 change 结构并初始化 `.comate.yaml` |
18
+ | design | `/comate-design` | 技术设计与方案 brainstorming |
19
+ | build | `/comate-build` | 按任务实现并提交 |
20
+ | verify | `/comate-verify` | 验证、出报告、处理分支 |
21
+ | archive | `/comate-archive` | 同步增量规约并归档 |
22
+
23
+ 预设路径:`/comate-hotfix`(热修)、`/comate-tweak`(文案/配置小调整)。
24
+
25
+ ## 安装
26
+
27
+ ```bash
28
+ npx @house365/comate init # 装 comate + 依赖到当前项目 ./.agents/skills/
29
+ npx @house365/comate init -g # 装到用户全局
30
+ ```
31
+
32
+ `init` 会一并装齐两个依赖(OpenSpec CLI、Superpowers),已装的自动跳过,可重复执行。
33
+
34
+ 只装 comate 本身、不碰依赖:
35
+
36
+ ```bash
37
+ npx @house365/comate # 装到当前项目 ./.agents/skills/
38
+ npx @house365/comate -g # 装到用户全局
39
+ npx @house365/comate --list # 只看有哪些 skill,不安装
40
+ ```
41
+
42
+ 需要同时创建 `openspec/` 项目结构时加 `--openspec-init`。
43
+
44
+ 支持 Claude Code、Codex、Cursor 等 70+ agent(底层复用 [`skills`](https://www.npmjs.com/package/skills) CLI,自动探测当前 agent)。
45
+
46
+ 装完跑一次健康检查:
47
+
48
+ ```bash
49
+ bash .agents/skills/comate/scripts/comate.sh doctor
50
+ ```
51
+
52
+ 依赖详情见 [docs/COMATE_MANUAL.md](docs/COMATE_MANUAL.md) 第 2 节。
53
+
54
+ ## 用法
55
+
56
+ 输入 `/comate` 自动探测当前阶段并分派,或直接调用某个阶段 skill。状态记录在每个 change 的 `openspec/changes/<name>/.comate.yaml` 中,支持上下文压缩后断点恢复。
57
+
58
+ ## 目录结构
59
+
60
+ ```
61
+ skills/
62
+ comate/ 主编排器:SKILL.md + 脚本 + 参考文档 + 测试
63
+ scripts/ comate.sh(CLI)、comate-lib.sh(共享库)
64
+ reference/ 决策点、脏工作树协议、前置条件
65
+ tests/ bats 测试
66
+ comate-open/ design/ build/ verify/ archive/ 各阶段 skill
67
+ comate-hotfix/ tweak/ 预设路径 skill
68
+ docs/
69
+ COMATE_MANUAL.md 完整操作手册
70
+ ```
71
+
72
+ ## 文档
73
+
74
+ 完整说明见 [docs/COMATE_MANUAL.md](docs/COMATE_MANUAL.md):安装前置、命令参考、阶段详解、状态文件字段、决策阻塞点、故障排除。
75
+
76
+ ## License
77
+
78
+ [MIT](LICENSE)
package/bin/cli.mjs ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ // comate CLI. Two jobs:
3
+ // init — install comate + its prerequisites (OpenSpec CLI, Superpowers)
4
+ // * — everything else is forwarded to the `skills` CLI, which already
5
+ // handles agent detection, symlinking, update/remove for 70+ agents.
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname } from 'node:path';
8
+
9
+ import { run, skillsInvocation, SKILLS_TIMEOUT_MS } from './init.mjs';
10
+
11
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
12
+ const args = process.argv.slice(2);
13
+
14
+ if (args[0] === 'init') {
15
+ const { init } = await import('./init-run.mjs');
16
+ process.exit(await init(pkgRoot, args.slice(1)));
17
+ }
18
+
19
+ if (args[0] === '--help' || args[0] === '-h') {
20
+ console.log(`comate — OpenSpec + Superpowers 规约驱动的开发编排器
21
+
22
+ 用法:
23
+ comate-skills init [-g] [--openspec-init]
24
+ 安装 comate 及依赖 (OpenSpec CLI、Superpowers)。已装的自动跳过,可重复执行。
25
+ -g, --global 安装到用户全局目录 (默认: 当前项目)
26
+ --openspec-init 同时运行 openspec init 创建项目结构
27
+
28
+ comate-skills [add|list|remove|update ...]
29
+ 直接透传给 skills CLI。不带子命令时安装本包全部 skill。
30
+
31
+ 安装后在 agent 中输入 /comate 开始使用。`);
32
+ process.exit(0);
33
+ }
34
+
35
+ // No args, or only flags (-g, --list, …) = install every comate skill from this
36
+ // package, forwarding the flags. A leading subcommand passes straight through,
37
+ // so remove/list/update all still work.
38
+ const argv =
39
+ args.length && !args[0].startsWith('-') ? args : ['add', pkgRoot, '--skill', '*', ...args];
40
+
41
+ const result = run(skillsInvocation(pkgRoot, argv), SKILLS_TIMEOUT_MS);
42
+ if (!result.ok) {
43
+ console.error(`comate: skills CLI 执行失败 — ${result.reason}`);
44
+ process.exit(1);
45
+ }
@@ -0,0 +1,99 @@
1
+ // The `init` flow: pick a scope, then install what's missing.
2
+ import { existsSync } from 'node:fs';
3
+
4
+ import {
5
+ MIN_OPENSPEC,
6
+ NPM_TIMEOUT_MS,
7
+ SKILLS_TIMEOUT_MS,
8
+ npmInvocation,
9
+ openSpecState,
10
+ run,
11
+ skillsInvocation,
12
+ superpowersInstalled,
13
+ } from './init.mjs';
14
+
15
+ // Same convention as the bare `comate-skills` command: no flag means project,
16
+ // -g means global. No prompt — the flag already says it.
17
+ export function resolveScope(argv) {
18
+ return argv.includes('-g') || argv.includes('--global') ? 'global' : 'project';
19
+ }
20
+
21
+ export async function init(pkgRoot, argv) {
22
+ const scope = resolveScope(argv);
23
+ const withOpenSpecInit = argv.includes('--openspec-init');
24
+ const results = [];
25
+
26
+ console.log(`\n安装范围: ${scope === 'global' ? '全局' : '项目'}\n`);
27
+
28
+ // 1. OpenSpec CLI — a PATH command, so global regardless of skill scope.
29
+ const os = openSpecState();
30
+ if (os.status === 'ready') {
31
+ results.push(['OpenSpec CLI', 'skipped', `已安装 ${os.version}`]);
32
+ } else {
33
+ const label = os.status === 'outdated' ? `升级 (${os.version} < ${MIN_OPENSPEC})` : '安装';
34
+ console.log(`→ ${label} OpenSpec CLI (npm install -g)`);
35
+ const r = run(npmInvocation(['install', '-g', '@fission-ai/openspec@latest']), NPM_TIMEOUT_MS);
36
+ if (!r.ok) {
37
+ // An outdated-but-usable CLI is better than a hard failure.
38
+ const usable = os.status === 'outdated';
39
+ results.push(['OpenSpec CLI', usable ? 'skipped' : 'failed', usable ? `升级失败,沿用 ${os.version}` : r.reason]);
40
+ } else if (openSpecState().status === 'missing') {
41
+ results.push(['OpenSpec CLI', 'failed', 'npm 安装完成但 PATH 中仍找不到 openspec,请重启终端']);
42
+ } else {
43
+ results.push(['OpenSpec CLI', 'installed', '']);
44
+ }
45
+ }
46
+
47
+ // 2. Superpowers — the skills CLI detects the agent itself.
48
+ if (superpowersInstalled(scope)) {
49
+ results.push(['Superpowers', 'skipped', '已安装']);
50
+ } else {
51
+ console.log('\n→ 安装 Superpowers (skills add obra/superpowers)');
52
+ const args = ['add', 'obra/superpowers', '-y', ...(scope === 'global' ? ['-g'] : [])];
53
+ const r = run(skillsInvocation(pkgRoot, args), SKILLS_TIMEOUT_MS);
54
+ // It clones from GitHub, so a bare exit code hides the usual cause: no
55
+ // network. Say so rather than leaving the user with "exit code 1".
56
+ const note = r.ok
57
+ ? ''
58
+ : `${r.reason} — 需要访问 github.com,检查网络/代理后重试`;
59
+ results.push(['Superpowers', r.ok ? 'installed' : 'failed', note]);
60
+ }
61
+
62
+ // 3. comate itself.
63
+ console.log('\n→ 安装 comate skills');
64
+ const comateArgs = ['add', pkgRoot, '--skill', '*', '-y', ...(scope === 'global' ? ['-g'] : [])];
65
+ const r = run(skillsInvocation(pkgRoot, comateArgs), SKILLS_TIMEOUT_MS);
66
+ results.push(['comate', r.ok ? 'installed' : 'failed', r.ok ? '' : r.reason]);
67
+
68
+ // 4. openspec init — opt-in: it writes openspec/ into the user's project and
69
+ // edits agent config files, a bigger side effect than the installs above.
70
+ if (withOpenSpecInit) {
71
+ console.log('\n→ 初始化 OpenSpec 项目结构 (openspec init)');
72
+ const r2 = run({ command: 'openspec', args: ['init', '.'], shell: process.platform === 'win32' }, NPM_TIMEOUT_MS);
73
+ results.push(['openspec init', r2.ok ? 'installed' : 'failed', r2.ok ? '' : r2.reason]);
74
+ } else if (!existsSync('openspec')) {
75
+ results.push(['openspec init', 'skipped', '未初始化项目结构,需要时加 --openspec-init']);
76
+ }
77
+
78
+ return report(results, scope);
79
+ }
80
+
81
+ function report(results, scope) {
82
+ const mark = { installed: '✓', skipped: '·', failed: '✗' };
83
+ console.log('\n' + '─'.repeat(58));
84
+ for (const [name, status, note] of results) {
85
+ console.log(` ${mark[status]} ${name.padEnd(16)} ${status.padEnd(10)} ${note}`);
86
+ }
87
+ console.log('─'.repeat(58));
88
+
89
+ const failed = results.filter(([, s]) => s === 'failed');
90
+ if (failed.length > 0) {
91
+ console.error(`\n${failed.length} 项失败。修复后可重跑 comate-skills init(已安装的会自动跳过)。`);
92
+ return 1;
93
+ }
94
+ const doctorPath =
95
+ scope === 'global' ? '~/.claude/skills/comate/scripts/comate.sh' : '.agents/skills/comate/scripts/comate.sh';
96
+ console.log(`\n完成。健康检查: bash ${doctorPath} doctor`);
97
+ console.log('在 agent 中输入 /comate 开始使用。');
98
+ return 0;
99
+ }
package/bin/init.mjs ADDED
@@ -0,0 +1,159 @@
1
+ // `comate-skills init` — install comate's prerequisites, then comate itself.
2
+ //
3
+ // Three dependencies, each already has an installer we can drive:
4
+ // OpenSpec CLI -> npm install -g @fission-ai/openspec@latest (PATH command,
5
+ // so always global regardless of the chosen skill scope)
6
+ // Superpowers -> npx skills add obra/superpowers (scope-aware)
7
+ // comate -> npx skills add <this package> (scope-aware)
8
+ //
9
+ // ponytail: no SKILLS_AGENT_MAP here. The skills CLI detects the running agent
10
+ // on its own, so translating platform ids to agent names would be dead code.
11
+ import { spawnSync } from 'node:child_process';
12
+ import { existsSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { dirname, join } from 'node:path';
15
+
16
+ const MIN_OPENSPEC = '1.5.0';
17
+ const NPM_TIMEOUT_MS = 180_000;
18
+ const SKILLS_TIMEOUT_MS = 300_000;
19
+
20
+ // Locate npm's own JS entry. Two layouts, and checking only the Windows one
21
+ // silently sent every POSIX run down the shell fallback:
22
+ // Windows <prefix>/node_modules/npm/... (node.exe sits in <prefix>)
23
+ // POSIX <prefix>/lib/node_modules/npm/... (node sits in <prefix>/bin)
24
+ function findNpmCli() {
25
+ const bin = dirname(process.execPath);
26
+ const candidates = [
27
+ join(bin, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
28
+ join(bin, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
29
+ ];
30
+ return candidates.find((p) => existsSync(p)) ?? null;
31
+ }
32
+
33
+ // npm.cmd cannot be spawned directly since Node 20, and shell:true with args is
34
+ // both deprecated (Node 24) and an injection surface. Run npm's own JS entry.
35
+ function npmInvocation(args) {
36
+ const cli = findNpmCli();
37
+ return cli
38
+ ? { command: process.execPath, args: [cli, ...args] }
39
+ : { command: process.platform === 'win32' ? 'npm.cmd' : 'npm', args, shell: true };
40
+ }
41
+
42
+ // Shared by the release scripts: the packed file list straight from npm, so no
43
+ // reimplementation of its `files` glob rules.
44
+ function packedFiles() {
45
+ const argv = ['pack', '--json', '--dry-run', '--ignore-scripts'];
46
+ const { command, args, shell } = npmInvocation(argv);
47
+ const { status, stdout, stderr } = spawnSync(command, args, {
48
+ encoding: 'utf8',
49
+ ...(shell ? { shell: true } : {}),
50
+ });
51
+ if (status !== 0) throw new Error(`npm pack failed:\n${stderr ?? '(no stderr)'}`);
52
+
53
+ // Lifecycle scripts can print ahead of npm's payload, so scan `[` positions
54
+ // left-to-right and take the first that parses as a whole array.
55
+ const clean = stdout.replace(/\[[0-9;]*m/g, '').trim();
56
+ for (let i = clean.indexOf('['); i !== -1; i = clean.indexOf('[', i + 1)) {
57
+ try {
58
+ const [packed] = JSON.parse(clean.slice(i));
59
+ if (Array.isArray(packed?.files)) return packed.files.map((f) => f.path.replace(/\\/g, '/'));
60
+ } catch {
61
+ // Not the payload's opening bracket (e.g. a nested array); keep looking.
62
+ }
63
+ }
64
+ throw new Error(`no usable file list in npm pack output:\n${stdout}`);
65
+ }
66
+
67
+ function run({ command, args, shell }, timeout) {
68
+ const { status, error } = spawnSync(command, args, {
69
+ stdio: 'inherit',
70
+ timeout,
71
+ ...(shell ? { shell: true } : {}),
72
+ });
73
+ if (error) return { ok: false, reason: error.message };
74
+ if (status !== 0) return { ok: false, reason: `exit code ${String(status)}` };
75
+ return { ok: true };
76
+ }
77
+
78
+ function capture(command, args) {
79
+ const { status, stdout: out } = spawnSync(command, args, {
80
+ encoding: 'utf8',
81
+ stdio: ['ignore', 'pipe', 'ignore'],
82
+ timeout: 15_000,
83
+ shell: process.platform === 'win32',
84
+ });
85
+ return status === 0 ? out.trim() : null;
86
+ }
87
+
88
+ // Numeric compare so 1.10.0 sorts above 1.5.0 (string compare gets this wrong).
89
+ function atLeast(version, minimum) {
90
+ const parse = (v) => (v.match(/(\d+)\.(\d+)\.(\d+)/) ?? []).slice(1).map(Number);
91
+ const [a, b] = [parse(version), parse(minimum)];
92
+ if (a.length !== 3) return false;
93
+ for (let i = 0; i < 3; i++) {
94
+ if (a[i] !== b[i]) return a[i] > b[i];
95
+ }
96
+ return true;
97
+ }
98
+
99
+ // Prefer the pinned `skills` dependency (offline, version-locked); fall back to
100
+ // npx when resolution fails (e.g. a hoisting layout we can't see).
101
+ function skillsInvocation(pkgRoot, args) {
102
+ try {
103
+ const require = createRequire(join(pkgRoot, 'package.json'));
104
+ const manifest = require('skills/package.json');
105
+ const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin.skills;
106
+ const entry = join(dirname(require.resolve('skills/package.json')), bin);
107
+ return { command: process.execPath, args: [entry, ...args] };
108
+ } catch {
109
+ return {
110
+ command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
111
+ args: ['--yes', 'skills@latest', ...args],
112
+ shell: process.platform === 'win32',
113
+ };
114
+ }
115
+ }
116
+
117
+ function openSpecState() {
118
+ const version = capture('openspec', ['--version']);
119
+ if (version === null) return { status: 'missing' };
120
+ return { status: atLeast(version, MIN_OPENSPEC) ? 'ready' : 'outdated', version };
121
+ }
122
+
123
+ // Two install shapes to detect:
124
+ // skills CLI — flattens obra/superpowers into one dir per skill, so nothing
125
+ // is literally named "superpowers"; using-superpowers is the
126
+ // entry skill and the stable marker.
127
+ // Claude Code — installs it as a plugin under plugins/{cache,data}/.
128
+ const SUPERPOWERS_MARKERS = [
129
+ ['using-superpowers'],
130
+ ['brainstorming'],
131
+ ['superpowers'],
132
+ ['superpowers-claude-plugins-official'],
133
+ ['cache', 'claude-plugins-official', 'superpowers'],
134
+ ];
135
+
136
+ function superpowersInstalled(scope) {
137
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
138
+ const roots =
139
+ scope === 'global'
140
+ ? ['.agents/skills', '.claude/skills', '.claude/plugins', '.config/skills', '.cursor/skills']
141
+ .map((r) => join(home, r))
142
+ : ['.agents/skills', '.claude/skills', '.claude/plugins', '.cursor/skills', '.codex/skills'];
143
+ return roots.some((root) => SUPERPOWERS_MARKERS.some((m) => existsSync(join(root, ...m))));
144
+ }
145
+
146
+ export {
147
+ MIN_OPENSPEC,
148
+ NPM_TIMEOUT_MS,
149
+ SKILLS_TIMEOUT_MS,
150
+ findNpmCli,
151
+ npmInvocation,
152
+ packedFiles,
153
+ skillsInvocation,
154
+ run,
155
+ capture,
156
+ atLeast,
157
+ openSpecState,
158
+ superpowersInstalled,
159
+ };