@shinaps/skills 0.37.0 → 0.38.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.
Files changed (30) hide show
  1. package/README.md +35 -22
  2. package/dist/bin/skills.js +4 -1
  3. package/dist/lib/config-validation.js +2 -1
  4. package/dist/lib/install.js +1 -0
  5. package/dist/lib/layout.js +12 -0
  6. package/dist/lib/linear-config.js +2 -2
  7. package/dist/lib/managed-ant.js +70 -0
  8. package/dist/lib/managed-config.js +6 -6
  9. package/dist/lib/managed-session.js +274 -0
  10. package/dist/lib/managed-setup.js +167 -0
  11. package/dist/lib/managed.js +249 -0
  12. package/package.json +1 -1
  13. package/plugins/shinaps-claude/.claude-plugin/plugin.json +1 -1
  14. package/plugins/shinaps-claude/managed-agents/agents/shinaps-dev-managed-cc.md +1 -1
  15. package/plugins/shinaps-claude/references/linear/configuration.md +1 -1
  16. package/plugins/shinaps-claude/references/managed/configuration.md +55 -30
  17. package/plugins/shinaps-claude/references/managed/configuration.schema.json +27 -17
  18. package/plugins/shinaps-claude/references/managed/execute.md +38 -74
  19. package/plugins/shinaps-claude/references/managed/setup.md +27 -39
  20. package/plugins/shinaps-claude/skills/managed-execute/SKILL.md +6 -6
  21. package/plugins/shinaps-claude/skills/managed-setup/SKILL.md +6 -7
  22. package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
  23. package/plugins/shinaps-codex/managed-agents/agents/shinaps-dev-managed-cc.md +1 -1
  24. package/plugins/shinaps-codex/references/linear/configuration.md +1 -1
  25. package/plugins/shinaps-codex/references/managed/configuration.md +55 -30
  26. package/plugins/shinaps-codex/references/managed/configuration.schema.json +27 -17
  27. package/plugins/shinaps-codex/references/managed/execute.md +38 -74
  28. package/plugins/shinaps-codex/references/managed/setup.md +27 -39
  29. package/plugins/shinaps-codex/skills/managed-execute/SKILL.md +6 -6
  30. package/plugins/shinaps-codex/skills/managed-setup/SKILL.md +6 -7
