@shinaps/skills 0.49.0 → 0.50.1
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 +6 -1
- package/dist/bin/skills.js +6 -0
- package/dist/lib/args.js +11 -0
- package/dist/lib/fs.js +21 -2
- package/dist/lib/install.js +8 -4
- package/dist/lib/lint.js +9 -6
- package/dist/lib/managed-session.js +2 -3
- package/dist/lib/rule-coverage.js +151 -0
- package/dist/lib/rule-snapshot.js +261 -0
- package/dist/lib/transaction.js +2 -5
- package/docs/development.md +3 -1
- package/docs/install.md +13 -2
- package/docs/lint.md +2 -0
- package/package.json +1 -1
- package/plugins/shinaps-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/shinaps-claude/agents/shinaps-reviewer.md +10 -7
- package/plugins/shinaps-claude/hooks/capture-rule-response.mjs +27 -0
- package/plugins/shinaps-claude/hooks/hooks.json +15 -0
- package/plugins/shinaps-claude/managed-agents/agents/shinaps-reviewer.md +10 -7
- package/plugins/shinaps-claude/managed-agents/skills/dev-managed-cc/references/review.md +3 -3
- package/plugins/shinaps-claude/references/dev/workflow.md +8 -4
- package/plugins/shinaps-claude/references/linear/configuration.md +1 -1
- package/plugins/shinaps-claude/references/lint.md +6 -5
- package/plugins/shinaps-claude/references/review-standalone.md +5 -5
- package/plugins/shinaps-claude/references/review.md +3 -3
- package/plugins/shinaps-claude/references/rules.md +56 -4
- package/plugins/shinaps-claude/skills/review/SKILL.md +1 -1
- package/plugins/shinaps-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/shinaps-codex/agents/shinaps-reviewer.toml +1 -1
- package/plugins/shinaps-codex/hooks/capture-rule-response.mjs +27 -0
- package/plugins/shinaps-codex/hooks/hooks.json +15 -0
- package/plugins/shinaps-codex/managed-agents/agents/shinaps-reviewer.md +10 -7
- package/plugins/shinaps-codex/managed-agents/skills/dev-managed-cc/references/review.md +3 -3
- package/plugins/shinaps-codex/references/dev/workflow.md +8 -4
- package/plugins/shinaps-codex/references/linear/configuration.md +1 -1
- package/plugins/shinaps-codex/references/lint.md +6 -5
- package/plugins/shinaps-codex/references/review-standalone.md +5 -5
- package/plugins/shinaps-codex/references/review.md +3 -3
- package/plugins/shinaps-codex/references/rules.md +56 -4
- package/plugins/shinaps-codex/skills/review/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -73,6 +73,11 @@ Claude Code では `/shinaps:<スキル名>`(例 `/shinaps:review`)で呼び
|
|
|
73
73
|
|
|
74
74
|
旧版の設定は自動では変換しません。Linear は [旧設定の移行](plugins/shinaps-claude/references/linear/configuration.md#旧設定の移行)(0.48.0 から `config_version: 3`。操作ごとの許可の一覧を廃止し `constraints` に置き換え)、Managed Agents は [version 1 からの移行](plugins/shinaps-claude/references/managed/configuration.md#version-1-からの移行)に従ってください。
|
|
75
75
|
|
|
76
|
+
### 0.50.0 の変更
|
|
77
|
+
|
|
78
|
+
- 規約の照合は、plugin の hook が保存した応答から作る snapshot を担当と機械検査が読みます。Codex では hook の信頼が要り、plugin の更新のたびに `/hooks` で信頼し直します([導入と更新](docs/install.md#規約の-hook))。
|
|
79
|
+
- 確定した規約の適用結果を既定で本人の集計へ記録します。作業ごとに OFF にでき、直近 90 日で集計から外れて削除されます([導入と更新](docs/install.md#規約の適用結果の記録))。Shinaps AI 側の配備が先に要ります。
|
|
80
|
+
|
|
76
81
|
### 0.49.0 の変更
|
|
77
82
|
|
|
78
83
|
`refine`・`simplify`・`extract-rules` のスキルを廃止しました。規約照合と簡素化は `review` に依頼します(「規約照合だけ」「簡素化だけ」と限定できます)。規約の登録はその場で AI に依頼します。PR からの一括抽出の代わりになるスキルはありません。計画の簡素化だけを単独で起動する手段も無くなり、dev の計画レビューに含まれます。
|
|
@@ -88,7 +93,7 @@ dev の実装後レビューでは、規約に機械検査があるときに配
|
|
|
88
93
|
|
|
89
94
|
## Shinaps AI との接続
|
|
90
95
|
|
|
91
|
-
`review` や `dev-claude` の規約照合など rule
|
|
96
|
+
`review` や `dev-claude` の規約照合など rule を使う工程に必要です。確定した規約の適用結果は既定で本人の集計へ記録します(送るのは規約の id、作業の key、結果の別だけで、作業ごとに OFF にでき、90 日で削除されます。[導入と更新](docs/install.md#規約の適用結果の記録))。規約の全文は plugin の hook が応答をそのまま保存した snapshot から担当へ渡します。Codex では hook の信頼が要り、plugin の更新のたびに信頼し直します([導入と更新](docs/install.md#規約の-hook))。規約の登録・更新はその場で AI に依頼します([規約の書き方](plugins/shinaps-claude/references/rule-writing.md))。導入とは別に設定し、既存の登録があれば再利用します。同じサーバーを plugin とグローバル設定の両方に登録しないでください。認証情報は各クライアントの認証で管理し、このリポジトリの CLI では扱いません。
|
|
92
97
|
|
|
93
98
|
```sh
|
|
94
99
|
# Claude Code(登録後、Claude Code 内の /mcp から認証)
|
package/dist/bin/skills.js
CHANGED
|
@@ -8,6 +8,8 @@ import { managed } from "../lib/managed.js";
|
|
|
8
8
|
import { run } from "../lib/install.js";
|
|
9
9
|
import { autoUpdate } from "../lib/auto-update.js";
|
|
10
10
|
import { lint } from "../lib/lint.js";
|
|
11
|
+
import { ruleCoverage } from "../lib/rule-coverage.js";
|
|
12
|
+
import { ruleSnapshot } from "../lib/rule-snapshot.js";
|
|
11
13
|
import { errorMessage } from "../lib/fs.js";
|
|
12
14
|
const [command, ...rest] = process.argv.slice(2);
|
|
13
15
|
try {
|
|
@@ -32,6 +34,10 @@ try {
|
|
|
32
34
|
await autoUpdate(rest);
|
|
33
35
|
else if (command === 'lint')
|
|
34
36
|
await lint(rest);
|
|
37
|
+
else if (command === 'rule-snapshot')
|
|
38
|
+
await ruleSnapshot(rest);
|
|
39
|
+
else if (command === 'rule-coverage')
|
|
40
|
+
await ruleCoverage(rest);
|
|
35
41
|
else
|
|
36
42
|
await run(process.argv.slice(2));
|
|
37
43
|
}
|
package/dist/lib/args.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `--` の前の引数と、後ろの対象ファイルに分ける。 */
|
|
2
|
+
export function splitFiles(args) {
|
|
3
|
+
const separator = args.indexOf('--');
|
|
4
|
+
return separator === -1
|
|
5
|
+
? { flags: args, files: [] }
|
|
6
|
+
: { flags: args.slice(0, separator), files: args.slice(separator + 1) };
|
|
7
|
+
}
|
|
8
|
+
/** `--name value` を、繰り返しも含めて読む。 */
|
|
9
|
+
export function flagValues(args, name) {
|
|
10
|
+
return args.flatMap((arg, index) => (arg === name && args[index + 1] ? [args[index + 1]] : []));
|
|
11
|
+
}
|
package/dist/lib/fs.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { basename, dirname, join } from 'node:path';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
/** OS エラーの code (ENOENT など)。Error 以外や code を持たないエラーは undefined。 */
|
|
5
5
|
export const errorCode = (error) => error instanceof Error ? error.code : undefined;
|
|
6
6
|
export const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
7
|
+
export const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
8
|
+
/**
|
|
9
|
+
* ファイルの内容の sha256。
|
|
10
|
+
* @param root files の基準のディレクトリ
|
|
11
|
+
* @param files root からの相対パス。無いファイルは結果に含めない
|
|
12
|
+
*/
|
|
13
|
+
export async function hashFiles(root, files) {
|
|
14
|
+
const hashes = {};
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
try {
|
|
17
|
+
hashes[file] = sha256(await readFile(resolve(root, file)));
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (errorCode(error) !== 'ENOENT')
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return hashes;
|
|
25
|
+
}
|
|
7
26
|
export const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
|
|
8
27
|
/** JSON ファイルを読む。無ければ null。 */
|
|
9
28
|
export async function readJsonIfExists(path) {
|
package/dist/lib/install.js
CHANGED
|
@@ -2,9 +2,11 @@ import { readFile, readdir, cp, rm, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import {
|
|
5
|
+
import { registerPlugin, replaceInstallation, restore, } from "./transaction.js";
|
|
6
6
|
import { withLock } from "./lock.js";
|
|
7
|
-
import { readTextIfExists, writeFileAtomic } from "./fs.js";
|
|
7
|
+
import { readTextIfExists, sha256, writeFileAtomic } from "./fs.js";
|
|
8
|
+
import { RULE_COVERAGE_COMMAND } from "./rule-coverage.js";
|
|
9
|
+
import { RULE_SNAPSHOT_COMMAND } from "./rule-snapshot.js";
|
|
8
10
|
import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedAgent, managedRoot, marketplaceName, packageRoot, payloadDir, payloadPaths, pluginDir, pluginId, statePath, } from "./layout.js";
|
|
9
11
|
const actions = ['install', 'update', 'uninstall', 'recover'];
|
|
10
12
|
const isAction = (value) => actions.includes(value);
|
|
@@ -15,6 +17,8 @@ const usage = [
|
|
|
15
17
|
' shinaps-skills managed <setup|run|status|reply|collect> (see managed --help)',
|
|
16
18
|
' shinaps-skills auto-update <enable|disable|status|check> codex',
|
|
17
19
|
' shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>',
|
|
20
|
+
` ${RULE_SNAPSHOT_COMMAND}`,
|
|
21
|
+
` ${RULE_COVERAGE_COMMAND}`,
|
|
18
22
|
'Claude Code: /plugin marketplace add sh1n4ps/shinaps-skills',
|
|
19
23
|
' /plugin install shinaps@shinaps-skills',
|
|
20
24
|
].join('\n');
|
|
@@ -173,7 +177,7 @@ async function readVerifiedState(installation) {
|
|
|
173
177
|
const current = await readTextIfExists(join(agentsDir, name));
|
|
174
178
|
if (current === null)
|
|
175
179
|
continue;
|
|
176
|
-
const currentHash =
|
|
180
|
+
const currentHash = sha256(current);
|
|
177
181
|
if (currentHash !== hash && currentHash !== state.pending?.[name]) {
|
|
178
182
|
throw new Error(`Modified agent preserved: ${join(agentsDir, name)}. Move the edited file elsewhere or restore its content, then retry.`);
|
|
179
183
|
}
|
|
@@ -192,7 +196,7 @@ async function readPackagedAgents(installation, state) {
|
|
|
192
196
|
if (!(name in state.agents) && (await readTextIfExists(join(agentsDir, name))) !== null) {
|
|
193
197
|
throw new Error(`Unmanaged agent preserved: ${join(agentsDir, name)}. Shinaps did not install this file. Move or rename it, then retry.`);
|
|
194
198
|
}
|
|
195
|
-
agents[name] = { content, hash:
|
|
199
|
+
agents[name] = { content, hash: sha256(content) };
|
|
196
200
|
}
|
|
197
201
|
return agents;
|
|
198
202
|
}
|
package/dist/lib/lint.js
CHANGED
|
@@ -2,7 +2,8 @@ import { readFile, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { checkKey, classify, parseLintInput, planUnits, } from "./lint-plan.js";
|
|
4
4
|
import { shinapsPlugin } from "./lint-rules.js";
|
|
5
|
-
import {
|
|
5
|
+
import { splitFiles } from "./args.js";
|
|
6
|
+
import { errorMessage, hashFiles, sha256 } from "./fs.js";
|
|
6
7
|
const USAGE = 'Usage: shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>';
|
|
7
8
|
/**
|
|
8
9
|
* `shinaps-skills lint`: 規約の検査を、プロジェクトの設定を使わずに ESLint の Node API で実行し、
|
|
@@ -12,9 +13,13 @@ const USAGE = 'Usage: shinaps-skills lint --checks <checks.json> --root <project
|
|
|
12
13
|
*/
|
|
13
14
|
export async function lint(args) {
|
|
14
15
|
const options = parseArgs(args);
|
|
15
|
-
const
|
|
16
|
+
const checksText = await readFile(options.checks);
|
|
17
|
+
const input = parseLintInput(JSON.parse(checksText.toString('utf8')));
|
|
18
|
+
const files = await hashFiles(options.root, options.files.map((file) => toRelative(options.root, file)));
|
|
16
19
|
const result = await runLint(input, options.root, options.files);
|
|
17
|
-
|
|
20
|
+
// 結果がどの検査とどの内容のファイルから作られたかを残し、古い結果を機械確認として使わないようにする。
|
|
21
|
+
const output = { ...result, checksSha256: sha256(checksText), files };
|
|
22
|
+
await writeFile(options.out, `${JSON.stringify(output, null, 2)}\n`);
|
|
18
23
|
const counts = Object.fromEntries(['covered', 'violation', 'partial', 'not-run'].map((state) => [
|
|
19
24
|
state,
|
|
20
25
|
result.pairs.filter((pair) => pair.state === state).length,
|
|
@@ -22,9 +27,7 @@ export async function lint(args) {
|
|
|
22
27
|
console.log(`Shinaps lint: ${JSON.stringify(counts)} → ${options.out}`);
|
|
23
28
|
}
|
|
24
29
|
function parseArgs(args) {
|
|
25
|
-
const
|
|
26
|
-
const flags = separator === -1 ? args : args.slice(0, separator);
|
|
27
|
-
const files = separator === -1 ? [] : args.slice(separator + 1);
|
|
30
|
+
const { flags, files } = splitFiles(args);
|
|
28
31
|
const value = (name) => {
|
|
29
32
|
const index = flags.indexOf(name);
|
|
30
33
|
const found = index === -1 ? undefined : flags[index + 1];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readdir, rename, rm, stat } from 'node:fs/promises';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import { readJson, readJsonIfExists, writeFileAtomic } from "./fs.js";
|
|
4
|
+
import { readJson, readJsonIfExists, sha256, writeFileAtomic } from "./fs.js";
|
|
5
5
|
import { withLock } from "./lock.js";
|
|
6
6
|
import { filesBeta, managedBeta } from "./managed-ant.js";
|
|
7
7
|
import { importSnapshot } from "./managed-snapshot.js";
|
|
@@ -14,7 +14,6 @@ const permissionLabels = {
|
|
|
14
14
|
push: 'headへのpush',
|
|
15
15
|
draft_pr: 'draft PRの作成',
|
|
16
16
|
};
|
|
17
|
-
const sha256 = (text) => createHash('sha256').update(text).digest('hex');
|
|
18
17
|
/**
|
|
19
18
|
* `--transform id -r` の出力から ID を取り出す。ant は transform のパスが応答に無いと応答の全体を
|
|
20
19
|
* 出すので、ID の形でない出力を ID として記録しない。
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, normalize, relative } from 'node:path';
|
|
3
|
+
import { flagValues, splitFiles } from "./args.js";
|
|
4
|
+
import { hashFiles, readJson } from "./fs.js";
|
|
5
|
+
import { verifySnapshot } from "./rule-snapshot.js";
|
|
6
|
+
export const RULE_COVERAGE_COMMAND = 'shinaps-skills rule-coverage --manifest <manifest.json> --target-version <version> [--lint <result.json>] [--report <report.json> ...] --out <coverage.json> -- <files...>';
|
|
7
|
+
const NO_REPORT = 'no report for this snapshot and target version covers this file';
|
|
8
|
+
/**
|
|
9
|
+
* 規約×ファイルの組を次の優先順でちょうど一つに区分する。機械が保証するのは同じ snapshot と ID・保存データ・対象の版の
|
|
10
|
+
* 整合までで、reported は担当の申告(読了や理解の証明ではない)。
|
|
11
|
+
* - machine: lint の covered・violation
|
|
12
|
+
* - reported: この snapshot と版の報告のどれかが、そのファイルを completedFiles に挙げ、その規約を unchecked に挙げていない
|
|
13
|
+
* - unconfirmed: それ以外。報告に書かれていないファイルは、省略から自動で reported にならない
|
|
14
|
+
* @param input.manifest snapshot の manifest
|
|
15
|
+
* @param input.targetVersion 確認対象の固定版。報告の版と比べる
|
|
16
|
+
* @param input.files 対象ファイル(lint の結果と同じ基準の相対パス)
|
|
17
|
+
* @param input.lint 最新の版で実行した lint の結果。実行できなければ null
|
|
18
|
+
* @param input.reports 担当の報告。分担したときは担当ごとの要素を全部渡す。対象の外のファイルの記述は数えない
|
|
19
|
+
*/
|
|
20
|
+
export function classifyCoverage(input) {
|
|
21
|
+
const { manifest, targetVersion } = input;
|
|
22
|
+
const targets = new Set(input.files.map((file) => normalize(file)));
|
|
23
|
+
const ids = new Set(manifest.ruleIds);
|
|
24
|
+
const machinePairKeys = new Set((input.lint?.pairs ?? [])
|
|
25
|
+
.filter((pair) => pair.state === 'covered' || pair.state === 'violation')
|
|
26
|
+
.map((pair) => `${pair.ruleId}\0${normalize(pair.file)}`));
|
|
27
|
+
const reports = input.reports
|
|
28
|
+
.filter((entry) => entry.contentHash === manifest.contentHash && entry.targetVersion === targetVersion)
|
|
29
|
+
.map((entry) => {
|
|
30
|
+
const incomplete = new Map(entry.incompleteFiles.map((file) => [normalize(file.path), file.reason]));
|
|
31
|
+
return {
|
|
32
|
+
incomplete,
|
|
33
|
+
completed: new Set(entry.completedFiles
|
|
34
|
+
.map((file) => normalize(file))
|
|
35
|
+
.filter((file) => !incomplete.has(file))),
|
|
36
|
+
unchecked: new Map(entry.unchecked.map((rule) => [rule.id, rule.reason])),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
for (const { unchecked } of reports) {
|
|
40
|
+
for (const id of unchecked.keys()) {
|
|
41
|
+
if (!ids.has(id))
|
|
42
|
+
throw new Error(`The report names a rule outside the snapshot: ${id}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const counts = { machine: 0, reported: 0, unconfirmed: 0 };
|
|
46
|
+
const unconfirmedRules = new Map();
|
|
47
|
+
const unconfirmedFiles = new Map();
|
|
48
|
+
for (const id of manifest.ruleIds) {
|
|
49
|
+
for (const file of targets) {
|
|
50
|
+
const covering = reports.filter((report) => report.completed.has(file));
|
|
51
|
+
if (machinePairKeys.has(`${id}\0${file}`)) {
|
|
52
|
+
counts.machine++;
|
|
53
|
+
}
|
|
54
|
+
else if (covering.some((report) => !report.unchecked.has(id))) {
|
|
55
|
+
counts.reported++;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
counts.unconfirmed++;
|
|
59
|
+
const ruleReason = reports.find((report) => report.unchecked.has(id))?.unchecked.get(id);
|
|
60
|
+
if (ruleReason !== undefined)
|
|
61
|
+
unconfirmedRules.set(id, ruleReason);
|
|
62
|
+
if (covering.length === 0) {
|
|
63
|
+
const fileReason = reports
|
|
64
|
+
.find((report) => report.incomplete.has(file))
|
|
65
|
+
?.incomplete.get(file);
|
|
66
|
+
unconfirmedFiles.set(file, fileReason ?? NO_REPORT);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
contentHash: manifest.contentHash,
|
|
73
|
+
targetVersion,
|
|
74
|
+
counts,
|
|
75
|
+
unconfirmedRules: [...unconfirmedRules].map(([id, reason]) => ({ id, reason })),
|
|
76
|
+
unconfirmedFiles: [...unconfirmedFiles].map(([path, reason]) => ({ path, reason })),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const isKeyWithReason = (entry, key) => typeof entry?.[key] === 'string' && typeof entry.reason === 'string';
|
|
80
|
+
const isReport = (entry) => typeof entry?.contentHash === 'string' &&
|
|
81
|
+
typeof entry.targetVersion === 'string' &&
|
|
82
|
+
Array.isArray(entry.completedFiles) &&
|
|
83
|
+
entry.completedFiles.every((file) => typeof file === 'string') &&
|
|
84
|
+
Array.isArray(entry.unchecked) &&
|
|
85
|
+
entry.unchecked.every((item) => isKeyWithReason(item, 'id')) &&
|
|
86
|
+
Array.isArray(entry.incompleteFiles) &&
|
|
87
|
+
entry.incompleteFiles.every((item) => isKeyWithReason(item, 'path'));
|
|
88
|
+
/** AI が書いた報告を読む。形の揺れを受け入れると未確認が一覧から消えるので、何を直すかが分かるように失敗する。 */
|
|
89
|
+
function parseReports(raw) {
|
|
90
|
+
if (!Array.isArray(raw) || !raw.every(isReport)) {
|
|
91
|
+
throw new Error('The report must be an array of { contentHash, targetVersion, completedFiles: [], unchecked: [{ id, reason }], incompleteFiles: [{ path, reason }] }.');
|
|
92
|
+
}
|
|
93
|
+
return raw;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* lint の結果を、この snapshot と現在の内容のものだけ受け入れる。機械確認の組は担当の照合から外すので、古い結果を使うと
|
|
97
|
+
* 誰も見ていない組が残る。
|
|
98
|
+
*/
|
|
99
|
+
async function readCurrentLint(path, manifest) {
|
|
100
|
+
const lint = await readJson(path);
|
|
101
|
+
if (lint.checksSha256 !== manifest.lintChecksSha256) {
|
|
102
|
+
throw new Error('The lint result was not made from this snapshot. Run lint again.');
|
|
103
|
+
}
|
|
104
|
+
const current = await hashFiles(lint.root, Object.keys(lint.files));
|
|
105
|
+
for (const [file, hash] of Object.entries(lint.files)) {
|
|
106
|
+
if (current[file] !== hash)
|
|
107
|
+
throw new Error(`${file} changed after lint. Run lint again.`);
|
|
108
|
+
}
|
|
109
|
+
return lint;
|
|
110
|
+
}
|
|
111
|
+
export async function ruleCoverage(args) {
|
|
112
|
+
const { flags, files } = splitFiles(args);
|
|
113
|
+
const value = (name) => flagValues(flags, name)[0];
|
|
114
|
+
const manifestPath = value('--manifest');
|
|
115
|
+
const targetVersion = value('--target-version');
|
|
116
|
+
const out = value('--out');
|
|
117
|
+
if (manifestPath === undefined ||
|
|
118
|
+
targetVersion === undefined ||
|
|
119
|
+
out === undefined ||
|
|
120
|
+
files.length === 0) {
|
|
121
|
+
throw new Error(`Usage: ${RULE_COVERAGE_COMMAND}`);
|
|
122
|
+
}
|
|
123
|
+
const manifest = await verifySnapshot(manifestPath);
|
|
124
|
+
const lintPath = value('--lint');
|
|
125
|
+
const lint = lintPath === undefined ? null : await readCurrentLint(lintPath, manifest);
|
|
126
|
+
const toTarget = (file) => normalize(isAbsolute(file) ? relative(lint?.root ?? process.cwd(), file) : file);
|
|
127
|
+
const targets = files.map(toTarget);
|
|
128
|
+
// lint の対象に無いファイルは機械確認にならない。パスの基準がずれると、外した組を誰も見ないまま申告に数えてしまう。
|
|
129
|
+
const linted = new Set(Object.keys(lint?.files ?? {}).map((file) => normalize(file)));
|
|
130
|
+
const outside = lint === null ? undefined : targets.find((file) => !linted.has(file));
|
|
131
|
+
if (outside !== undefined) {
|
|
132
|
+
throw new Error(`${outside} is not in the lint result. Run lint again with the same files.`);
|
|
133
|
+
}
|
|
134
|
+
const reports = await Promise.all(flagValues(flags, '--report').map(async (path) => parseReports(await readJson(path))));
|
|
135
|
+
const coverage = classifyCoverage({
|
|
136
|
+
manifest,
|
|
137
|
+
targetVersion,
|
|
138
|
+
files: targets,
|
|
139
|
+
lint,
|
|
140
|
+
reports: reports.flat().map((report) => ({
|
|
141
|
+
...report,
|
|
142
|
+
completedFiles: report.completedFiles.map(toTarget),
|
|
143
|
+
incompleteFiles: report.incompleteFiles.map((file) => ({
|
|
144
|
+
...file,
|
|
145
|
+
path: toTarget(file.path),
|
|
146
|
+
})),
|
|
147
|
+
})),
|
|
148
|
+
});
|
|
149
|
+
await writeFile(out, `${JSON.stringify(coverage, null, 1)}\n`);
|
|
150
|
+
console.log(`Shinaps rule coverage: ${JSON.stringify(coverage.counts)} → ${out}`);
|
|
151
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { flagValues } from "./args.js";
|
|
6
|
+
import { errorMessage, readJson, sha256 } from "./fs.js";
|
|
7
|
+
export const RULE_SNAPSHOT_COMMAND = 'shinaps-skills rule-snapshot --since <epoch-ms> --input <search-input.json> [--input ...] [--exclude-label <slug> ...] [--captures <dir>] --out <dir>';
|
|
8
|
+
// Codex の担当のファイル読取りが切り詰められない大きさ(モックのモデルで 8KB は全文が届き、18KB は切り詰められた)。
|
|
9
|
+
// 1 件で超える規約は 1 件だけの part にし、行範囲で読めるよう整形する。
|
|
10
|
+
const PART_MAX_BYTES = 8000;
|
|
11
|
+
const CAPTURE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
function canonical(value) {
|
|
14
|
+
if (Array.isArray(value))
|
|
15
|
+
return value.map(canonical);
|
|
16
|
+
if (!isRecord(value))
|
|
17
|
+
return value;
|
|
18
|
+
return Object.fromEntries(Object.entries(value)
|
|
19
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
20
|
+
.map(([key, item]) => [key, canonical(item)]));
|
|
21
|
+
}
|
|
22
|
+
const inputKey = ({ cursor: _cursor, ...rest }) => JSON.stringify(canonical(rest));
|
|
23
|
+
const isRule = (value) => isRecord(value) && typeof value.id === 'string';
|
|
24
|
+
const isPage = (value) => isRecord(value) &&
|
|
25
|
+
Array.isArray(value.userRules) &&
|
|
26
|
+
value.userRules.every(isRule) &&
|
|
27
|
+
Array.isArray(value.officialRules) &&
|
|
28
|
+
value.officialRules.every(isRule) &&
|
|
29
|
+
'nextCursor' in value;
|
|
30
|
+
/** 応答の本文からページを取り出す。切り詰めやエラーの応答はページの形にならないので null になる。 */
|
|
31
|
+
function parsePage(response) {
|
|
32
|
+
const content = isRecord(response) ? response.content : response;
|
|
33
|
+
if (!Array.isArray(content))
|
|
34
|
+
return null;
|
|
35
|
+
const text = content.map((item) => (isRecord(item) ? item.text : '')).join('');
|
|
36
|
+
let page;
|
|
37
|
+
try {
|
|
38
|
+
page = JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return isPage(page) ? page : null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 捕捉の形式: plugin の hook が PostToolUse の標準入力の全体を
|
|
47
|
+
* `<captures>/<session>/<時刻 ms>-<tool_use_id>.json` に書いたもの。使う項目は tool_input と tool_response で、
|
|
48
|
+
* tool_response は Claude Code では content の配列、Codex では `{ content }`。
|
|
49
|
+
* @param session 捕捉の session のディレクトリ名
|
|
50
|
+
* @param name 捕捉のファイル名(先頭が時刻 ms)
|
|
51
|
+
* @param text 捕捉のファイルの内容
|
|
52
|
+
*/
|
|
53
|
+
function parseCapture(session, name, text) {
|
|
54
|
+
const event = JSON.parse(text);
|
|
55
|
+
const time = Number.parseInt(name, 10);
|
|
56
|
+
if (!isRecord(event) || !isRecord(event.tool_input) || Number.isNaN(time))
|
|
57
|
+
return null;
|
|
58
|
+
return { session, time, name, input: event.tool_input, page: parsePage(event.tool_response) };
|
|
59
|
+
}
|
|
60
|
+
export async function readCaptures(root) {
|
|
61
|
+
if (!existsSync(root))
|
|
62
|
+
return [];
|
|
63
|
+
const captures = [];
|
|
64
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
65
|
+
if (!entry.isDirectory())
|
|
66
|
+
continue;
|
|
67
|
+
const session = entry.name;
|
|
68
|
+
for (const name of await readdir(join(root, session))) {
|
|
69
|
+
if (!name.endsWith('.json'))
|
|
70
|
+
continue;
|
|
71
|
+
const capture = parseCapture(session, name, await readFile(join(root, session, name), 'utf8'));
|
|
72
|
+
if (capture !== null)
|
|
73
|
+
captures.push(capture);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return captures.sort((a, b) => a.time - b.time || (a.name < b.name ? -1 : 1));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* since 以降で最新の先頭ページから nextCursor null まで辿る。古い連鎖へは戻らない。戻ると、hook が止まった後や
|
|
80
|
+
* 取り直しが途中で止まった後に、以前の規約で成功してしまうため。
|
|
81
|
+
* @param captures 時刻の昇順の捕捉
|
|
82
|
+
* @param input cursor を含まない search_rules の入力
|
|
83
|
+
* @param since この時刻 (epoch ms) より前の捕捉を使わない
|
|
84
|
+
*/
|
|
85
|
+
function walkChain(captures, input, since) {
|
|
86
|
+
const key = inputKey(input);
|
|
87
|
+
const matching = captures.filter((row) => row.time >= since && inputKey(row.input) === key);
|
|
88
|
+
const first = matching.findLast((row) => row.input.cursor === undefined);
|
|
89
|
+
if (first === undefined) {
|
|
90
|
+
throw new Error(`No captured response since ${since} for ${key}. Check that the Shinaps hook is enabled and trusted, that this command runs in the same Git root as the fetch (or pass --captures), and that --input is the exact input of the fetch. Then fetch again from the first page.`);
|
|
91
|
+
}
|
|
92
|
+
const sessionRows = matching.filter((row) => row.session === first.session);
|
|
93
|
+
const pages = [];
|
|
94
|
+
let currentIndex = sessionRows.indexOf(first);
|
|
95
|
+
for (;;) {
|
|
96
|
+
const { page } = sessionRows[currentIndex];
|
|
97
|
+
if (page === null) {
|
|
98
|
+
throw new Error(`Incomplete chain: unusable response for ${key}. Fetch again from the first page.`);
|
|
99
|
+
}
|
|
100
|
+
pages.push(page);
|
|
101
|
+
if (page.nextCursor === null)
|
|
102
|
+
return pages;
|
|
103
|
+
const after = currentIndex;
|
|
104
|
+
const nextIndex = sessionRows.findIndex((row, index) => index > after && row.input.cursor === page.nextCursor && row.page !== null);
|
|
105
|
+
if (nextIndex === -1) {
|
|
106
|
+
throw new Error(`Incomplete chain: missing cursor ${page.nextCursor} for ${key}. Fetch again from the first page.`);
|
|
107
|
+
}
|
|
108
|
+
currentIndex = nextIndex;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @param source.captures 時刻の昇順の捕捉
|
|
113
|
+
* @param source.inputs search_rules の入力。複数のときは id の和を取る
|
|
114
|
+
* @param source.since この時刻 (epoch ms) より前の捕捉を使わない
|
|
115
|
+
* @param source.excludeLabels これらの label を持つ規約を除外する(理由を残す)
|
|
116
|
+
*/
|
|
117
|
+
export function buildSnapshot(source) {
|
|
118
|
+
const byId = new Map();
|
|
119
|
+
for (const input of source.inputs) {
|
|
120
|
+
for (const page of walkChain(source.captures, input, source.since)) {
|
|
121
|
+
for (const rule of [...page.userRules, ...page.officialRules]) {
|
|
122
|
+
const seen = byId.get(rule.id);
|
|
123
|
+
if (seen !== undefined && !isDeepStrictEqual(seen, rule)) {
|
|
124
|
+
throw new Error(`Rule ${rule.id} was captured with different content. Fetch again.`);
|
|
125
|
+
}
|
|
126
|
+
byId.set(rule.id, rule);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const rules = [];
|
|
131
|
+
const excluded = [];
|
|
132
|
+
for (const rule of [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : 1))) {
|
|
133
|
+
const label = rule.labels?.find((slug) => source.excludeLabels?.includes(slug) === true);
|
|
134
|
+
if (label === undefined)
|
|
135
|
+
rules.push(rule);
|
|
136
|
+
else
|
|
137
|
+
excluded.push({ id: rule.id, reason: `label ${label} is on an empty axis` });
|
|
138
|
+
}
|
|
139
|
+
return { rules, excluded };
|
|
140
|
+
}
|
|
141
|
+
const partText = (rules) => `${JSON.stringify(rules, null, 1)}\n`;
|
|
142
|
+
const checksText = (rules) => `${JSON.stringify({ rules: rules.filter((rule) => (rule.checks?.length ?? 0) > 0).map((rule) => ({ id: rule.id, checks: rule.checks })) }, null, 1)}\n`;
|
|
143
|
+
const contentHashOf = (rules) => sha256(JSON.stringify(canonical(rules)));
|
|
144
|
+
export function splitParts(rules) {
|
|
145
|
+
const parts = [];
|
|
146
|
+
for (const rule of rules) {
|
|
147
|
+
const last = parts.at(-1);
|
|
148
|
+
if (last !== undefined && Buffer.byteLength(partText([...last, rule])) <= PART_MAX_BYTES) {
|
|
149
|
+
last.push(rule);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
parts.push([rule]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return parts;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* @param snapshot buildSnapshot の結果
|
|
159
|
+
* @param inputs manifest に残す search_rules の入力
|
|
160
|
+
* @param out 出力先のディレクトリ
|
|
161
|
+
*/
|
|
162
|
+
export async function writeSnapshot(snapshot, inputs, out) {
|
|
163
|
+
await rm(join(out, 'parts'), { recursive: true, force: true });
|
|
164
|
+
await mkdir(join(out, 'parts'), { recursive: true });
|
|
165
|
+
const parts = [];
|
|
166
|
+
for (const [index, rules] of splitParts(snapshot.rules).entries()) {
|
|
167
|
+
const file = `parts/part-${String(index + 1).padStart(3, '0')}.json`;
|
|
168
|
+
const text = partText(rules);
|
|
169
|
+
await writeFile(join(out, file), text);
|
|
170
|
+
parts.push({ file, sha256: sha256(text) });
|
|
171
|
+
}
|
|
172
|
+
const lintChecks = checksText(snapshot.rules);
|
|
173
|
+
await writeFile(join(out, 'lint-checks.json'), lintChecks);
|
|
174
|
+
const manifest = {
|
|
175
|
+
contentHash: contentHashOf(snapshot.rules),
|
|
176
|
+
inputs,
|
|
177
|
+
ruleIds: snapshot.rules.map((rule) => rule.id),
|
|
178
|
+
excluded: snapshot.excluded,
|
|
179
|
+
parts,
|
|
180
|
+
lintChecksSha256: sha256(lintChecks),
|
|
181
|
+
};
|
|
182
|
+
await writeFile(join(out, 'manifest.json'), `${JSON.stringify(manifest, null, 1)}\n`);
|
|
183
|
+
return manifest;
|
|
184
|
+
}
|
|
185
|
+
/** snapshot のファイルが作成時から変わっていないことを確かめ、manifest を返す。 */
|
|
186
|
+
export async function verifySnapshot(manifestPath) {
|
|
187
|
+
const directory = dirname(manifestPath);
|
|
188
|
+
const manifest = await readJson(manifestPath);
|
|
189
|
+
const rules = [];
|
|
190
|
+
for (const part of manifest.parts) {
|
|
191
|
+
const text = await readFile(join(directory, part.file), 'utf8');
|
|
192
|
+
if (sha256(text) !== part.sha256)
|
|
193
|
+
throw new Error(`Snapshot part changed: ${part.file}. Run rule-snapshot again.`);
|
|
194
|
+
const partRules = JSON.parse(text);
|
|
195
|
+
if (!Array.isArray(partRules))
|
|
196
|
+
throw new Error(`Snapshot part is not a list: ${part.file}. Run rule-snapshot again.`);
|
|
197
|
+
rules.push(...partRules);
|
|
198
|
+
}
|
|
199
|
+
if (contentHashOf(rules) !== manifest.contentHash)
|
|
200
|
+
throw new Error('Snapshot content changed. Run rule-snapshot again.');
|
|
201
|
+
if (!isDeepStrictEqual(rules.map((rule) => rule.id), manifest.ruleIds)) {
|
|
202
|
+
throw new Error('manifest.json ruleIds do not match the parts. Run rule-snapshot again.');
|
|
203
|
+
}
|
|
204
|
+
const checks = await readFile(join(directory, 'lint-checks.json'), 'utf8');
|
|
205
|
+
if (sha256(checks) !== manifest.lintChecksSha256)
|
|
206
|
+
throw new Error('lint-checks.json changed. Run rule-snapshot again.');
|
|
207
|
+
return manifest;
|
|
208
|
+
}
|
|
209
|
+
/** 24 時間より古い捕捉だけを消す。他の session の取得中の捕捉は残す。 */
|
|
210
|
+
async function removeOldCaptures(root, now) {
|
|
211
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
212
|
+
if (!entry.isDirectory())
|
|
213
|
+
continue;
|
|
214
|
+
const directory = join(root, entry.name);
|
|
215
|
+
for (const name of await readdir(directory)) {
|
|
216
|
+
if (now - Number.parseInt(name, 10) > CAPTURE_MAX_AGE_MS)
|
|
217
|
+
await rm(join(directory, name), { force: true });
|
|
218
|
+
}
|
|
219
|
+
if ((await readdir(directory)).length === 0)
|
|
220
|
+
await rm(directory, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// plugin の hook は依存なしで動かすため同じ処理を持つ。同じ保存先になることは test/rule-snapshot.test.ts の通しの試験が確かめる。
|
|
224
|
+
function projectRoot(cwd) {
|
|
225
|
+
for (let directory = cwd;; directory = dirname(directory)) {
|
|
226
|
+
if (existsSync(join(directory, '.git')))
|
|
227
|
+
return directory;
|
|
228
|
+
if (dirname(directory) === directory)
|
|
229
|
+
return cwd;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function ruleSnapshot(args) {
|
|
233
|
+
const since = Number(flagValues(args, '--since')[0]);
|
|
234
|
+
const inputPaths = flagValues(args, '--input');
|
|
235
|
+
const out = flagValues(args, '--out')[0];
|
|
236
|
+
if (!Number.isFinite(since) || inputPaths.length === 0 || out === undefined) {
|
|
237
|
+
throw new Error(`Usage: ${RULE_SNAPSHOT_COMMAND}`);
|
|
238
|
+
}
|
|
239
|
+
const rawInputs = await Promise.all(inputPaths.map((path) => readJson(path)));
|
|
240
|
+
const inputs = rawInputs.filter(isRecord);
|
|
241
|
+
if (inputs.length !== rawInputs.length || inputs.some((input) => input.fields !== 'full')) {
|
|
242
|
+
throw new Error(`--input must be an object with fields "full": ${JSON.stringify(rawInputs)}`);
|
|
243
|
+
}
|
|
244
|
+
const root = flagValues(args, '--captures')[0] ??
|
|
245
|
+
join(projectRoot(process.cwd()), '.shinaps/work/rule-captures');
|
|
246
|
+
let snapshot;
|
|
247
|
+
try {
|
|
248
|
+
snapshot = buildSnapshot({
|
|
249
|
+
captures: await readCaptures(root),
|
|
250
|
+
inputs,
|
|
251
|
+
since,
|
|
252
|
+
excludeLabels: flagValues(args, '--exclude-label'),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
throw new Error(`${errorMessage(error)} Captures: ${root}`, { cause: error });
|
|
257
|
+
}
|
|
258
|
+
const manifest = await writeSnapshot(snapshot, inputs, resolve(out));
|
|
259
|
+
await removeOldCaptures(root, Date.now());
|
|
260
|
+
console.log(`Shinaps rule snapshot: ${manifest.ruleIds.length} rules, ${manifest.excluded.length} excluded, ${manifest.parts.length} parts, contentHash ${manifest.contentHash} → ${out}`);
|
|
261
|
+
}
|
package/dist/lib/transaction.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { mkdir, rm, rename, cp } from 'node:fs/promises';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from "./layout.js";
|
|
5
|
-
import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from "./fs.js";
|
|
6
|
-
/** エージェント内容の sha256。state.json と journal の `nextHashes` はこの形式で記録する。 */
|
|
7
|
-
export const digest = (text) => createHash('sha256').update(text).digest('hex');
|
|
4
|
+
import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, sha256, } from "./fs.js";
|
|
8
5
|
/** 配布物を Codex に登録する。install と restore で同じ手順を使う。 */
|
|
9
6
|
export async function registerPlugin(command, payload) {
|
|
10
7
|
await command(['plugin', 'marketplace', 'add', payload]);
|
|
@@ -83,7 +80,7 @@ export async function restore(target) {
|
|
|
83
80
|
const current = await readTextIfExists(join(agentsDir, name));
|
|
84
81
|
if (current === null)
|
|
85
82
|
continue;
|
|
86
|
-
if (current !== priorContent &&
|
|
83
|
+
if (current !== priorContent && sha256(current) !== journal.nextHashes[name]) {
|
|
87
84
|
throw new Error(`Modified agent preserved during recovery: ${join(agentsDir, name)}. Move the edited file elsewhere, then run "shinaps-skills recover codex".`);
|
|
88
85
|
}
|
|
89
86
|
}
|
package/docs/development.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- Claude 版: `plugins/shinaps-claude/`、marketplace は `.claude-plugin/marketplace.json`
|
|
8
8
|
- Codex 版: `plugins/shinaps-codex/`、marketplace は `.agents/plugins/marketplace.json`。サブエージェントは `plugins/shinaps-codex/agents/`
|
|
9
9
|
- Claude Managed Agents 版: 両 plugin の `managed-agents/`。plugin と一緒に配布され、npm パッケージの `plugins/` にも含まれる
|
|
10
|
+
- 規約の受け渡し: plugin の hook が `search_rules` の応答を保存し、配布 CLI の `rule-snapshot`・`rule-coverage` が担当と機械検査の読むファイルと報告の区分を作る(手順は `references/rules.md`)。
|
|
10
11
|
- 配布 CLI: ソースは `bin/skills.ts` と `lib/*.ts`。規約の機械検査(`lint`)は ESLint の Node API を使い、仕組みと同梱ルールの追加手順は [規約の機械検査](lint.md) にある。TypeScript は typescript-eslint が対応する 6 系を実行時の依存に持ち、開発用の `tsc` も同じ版を使う(typescript-eslint が 7 に対応したら上げる)。`npm run build`(`tsc`)で `dist/` に JS を出力し、npm にはそれを配布する。Node.js は `node_modules` 配下の `.ts` に型除去を適用しないため、ソースのままは配布できない。checkout では型除去で直接実行するので、`enum` など型除去で消せない構文は使わない(`tsconfig.json` の `erasableSyntaxOnly`)
|
|
11
12
|
|
|
12
13
|
品質基準とプロジェクト設定の方針は両版で揃え、クライアント固有の実行手順とエージェントの形式は別々に管理する。スキルを追加・変更したら各版への適用可否を判断する。
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
- Managed の共通定義は `plugins/shinaps-claude/managed-agents/` を編集元として Codex 版へ複製し、テストで一致を確認する。Managed の skill の `staffing.md`・`workflow.md`・`launch.md` は Claude 版を写した適応版で一致はテストしないため(`review.md`・`design.md` は下の正本の複製でテストする)、Claude 版の工程・体制の基準を変えたら Managed への適用可否を判断する。
|
|
16
17
|
- レビューの共通契約は `references/review.md` を正本とし、本文を Managed の skill の `references/review.md` に複製してテストで一致を確認する。Codex の `shinaps-reviewer` は、Claude 版の「レビューの種類」以降の本文を同じ文で持ち、テストで一致を確認する。担当の構成・対象の固定・完了の判定・採否は契約に、種類ごとの作業手順は役割の定義に、単独起動の対象・体制・到達点は `references/review-standalone.md` に、担当の起動と修正の引き継ぎ先だけを両版の `review` の `SKILL.md` に書く。
|
|
17
18
|
- テスト方針は各版の `references/testing.md` を正本とし、その段落を役割定義と Managed の lead の定義に複製する。設計の観点は `references/dev/design.md` を正本とし、本文を独立設計担当(`shinaps-architect`)の定義と Managed の skill の `references/design.md` に複製する。どちらもテストで一致を確認する。
|
|
18
|
-
- 役割の定義は Claude 版が Markdown、Codex 版が TOML、Managed 版が `ant apply` 用の frontmatter を付けた Markdown。サブエージェント内では shinaps MCP を直接呼ばず、rule
|
|
19
|
+
- 役割の定義は Claude 版が Markdown、Codex 版が TOML、Managed 版が `ant apply` 用の frontmatter を付けた Markdown。サブエージェント内では shinaps MCP を直接呼ばず、rule の全文はスキル側から渡すか、スキル側が機械保存した snapshot のファイルを読ませる(MCP の利用量が重複せず、全文をモデルに書き写させないため)。この原則を全ての役割定義に書く。
|
|
19
20
|
- `autonomous-commit-review` の役割は `commit` に統合した。旧 `linear` の入口は廃止し、`linear-plan`・`linear-execute` に分けた。規約照合と簡素化の独立した入口は `review` に統合し、規約の一括抽出の入口は廃止した(規約の登録は利用者がその場で依頼する)。個人用の別 AI 呼び出し・ブラウザ管理・ゴール管理・動画制作はこの plugin に含めない。Bridge 本体は同梱せず、導入済みの MCP と WebStorm プラグインを使う。
|
|
20
21
|
|
|
21
22
|
## ローカル開発
|
|
@@ -41,6 +42,7 @@ npm run verify:package
|
|
|
41
42
|
|
|
42
43
|
`.github/workflows/publish.yml` は main への push・PR・手動実行でテストと配布内容を検証する。通常の GitHub Release を公開すると、検証の成功後に同じ tag のパッケージを npm へ公開する。prerelease と手動実行では公開しない。
|
|
43
44
|
|
|
45
|
+
- plugin の hook(`hooks/`)は両版で本文を揃える。Codex は plugin の更新のたびに hook の信頼が外れるので、リリースの案内に `/hooks` での再信頼を書く。Shinaps AI の MCP の応答の形を変える版は、API の配備を先に行う。
|
|
44
46
|
- リリース時は `package.json` と両 plugin の manifest のバージョンを揃えてコミットし、`v<version>` の tag で GitHub Release を公開する。tag とバージョンが一致しなければ止まる。既存の npm の版は再公開できないため、次の変更には新しいバージョンを使う。`docs/install.md` の配布 CLI の導入コマンドと、`references/linear/configuration.md`(両版)の CLI の版も新しい版に揃える。設定例の `$schema` と同梱 schema の `$id`(Linear・Managed、両版)は、schema の契約を変えた版でだけ上げる。公開前に npm スコープ `@shinaps` の権限と tarball の内容を確認する。
|
|
45
47
|
- npm の `@shinaps/skills` には GitHub Actions の Trusted Publisher(owner `sh1n4ps`、repository `shinaps-skills`、workflow `publish.yml`、direct publish を許可)を登録している。publish job だけに `id-token: write` を付け、`NPM_TOKEN` secret は使わない。CI は GitHub-hosted の Ubuntu と Node.js 24 を使う。初回の信頼設定には本人認証が必要だが、その後の公開に毎回のセキュリティキー操作は不要。
|
|
46
48
|
- npm の provenance は公開リポジトリからの Trusted Publishing で生成される。
|