@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.
- package/README.md +35 -22
- package/dist/bin/skills.js +4 -1
- package/dist/lib/config-validation.js +2 -1
- package/dist/lib/install.js +1 -0
- package/dist/lib/layout.js +12 -0
- package/dist/lib/linear-config.js +2 -2
- package/dist/lib/managed-ant.js +70 -0
- package/dist/lib/managed-config.js +6 -6
- package/dist/lib/managed-session.js +274 -0
- package/dist/lib/managed-setup.js +167 -0
- package/dist/lib/managed.js +249 -0
- package/package.json +1 -1
- package/plugins/shinaps-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/shinaps-claude/managed-agents/agents/shinaps-dev-managed-cc.md +1 -1
- package/plugins/shinaps-claude/references/linear/configuration.md +1 -1
- package/plugins/shinaps-claude/references/managed/configuration.md +55 -30
- package/plugins/shinaps-claude/references/managed/configuration.schema.json +27 -17
- package/plugins/shinaps-claude/references/managed/execute.md +38 -74
- package/plugins/shinaps-claude/references/managed/setup.md +27 -39
- package/plugins/shinaps-claude/skills/managed-execute/SKILL.md +6 -6
- package/plugins/shinaps-claude/skills/managed-setup/SKILL.md +6 -7
- package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/shinaps-codex/managed-agents/agents/shinaps-dev-managed-cc.md +1 -1
- package/plugins/shinaps-codex/references/linear/configuration.md +1 -1
- package/plugins/shinaps-codex/references/managed/configuration.md +55 -30
- package/plugins/shinaps-codex/references/managed/configuration.schema.json +27 -17
- package/plugins/shinaps-codex/references/managed/execute.md +38 -74
- package/plugins/shinaps-codex/references/managed/setup.md +27 -39
- package/plugins/shinaps-codex/skills/managed-execute/SKILL.md +6 -6
- package/plugins/shinaps-codex/skills/managed-setup/SKILL.md +6 -7
package/README.md
CHANGED
|
@@ -18,10 +18,10 @@ sharedで採用する `instructions.md` は共有JSONの `references.specificati
|
|
|
18
18
|
|
|
19
19
|
0.35.0から通常設定の `review_flow.mode` に `ask` を追加しました(パス別ルールは `auto`・`manual` だけ)。既存の設定は変更不要です。
|
|
20
20
|
|
|
21
|
-
CLIはpluginに含まれないため、利用中のskillと同じ `@shinaps/skills@0.
|
|
21
|
+
CLIはpluginに含まれないため、利用中のskillと同じ `@shinaps/skills@0.38.0` を導入し、`shinaps-skills --version` で確認します。`shinaps-skills validate-linear-config <明示ファイル>` は同梱schemaだけで検証し、ネットワーク取得・正本探索・Git操作・入力変更は行いません。`$schema` は固定URLによるエディタ補助で、契約の正本は[同梱schema](plugins/shinaps-claude/references/linear/configuration.schema.json)です。Managed Agentsの `.shinaps/managed.json` も `shinaps-skills validate-managed-config <明示ファイル>` で同じ方式で検証します。0.38.0から `config_version: 2`(リポジトリとアカウント別のvault)になり、lockはアカウントごとの管理ディレクトリへ移ります([Managedの設定と移行](plugins/shinaps-claude/references/managed/configuration.md))。
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
npm install -g @shinaps/skills@0.
|
|
24
|
+
npm install -g @shinaps/skills@0.38.0
|
|
25
25
|
shinaps-skills --version
|
|
26
26
|
shinaps-skills validate-linear-config /absolute/repository/.shinaps/linear.json
|
|
27
27
|
shinaps-skills validate-managed-config /absolute/repository/.shinaps/managed.json
|
|
@@ -44,8 +44,8 @@ shinaps-skills validate-managed-config /absolute/repository/.shinaps/managed.jso
|
|
|
44
44
|
| `release` | 公開条件を確定し、対象バージョンの利用可能性を確認する |
|
|
45
45
|
| `linear-plan` | 相談・必要なspec/explore・Issue作成と整理・プロジェクト設定を行い、実装前で止める |
|
|
46
46
|
| `linear-execute` | Issue単体・Cycle全体・バックログ消化を選び、設定に従って実装・QA・許可された公開を進める |
|
|
47
|
-
| `managed-setup` | Claude Managed Agentsの共通dev
|
|
48
|
-
| `managed-execute` | Managed Agentsのleadへdev
|
|
47
|
+
| `managed-setup` | Claude Managed Agentsの共通dev構成を1つ以上のアカウントへ一コマンドで導入・更新する。アカウントごとのlockを使い、重複して作らない |
|
|
48
|
+
| `managed-execute` | Managed Agentsのleadへdevを依頼し、観測・質問の取り次ぎ・出力のローカル回収とdraft PRの確認を行う |
|
|
49
49
|
|
|
50
50
|
コミット・PR・リリースは、利用者の依頼またはLinear進行で合意済みの継続許可に従う。[共通設定](plugins/shinaps-claude/references/linear/configuration.md)・[担当と工程](plugins/shinaps-claude/references/linear/workflow.md)・[終了と待機](plugins/shinaps-claude/references/linear/execution.md)を参照し、必要な前工程が揃えば再確認せずに進む。単独スキル利用にはLinear進行や新しいゴールを必須にしない。`autonomous-commit-review` の役割は `commit` に統合した。個人用の別AI呼び出し・ブラウザ管理・ゴール管理・動画制作はこのpluginに含めない。
|
|
51
51
|
|
|
@@ -134,38 +134,50 @@ Claude Codeは標準のmarketplace自動更新機能を使う。`/plugin` のMar
|
|
|
134
134
|
|
|
135
135
|
## Claude Managed Agents
|
|
136
136
|
|
|
137
|
-
両pluginの `managed-agents/` は、Claude Managed Agentsでdev工程を進めるための共通定義である。lead `shinaps-dev-managed-cc`、devの3役割、skill `dev-managed-cc`、環境 `shinaps-dev` を置く。クラウドのIDを記録する `claude-lock.json`
|
|
137
|
+
両pluginの `managed-agents/` は、Claude Managed Agentsでdev工程を進めるための共通定義である。lead `shinaps-dev-managed-cc`、devの3役割、skill `dev-managed-cc`、環境 `shinaps-dev` を置く。クラウドのIDを記録する `claude-lock.json` は利用者のアカウント(APIキーの組織・workspace)ごとのものなので、マシン単位の管理ディレクトリに置き、公開する定義には含めない。予定実行はこの構成に含めない。
|
|
138
138
|
|
|
139
|
-
- lead
|
|
140
|
-
- 体制はLITE・BASIC・ULTRAを初回メッセージで受け、`dev-claude` と同じ体制の表に従い、BASIC・ULTRAではleadが追加の担当を決める。lead
|
|
139
|
+
- leadと3役割はOpus 5.5(`claude-opus-5-5` / medium)で動く。全agent定義に `speed: standard` を明示する(既存リソースに残るfastを戻すため)。leadのモデルは定義で決まり、Linear進行の経路ごとのメインのモデルとは別に扱う。
|
|
140
|
+
- 体制はLITE・BASIC・ULTRAを初回メッセージで受け、`dev-claude` と同じ体制の表に従い、BASIC・ULTRAではleadが追加の担当を決める。leadがメインとして調査・設計・実装・採否を行い、独立設計(ULTRAでは必須)・計画レビュー・実装後の独立レビューだけを登録した3役割(architect・plan-reviewer・reviewer)へ委任する。Managedで設計・実装を行うモデルはleadのOpusになる。廃止したdev-ultraなどの入口・explorer・refine専用の役割は置かず、探索はleadと独立設計担当が行う。
|
|
141
141
|
- Shinaps AIのMCPとruleを使わない。規約はマウントしたリポジトリの `AGENTS.md`・`CLAUDE.md` と参照先、添付された利用者CLAUDE.mdだけで、最終報告ではShinaps AI ruleを未照合とする。
|
|
142
|
-
- 初回メッセージにリポジトリ・base・head
|
|
142
|
+
- 初回メッセージにリポジトリ・base・head・範囲と受入条件・体制・許可・予算が揃わなければ、leadは作業を始めず欠落を質問する。許可はそのメッセージで明示された操作だけで、構成の導入からは生じない。
|
|
143
143
|
- 終了地点は範囲を限ったコミット・headブランチへのpush・draft PR。merge・deploy・release・Issueの更新はしない。CodexのQAはセッションの外で、PRの後に行う。push前に最終QAを行うLinear進行とは順序が違う。
|
|
144
144
|
|
|
145
|
-
###
|
|
145
|
+
### CLI
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
2. 作業先では `managed-setup` が、`.shinaps/managed.json`([設定](plugins/shinaps-claude/references/managed/configuration.md))の `definitions` が指すディレクトリ(既定は `.shinaps/managed/`、Git対象外)へ共通定義をコピーする。
|
|
149
|
-
3. そのディレクトリで `ant apply --dry-run .` を実行し、計画を確認する。
|
|
150
|
-
4. 利用者の合意後に `ant apply .` で適用する。`--prune`・`--force` は使わず、リソースを削除しない。
|
|
151
|
-
5. 適用前に複製したlock(`.shinaps/work/` に置く)と比べ、更新したリソースの `version`・`hash`・`remote_hash` 以外が変わっていないことを確かめる。
|
|
147
|
+
導入・依頼・観測・回収は配布CLIの `shinaps-skills managed` で行い、クラウドの操作は公式の `ant` CLI(2.4.0以上。コマンド・フラグ・stdin・出力はこの版のソースで照合した)へ委ねる。各アカウントのAPIキーは環境変数 `SHINAPS_MANAGED_API_KEY_<NAME>` か引数 `--api-key <name>=<key>` で渡し、CLIは保存・表示しない。lock・セッション記録・回収した出力は `~/.local/share/shinaps-ai/skills/managed/<account>/` にアカウントごとに分かれる。
|
|
152
148
|
|
|
153
|
-
|
|
149
|
+
```sh
|
|
150
|
+
shinaps-skills managed setup --account personal --account work # 計画の表示だけ
|
|
151
|
+
shinaps-skills managed setup --account personal --account work --apply # 合意後に適用
|
|
152
|
+
shinaps-skills managed run .shinaps/managed.json --account work --base main --base-sha <SHA> --head feat/x \
|
|
153
|
+
--staffing BASIC --allow commit,push,draft_pr --budget-cents 4000 --task-file task.md
|
|
154
|
+
shinaps-skills managed status --account work --session <ID>
|
|
155
|
+
shinaps-skills managed reply --account work --session <ID> --message-file answer.md
|
|
156
|
+
shinaps-skills managed collect --account work --session <ID>
|
|
157
|
+
```
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
- setupはアカウントごとのlockで既存のリソースを更新し、lockが無いのに同名のリソースがあれば作らずに止まる。`--apply` を付けない限りクラウドを変更しない。
|
|
160
|
+
- runはリポジトリ・base・head・範囲・体制・許可・予算を毎回明示し、同じ入力の再実行では新しいセッションを作らない。
|
|
161
|
+
- collectはセッションの出力を保存するだけで、差分を適用しない。CLIはgitを呼ばず、pushしない。
|
|
162
|
+
|
|
163
|
+
手順・安全上の注意・止める条件は[設定とアカウント](plugins/shinaps-claude/references/managed/configuration.md)・[導入と更新の手順](plugins/shinaps-claude/references/managed/setup.md)・[依頼と観測の手順](plugins/shinaps-claude/references/managed/execute.md)にある。これらはpluginに含まれ、どちらのruntimeからも読める。
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
### 定義の更新と適用
|
|
166
|
+
|
|
167
|
+
共通定義は `plugins/shinaps-claude/managed-agents/` を編集元とし、`plugins/shinaps-codex/managed-agents/` へ同じ内容を複製する。一致はテストで確認する。定義を変えたら `managed setup` で計画を確かめてから `--apply` で適用する。`--prune`・`--force` は使わず、リソースを削除しない。
|
|
158
168
|
|
|
159
|
-
|
|
169
|
+
### このリポジトリのlock
|
|
160
170
|
|
|
161
|
-
|
|
171
|
+
shinaps-skills自身のlockは0.34.0から0.37.0まで `.shinaps/managed/claude-lock.json`(Git対象外)に置いていた。0.38.0では[version 1 からの移行](plugins/shinaps-claude/references/managed/configuration.md#version-1-からの移行)に従い、アカウントの `definitions/claude-lock.json` へ移してから適用する。それ以前の内容は `git show 8679d8f:managed-agents/claude-lock.json` で取り出せる。旧CC Ultraのleadと専用skill、旧explorerと旧review-verifierはクラウドでアーカイブ済みで、lockでも追跡しない。
|
|
172
|
+
|
|
173
|
+
GitHub MCP(`https://api.githubcopilot.com/mcp`)の資格情報はアカウントの組織のvaultに登録し、セッション作成時に渡す。vaultの資格情報はMCPサーバーのURLで照合されるので、lead定義のURLと末尾のスラッシュまで揃える。leadは最後のメッセージの1行目に `状態: 完了`・`状態: 質問`・`状態: 未完了` を書き、`changes.patch` と作業記録のtarを `/mnt/session/outputs/` に置く([起動手順](plugins/shinaps-claude/managed-agents/skills/dev-managed-cc/references/launch.md))。CLIの実クラウドでの実行、マウントしたリポジトリへのpushとdraft PRの作成は、実際のセッションで確かめるまで未検証である。
|
|
162
174
|
|
|
163
175
|
## 開発と配布
|
|
164
176
|
|
|
165
177
|
- Claude版: `plugins/shinaps-claude/`、marketplaceは`.claude-plugin/marketplace.json`
|
|
166
178
|
- Codex版: `plugins/shinaps-codex/`、marketplaceは`.agents/plugins/marketplace.json`
|
|
167
179
|
- Codexカスタムエージェント: `plugins/shinaps-codex/agents/`
|
|
168
|
-
- Claude Managed Agents版: 両pluginの `managed-agents/`(Claude版が編集元)。pluginと一緒に配布され、npmパッケージの `plugins/`
|
|
180
|
+
- Claude Managed Agents版: 両pluginの `managed-agents/`(Claude版が編集元)。pluginと一緒に配布され、npmパッケージの `plugins/` にも含まれる。適用は `shinaps-skills managed setup` がアカウントごとの管理ディレクトリへコピーしてから行い、`--prune`・`--force` を使わない
|
|
169
181
|
- 配布CLI: ソースは `bin/skills.ts` と `lib/*.ts`。`npm run build` (`tsc`) で `dist/` にJSを出力し、npmにはそれを配布する。Node.jsは`node_modules`配下の`.ts`に型除去を適用しないため、ソースのままは配布できない。checkoutでは型除去で直接実行するので、`enum`など型除去で消せない構文は使わない (`tsconfig.json` の `erasableSyntaxOnly`)
|
|
170
182
|
|
|
171
183
|
品質基準とプロジェクト設定の方針は揃えつつ、クライアント固有の実行手順・エージェント形式は別々に管理する。Codex版の工程の資料(`references/` の、`dev/launch.md` を除くもの)はClaude版と一致させ、テストで確認する。Codex版は役割の本文とスキルを書き分け、Claude向けのエージェント呼び出しをCodexに読み替えさせる方式は使わない。スキルを追加・変更した場合は各版への適用可否を判断する。
|
|
@@ -254,8 +266,7 @@ Claude版は全14エージェントが `opus / medium`。Codex版の役割定義
|
|
|
254
266
|
├── .gitignore # 保存方式に応じた除外・共有対象の例外
|
|
255
267
|
├── linear.json # linear のプロジェクト設定(任意)
|
|
256
268
|
├── linear/ # linear の補足(任意)
|
|
257
|
-
├── managed.json # Managed Agents
|
|
258
|
-
├── managed/ # Managed の共通定義のコピーと claude-lock.json(任意・Git対象外)
|
|
269
|
+
├── managed.json # Managed Agents の依頼先リポジトリとアカウント別のvault(任意・Git対象外)
|
|
259
270
|
└── work/
|
|
260
271
|
├── {日時}-{作業名}/
|
|
261
272
|
├── explorations/
|
|
@@ -264,6 +275,8 @@ Claude版は全14エージェントが `opus / medium`。Codex版の役割定義
|
|
|
264
275
|
└── debug/
|
|
265
276
|
```
|
|
266
277
|
|
|
278
|
+
Managed Agentsのアカウントごとのlock・セッション記録・回収した出力は、リポジトリではなく `~/.local/share/shinaps-ai/skills/managed/<account>/` に置く([設定とアカウント](plugins/shinaps-claude/references/managed/configuration.md))。
|
|
279
|
+
|
|
267
280
|
旧 `.claude/shinaps/` の成果物はパスを指定して引き継げる。既存記録を自動移動・削除しない。共有する仕様書などの明示された納品先は尊重し、作業記録に秘密情報を含めない。
|
|
268
281
|
|
|
269
282
|
## 対応範囲
|
package/dist/bin/skills.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { packageRoot } from '../lib/layout.js';
|
|
5
5
|
import { validateLinearConfig } from '../lib/linear-config.js';
|
|
6
6
|
import { validateManagedConfig } from '../lib/managed-config.js';
|
|
7
|
+
import { managed } from '../lib/managed.js';
|
|
7
8
|
import { run } from '../lib/install.js';
|
|
8
9
|
import { autoUpdate } from '../lib/auto-update.js';
|
|
9
10
|
import { errorMessage } from '../lib/fs.js';
|
|
@@ -22,8 +23,10 @@ try {
|
|
|
22
23
|
if (rest.length !== 1)
|
|
23
24
|
throw new Error('Usage: shinaps-skills validate-managed-config <file>');
|
|
24
25
|
await validateManagedConfig(rest[0]);
|
|
25
|
-
console.log('Managed configuration is valid (config_version:
|
|
26
|
+
console.log('Managed configuration is valid (config_version: 2).');
|
|
26
27
|
}
|
|
28
|
+
else if (command === 'managed')
|
|
29
|
+
await managed(rest);
|
|
27
30
|
else if (command === 'auto-update')
|
|
28
31
|
await autoUpdate(rest);
|
|
29
32
|
else
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Ajv } from 'ajv';
|
|
4
4
|
import { packageRoot } from './layout.js';
|
|
5
|
-
/**
|
|
5
|
+
/** 明示されたファイルを同梱の契約で検証し、検証した内容を返す。設定の探索・変更や外部取得は行わない。 */
|
|
6
6
|
export async function validateConfig(path, contract) {
|
|
7
7
|
const input = JSON.parse(await readFile(path, 'utf8'));
|
|
8
8
|
// 版の不一致は設定修正とskill更新で対処が異なるため、schema検証より先に案内する。
|
|
@@ -30,4 +30,5 @@ export async function validateConfig(path, contract) {
|
|
|
30
30
|
.join('\n');
|
|
31
31
|
throw new Error(`Invalid ${contract.name} configuration. Correct the JSON to match configuration.schema.json:\n${errors}`);
|
|
32
32
|
}
|
|
33
|
+
return input;
|
|
33
34
|
}
|
package/dist/lib/install.js
CHANGED
|
@@ -12,6 +12,7 @@ const usage = [
|
|
|
12
12
|
'Usage: shinaps-skills <install|update|uninstall|recover> codex',
|
|
13
13
|
' shinaps-skills validate-linear-config <file>',
|
|
14
14
|
' shinaps-skills validate-managed-config <file>',
|
|
15
|
+
' shinaps-skills managed <setup|run|status|reply|collect> (see managed --help)',
|
|
15
16
|
' shinaps-skills auto-update <enable|disable|status|check> codex',
|
|
16
17
|
'Claude Code: /plugin marketplace add sh1n4ps/shinaps-skills',
|
|
17
18
|
' /plugin install shinaps@shinaps-skills',
|
package/dist/lib/layout.js
CHANGED
|
@@ -5,6 +5,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
// launchd ジョブは enable した版の CLI ではなく、実行のたびに npm から取得した最新版の CLI を起動する。
|
|
6
6
|
// そのためここに定義した場所と形式 (auto-update.json、state.json、transaction/journal.json) は
|
|
7
7
|
// どの版でも読めるように保ち、変える場合は旧形式も読めるようにする。
|
|
8
|
+
// Managed Agents のアカウントごとの配置 (managed/<account>/ の lock とセッション記録) も同じ約束で、
|
|
9
|
+
// 版を変えても既存の lock を見失わないようにする。lock を見失うと、次の適用が同じリソースを重複して作る。
|
|
8
10
|
/**
|
|
9
11
|
* このパッケージのルートディレクトリ。リポジトリの checkout、または npm が展開したディレクトリ。
|
|
10
12
|
* このファイルは checkout では `lib/`、配布物では `dist/lib/` にあり深さが違うので、package.json が
|
|
@@ -39,6 +41,16 @@ export const managedRoot = (home) => join(home, '.local', 'share', 'shinaps-ai',
|
|
|
39
41
|
/** 配置済みの配布物。Codex の marketplace はここを指す。 */
|
|
40
42
|
export const payloadDir = (root) => join(root, 'codex');
|
|
41
43
|
export const statePath = (root) => join(root, 'state.json');
|
|
44
|
+
/**
|
|
45
|
+
* Managed Agents のアカウントのディレクトリ。lock は組織・workspace (APIキー) に属し、リポジトリには
|
|
46
|
+
* 属さないので、リポジトリではなくマシン単位の管理ディレクトリに置く。
|
|
47
|
+
* - `definitions/`: `ant apply` の cwd。同梱の共通定義の写しと `claude-lock.json` だけを置く。
|
|
48
|
+
* - `sessions/<request_id>.json`: このアカウントで作成したセッションの記録。
|
|
49
|
+
* - `sessions/<session_id>/outputs/`: 回収したファイル。
|
|
50
|
+
*/
|
|
51
|
+
export const managedAccountDir = (root, account) => join(root, 'managed', account);
|
|
52
|
+
/** 同梱の Managed 共通定義。両 plugin の内容は同一 (テストで確認) なので Claude 版を使う。 */
|
|
53
|
+
export const managedTemplatePath = 'plugins/shinaps-claude/managed-agents';
|
|
42
54
|
/** CODEX_HOME の解決値。オプション (`auto-update check` が渡す) → 環境変数 → Codex の既定の順。 */
|
|
43
55
|
export const codexHomeOf = (options, home) => resolve(options.codexHome ?? process.env.CODEX_HOME ?? join(home, '.codex'));
|
|
44
56
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateConfig } from './config-validation.js';
|
|
2
2
|
/** `.shinaps/linear.json` を同梱の契約で検証する。 */
|
|
3
|
-
export function validateLinearConfig(path) {
|
|
4
|
-
|
|
3
|
+
export async function validateLinearConfig(path) {
|
|
4
|
+
await validateConfig(path, {
|
|
5
5
|
name: 'Linear',
|
|
6
6
|
schema: 'plugins/shinaps-claude/references/linear/configuration.schema.json',
|
|
7
7
|
version: 2,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Managed Agents の beta ヘッダ。ant の `--beta` は既定の beta ヘッダを置き換えるので、既定に頼らず
|
|
4
|
+
* 毎回明示する (agents・environments・sessions の既定と同じ値)。Files は `filesBeta` を使う。
|
|
5
|
+
*/
|
|
6
|
+
export const managedBeta = ['--beta', 'managed-agents-2026-04-01'];
|
|
7
|
+
/** セッションの Files には Files API と Managed Agents の両方の beta が要る。 */
|
|
8
|
+
export const filesBeta = ['--beta', 'files-api-2025-04-14', ...managedBeta];
|
|
9
|
+
/**
|
|
10
|
+
* 子プロセスの環境。親の `ANTHROPIC_*` (別のキー・ログインのプロファイル・workspace・federation) と
|
|
11
|
+
* 他アカウントのキー (`SHINAPS_MANAGED_*`) を渡さず、そのアカウントのキーだけを資格情報にする。
|
|
12
|
+
* ant は環境変数をログインより優先するが、複数の資格情報が残ると、どれが使われるかが ant の優先順位に
|
|
13
|
+
* 依存し、別アカウントのリソースを操作しかねないため。
|
|
14
|
+
*/
|
|
15
|
+
export function antEnvironment(env, apiKey) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const [name, value] of Object.entries(env)) {
|
|
18
|
+
if (!name.startsWith('ANTHROPIC_') && !name.startsWith('SHINAPS_MANAGED_')) {
|
|
19
|
+
result[name] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
result.ANTHROPIC_API_KEY = apiKey;
|
|
23
|
+
// ant は FORCE_COLOR=1 で非端末にも色を付ける。計画の文言や jsonl の解析が ANSI で壊れないよう止める。
|
|
24
|
+
result.FORCE_COLOR = '0';
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* stdin に渡すリクエスト本文。ant は本文の文字列値のうち `@` で始まるものをファイル参照として展開し
|
|
29
|
+
* (`@notes.md` ならそのファイルの内容を送り、無ければ失敗する)、`\@` で始まるものだけを `@` の文字列に
|
|
30
|
+
* 戻す。題名や回答の本文を書かれたとおりに送るため、`@` で始まる文字列を全て `\@` に置き換える。
|
|
31
|
+
* `\@` で始まる文字列は ant が必ず `@` に変えるので、そのままは送れない (ant の制約)。
|
|
32
|
+
*/
|
|
33
|
+
export function antRequestBody(value) {
|
|
34
|
+
return JSON.stringify(value, (_key, item) => typeof item === 'string' && item.startsWith('@') ? `\\${item}` : item);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* PATH 上の `ant` を実行する。本文が無ければ stdin を /dev/null にする。ant は stdin がパイプなら
|
|
38
|
+
* 本文の有無を 10 ms 待って確かめるので、本文の無い呼び出しにパイプを渡さない。本文は起動と同時に
|
|
39
|
+
* パイプへ書き込むので、ant が確かめる時点では既に読める。
|
|
40
|
+
*/
|
|
41
|
+
export const antCommand = (env) => ({ argv, apiKey, cwd, body }) => new Promise((resolve, reject) => {
|
|
42
|
+
const child = spawn('ant', argv, {
|
|
43
|
+
cwd,
|
|
44
|
+
env: antEnvironment(env, apiKey),
|
|
45
|
+
stdio: [body === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
const stdout = [];
|
|
48
|
+
const stderr = [];
|
|
49
|
+
child.stdout?.on('data', (chunk) => stdout.push(chunk));
|
|
50
|
+
child.stderr?.on('data', (chunk) => stderr.push(chunk));
|
|
51
|
+
child.on('error', reject);
|
|
52
|
+
child.on('close', (status, signal) => {
|
|
53
|
+
if (status === 0) {
|
|
54
|
+
resolve(Buffer.concat(stdout).toString('utf8'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// ant は API エラーの本文と、途中で失敗した apply の結果を stdout に出すので、両方を残す。
|
|
58
|
+
// 呼び出し側が秘密値を伏字にしてから表示する。
|
|
59
|
+
const output = [Buffer.concat(stdout), Buffer.concat(stderr)]
|
|
60
|
+
.map((chunk) => chunk.toString('utf8').trim())
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.join('\n');
|
|
63
|
+
reject(new Error(`ant ${argv.slice(0, 2).join(' ')} exited with ${status ?? signal}:\n${output}`));
|
|
64
|
+
});
|
|
65
|
+
// ant が本文を読む前に終わる (引数の誤りなど) と書き込みは EPIPE で失敗する。その成否は
|
|
66
|
+
// 終了コードとエラー出力で報告するので、書き込みのエラーでプロセスを落とさない。
|
|
67
|
+
child.stdin?.on('error', () => { });
|
|
68
|
+
if (body !== undefined)
|
|
69
|
+
child.stdin?.end(antRequestBody(body));
|
|
70
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { validateConfig } from './config-validation.js';
|
|
2
|
-
/** `.shinaps/managed.json`
|
|
3
|
-
export function validateManagedConfig(path) {
|
|
4
|
-
return validateConfig(path, {
|
|
2
|
+
/** `.shinaps/managed.json` を同梱の契約で検証し、内容を返す。 */
|
|
3
|
+
export async function validateManagedConfig(path) {
|
|
4
|
+
return (await validateConfig(path, {
|
|
5
5
|
name: 'Managed',
|
|
6
6
|
schema: 'plugins/shinaps-claude/references/managed/configuration.schema.json',
|
|
7
|
-
version:
|
|
8
|
-
migration: '
|
|
9
|
-
});
|
|
7
|
+
version: 2,
|
|
8
|
+
migration: 'Recreate it as config_version: 2 with managed-execute and move each existing claude-lock.json to its account directory (references/managed/configuration.md, migration from version 1).',
|
|
9
|
+
}));
|
|
10
10
|
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { readJson, readJsonIfExists, writeFileAtomic } from './fs.js';
|
|
5
|
+
import { withLock } from './lock.js';
|
|
6
|
+
import { filesBeta, managedBeta } from './managed-ant.js';
|
|
7
|
+
export const staffings = ['LITE', 'BASIC', 'ULTRA'];
|
|
8
|
+
export const permissions = ['commit', 'push', 'draft_pr'];
|
|
9
|
+
const leadKey = './agents/shinaps-dev-managed-cc.md';
|
|
10
|
+
const environmentKey = './environments/shinaps-dev.yaml';
|
|
11
|
+
const permissionLabels = {
|
|
12
|
+
commit: 'commit',
|
|
13
|
+
push: 'headへのpush',
|
|
14
|
+
draft_pr: 'draft PRの作成',
|
|
15
|
+
};
|
|
16
|
+
const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
17
|
+
/**
|
|
18
|
+
* `--transform id -r` の出力から ID を取り出す。ant は transform のパスが応答に無いと応答の全体を
|
|
19
|
+
* 出すので、ID の形でない出力を ID として記録しない。
|
|
20
|
+
*/
|
|
21
|
+
async function readId(output, command) {
|
|
22
|
+
const id = (await output).trim();
|
|
23
|
+
if (!/^[A-Za-z0-9_]+$/.test(id))
|
|
24
|
+
throw new Error(`${command} returned no ID.`);
|
|
25
|
+
return id;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 依頼を lead へ送るセッションを作る。同じ入力の依頼は同じセッションを返し、作り直さない。
|
|
29
|
+
* 作成の途中で止まった記録があれば、重複を避けるために作らずに止める。
|
|
30
|
+
* @param githubToken マウントするリポジトリの token。本文 (stdin) だけで渡す。
|
|
31
|
+
* @param claudeMdPath `request.claude_md` の元のファイル。upload に使う。
|
|
32
|
+
*/
|
|
33
|
+
export async function runSession(account, request, options) {
|
|
34
|
+
const { ant, log } = options;
|
|
35
|
+
return withLock(account.dir, async () => {
|
|
36
|
+
const { task, claude_md, ...rest } = request;
|
|
37
|
+
const recorded = {
|
|
38
|
+
...rest,
|
|
39
|
+
task_sha256: sha256(task),
|
|
40
|
+
claude_md_sha256: claude_md === null ? null : sha256(claude_md),
|
|
41
|
+
};
|
|
42
|
+
const requestId = sha256(JSON.stringify(recorded)).slice(0, 32);
|
|
43
|
+
const recordPath = join(account.dir, 'sessions', `${requestId}.json`);
|
|
44
|
+
const previous = await readJsonIfExists(recordPath);
|
|
45
|
+
if (previous?.session_id) {
|
|
46
|
+
log(`Session already exists for this request: ${previous.session_id}`);
|
|
47
|
+
return previous.session_id;
|
|
48
|
+
}
|
|
49
|
+
if (previous) {
|
|
50
|
+
throw new Error(`A previous run of this request stopped while creating a session. Check the sessions of account ${account.name} for metadata shinaps_request=${requestId}; delete ${recordPath} only after confirming none was created, then retry.`);
|
|
51
|
+
}
|
|
52
|
+
const lock = await readJsonIfExists(join(account.dir, 'definitions', 'claude-lock.json'));
|
|
53
|
+
const agent = lock?.resources?.[leadKey]?.id;
|
|
54
|
+
const environment = lock?.resources?.[environmentKey]?.id;
|
|
55
|
+
if (!agent || !environment) {
|
|
56
|
+
throw new Error(`Account ${account.name} has no applied lead and environment. Run managed setup --apply first.`);
|
|
57
|
+
}
|
|
58
|
+
await mkdir(join(account.dir, 'sessions'), { recursive: true });
|
|
59
|
+
const record = {
|
|
60
|
+
request_id: requestId,
|
|
61
|
+
session_id: null,
|
|
62
|
+
created_at: options.now().toISOString(),
|
|
63
|
+
request: recorded,
|
|
64
|
+
};
|
|
65
|
+
// 作成より前に記録する。作成後に記録できずに終わっても、次の実行は止まり、重複して作らない。
|
|
66
|
+
await writeFileAtomic(recordPath, JSON.stringify(record, null, 2) + '\n');
|
|
67
|
+
const resources = [
|
|
68
|
+
{
|
|
69
|
+
type: 'github_repository',
|
|
70
|
+
url: request.repository.url,
|
|
71
|
+
mount_path: request.repository.mount_path,
|
|
72
|
+
...(options.githubToken ? { authorization_token: options.githubToken } : {}),
|
|
73
|
+
checkout: { type: 'commit', sha: request.base_sha },
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
if (options.claudeMdPath) {
|
|
77
|
+
// upload は記録の後に行う。記録の前だと、途中で止まった実行の再実行のたびに Files が増える。
|
|
78
|
+
let fileId;
|
|
79
|
+
try {
|
|
80
|
+
fileId = await readId(ant({
|
|
81
|
+
// セッションの Files と同じ beta の Files API に上げる。パスは絶対パスにする (ant は `-` を
|
|
82
|
+
// stdin と解釈する)。
|
|
83
|
+
argv: [
|
|
84
|
+
'beta:files',
|
|
85
|
+
'upload',
|
|
86
|
+
'--file',
|
|
87
|
+
resolve(options.claudeMdPath),
|
|
88
|
+
...filesBeta,
|
|
89
|
+
'--transform',
|
|
90
|
+
'id',
|
|
91
|
+
'-r',
|
|
92
|
+
],
|
|
93
|
+
apiKey: account.apiKey,
|
|
94
|
+
}), 'ant files upload');
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
// セッションはまだ作っていないので、記録を残すと同じ依頼の再実行が不要に止まる。
|
|
98
|
+
await rm(recordPath, { force: true });
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
resources.push({ type: 'file', file_id: fileId, mount_path: '/CLAUDE.md' });
|
|
102
|
+
}
|
|
103
|
+
const body = {
|
|
104
|
+
agent,
|
|
105
|
+
environment_id: environment,
|
|
106
|
+
...(request.vault_id ? { vault_ids: [request.vault_id] } : {}),
|
|
107
|
+
title: request.title ?? request.head,
|
|
108
|
+
metadata: { shinaps_request: requestId, head: request.head, staffing: request.staffing },
|
|
109
|
+
budget: {
|
|
110
|
+
type: 'limit',
|
|
111
|
+
max_list_cost: { amount: request.budget_cents, currency: 'USD' },
|
|
112
|
+
},
|
|
113
|
+
resources,
|
|
114
|
+
initial_events: [
|
|
115
|
+
{ type: 'user.message', content: [{ type: 'text', text: initialMessage(request) }] },
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
const sessionId = await readId(ant({
|
|
119
|
+
argv: ['beta:sessions', 'create', ...managedBeta, '--transform', 'id', '-r'],
|
|
120
|
+
apiKey: account.apiKey,
|
|
121
|
+
body,
|
|
122
|
+
}), 'ant beta:sessions create');
|
|
123
|
+
await writeFileAtomic(recordPath, JSON.stringify({ ...record, session_id: sessionId }, null, 2) + '\n');
|
|
124
|
+
log(`Created session ${sessionId} for account ${account.name}.`);
|
|
125
|
+
return sessionId;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// lead の契約 (managed-agents/skills/dev-managed-cc/SKILL.md の初回メッセージ) の必須入力を、
|
|
129
|
+
// すべて明示する。欠けると lead は質問で止まり、それまでの費用が無駄になる。
|
|
130
|
+
function initialMessage(request) {
|
|
131
|
+
const budget = Number(request.budget_cents) / 100;
|
|
132
|
+
return [
|
|
133
|
+
`リポジトリ: ${request.repository.url}(マウント先 ${request.repository.mount_path})`,
|
|
134
|
+
`base: ${request.base} @ ${request.base_sha}`,
|
|
135
|
+
`head: ${request.head}`,
|
|
136
|
+
`範囲と受入条件:\n${request.task.trimEnd()}`,
|
|
137
|
+
`体制: ${request.staffing}`,
|
|
138
|
+
`許可: ${request.allow.length ? request.allow.map((name) => permissionLabels[name]).join('・') : 'なし(commit・push・draft PRの作成はしない)'}`,
|
|
139
|
+
`予算: ${budget} USD(セッションの上限 ${request.budget_cents} セント)`,
|
|
140
|
+
`必須検証(任意): ${request.verify ?? '指定なし。リポジトリの規約に従う'}`,
|
|
141
|
+
`author(任意): ${request.author ?? '指定なし'}`,
|
|
142
|
+
...(request.claude_md === null ? [] : ['利用者 CLAUDE.md: /mnt/session/uploads/CLAUDE.md']),
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* このアカウントの記録にあるセッションか確かめる。別アカウントのキーや、CLI が作っていない
|
|
147
|
+
* セッションを観測・操作しないため。
|
|
148
|
+
*/
|
|
149
|
+
async function requireRecordedSession(account, sessionId) {
|
|
150
|
+
const directory = join(account.dir, 'sessions');
|
|
151
|
+
const names = await readdir(directory).catch(() => []);
|
|
152
|
+
for (const name of names.filter((entry) => entry.endsWith('.json'))) {
|
|
153
|
+
const record = await readJson(join(directory, name));
|
|
154
|
+
if (record.session_id === sessionId)
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Session ${sessionId} was not created by managed run for account ${account.name}.`);
|
|
158
|
+
}
|
|
159
|
+
/** セッションの状態と、lead の最後のメッセージ (1 行目が `状態: 完了|質問|未完了`) を返す。 */
|
|
160
|
+
export async function sessionStatus(account, sessionId, ant) {
|
|
161
|
+
await requireRecordedSession(account, sessionId);
|
|
162
|
+
const call = (argv) => ant({ argv, apiKey: account.apiKey });
|
|
163
|
+
const session = JSON.parse(await call([
|
|
164
|
+
'beta:sessions',
|
|
165
|
+
'retrieve',
|
|
166
|
+
'--session-id',
|
|
167
|
+
sessionId,
|
|
168
|
+
...managedBeta,
|
|
169
|
+
'--format',
|
|
170
|
+
'json',
|
|
171
|
+
'--transform',
|
|
172
|
+
'{status,usage,budget}',
|
|
173
|
+
]));
|
|
174
|
+
// 最新の agent.message 1 件だけを取る。全イベント (ツールの呼び出しを含む) を取得しないため。
|
|
175
|
+
const [last] = (await call([
|
|
176
|
+
'beta:sessions:events',
|
|
177
|
+
'list',
|
|
178
|
+
'--session-id',
|
|
179
|
+
sessionId,
|
|
180
|
+
...managedBeta,
|
|
181
|
+
'--type',
|
|
182
|
+
'agent.message',
|
|
183
|
+
'--order',
|
|
184
|
+
'desc',
|
|
185
|
+
'--max-items',
|
|
186
|
+
'1',
|
|
187
|
+
'--format',
|
|
188
|
+
'jsonl',
|
|
189
|
+
'--transform',
|
|
190
|
+
'{content}',
|
|
191
|
+
]))
|
|
192
|
+
.split('\n')
|
|
193
|
+
.filter((line) => line.trim())
|
|
194
|
+
.map((line) => JSON.parse(line));
|
|
195
|
+
const message = last?.content
|
|
196
|
+
?.filter((block) => block.type === 'text')
|
|
197
|
+
.map((block) => block.text)
|
|
198
|
+
.join('\n') ?? null;
|
|
199
|
+
const state = message?.match(/^状態: (完了|質問|未完了)/)?.[1] ?? null;
|
|
200
|
+
return { session_id: sessionId, ...session, state, message };
|
|
201
|
+
}
|
|
202
|
+
/** 質問への回答を user.message として送る。新しい許可・予算はここでは加えられない (本文だけ)。 */
|
|
203
|
+
export async function replySession(account, sessionId, text, ant) {
|
|
204
|
+
await requireRecordedSession(account, sessionId);
|
|
205
|
+
await ant({
|
|
206
|
+
argv: ['beta:sessions:events', 'send', '--session-id', sessionId, ...managedBeta],
|
|
207
|
+
apiKey: account.apiKey,
|
|
208
|
+
body: { events: [{ type: 'user.message', content: [{ type: 'text', text }] }] },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* セッションの出力ファイルを `sessions/<session_id>/outputs/` へ保存し、保存したパスを返す。
|
|
213
|
+
* 差分の適用はしない。同名のファイルは最新のものだけを保存する (lead は毎ターン changes.patch を
|
|
214
|
+
* 全体の差分で置き換えるため)。
|
|
215
|
+
*/
|
|
216
|
+
export async function collectOutputs(account, sessionId, ant) {
|
|
217
|
+
await requireRecordedSession(account, sessionId);
|
|
218
|
+
const listed = (await ant({
|
|
219
|
+
argv: [
|
|
220
|
+
'beta:files',
|
|
221
|
+
'list',
|
|
222
|
+
'--scope-id',
|
|
223
|
+
sessionId,
|
|
224
|
+
...filesBeta,
|
|
225
|
+
'--format',
|
|
226
|
+
'jsonl',
|
|
227
|
+
'--transform',
|
|
228
|
+
'{id,filename,size_bytes,created_at}',
|
|
229
|
+
],
|
|
230
|
+
apiKey: account.apiKey,
|
|
231
|
+
}))
|
|
232
|
+
.split('\n')
|
|
233
|
+
.filter((line) => line.trim())
|
|
234
|
+
.map((line) => JSON.parse(line));
|
|
235
|
+
const latest = new Map();
|
|
236
|
+
for (const file of listed) {
|
|
237
|
+
// 名前はクラウドのエージェントが決める。保存先の外へ書かないよう、ファイル名だけのものに限る。
|
|
238
|
+
// ID は ant の引数に渡すので、フラグとして解釈される形 (`-` で始まる) を受け付けない。
|
|
239
|
+
if (!/^[^/\\\0]+$/.test(file.filename) ||
|
|
240
|
+
file.filename === '.' ||
|
|
241
|
+
file.filename === '..' ||
|
|
242
|
+
!/^[A-Za-z0-9_]+$/.test(file.id)) {
|
|
243
|
+
throw new Error(`Refusing to save an output with an unsafe file name (file ${file.id}).`);
|
|
244
|
+
}
|
|
245
|
+
const current = latest.get(file.filename);
|
|
246
|
+
// 時刻は小数秒の桁数が揃うとは限らないので、文字列ではなく時刻として比べる。
|
|
247
|
+
if (!current || Date.parse(current.created_at) < Date.parse(file.created_at)) {
|
|
248
|
+
latest.set(file.filename, file);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const directory = join(account.dir, 'sessions', sessionId, 'outputs');
|
|
252
|
+
await mkdir(directory, { recursive: true });
|
|
253
|
+
const saved = [];
|
|
254
|
+
for (const file of latest.values()) {
|
|
255
|
+
const path = join(directory, file.filename);
|
|
256
|
+
const temporary = join(directory, `.${file.filename}.${randomUUID()}.tmp`);
|
|
257
|
+
try {
|
|
258
|
+
await ant({
|
|
259
|
+
argv: ['beta:files', 'download', '--file-id', file.id, ...filesBeta, '--output', temporary],
|
|
260
|
+
apiKey: account.apiKey,
|
|
261
|
+
});
|
|
262
|
+
const { size } = await stat(temporary);
|
|
263
|
+
if (file.size_bytes !== undefined && size !== file.size_bytes) {
|
|
264
|
+
throw new Error(`Downloaded ${file.filename} has ${size} bytes, but the Files API lists ${file.size_bytes}.`);
|
|
265
|
+
}
|
|
266
|
+
await rename(temporary, path);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
await rm(temporary, { force: true });
|
|
270
|
+
}
|
|
271
|
+
saved.push(path);
|
|
272
|
+
}
|
|
273
|
+
return saved;
|
|
274
|
+
}
|