@@ -0,0 +1,167 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, mkdir, readdir, readFile, rm } from 'node:fs/promises';
3
+ import { dirname, join, relative, sep } from 'node:path';
4
+ import { errorCode, readJsonIfExists, readTextIfExists, writeFileAtomic } from './fs.js';
5
+ import { withLock } from './lock.js';
6
+ import { managedTemplatePath } from './layout.js';
7
+ import { managedBeta } from './managed-ant.js';
8
+ /** 共通定義の 3 ディレクトリ。`definitions/` には、これと `claude-lock.json` 以外を置かない。 */
9
+ const definitionDirs = ['agents', 'environments', 'skills'];
10
+ const lockName = 'claude-lock.json';
11
+ const upToDate = /Everything is up to date\./;
12
+ /**
13
+ * 1 アカウントへ共通定義を導入・更新する。`apply` が false なら dry-run の計画を表示するだけで、
14
+ * クラウドを変更しない。`--prune`・`--force` は使わない (リソースを削除・上書きしないため)。
15
+ * @param source 同梱の共通定義を含むパッケージのルート。
16
+ * @returns 計画だけを示した (`planned`)、適用した (`applied`)、変更が無かった (`up-to-date`)。
17
+ */
18
+ export async function setupAccount(account, options) {
19
+ const { ant, log } = options;
20
+ const definitions = join(account.dir, 'definitions');
21
+ // ant は --lock-file が無いと lock を cwd から上へ探す。definitions に lock が無いとき祖先の lock を
22
+ // 拾うと、別の組織や構成の lock で対応づけてしまうので、definitions 直下の lock を常に指定する
23
+ // (指定した lock が無ければ ant はそこへ新しく作る。キーは lock のディレクトリ基準で変わらない)。
24
+ const run = (mode) => ant({
25
+ argv: ['apply', mode, '--lock-file', lockName, '.'],
26
+ apiKey: account.apiKey,
27
+ cwd: definitions,
28
+ });
29
+ return withLock(account.dir, async () => {
30
+ await syncDefinitions(definitions, join(options.source, managedTemplatePath), join(account.dir, 'definitions.json'));
31
+ if ((await readTextIfExists(join(definitions, lockName))) === null) {
32
+ await refuseDuplicates(account, definitions, ant);
33
+ }
34
+ const plan = await run('--dry-run');
35
+ log(plan.trimEnd());
36
+ // dry-run は適用不能な計画でも終了コード 0 で返るので、計画の文言で判定する。
37
+ if (/This plan cannot be applied/.test(plan)) {
38
+ throw new Error(`The plan for account ${account.name} cannot be applied. See the plan above.`);
39
+ }
40
+ if (upToDate.test(plan))
41
+ return 'up-to-date';
42
+ if (!options.apply)
43
+ return 'planned';
44
+ log((await run('--yes')).trimEnd());
45
+ if (!upToDate.test(await run('--dry-run'))) {
46
+ throw new Error(`Account ${account.name} still has pending changes after apply. Run setup again without --apply to see them.`);
47
+ }
48
+ return 'applied';
49
+ });
50
+ }
51
+ /**
52
+ * definitions の3ディレクトリを同梱の定義に揃える。書き換え・削除するのは、setup が最後に書いた
53
+ * 内容 (`definitions.json` の記録) か今回書く内容と一致するファイルだけ。利用者の編集・追加や、
54
+ * 記録の無い既存ファイル (旧版の定義ごと複写した場合など) があれば、何も変えずに止める。apply は
55
+ * ディレクトリ内の定義をすべて送るので、利用者の変更を黙って送ることも失うこともしないため。
56
+ * definitions には同梱の定義だけを置く。利用者の変更は取り込まず、止めて退避してもらう。
57
+ * 途中で止まった同期は、各ファイルが記録か今回のどちらかに一致するので、再実行で続きから揃う。
58
+ * @param record 最後に書いたファイルの相対パス → sha256。apply が送らないよう definitions の外に置く。
59
+ */
60
+ async function syncDefinitions(definitions, template, record) {
61
+ await mkdir(definitions, { recursive: true });
62
+ const unexpected = (await readdir(definitions)).filter((name) => name !== lockName && !definitionDirs.includes(name));
63
+ const current = await hashTree(definitions, unexpected);
64
+ const next = await hashTree(template, []);
65
+ const written = (await readJsonIfExists(record)) ?? {};
66
+ // 無いファイルは変更にしない。同梱の定義を書き直すだけで利用者の内容は失われず、停止して退避を
67
+ // 求めると、退避した後の再実行も同じ理由で止まり続けるため。
68
+ const changed = Object.keys(current)
69
+ .filter((path) => current[path] !== written[path] && current[path] !== next[path])
70
+ .sort();
71
+ if (changed.length) {
72
+ throw new Error(`Files in ${definitions} differ from what setup wrote: ${changed.join(', ')}. Setup does not overwrite, delete or apply them; it applies only the bundled definitions. Move them out of ${definitions} (keep ${lockName}), then retry and review the plan: it reverts cloud resources that were changed to match the bundled definitions.`);
73
+ }
74
+ for (const [path, hash] of Object.entries(next)) {
75
+ if (current[path] === hash)
76
+ continue;
77
+ await mkdir(dirname(join(definitions, path)), { recursive: true });
78
+ await writeFileAtomic(join(definitions, path), await readFile(join(template, path), 'utf8'));
79
+ }
80
+ for (const path of Object.keys(current).filter((path) => !(path in next))) {
81
+ await rm(join(definitions, path));
82
+ }
83
+ await removeEmptyDirectories(definitions);
84
+ await writeFileAtomic(record, JSON.stringify(next, null, 2) + '\n');
85
+ }
86
+ /**
87
+ * 3ディレクトリ配下の通常ファイルの相対パス → sha256。通常ファイル以外 (シンボリックリンクなど) と
88
+ * `extra` の項目は、内容を比べられない変更として値を空にする (記録・同梱のどちらとも一致しない)。
89
+ */
90
+ async function hashTree(root, extra) {
91
+ const result = Object.fromEntries(extra.map((name) => [name, '']));
92
+ for (const directory of definitionDirs) {
93
+ const kind = await lstat(join(root, directory)).catch((error) => {
94
+ if (errorCode(error) === 'ENOENT')
95
+ return null;
96
+ throw error;
97
+ });
98
+ if (!kind)
99
+ continue;
100
+ // 3ディレクトリ自体がシンボリックリンクなどなら、辿るとリンク先へ書き込むので変更として止める。
101
+ if (!kind.isDirectory()) {
102
+ result[directory] = '';
103
+ continue;
104
+ }
105
+ const entries = await readdir(join(root, directory), { recursive: true, withFileTypes: true });
106
+ for (const entry of entries) {
107
+ if (entry.isDirectory())
108
+ continue;
109
+ const path = relative(root, join(entry.parentPath, entry.name)).split(sep).join('/');
110
+ result[path] = entry.isFile()
111
+ ? createHash('sha256')
112
+ .update(await readFile(join(root, path)))
113
+ .digest('hex')
114
+ : '';
115
+ }
116
+ }
117
+ return result;
118
+ }
119
+ // 定義を消した後に残る空のディレクトリを消す。中身の無い skill のディレクトリを apply に渡さないため。
120
+ async function removeEmptyDirectories(definitions) {
121
+ for (const directory of definitionDirs) {
122
+ const root = join(definitions, directory);
123
+ const nested = (await readdir(root, { recursive: true, withFileTypes: true }))
124
+ .filter((entry) => entry.isDirectory())
125
+ .map((entry) => join(entry.parentPath, entry.name))
126
+ .sort((a, b) => b.length - a.length);
127
+ for (const path of nested) {
128
+ if ((await readdir(path)).length === 0)
129
+ await rm(path, { recursive: true });
130
+ }
131
+ }
132
+ }
133
+ // lock が無いと apply は既存のリソースと対応づかず、同じ名前で新しく作る。lock の hash は既存の
134
+ // リソースから作り直せないので、同名のリソースがあれば作らずに止め、lock を戻してもらう。
135
+ async function refuseDuplicates(account, definitions, ant) {
136
+ const names = new Set();
137
+ for (const directory of ['agents', 'environments']) {
138
+ for (const file of await readdir(join(definitions, directory))) {
139
+ const name = (await readFile(join(definitions, directory, file), 'utf8')).match(/^name: (.+)$/m)?.[1];
140
+ if (name)
141
+ names.add(name.trim());
142
+ }
143
+ }
144
+ const existing = [];
145
+ for (const resource of ['beta:agents', 'beta:environments']) {
146
+ const output = await ant({
147
+ argv: [
148
+ resource,
149
+ 'list',
150
+ ...managedBeta,
151
+ '--format',
152
+ 'jsonl',
153
+ '--transform',
154
+ '{name,archived_at}',
155
+ ],
156
+ apiKey: account.apiKey,
157
+ });
158
+ for (const line of output.split('\n').filter((text) => text.trim())) {
159
+ const item = JSON.parse(line);
160
+ if (item.name && names.has(item.name) && !item.archived_at)
161
+ existing.push(item.name);
162
+ }
163
+ }
164
+ if (existing.length) {
165
+ throw new Error(`Account ${account.name} has no lock, but resources with the same names exist: ${[...new Set(existing)].join(', ')}. Applying would create duplicates. If you used Managed Agents before, move the previous claude-lock.json (for example <definitions>/claude-lock.json from .shinaps/managed.json version 1) to ${definitions}/, then retry.`);
166
+ }
167
+ }
@@ -0,0 +1,249 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { errorMessage } from './fs.js';
4
+ import { managedAccountDir, managedRoot, packageRoot } from './layout.js';
5
+ import { antCommand } from './managed-ant.js';
6
+ import { validateManagedConfig } from './managed-config.js';
7
+ import { collectOutputs, permissions, replySession, runSession, sessionStatus, staffings, } from './managed-session.js';
8
+ import { setupAccount } from './managed-setup.js';
9
+ const managedUsage = [
10
+ 'Usage: shinaps-skills managed setup --account <name> [--account <name> ...] [--apply]',
11
+ ' shinaps-skills managed run <managed.json> --account <name> --base <branch> --base-sha <sha> --head <branch> --staffing <LITE|BASIC|ULTRA> --allow <none|commit,push,draft_pr> --budget-cents <n> --task-file <file> [--verify <command>] [--author "<name> <email>"] [--title <title>] [--claude-md <file>]',
12
+ ' shinaps-skills managed status --account <name> --session <id>',
13
+ ' shinaps-skills managed reply --account <name> --session <id> --message-file <file>',
14
+ ' shinaps-skills managed collect --account <name> --session <id>',
15
+ 'API keys: --api-key <name>=<key>, or SHINAPS_MANAGED_API_KEY_<NAME> (prefer the environment variable; arguments are visible to other processes).',
16
+ 'GitHub token for the mounted repository: SHINAPS_MANAGED_GITHUB_TOKEN.',
17
+ ].join('\n');
18
+ const accountName = /^[a-z0-9]+(-[a-z0-9]+)*$/;
19
+ const githubTokenVariable = 'SHINAPS_MANAGED_GITHUB_TOKEN';
20
+ const valueFlags = new Set([
21
+ 'account',
22
+ 'api-key',
23
+ 'base',
24
+ 'base-sha',
25
+ 'head',
26
+ 'staffing',
27
+ 'allow',
28
+ 'budget-cents',
29
+ 'task-file',
30
+ 'verify',
31
+ 'author',
32
+ 'title',
33
+ 'claude-md',
34
+ 'session',
35
+ 'message-file',
36
+ ]);
37
+ const repeatableFlags = new Set(['account', 'api-key']);
38
+ /** アカウント名ごとのキーの環境変数名。名前に `_` を許さないので、名前と一対一に対応する。 */
39
+ const apiKeyVariable = (account) => `SHINAPS_MANAGED_API_KEY_${account.toUpperCase().replaceAll('-', '_')}`;
40
+ /**
41
+ * `shinaps-skills managed <setup|run|status|reply|collect>` を実行する。出力とエラー文から、渡された
42
+ * APIキーと GitHub token の値を伏字にする。
43
+ */
44
+ export async function managed(args, options = {}) {
45
+ const env = options.env ?? process.env;
46
+ const secrets = collectSecrets(args, env);
47
+ const redact = (text) => secrets.reduce((result, secret) => result.replaceAll(secret, '<redacted>'), text);
48
+ const log = (message) => (options.log ?? console.log)(redact(message));
49
+ try {
50
+ await dispatch(args, { ...options, env, log });
51
+ }
52
+ catch (error) {
53
+ throw new Error(redact(errorMessage(error)));
54
+ }
55
+ }
56
+ // 伏字にする値。解決に使うかどうかによらず、引数と環境変数で渡された全てのキーと token を含める。
57
+ function collectSecrets(args, env) {
58
+ const values = Object.entries(env)
59
+ .filter(([name]) => name.startsWith('SHINAPS_MANAGED_'))
60
+ .map(([, value]) => value ?? '');
61
+ args.forEach((arg, index) => {
62
+ if (arg === '--api-key')
63
+ values.push(args[index + 1]?.split('=').slice(1).join('=') ?? '');
64
+ });
65
+ return values.filter((value) => value.length > 0);
66
+ }
67
+ async function dispatch(args, context) {
68
+ const [command, ...rest] = args;
69
+ if (!command || command === '--help' || command === 'help') {
70
+ context.log(managedUsage);
71
+ return;
72
+ }
73
+ const { positional, flags, apply } = parse(rest);
74
+ const home = context.home ?? homedir();
75
+ const ant = context.ant ?? antCommand(context.env);
76
+ const accounts = flags.get('account') ?? [];
77
+ if (command === 'setup') {
78
+ if (positional.length || !accounts.length)
79
+ throw usageError();
80
+ // キーを先に全て解決する。途中のアカウントで欠けて止まると、一部だけが更新された状態になる。
81
+ const resolved = resolveAccounts(accounts, flags, context.env, home);
82
+ const results = [];
83
+ const failures = [];
84
+ for (const account of resolved) {
85
+ try {
86
+ const result = await setupAccount(account, {
87
+ apply,
88
+ ant,
89
+ log: context.log,
90
+ source: context.source ?? packageRoot,
91
+ });
92
+ results.push(result);
93
+ context.log(`Account ${account.name}: ${result}.`);
94
+ }
95
+ catch (error) {
96
+ failures.push(account.name);
97
+ context.log(`Account ${account.name}: failed. ${errorMessage(error)}`);
98
+ }
99
+ }
100
+ if (failures.length)
101
+ throw new Error(`Setup failed for: ${failures.join(', ')}.`);
102
+ if (results.includes('planned')) {
103
+ context.log('No changes were applied. Re-run with --apply after reviewing the plans.');
104
+ }
105
+ return;
106
+ }
107
+ if (apply || accounts.length !== 1)
108
+ throw usageError();
109
+ const [account] = resolveAccounts(accounts, flags, context.env, home);
110
+ if (command === 'run') {
111
+ if (positional.length !== 1)
112
+ throw usageError();
113
+ return run(positional[0], account, flags, context, ant);
114
+ }
115
+ if (positional.length)
116
+ throw usageError();
117
+ const sessionId = required(flags, 'session');
118
+ if (command === 'status') {
119
+ context.log(JSON.stringify(await sessionStatus(account, sessionId, ant), null, 2));
120
+ }
121
+ else if (command === 'reply') {
122
+ await replySession(account, sessionId, await readFile(required(flags, 'message-file'), 'utf8'), ant);
123
+ context.log(`Sent the reply to session ${sessionId}.`);
124
+ }
125
+ else if (command === 'collect') {
126
+ const saved = await collectOutputs(account, sessionId, ant);
127
+ context.log([
128
+ ...saved,
129
+ 'Saved only. Nothing was applied; review a patch with `git apply --check <changes.patch>` in a checkout of the agreed base.',
130
+ ].join('\n'));
131
+ }
132
+ else {
133
+ throw usageError();
134
+ }
135
+ }
136
+ async function run(configPath, account, flags, context, ant) {
137
+ const config = await validateManagedConfig(configPath);
138
+ if (!Object.hasOwn(config.accounts, account.name)) {
139
+ throw new Error(`Account ${account.name} is not listed in ${configPath}.`);
140
+ }
141
+ const settings = config.accounts[account.name];
142
+ const staffing = required(flags, 'staffing');
143
+ if (!staffings.includes(staffing)) {
144
+ throw new Error(`--staffing must be one of ${staffings.join(', ')}.`);
145
+ }
146
+ const allowText = required(flags, 'allow');
147
+ const listed = allowText === 'none' ? [] : allowText.split(',');
148
+ if (listed.some((permission) => !permissions.includes(permission)) ||
149
+ new Set(listed).size !== listed.length) {
150
+ throw new Error(`--allow must be none or a comma-separated subset of ${permissions.join(',')}.`);
151
+ }
152
+ // 並び順だけが違う同じ許可を別の依頼として扱わないよう、正規の順に揃える。
153
+ const allow = permissions.filter((permission) => listed.includes(permission));
154
+ const budget = required(flags, 'budget-cents');
155
+ if (!/^[1-9][0-9]*$/.test(budget))
156
+ throw new Error('--budget-cents must be a positive integer.');
157
+ // 次の 2 つは、セッションを作ってから lead が気づいても予算を消費した後になるので、作る前に止める。
158
+ if (allow.includes('draft_pr') && settings.vault_id === null) {
159
+ throw new Error(`draft_pr needs a vault with the GitHub MCP credential, but account ${account.name} has vault_id null.`);
160
+ }
161
+ const githubToken = context.env[githubTokenVariable] || null;
162
+ if ((allow.includes('push') || allow.includes('draft_pr')) && !githubToken) {
163
+ throw new Error(`push and draft_pr need ${githubTokenVariable} for the mounted repository.`);
164
+ }
165
+ const optional = (flag) => flags.get(flag)?.[0] ?? null;
166
+ const claudeMdPath = optional('claude-md');
167
+ const request = {
168
+ account: account.name,
169
+ repository: config.repository,
170
+ vault_id: settings.vault_id,
171
+ base: required(flags, 'base'),
172
+ base_sha: required(flags, 'base-sha'),
173
+ head: required(flags, 'head'),
174
+ staffing: staffing,
175
+ allow,
176
+ budget_cents: budget,
177
+ task: await readFile(required(flags, 'task-file'), 'utf8'),
178
+ verify: optional('verify'),
179
+ author: optional('author'),
180
+ title: optional('title'),
181
+ claude_md: claudeMdPath === null ? null : await readFile(claudeMdPath, 'utf8'),
182
+ };
183
+ const sessionId = await runSession(account, request, {
184
+ ant,
185
+ log: context.log,
186
+ githubToken,
187
+ claudeMdPath,
188
+ now: context.now ?? (() => new Date()),
189
+ });
190
+ context.log(`Observe: shinaps-skills managed status --account ${account.name} --session ${sessionId}`);
191
+ }
192
+ function resolveAccounts(names, flags, env, home) {
193
+ const keys = new Map();
194
+ for (const pair of flags.get('api-key') ?? []) {
195
+ const separator = pair.indexOf('=');
196
+ if (separator <= 0)
197
+ throw new Error('--api-key expects <account>=<key>.');
198
+ keys.set(pair.slice(0, separator), pair.slice(separator + 1));
199
+ }
200
+ const unique = [...new Set(names)];
201
+ const invalid = unique.filter((name) => !accountName.test(name));
202
+ if (invalid.length) {
203
+ throw new Error(`Invalid account name: ${invalid.join(', ')}. Use lowercase letters, digits and single hyphens.`);
204
+ }
205
+ const missing = unique.filter((name) => !(keys.get(name) || env[apiKeyVariable(name)]));
206
+ if (missing.length) {
207
+ throw new Error(`Missing API key for ${missing.map((name) => `${name} (--api-key ${name}=<key> or ${apiKeyVariable(name)})`).join(', ')}.`);
208
+ }
209
+ return unique.map((name) => ({
210
+ name,
211
+ apiKey: keys.get(name) || env[apiKeyVariable(name)],
212
+ dir: managedAccountDir(managedRoot(home), name),
213
+ }));
214
+ }
215
+ // 値を取るフラグは `--name value` の形だけを受け付ける。エラー文には引数の値を含めない (キーを
216
+ // 含みうるため)。
217
+ function parse(args) {
218
+ const positional = [];
219
+ const flags = new Map();
220
+ let apply = false;
221
+ for (let index = 0; index < args.length; index++) {
222
+ const arg = args[index];
223
+ if (!arg.startsWith('--')) {
224
+ positional.push(arg);
225
+ continue;
226
+ }
227
+ const name = arg.slice(2);
228
+ if (name === 'apply') {
229
+ apply = true;
230
+ continue;
231
+ }
232
+ const value = args[index + 1];
233
+ if (!valueFlags.has(name) || value === undefined)
234
+ throw usageError();
235
+ const values = flags.get(name) ?? [];
236
+ if (values.length && !repeatableFlags.has(name))
237
+ throw usageError();
238
+ flags.set(name, [...values, value]);
239
+ index++;
240
+ }
241
+ return { positional, flags, apply };
242
+ }
243
+ function required(flags, name) {
244
+ const value = flags.get(name)?.[0];
245
+ if (!value)
246
+ throw new Error(`Missing --${name}. See shinaps-skills managed --help.`);
247
+ return value;
248
+ }
249
+ const usageError = () => new Error('Invalid arguments for shinaps-skills managed. See shinaps-skills managed --help.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shinaps/skills",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "description": "Install Shinaps AI workflows and custom agents for Codex",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shinaps",
3
3
  "description": "Shinaps AIの開発ワークフロー。プロジェクトの指示から条件を読み、調査・設計・実装・簡素化・レビュー・コミット・PR・リリースとLinearでの開発進行を支援する。",
4
- "version": "0.37.0",
4
+ "version": "0.38.0",
5
5
  "author": {
6
6
  "name": "shinaps",
7
7
  "email": "ryuta@shinaps.jp"
@@ -2,7 +2,7 @@
2
2
  name: shinaps-dev-managed-cc
3
3
  description: shinaps の dev 工程を Claude Managed Agents で進める lead。メインとして調査・設計・実装・指摘の採否を行い、選んだ体制の独立した担当に別案と独立レビューを委任し(BASIC・ULTRAでは追加の担当を判断する)、draft PR まで進める。
4
4
  model:
5
- id: claude-fable-5-1
5
+ id: claude-opus-5-5
6
6
  effort: medium
7
7
  speed: standard
8
8
  mcp_servers:
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 検証CLIと契約版
6
6
 
7
- 利用中のskillとCLIの版を確認し、0.37.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.37.0`)。pluginだけにはCLIは含まれない。導入・更新が必要ならその権限を確認し、検証のたびに `npx latest` を取得しない。
7
+ 利用中のskillとCLIの版を確認し、0.38.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.38.0`)。pluginだけにはCLIは含まれない。導入・更新が必要ならその権限を確認し、検証のたびに `npx latest` を取得しない。
8
8
 
9
9
  0.35.0から通常設定の `review_flow.mode` に `ask` を受理する。`config_version` は2のままで、既存のauto・manualの設定は変更不要。0.33.0〜0.34.xのCLIと `$schema` のURLは `ask` を拒否するため、`ask` を使う設定では `$schema` も0.35.0以降のURLにする。
10
10
 
@@ -1,59 +1,84 @@
1
- # Claude Managed Agents の設定
1
+ # Claude Managed Agents の設定とアカウント
2
2
 
3
- 作業先リポジトリの `.shinaps/managed.json` に置く。`config_version: 1` のJSONで、[JSON Schema](configuration.schema.json)を契約の正本とする。
3
+ Managed Agents は配布CLI `shinaps-skills managed` で導入・依頼・観測・回収する([導入](setup.md)・[依頼と観測](execute.md))。CLI は利用中の skill と同じ版の `@shinaps/skills` を使う。クラウドの操作は公式の `ant` CLI(2.4.0 以上)へ委ね、CLI は PATH 上の `ant` を呼ぶ。CLI が使うコマンド・フラグ・stdin の本文・出力の形は、ant 2.4.0 の公式ソースで照合した(実行は未確認。[未検証の境界](execute.md#未検証の境界))。
4
4
 
5
- 設定が持つのは、共通定義のコピーの置き場所・対象リポジトリ・GitHub MCP の資格情報を登録した vault だけである。許可・予算・Issueの範囲・base と head・終了条件は呼び出しのたびに利用者と合意して決め、設定には書かない。lead・役割・環境のリソース ID は lock から読み、設定には書かない。vault は lock に記録されないため、`vault_id` だけを設定に持つ。秘密値は書かない。schema は未知のキーを拒否するので、許可や予算などのキーを書いた設定は検証で失敗し、許可や予算として扱われることはない。
5
+ ## アカウント
6
6
 
7
- 認証は `ant` の既存のログイン(利用者本人が `ant auth login` を行う)と環境変数、GitHub MCP は vault の資格情報を使う。
7
+ アカウントは、APIキー1つが属する組織・workspace に利用者が付ける名前で、英小文字・数字と単独のハイフン(`^[a-z0-9]+(-[a-z0-9]+)*$`)にする。同じマシンでは、同じ名前は常に同じ組織・workspace を表すようにする。lock がアカウント名で引かれるので、別の組織に同じ名前を使うと別の組織の lock で適用しようとする(`ant apply` は lock と違う組織・workspace の資格情報を拒否して止まる)。
8
8
 
9
- ## 配置
9
+ ### APIキー
10
+
11
+ コマンドごとに、各アカウントのキーを次のどちらかで渡す。
12
+
13
+ - 環境変数 `SHINAPS_MANAGED_API_KEY_<NAME>`(`<NAME>` はアカウント名の英大文字で、`-` は `_`。例 `work-us` → `SHINAPS_MANAGED_API_KEY_WORK_US`)。推奨。
14
+ - 引数 `--api-key <name>=<key>`(環境変数より優先)。引数は同じマシンの他のプロセスやシェルの履歴から見えるので、共有のマシンでは使わない。
15
+
16
+ CLI はキーを保存せず、出力とエラー文では伏字にする。`ant` へは子プロセスの環境変数 `ANTHROPIC_API_KEY` だけで渡し、親の `ANTHROPIC_*`(別のキー・`ant auth login` のプロファイル・workspace の指定)と他アカウントのキーは子へ渡さない。workspace をまたげる資格情報(`--workspace-id` が必要なもの)は扱わないので、workspace に属するキーを使う。
17
+
18
+ ### アカウントの状態
19
+
20
+ 各アカウントの状態は、マシン単位の管理ディレクトリ `~/.local/share/shinaps-ai/skills/managed/<account>/` に置く。lock は組織・workspace に属し、リポジトリには属さないため。複数のリポジトリから同じアカウントを使っても lock は1つになり、clone のやり直し・worktree・`git clean` で失われない。
10
21
 
11
22
  | パス | 内容 |
12
23
  | --- | --- |
13
- | `.shinaps/managed.json` | この設定 |
14
- | `<definitions>/agents/`・`environments/`・`skills/` | plugin の `managed-agents/` からコピーした共通定義。テンプレートの所有で、更新時は丸ごと置き換える |
15
- | `<definitions>/claude-lock.json` | `ant apply` が記録するクラウド上のリソースの ID と版 |
24
+ | `definitions/agents/`・`environments/`・`skills/` | 同梱の共通定義の写し。setup が同梱の定義に揃える |
25
+ | `definitions/claude-lock.json` | `ant apply` が記録するクラウド上の lead・役割・skill・環境の ID と版。setup は読むだけで書き換えない。手で編集しない |
26
+ | `definitions.json` | setup が最後に書いた共通定義の各ファイルのハッシュ。利用者の変更の検出に使う |
27
+ | `sessions/<request_id>.json` | このアカウントで作ったセッションの記録(入力と session ID。秘密値は含まない) |
28
+ | `sessions/<session_id>/outputs/` | 回収した出力ファイル |
29
+ | `update.lock` | 同じアカウントの setup・run を同時に1つにする排他(`ant apply` は lock を排他しないため) |
30
+
31
+ `definitions/` には同梱の共通定義と lock だけを置き、共通定義を手で編集しない。適用はディレクトリ内の定義をすべて送るため。利用者が加えた定義の変更は導入に取り込まない(クラウドへ持ち越さない)。setup は、自分が最後に書いた内容か今回書く内容と一致するファイルだけを書き換え・削除する。利用者の編集・追加、記録の無い既存のファイル(旧版の定義ごと複写した場合など)、通常のファイル・ディレクトリ以外のもの、ほかの項目が一つでもあれば、何も変えずにそのアカウントを失敗にし、該当するパスを示す。それらを `definitions/` の外へ退避してから(lock は残す)再実行すると、同梱の定義を書き直して進む。消えた共通定義のファイルは書き直す。
32
+
33
+ ## lock の扱い
16
34
 
17
- いずれも Git 対象外にする。`.shinaps/.gitignore` が `*` で始まる場合は追加の設定なしで除外される。書き込む前に `git check-ignore --no-index -v <パス>` で除外を確認する。除外されていなければ、[導入の手順](setup.md#1-設定を読む)に従って既存の規則と共有例外を保ったまま `.shinaps/.gitignore` に必要な行だけを加えてから書き込む(新規作成の内容は[作業記録の共通手順](../work-records.md)と同じ)。`<definitions>` をリポジトリの外に置く場合は Git の対象にならないので、この確認は `.shinaps/managed.json` だけに行う。
35
+ - `definitions/claude-lock.json` がクラウド上の ID の正本である。lead の ID は `./agents/shinaps-dev-managed-cc.md`、環境の ID は `./environments/shinaps-dev.yaml` の `id` にある。
36
+ - lock のキーは lock のあるディレクトリからの相対パスである。共通定義を同じ構造で `definitions/` 直下に置き、そこで適用するので、既存のリソースに対応づく。
37
+ - lock が無いと適用は既存のリソースと対応づかず、同じ名前のリソースを重複して作る。lock の `hash` は既存のリソースから作り直せないので、setup は lock が無いアカウントに同名の未アーカイブの lead・役割・環境があれば止まる。バックアップや以前の置き場所から lock を戻して再実行する。見つからなければ適用せず、利用者に相談する。
38
+ - 別のマシンでは lock が無い。同じ組織を別のマシンから導入するときは、元のマシンの lock を同じ名前のアカウントへ複写する。
18
39
 
19
- ## 項目
40
+ ## 作業先の設定(`.shinaps/managed.json`)
20
41
 
21
- 全項目が必須で、`$schema` だけは省略できる。
42
+ `managed run` だけが読む作業先の設定。`config_version: 2` のJSONで、[JSON Schema](configuration.schema.json)を契約の正本とする。持つのは、セッションにマウントするリポジトリと、アカウントごとの GitHub MCP の vault だけである。許可・予算・base と head・範囲・体制は依頼のたびに引数で明示し、設定には書かない。schema は未知のキーを拒否するので、許可や予算のキーを書いた設定は検証で失敗する。lead と環境の ID はアカウントの lock から読む。秘密値は書かない。
22
43
 
23
44
  | キー | 内容 |
24
45
  | --- | --- |
25
46
  | `$schema` | 任意のエディタ補助。CLI はこの URL を取得しない |
26
- | `config_version` | `1` |
27
- | `definitions` | 共通定義のコピーと lock を置くディレクトリ。リポジトリのルートからの相対パス、または絶対パス。clone のやり直し・worktree・`git clean` の影響を避けたい場合は、リポジトリの外の絶対パスにする。同じマシンの同じ組織で複数のリポジトリから使う場合は、全リポジトリの `definitions` に同じ絶対パスを指定し、lock を 1 つにする。別のマシンでは[lock の扱い](#lock-の扱い)に従う |
47
+ | `config_version` | `2` |
28
48
  | `repository.url` | セッションにマウントする GitHub リポジトリの URL |
29
49
  | `repository.mount_path` | セッション内のマウント先(例 `/workspace/widgets`) |
30
- | `vault_id` | GitHub MCP の資格情報を登録した vault の ID。未登録なら `null` |
50
+ | `accounts.<name>.vault_id` | そのアカウントで GitHub MCP の資格情報を登録した vault の ID(vault は組織ごと)。未登録なら `null`。依頼に使うアカウントを全て書く |
31
51
 
32
- ## 検証
52
+ 設定は Git 対象外にする(`.shinaps/.gitignore` が `*` で始まる場合は追加の設定なしで除外される)。書き込む前に `git check-ignore --no-index -v .shinaps/managed.json` で除外を確認し、除外されていなければ既存の規則と共有例外を保ったまま `.shinaps/.gitignore` の末尾に `/managed.json` を加える(新規作成の内容は[作業記録の共通手順](../work-records.md)と同じ)。
33
53
 
34
- 利用中の skill と同じ版の `@shinaps/skills` を導入し、`shinaps-skills validate-managed-config <明示ファイル>` で検証する。ローカル開発では `node bin/skills.ts validate-managed-config <file>` を使える。CLI は指定された一つのJSONを同梱 schema で検証し、ネットワーク取得・設定の探索・Git 操作・入力の変更はしない。成功は構造が正しいことだけを表し、lock や vault がクラウドに存在することは保証しない。
54
+ ### 検証
35
55
 
36
- `config_version` が欠落または旧版なら setup で作り直し、未来版なら skill と CLI を更新する。
56
+ `shinaps-skills validate-managed-config <明示ファイル>` で検証する。ローカル開発では `node bin/skills.ts validate-managed-config <file>` を使える。CLI は指定された一つのJSONを同梱 schema で検証し、ネットワーク取得・設定の探索・Git 操作・入力の変更はしない。成功は構造が正しいことだけを表し、vault やアカウントの lock がクラウドに存在することは保証しない。
37
57
 
38
- ## 設定例
58
+ `config_version` が欠落または旧版なら作り直し、未来版なら skill と CLI を更新する。
59
+
60
+ ### version 1 からの移行
61
+
62
+ version 1 は lock を作業先の `definitions`(既定 `.shinaps/managed/`)に置いていた。
63
+
64
+ 1. 使っていた組織・workspace にアカウント名を決める。
65
+ 2. `<definitions>/claude-lock.json` だけを `~/.local/share/shinaps-ai/skills/managed/<account>/definitions/claude-lock.json` へ移す(複数のリポジトリで同じ `definitions` を共有していた場合も1回でよい)。旧版の `agents/` などは移さない。移すと記録の無いファイルとして setup が止まる。
66
+ 3. 設定を version 2 で作り直す。`definitions` を除き、`vault_id` を `accounts.<account>.vault_id` へ移す。
67
+ 4. `managed setup --account <account>` で計画を確かめる([導入](setup.md))。lock を移し忘れると同名のリソースで止まる。旧版の定義を手で編集していた場合、その変更は持ち越されない。計画に、クラウド上の定義を同梱の定義へ戻す update が含まれることを確かめてから適用する。
68
+
69
+ ### 設定例
39
70
 
40
71
  ```json
41
72
  {
42
- "$schema": "https://cdn.jsdelivr.net/npm/@shinaps/skills@0.34.0/plugins/shinaps-claude/references/managed/configuration.schema.json",
43
- "config_version": 1,
44
- "definitions": ".shinaps/managed",
73
+ "$schema": "https://cdn.jsdelivr.net/npm/@shinaps/skills@0.38.0/plugins/shinaps-claude/references/managed/configuration.schema.json",
74
+ "config_version": 2,
45
75
  "repository": {
46
76
  "url": "https://github.com/acme/widgets",
47
77
  "mount_path": "/workspace/widgets"
48
78
  },
49
- "vault_id": null
79
+ "accounts": {
80
+ "personal": { "vault_id": null },
81
+ "work": { "vault_id": "vlt_011CZkZDLs7fYzm1hXNPeRjv" }
82
+ }
50
83
  }
51
84
  ```
52
-
53
- ## lock の扱い
54
-
55
- - `<definitions>/claude-lock.json` がクラウド上の lead・役割・skill・環境の ID の正本である。`ant apply` が作成・更新するので手で編集しない。
56
- - lock のキーは lock のあるディレクトリからの相対パス(例 `./agents/shinaps-dev-managed-cc.md`)である。共通定義を同じ構造で `<definitions>` 直下に置くので、そこで適用すると既存のリソースに対応づく。
57
- - lead の ID は `./agents/shinaps-dev-managed-cc.md`、環境の ID は `./environments/shinaps-dev.yaml` の `id` から読む。
58
- - lock が無いと適用は既存のリソースと対応づかず、同じ名前のリソースを重複して作る。lock の `hash` は既存のリソースから作り直せないため、バックアップや以前の置き場所から戻す。見つからなければ適用せず、利用者に相談する。
59
- - 適用の前後に lock を作業記録(`.shinaps/work/`)へ複製し、適用後の差分を比べる。
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://cdn.jsdelivr.net/npm/@shinaps/skills@0.34.0/plugins/shinaps-claude/references/managed/configuration.schema.json",
4
- "title": "Shinaps Managed Agents configuration v1",
3
+ "$id": "https://cdn.jsdelivr.net/npm/@shinaps/skills@0.38.0/plugins/shinaps-claude/references/managed/configuration.schema.json",
4
+ "title": "Shinaps Managed Agents configuration v2",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {
@@ -9,11 +9,7 @@
9
9
  },
10
10
  "config_version": {
11
11
  "type": "integer",
12
- "const": 1
13
- },
14
- "definitions": {
15
- "type": "string",
16
- "minLength": 1
12
+ "const": 2
17
13
  },
18
14
  "repository": {
19
15
  "type": "object",
@@ -30,18 +26,32 @@
30
26
  "required": ["url", "mount_path"],
31
27
  "additionalProperties": false
32
28
  },
33
- "vault_id": {
34
- "anyOf": [
35
- {
36
- "type": "string",
37
- "minLength": 1
29
+ "accounts": {
30
+ "type": "object",
31
+ "minProperties": 1,
32
+ "propertyNames": {
33
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
34
+ },
35
+ "additionalProperties": {
36
+ "type": "object",
37
+ "properties": {
38
+ "vault_id": {
39
+ "anyOf": [
40
+ {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ {
45
+ "type": "null"
46
+ }
47
+ ]
48
+ }
38
49
  },
39
- {
40
- "type": "null"
41
- }
42
- ]
50
+ "required": ["vault_id"],
51
+ "additionalProperties": false
52
+ }
43
53
  }
44
54
  },
45
- "required": ["config_version", "definitions", "repository", "vault_id"],
55
+ "required": ["config_version", "repository", "accounts"],
46
56
  "additionalProperties": false
47
57
  }