@shinaps/skills 0.43.0 → 0.44.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 CHANGED
@@ -8,7 +8,7 @@ Claude Code と Codex で使う、開発ワークフローのスキル集です
8
8
  ## 必要なもの
9
9
 
10
10
  - Claude Code または Codex CLI
11
- - Node.js 22 以上(Codex への導入と、設定の検証・Managed Agents の操作に使う[配布 CLI](docs/install.md#配布-cli)に必要)
11
+ - Node.js 22.13 以上の 22 系、または 24 以上(Codex への導入と、設定の検証・Managed Agents の操作・規約の機械検査に使う[配布 CLI](docs/install.md#配布-cli)に必要。機械検査の ESLint の要件に合わせている)。配布 CLI は機械検査のために ESLint・typescript-eslint・TypeScript などを依存に持ち、導入に約 50MB を使います。
12
12
  - 機能によって追加で必要なもの(Linear・Claude Managed Agents など)は、下の「機能別の資料」の各資料に書いています。
13
13
 
14
14
  ## インストール
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { packageRoot } from '../lib/layout.js';
5
- import { validateLinearConfig } from '../lib/linear-config.js';
6
- import { validateManagedConfig } from '../lib/managed-config.js';
7
- import { managed } from '../lib/managed.js';
8
- import { run } from '../lib/install.js';
9
- import { autoUpdate } from '../lib/auto-update.js';
10
- import { errorMessage } from '../lib/fs.js';
4
+ import { packageRoot } from "../lib/layout.js";
5
+ import { validateLinearConfig } from "../lib/linear-config.js";
6
+ import { validateManagedConfig } from "../lib/managed-config.js";
7
+ import { managed } from "../lib/managed.js";
8
+ import { run } from "../lib/install.js";
9
+ import { autoUpdate } from "../lib/auto-update.js";
10
+ import { lint } from "../lib/lint.js";
11
+ import { errorMessage } from "../lib/fs.js";
11
12
  const [command, ...rest] = process.argv.slice(2);
12
13
  try {
13
14
  if (command === '--version') {
@@ -29,6 +30,8 @@ try {
29
30
  await managed(rest);
30
31
  else if (command === 'auto-update')
31
32
  await autoUpdate(rest);
33
+ else if (command === 'lint')
34
+ await lint(rest);
32
35
  else
33
36
  await run(process.argv.slice(2));
34
37
  }
@@ -2,10 +2,10 @@ import { mkdir, writeFile, rm } from 'node:fs/promises';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { execFileSync, spawnSync } from 'node:child_process';
5
- import { withLock } from './lock.js';
6
- import { install, openInstallation, recover } from './install.js';
7
- import { errorMessage, readJson, readJsonIfExists } from './fs.js';
8
- import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedRoot, manifestPath, payloadDir, statePath, } from './layout.js';
5
+ import { withLock } from "./lock.js";
6
+ import { install, openInstallation, recover } from "./install.js";
7
+ import { errorMessage, readJson, readJsonIfExists } from "./fs.js";
8
+ import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedRoot, manifestPath, payloadDir, statePath, } from "./layout.js";
9
9
  const actions = ['enable', 'disable', 'status', 'check'];
10
10
  const isAction = (value) => actions.includes(value);
11
11
  const label = 'io.shinaps.skills.update';
@@ -1,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { Ajv } from 'ajv';
4
- import { packageRoot } from './layout.js';
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'));
@@ -2,10 +2,10 @@ 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 { digest, registerPlugin, replaceInstallation, restore, } from './transaction.js';
6
- import { withLock } from './lock.js';
7
- import { readTextIfExists, writeFileAtomic } from './fs.js';
8
- import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedAgent, managedRoot, marketplaceName, packageRoot, payloadDir, payloadPaths, pluginDir, pluginId, statePath, } from './layout.js';
5
+ import { digest, registerPlugin, replaceInstallation, restore, } from "./transaction.js";
6
+ import { withLock } from "./lock.js";
7
+ import { readTextIfExists, writeFileAtomic } from "./fs.js";
8
+ import { codexHomeMismatch, codexHomeOf, isRecordedCodexHome, managedAgent, managedRoot, marketplaceName, packageRoot, payloadDir, payloadPaths, pluginDir, pluginId, statePath, } from "./layout.js";
9
9
  const actions = ['install', 'update', 'uninstall', 'recover'];
10
10
  const isAction = (value) => actions.includes(value);
11
11
  const usage = [
@@ -14,6 +14,7 @@ const usage = [
14
14
  ' shinaps-skills validate-managed-config <file>',
15
15
  ' shinaps-skills managed <setup|run|status|reply|collect> (see managed --help)',
16
16
  ' shinaps-skills auto-update <enable|disable|status|check> codex',
17
+ ' shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>',
17
18
  'Claude Code: /plugin marketplace add sh1n4ps/shinaps-skills',
18
19
  ' /plugin install shinaps@shinaps-skills',
19
20
  ].join('\n');
@@ -124,7 +125,7 @@ async function uninstall(installation, options) {
124
125
  return;
125
126
  }
126
127
  // auto-update.ts は check でこのモジュールを使う。相互 import を避けるため実行時に読み込む。
127
- const { autoUpdate } = await import('./auto-update.js');
128
+ const { autoUpdate } = await import("./auto-update.js");
128
129
  await autoUpdate(['disable', 'codex'], options);
129
130
  await command(['plugin', 'remove', pluginId]);
130
131
  await command(['plugin', 'marketplace', 'remove', marketplaceName]);
@@ -1,4 +1,4 @@
1
- import { validateConfig } from './config-validation.js';
1
+ import { validateConfig } from "./config-validation.js";
2
2
  /** `.shinaps/linear.json` を同梱の契約で検証する。 */
3
3
  export async function validateLinearConfig(path) {
4
4
  await validateConfig(path, {
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `shinaps-skills lint` の入力・実行単位・結果の分類。ESLint を呼ばない純関数だけを置き、実行は lint.ts が行う。
3
+ *
4
+ * 規約 R × ファイル F の組を AI 照合から外してよいのは、次がすべて成り立つときだけ(docs/lint.md)。
5
+ * - R の検査の coverage が full(登録時の意味のレビューの結果。ここでは値を読むだけ)
6
+ * - その検査が F に適用されると ESLint が判定し、検査が属する実行単位が完了し、F に致命的な報告も
7
+ * ruleId の無い報告も無い
8
+ * どれかが欠けた組は not-run にして AI へ回す。
9
+ */
10
+ /** 検査を引く key。規約 id と、その規約の checks の中の位置で決まる。 */
11
+ export const checkKey = (ruleId, index) => `${ruleId}#${index}`;
12
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ /**
14
+ * 入力 JSON を検査の一覧にする。形が違えば throw し、呼び出し側は結果を書かずに失敗する(全組を AI へ)。
15
+ * 形だけを見る。ルール ID・options・glob の中身は検査せず、ESLint が拒否すればその検査だけが not-run になる。
16
+ */
17
+ export function parseLintInput(raw) {
18
+ if (!isObject(raw) || !Array.isArray(raw.rules))
19
+ throw new Error('checks: expected { "rules": [...] }');
20
+ return {
21
+ rules: raw.rules.map((rule, ruleIndex) => {
22
+ const where = `checks: rules[${ruleIndex}]`;
23
+ if (!isObject(rule) || typeof rule.id !== 'string' || rule.id === '')
24
+ throw new Error(`${where}: expected a non-empty "id"`);
25
+ if (!Array.isArray(rule.checks))
26
+ throw new Error(`${where}: expected "checks" to be an array`);
27
+ return {
28
+ id: rule.id,
29
+ checks: rule.checks.map((check, checkIndex) => parseCheck(check, `${where}.checks[${checkIndex}]`)),
30
+ };
31
+ }),
32
+ };
33
+ }
34
+ function parseCheck(raw, where) {
35
+ if (!isObject(raw))
36
+ throw new Error(`${where}: expected an object`);
37
+ const { rule, options, files, coverage } = raw;
38
+ if (typeof rule !== 'string' || rule === '')
39
+ throw new Error(`${where}: expected a non-empty "rule"`);
40
+ if (options !== undefined && !Array.isArray(options))
41
+ throw new Error(`${where}: expected "options" to be an array`);
42
+ if (!Array.isArray(files) ||
43
+ files.length === 0 ||
44
+ !files.every((file) => typeof file === 'string' && file !== ''))
45
+ throw new Error(`${where}: expected "files" to be a non-empty array of globs`);
46
+ if (coverage !== 'full' && coverage !== 'partial')
47
+ throw new Error(`${where}: expected "coverage" to be "full" or "partial"`);
48
+ return { rule, options: options ?? [], files, coverage };
49
+ }
50
+ /** キーの順を揃えた JSON。同じ options を同じ文字列にして、実行単位でまとめるために使う。 */
51
+ function canonicalJson(value) {
52
+ if (Array.isArray(value))
53
+ return `[${value.map(canonicalJson).join(',')}]`;
54
+ if (isObject(value))
55
+ return `{${Object.keys(value)
56
+ .sort()
57
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
58
+ .join(',')}}`;
59
+ return JSON.stringify(value);
60
+ }
61
+ /**
62
+ * 検査を実行単位に分ける。同じルール・同じ options の検査は 1 つの設定にまとめ、同じルールで options が
63
+ * 違うものだけを別の単位にする(flat config では同じルールの設定は後勝ちで 1 つしか残らないため)。
64
+ * 型が要るルールは projectService を付けた単位に分ける。型の得られないファイルはその単位だけが fatal になり、
65
+ * 型なしの検査を巻き込まない。
66
+ * @param needsTypes ルール ID が型情報を要するか。解決できたルールについてだけ呼ぶ
67
+ */
68
+ export function planUnits(bindings, needsTypes) {
69
+ const groups = new Map();
70
+ for (const binding of bindings) {
71
+ const key = `${binding.check.rule}\0${canonicalJson(binding.check.options)}`;
72
+ const group = groups.get(key);
73
+ if (group)
74
+ group.bindings.push(binding);
75
+ else
76
+ groups.set(key, {
77
+ rule: binding.check.rule,
78
+ options: binding.check.options,
79
+ bindings: [binding],
80
+ });
81
+ }
82
+ const units = new Map();
83
+ const variants = new Map();
84
+ for (const group of groups.values()) {
85
+ const typed = needsTypes(group.rule);
86
+ const variant = variants.get(group.rule) ?? 0;
87
+ variants.set(group.rule, variant + 1);
88
+ const id = `${typed ? 'typed' : 'syntax'}-${variant}`;
89
+ const unit = units.get(id) ?? { id, typed, rules: [] };
90
+ unit.rules.push(group);
91
+ units.set(id, unit);
92
+ }
93
+ return [...units.values()];
94
+ }
95
+ /**
96
+ * 組を分類する。検査は checkKey で引く。
97
+ * @param applied 検査ごとに、ESLint が適用すると判定したファイル(root からの相対パス)
98
+ * @param outcomeOf 検査ごとの、その検査が属する実行単位の結果。unusable の検査は含まない
99
+ * @param unusable 実行しなかった検査とその理由(この版に無いルール、ESLint が拒否した設定)
100
+ */
101
+ export function classify(input) {
102
+ const pairs = [];
103
+ for (const rule of input.rules) {
104
+ if (rule.checks.length === 0)
105
+ continue;
106
+ for (const file of input.files) {
107
+ const findings = [];
108
+ const reasons = [];
109
+ let fullRan = false;
110
+ let anyRan = false;
111
+ for (const [index, check] of rule.checks.entries()) {
112
+ const key = checkKey(rule.id, index);
113
+ const unusable = input.unusable.get(key);
114
+ if (unusable !== undefined) {
115
+ reasons.push(`${check.rule}: ${unusable}`);
116
+ continue;
117
+ }
118
+ if (!input.applied.get(key)?.has(file))
119
+ continue;
120
+ const outcome = input.outcomeOf.get(key);
121
+ if (outcome?.error !== undefined) {
122
+ reasons.push(`${check.rule}: ${outcome.error}`);
123
+ continue;
124
+ }
125
+ const report = outcome?.reports.get(file);
126
+ if (!report) {
127
+ reasons.push(`${check.rule}: no result for the file`);
128
+ continue;
129
+ }
130
+ const ruleless = report.messages.find((message) => message.ruleId === null);
131
+ if (report.fatal || ruleless) {
132
+ reasons.push(`${check.rule}: ${ruleless?.message ?? 'fatal error'}`);
133
+ continue;
134
+ }
135
+ anyRan = true;
136
+ if (check.coverage === 'full')
137
+ fullRan = true;
138
+ for (const message of report.messages) {
139
+ if (message.ruleId === check.rule)
140
+ findings.push({
141
+ rule: check.rule,
142
+ line: message.line,
143
+ column: message.column,
144
+ message: message.message,
145
+ });
146
+ }
147
+ }
148
+ const state = fullRan
149
+ ? findings.length > 0
150
+ ? 'violation'
151
+ : 'covered'
152
+ : anyRan
153
+ ? 'partial'
154
+ : 'not-run';
155
+ const pair = { ruleId: rule.id, file, state, findings: dedupe(findings) };
156
+ if (state === 'not-run')
157
+ pair.reason = reasons.length > 0 ? reasons.join('; ') : 'no check applies to the file';
158
+ pairs.push(pair);
159
+ }
160
+ }
161
+ return pairs;
162
+ }
163
+ // 同じルール・同じ options の検査を 1 規約が 2 つ持つと、同じ報告が 2 回集まるため 1 つにする。
164
+ function dedupe(findings) {
165
+ const seen = new Set();
166
+ return findings.filter((finding) => {
167
+ const key = `${finding.rule}:${finding.line}:${finding.column}:${finding.message}`;
168
+ if (seen.has(key))
169
+ return false;
170
+ seen.add(key);
171
+ return true;
172
+ });
173
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Promise の executor が受け取る resolve / reject を、executor の外から使うために executor 内で別の場所へ
3
+ * 代入する書き方(deferred promise)を報告する。`Promise.withResolvers()` で書ける。
4
+ * 代入先は変数・プロパティを問わない。resolve / reject を値として渡す呼び出し(`setResolve(resolve)`)や、
5
+ * 包んだ関数の代入(`done = () => resolve()`)は報告しない(意図の判定に構文以上の情報が要るため)。
6
+ */
7
+ const preferPromiseWithResolvers = {
8
+ meta: {
9
+ type: 'suggestion',
10
+ docs: {
11
+ description: 'Create deferred promises with Promise.withResolvers instead of leaking the executor callbacks',
12
+ },
13
+ messages: {
14
+ leaked: "Promise executor's {{name}} is assigned to be used outside the executor. Use Promise.withResolvers().",
15
+ },
16
+ schema: [],
17
+ },
18
+ create(context) {
19
+ // 名前だけで判定すると、executor の内側の関数が同じ名前の引数を持つ場合を取り違える。
20
+ // 右辺を scope で解決し、それを定義した関数が、祖先の `new Promise(...)` の第 1 引数そのものかを確かめる。
21
+ return {
22
+ AssignmentExpression(node) {
23
+ if (node.right.type !== 'Identifier')
24
+ return;
25
+ const definition = findVariable(context.sourceCode.getScope(node), node.right.name)
26
+ ?.defs[0];
27
+ if (definition?.type !== 'Parameter')
28
+ return;
29
+ const leaksExecutorParameter = context.sourceCode
30
+ .getAncestors(node)
31
+ .some((ancestor) => ancestor.type === 'NewExpression' &&
32
+ ancestor.callee.type === 'Identifier' &&
33
+ ancestor.callee.name === 'Promise' &&
34
+ ancestor.arguments[0] === definition.node);
35
+ if (leaksExecutorParameter)
36
+ context.report({ node, messageId: 'leaked', data: { name: node.right.name } });
37
+ },
38
+ };
39
+ },
40
+ };
41
+ function findVariable(scope, name) {
42
+ for (let current = scope; current; current = current.upper) {
43
+ const variable = current.set.get(name);
44
+ if (variable)
45
+ return variable;
46
+ }
47
+ return undefined;
48
+ }
49
+ export const shinapsPlugin = {
50
+ meta: { name: 'shinaps' },
51
+ rules: { 'prefer-promise-with-resolvers': preferPromiseWithResolvers },
52
+ };
@@ -0,0 +1,233 @@
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ import { checkKey, classify, parseLintInput, planUnits, } from "./lint-plan.js";
4
+ import { shinapsPlugin } from "./lint-rules.js";
5
+ import { errorMessage } from "./fs.js";
6
+ const USAGE = 'Usage: shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>';
7
+ /**
8
+ * `shinaps-skills lint`: 規約の検査を、プロジェクトの設定を使わずに ESLint の Node API で実行し、
9
+ * 規約 × ファイルの組を分類して result.json に書く。プロジェクトの lint 設定・script・node_modules の
10
+ * パッケージは読み込まない(overrideConfigFile: true、プラグインはこのパッケージの依存から渡す)。
11
+ * 対象ファイルへは書き込まない。
12
+ */
13
+ export async function lint(args) {
14
+ const options = parseArgs(args);
15
+ const input = parseLintInput(JSON.parse(await readFile(options.checks, 'utf8')));
16
+ const result = await runLint(input, options.root, options.files);
17
+ await writeFile(options.out, `${JSON.stringify(result, null, 2)}\n`);
18
+ const counts = Object.fromEntries(['covered', 'violation', 'partial', 'not-run'].map((state) => [
19
+ state,
20
+ result.pairs.filter((pair) => pair.state === state).length,
21
+ ]));
22
+ console.log(`Shinaps lint: ${JSON.stringify(counts)} → ${options.out}`);
23
+ }
24
+ function parseArgs(args) {
25
+ const separator = args.indexOf('--');
26
+ const flags = separator === -1 ? args : args.slice(0, separator);
27
+ const files = separator === -1 ? [] : args.slice(separator + 1);
28
+ const value = (name) => {
29
+ const index = flags.indexOf(name);
30
+ const found = index === -1 ? undefined : flags[index + 1];
31
+ if (!found)
32
+ throw new Error(`${name} is required. ${USAGE}`);
33
+ return found;
34
+ };
35
+ return { checks: value('--checks'), root: resolve(value('--root')), out: value('--out'), files };
36
+ }
37
+ /**
38
+ * @param root 対象プロジェクトのルート(絶対パス)
39
+ * @param files 対象ファイル。root からの相対パスか絶対パス。結果では root からの相対パス(区切りは `/`)で表す
40
+ */
41
+ export async function runLint(input, root, files) {
42
+ const { ESLint } = await import('eslint');
43
+ const registry = await loadRegistry();
44
+ const relativeFiles = [...new Set(files.map((file) => toRelative(root, file)))];
45
+ const newESLint = (overrideConfig) => new ESLint({
46
+ cwd: root,
47
+ overrideConfigFile: true,
48
+ overrideConfig,
49
+ allowInlineConfig: false,
50
+ // 削除済みなどで存在しないファイルを渡しても単位全体を失敗にせず、そのファイルを「結果なし」
51
+ // (not-run) にするため。
52
+ errorOnUnmatchedPattern: false,
53
+ // 渡すのはファイルだけ。`src/*.ts` のような名前のファイルを glob として展開し、渡していない
54
+ // ファイルまで検査しないため。
55
+ globInputPaths: false,
56
+ });
57
+ const needsTypes = (rule) =>
58
+ // ESLint の型の docs には requiresTypeChecking が無い (typescript-eslint が付ける独自の項目)。
59
+ registry.rules.get(rule)?.meta?.docs
60
+ ?.requiresTypeChecking === true;
61
+ // 検査が F に適用されるかは ESLint だけが判定する。その検査の設定だけを入れたインスタンスで
62
+ // calculateConfigForFile を呼び、ルールが有効なら適用される。glob の解釈を CLI 側で再現しない。
63
+ // ESLint が設定(不正な options など)を拒否した検査は、ここで not-run にして実行単位に入れない。
64
+ // 単位に入れると、同じ単位の他の検査まで実行できなくなるため。
65
+ const unusable = new Map();
66
+ const applied = new Map();
67
+ const bindings = [];
68
+ for (const rule of input.rules) {
69
+ for (const [index, check] of rule.checks.entries()) {
70
+ const key = checkKey(rule.id, index);
71
+ if (!registry.rules.has(check.rule)) {
72
+ unusable.set(key, 'unsupported rule in this version of @shinaps/skills');
73
+ continue;
74
+ }
75
+ const binding = { ruleId: rule.id, index, check };
76
+ const single = newESLint(unitConfig(registry, root, needsTypes(check.rule), [
77
+ { rule: check.rule, options: check.options, bindings: [binding] },
78
+ ]));
79
+ try {
80
+ const hits = new Set();
81
+ for (const file of relativeFiles) {
82
+ if (isActive(await single.calculateConfigForFile(resolve(root, file)), check.rule))
83
+ hits.add(file);
84
+ }
85
+ applied.set(key, hits);
86
+ bindings.push(binding);
87
+ }
88
+ catch (error) {
89
+ unusable.set(key, oneLine(error));
90
+ }
91
+ }
92
+ }
93
+ const outcomeOf = new Map();
94
+ const units = [];
95
+ for (const unit of planUnits(bindings, needsTypes)) {
96
+ const unitBindings = unit.rules.flatMap((planned) => planned.bindings);
97
+ const targets = [
98
+ ...new Set(unitBindings.flatMap((binding) => [
99
+ ...(applied.get(checkKey(binding.ruleId, binding.index)) ?? []),
100
+ ])),
101
+ ];
102
+ const outcome = await runUnit(unit, targets);
103
+ for (const binding of unitBindings)
104
+ outcomeOf.set(checkKey(binding.ruleId, binding.index), outcome);
105
+ units.push({
106
+ id: unit.id,
107
+ typed: unit.typed,
108
+ rules: unit.rules.map((planned) => planned.rule),
109
+ // ESLint が結果を返したファイルの数。単位のエラーや存在しないファイルでは、渡した数より少なくなる。
110
+ fileCount: outcome.reports.size,
111
+ ...(outcome.error === undefined ? {} : { error: outcome.error }),
112
+ });
113
+ }
114
+ async function runUnit(unit, targets) {
115
+ // 渡すファイルが無ければ実行しない。ファイルを省くと ESLint は cwd 配下を全部検査するため。
116
+ if (targets.length === 0)
117
+ return { reports: new Map() };
118
+ try {
119
+ const results = await newESLint(unitConfig(registry, root, unit.typed, unit.rules)).lintFiles(targets.map((file) => resolve(root, file)));
120
+ const reports = new Map();
121
+ for (const result of results) {
122
+ reports.set(toRelative(root, result.filePath), {
123
+ fatal: result.fatalErrorCount > 0,
124
+ messages: result.messages.map((message) => ({
125
+ ruleId: message.ruleId,
126
+ line: message.line,
127
+ column: message.column,
128
+ message: message.message,
129
+ })),
130
+ });
131
+ }
132
+ return { reports };
133
+ }
134
+ catch (error) {
135
+ return { error: oneLine(error), reports: new Map() };
136
+ }
137
+ }
138
+ return {
139
+ root,
140
+ units,
141
+ pairs: classify({ rules: input.rules, files: relativeFiles, applied, outcomeOf, unusable }),
142
+ };
143
+ }
144
+ // ESLint の設定エラーは複数行で、理由が 2 行目以降にあるため、1 行にまとめて結果に残す。
145
+ const oneLine = (error) => errorMessage(error)
146
+ .replace(/\s*\n\s*/g, ' ')
147
+ .trim();
148
+ function toRelative(root, file) {
149
+ const absolute = isAbsolute(file) ? file : resolve(root, file);
150
+ return relative(root, absolute).split(sep).join('/');
151
+ }
152
+ function isActive(config, rule) {
153
+ const entry = config?.rules?.[rule];
154
+ const severity = Array.isArray(entry) ? entry[0] : entry;
155
+ return severity !== undefined && severity !== 0 && severity !== 'off';
156
+ }
157
+ async function loadRegistry() {
158
+ const [{ builtinRules }, tseslint, vue, vueParser] = await Promise.all([
159
+ import('eslint/use-at-your-own-risk'),
160
+ import('typescript-eslint'),
161
+ import('eslint-plugin-vue'),
162
+ import('vue-eslint-parser'),
163
+ ]);
164
+ // typescript-eslint と eslint-plugin-vue が公開する型は ESLint の Plugin・Parser・RuleModule の型と
165
+ // 細部が一致しない (どちらも実行時は ESLint の API に従う)。flat config に渡すために ESLint の型にする。
166
+ const plugins = {
167
+ '@typescript-eslint': tseslint.default.plugin,
168
+ vue: vue.default,
169
+ shinaps: shinapsPlugin,
170
+ };
171
+ const rules = new Map(builtinRules);
172
+ for (const [prefix, plugin] of Object.entries(plugins)) {
173
+ for (const [name, rule] of Object.entries(plugin.rules ?? {}))
174
+ rules.set(`${prefix}/${name}`, rule);
175
+ }
176
+ return {
177
+ plugins,
178
+ rules,
179
+ parsers: {
180
+ typescript: tseslint.default.parser,
181
+ vue: vueParser.default,
182
+ },
183
+ };
184
+ }
185
+ const TS_FILES = ['**/*.ts', '**/*.tsx', '**/*.mts', '**/*.cts'];
186
+ // 検査の対象にする拡張子。ESLint は `src/**` のように全てのファイルに一致する glob だけではファイルを検査の
187
+ // 対象にしないので、拡張子を指定したパターンを両方の単位に置く。単位の種類で適用範囲が変わらないよう、
188
+ // 型付き・型なしで同じ一覧を使う。
189
+ const LINTED_FILES = [...TS_FILES, '**/*.js', '**/*.jsx', '**/*.mjs', '**/*.cjs', '**/*.vue'];
190
+ /**
191
+ * 実行単位の設定。parser は拡張子ごとの設定オブジェクトで指定し、ルールは検査の glob の和に付ける。
192
+ * 型付きの単位は、対象の全てのファイルを projectService 付きの typescript-eslint で読む。型の得られない
193
+ * ファイル (tsconfig に含まれない JS、Vue など) はそのファイルだけが fatal (not-run) になる。JS を ESLint
194
+ * の既定の parser で読むと、型を要求するルールが単位全体を例外で止め、他の検査を巻き込むため。
195
+ * 型付きの単位では Vue を扱わない (vue-eslint-parser と projectService の組み合わせは使っていない)。
196
+ */
197
+ function unitConfig(registry, root, typed, rules) {
198
+ const parsers = typed
199
+ ? [
200
+ {
201
+ files: LINTED_FILES,
202
+ languageOptions: {
203
+ parser: registry.parsers.typescript,
204
+ parserOptions: { projectService: true, tsconfigRootDir: root },
205
+ },
206
+ },
207
+ ]
208
+ : [
209
+ { files: LINTED_FILES },
210
+ // 既定の parser は JSX を読まないので、.jsx では JSX を有効にする (無いと常に parse error で not-run)。
211
+ {
212
+ files: ['**/*.jsx'],
213
+ languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
214
+ },
215
+ { files: TS_FILES, languageOptions: { parser: registry.parsers.typescript } },
216
+ {
217
+ files: ['**/*.vue'],
218
+ languageOptions: {
219
+ parser: registry.parsers.vue,
220
+ parserOptions: { parser: registry.parsers.typescript, sourceType: 'module' },
221
+ },
222
+ },
223
+ ];
224
+ return [
225
+ ...parsers,
226
+ ...rules.map((planned) => ({
227
+ files: [...new Set(planned.bindings.flatMap((binding) => binding.check.files))],
228
+ plugins: registry.plugins,
229
+ // options は JSON の配列 (unknown[]) で、ESLint の RuleEntry は要素の型を持たないので型だけを合わせる。
230
+ rules: { [planned.rule]: ['error', ...planned.options] },
231
+ })),
232
+ ];
233
+ }
package/dist/lib/lock.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readFile, writeFile, rm } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { errorCode } from './fs.js';
3
+ import { errorCode } from "./fs.js";
4
4
  /**
5
5
  * `<directory>/update.lock` を保持したまま `work` を実行する。ロックはディレクトリで、保持者の pid を
6
6
  * `pid` ファイルに記録する。保持者が存在しないロックは置き換える。保持者が生きているか pid をまだ読め
@@ -1,4 +1,4 @@
1
- import { validateConfig } from './config-validation.js';
1
+ import { validateConfig } from "./config-validation.js";
2
2
  /** `.shinaps/managed.json` を同梱の契約で検証し、内容を返す。 */
3
3
  export async function validateManagedConfig(path) {
4
4
  return (await validateConfig(path, {
@@ -1,9 +1,9 @@
1
1
  import { createHash, 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';
5
- import { withLock } from './lock.js';
6
- import { filesBeta, managedBeta } from './managed-ant.js';
4
+ import { readJson, readJsonIfExists, writeFileAtomic } from "./fs.js";
5
+ import { withLock } from "./lock.js";
6
+ import { filesBeta, managedBeta } from "./managed-ant.js";
7
7
  export const staffings = ['LITE', 'BASIC', 'ULTRA'];
8
8
  export const permissions = ['commit', 'push', 'draft_pr'];
9
9
  const leadKey = './agents/shinaps-dev-managed-cc.md';
@@ -1,10 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { lstat, mkdir, readdir, readFile, rm } from 'node:fs/promises';
3
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';
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
8
  /** 共通定義の 3 ディレクトリ。`definitions/` には、これと `claude-lock.json` 以外を置かない。 */
9
9
  const definitionDirs = ['agents', 'environments', 'skills'];
10
10
  const lockName = 'claude-lock.json';
@@ -1,11 +1,11 @@
1
1
  import { readFile } from 'node:fs/promises';
2
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';
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
9
  const managedUsage = [
10
10
  'Usage: shinaps-skills managed setup --account <name> [--account <name> ...] [--apply]',
11
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>]',
@@ -1,8 +1,8 @@
1
1
  import { mkdir, rm, rename, cp } from 'node:fs/promises';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { join } from 'node:path';
4
- import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from './layout.js';
5
- import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from './fs.js';
4
+ import { managedAgent, marketplaceName, payloadDir, pluginId, statePath } from "./layout.js";
5
+ import { errorCode, errorMessage, readJsonIfExists, readTextIfExists, writeFileAtomic, } from "./fs.js";
6
6
  /** エージェント内容の sha256。state.json と journal の `nextHashes` はこの形式で記録する。 */
7
7
  export const digest = (text) => createHash('sha256').update(text).digest('hex');
8
8
  /** 配布物を Codex に登録する。install と restore で同じ手順を使う。 */
@@ -7,7 +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
- - 配布 CLI: ソースは `bin/skills.ts` と `lib/*.ts`。`npm run build`(`tsc`)で `dist/` に JS を出力し、npm にはそれを配布する。Node.js は `node_modules` 配下の `.ts` に型除去を適用しないため、ソースのままは配布できない。checkout では型除去で直接実行するので、`enum` など型除去で消せない構文は使わない(`tsconfig.json` の `erasableSyntaxOnly`)
10
+ - 配布 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
11
 
12
12
  品質基準とプロジェクト設定の方針は両版で揃え、クライアント固有の実行手順とエージェントの形式は別々に管理する。スキルを追加・変更したら各版への適用可否を判断する。
13
13
 
package/docs/install.md CHANGED
@@ -4,7 +4,7 @@ Claude Code 版の更新と、Codex 版の導入・更新・削除・macOS で
4
4
 
5
5
  ## 導入・更新・削除
6
6
 
7
- Node.js 22 以上と、`plugin marketplace add` / `plugin add` に対応した Codex CLI が必要です。Codex のスキルだけを使う場合、Claude Code のインストールは不要です。
7
+ Node.js 22.13 以上の 22 系または 24 以上と、`plugin marketplace add` / `plugin add` に対応した Codex CLI が必要です。Codex のスキルだけを使う場合、Claude Code のインストールは不要です。
8
8
 
9
9
  ```sh
10
10
  npx @shinaps/skills@latest install codex
@@ -23,10 +23,10 @@ npx @shinaps/skills@latest recover codex
23
23
 
24
24
  ## 配布 CLI
25
25
 
26
- 設定の検証(`validate-linear-config`・`validate-managed-config`)と Managed Agents の操作(`managed`)には、配布 CLI `shinaps-skills` を使います。CLI は plugin に含まれないため、利用中のスキルと同じ版を導入します。
26
+ 設定の検証(`validate-linear-config`・`validate-managed-config`)、Managed Agents の操作(`managed`)、規約の機械検査(`lint`、[規約の機械検査](lint.md))には、配布 CLI `shinaps-skills` を使います。CLI は plugin に含まれないため、利用中のスキルと同じ版を導入します。
27
27
 
28
28
  ```sh
29
- npm install -g @shinaps/skills@0.43.0
29
+ npm install -g @shinaps/skills@0.44.0
30
30
  shinaps-skills --version
31
31
  ```
32
32
 
package/docs/lint.md ADDED
@@ -0,0 +1,71 @@
1
+ # 規約の機械検査(`shinaps-skills lint`)
2
+
3
+ Shinaps AIの規約が持つ機械検査(`checks`)を手元で実行し、規約×ファイルの組ごとに、AI照合を省けるかを判定するCLIです。エージェント向けの使い方は plugin の `references/lint.md` にあります。
4
+
5
+ ## 前提と保証
6
+
7
+ - プロジェクトの変更は要りません。プロジェクトのlint設定(`eslint.config.*`・`.oxlintrc.json`・`bunfig.toml` など)・`package.json` のscript・`node_modules` のパッケージは読み込みません。ESLintのNode APIを `overrideConfigFile: true` で使い、パーサーとプラグインはこのパッケージの依存から渡します。読むのは対象ファイルと、型情報を使うルールのための `tsconfig.json`・型宣言だけです(実行はしません)。
8
+ - 対象ファイルへ書き込みません(自動修正はしません)。結果はローカルのファイルに書き、サーバーへは送りません。
9
+ - プロジェクトの `eslint-disable` コメントでは検査を抑止しません(`allowInlineConfig: false`)。プロジェクトの都合の例外は、指摘を確認する側で扱います。
10
+ - 組をAI照合から外してよいのは、coverage が `full` の検査が、ESLintの判定でそのファイルに適用され、検査の実行単位が完了し、そのファイルに致命的な報告もruleIdの無い報告も無いときだけです。それ以外の組は `not-run`(または `partial`)として返し、合格にしません。coverage は検査を登録したときの判断で、このCLIは値を読むだけです。
11
+
12
+ ## 使い方
13
+
14
+ ```sh
15
+ shinaps-skills lint --checks <checks.json> --root <project> --out <result.json> -- <files...>
16
+ ```
17
+
18
+ - `--checks`・`--out` のパスは、コマンドを実行したディレクトリが基準です。
19
+ - `--checks`: `{ "rules": [{ "id": "<規約id>", "checks": [{ "rule": "...", "options": [...], "files": ["**/*.ts"], "coverage": "full" }] }] }`。形が違えば何も書かずに終了コード1で終わります。
20
+ - `--root`: 対象プロジェクトのルート。ESLintの `cwd` と、結果のパスの基準になります。
21
+ - `<files...>`: 対象ファイル(`--root` からの相対パスか絶対パス)。どの検査も適用されないファイルはESLintに渡しません。渡すファイルが無ければESLintを起動しません(ファイルを省くとESLintはルート配下を全て検査するため)。
22
+ - `--out`: 結果のJSON。
23
+
24
+ 結果:
25
+
26
+ ```json
27
+ {
28
+ "root": "/path/to/project",
29
+ "units": [{ "id": "syntax-0", "typed": false, "rules": ["@typescript-eslint/method-signature-style"], "fileCount": 3 }],
30
+ "pairs": [
31
+ { "ruleId": "<規約id>", "file": "src/a.ts", "state": "covered", "findings": [] },
32
+ { "ruleId": "<規約id>", "file": "src/b.ts", "state": "violation", "findings": [{ "rule": "@typescript-eslint/method-signature-style", "line": 2, "column": 3, "message": "..." }] },
33
+ { "ruleId": "<規約id>", "file": "tests/c.ts", "state": "not-run", "reason": "...", "findings": [] }
34
+ ]
35
+ }
36
+ ```
37
+
38
+ | state | 意味 | 照合 |
39
+ | --- | --- | --- |
40
+ | `covered` | `full` の検査が実行され、違反が無い | 外す |
41
+ | `violation` | `full` の検査が実行され、指摘がある(同じ規約の `partial` の検査の指摘を含む) | 外し、`findings` を指摘にする |
42
+ | `partial` | `partial` の検査だけが実行された | 残し、`findings` も指摘にする |
43
+ | `not-run` | 実行していない(`reason` に理由) | 残す |
44
+
45
+ ## 実行の仕組み
46
+
47
+ 1. 各検査のルールを、ESLint core・`@typescript-eslint`・`vue`・同梱の `shinaps` から引きます。この版に無いルールの検査は `not-run` になり、他の検査は止まりません。
48
+ 2. 検査ごとに、その検査の設定だけを入れたESLintで `calculateConfigForFile` を呼び、ルールが有効なファイルを適用先にします。globの解釈はESLintだけが行います。ESLintが設定を拒否した検査(不正なoptionsなど)は、ここで `not-run` になります。
49
+ 3. 同じルール・同じoptionsの検査は1つの設定にまとめ、同じルールでoptionsが違うものだけを別の実行単位にします。型情報を使うルール(`meta.docs.requiresTypeChecking`)は `projectService` を使う単位に分けます。この単位は全てのファイルを typescript-eslint で読み、型の得られないファイル(`tsconfig.json` に含まれないTS・JS、Vue)はそのファイルだけが `not-run` になります。型なしの検査と、他のファイルの型付きの検査は影響を受けません。型情報を使うルールはVueのファイルでは実行できません。
50
+ 4. 実行単位ごとに `lintFiles` を1回呼び、組を分類します。指摘は、ESLintがそのファイルに適用すると判定した検査の規約にだけ付けます。
51
+
52
+ ## 同梱ルール
53
+
54
+ | ルールID | 内容 | 型情報 |
55
+ | --- | --- | --- |
56
+ | `shinaps/prefer-promise-with-resolvers` | Promiseのexecutorが受け取る `resolve`・`reject` を、executorの外で使うために変数・プロパティへ代入する書き方(deferred promise)を報告する。`Promise.withResolvers()` で書ける。包んだ関数の代入や、値として渡す呼び出しは報告しない | 不要 |
57
+
58
+ ### 同梱ルールの追加
59
+
60
+ 規約の検査に既存のルールが使えないとき、レビュー済みでテスト済みのルールをこのパッケージに同梱します。サーバーにはルールのIDとoptionsだけを保存し、コードは配布しません。同梱していないルールは、実装・テスト・レビュー・リリースを経るまで使えません(それまでは、その規約はAI照合のままです)。
61
+
62
+ 1. `lib/lint-rules.ts` にルールを足し、`shinapsPlugin.rules` に登録します。IDは意味を変えずに使い続けます(規約の検査はIDで保存され、登録時の意味のレビューがその挙動を前提にするため)。意味が変わるなら別のIDにします。型情報を使うルールには `meta.docs.requiresTypeChecking: true` を付けます(付けないと型なしの実行単位に入り、その単位の検査が全て `not-run` になります)。
63
+ 2. `test/lint.test.ts` に、ESLintの `RuleTester` で報告する例・しない例を書きます。規約の good・bad の例と、誤って報告しやすい形(同じ名前の別の変数など)を含めます。型情報を使わないルールは、型情報なしの `RuleTester` で動くことがそのまま確認になります。型情報を使うルールは、`languageOptions.parserOptions` に `projectService` を付けた `RuleTester` で試します。
64
+ 3. この文書の「同梱ルール」の表に追加し、通常の変更と同じくレビュー・リリースします。
65
+ 4. 規約に検査を付けるのはリリースの後です。規約の全文を読んで coverage を決めます(plugin の `references/lint.md`)。
66
+
67
+ 実例: `shinaps/prefer-promise-with-resolvers`(`lib/lint-rules.ts`)。右辺の識別子をscopeで解決し、Promiseのexecutorの引数そのものかを確かめることで、内側の関数の同じ名前の引数を取り違えないようにしています。テストは `test/lint.test.ts` の末尾にあります。
68
+
69
+ ## 依存
70
+
71
+ ESLint・typescript-eslint・TypeScript・vue-eslint-parser・eslint-plugin-vue を完全一致の版で依存に持ちます(パッケージの導入が約50MB増えます)。typescript-eslint は TypeScript 7 に対応していないため、TypeScript は6系です(開発用の `tsc` も同じ版を使います)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shinaps/skills",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Install Shinaps AI workflows and custom agents for Codex",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "engines": {
18
- "node": ">=22"
18
+ "node": "^22.13.0 || >=24"
19
19
  },
20
20
  "scripts": {
21
21
  "build": "tsc -p tsconfig.build.json",
@@ -36,10 +36,14 @@
36
36
  },
37
37
  "devDependencies": {
38
38
  "@biomejs/biome": "2.5.15",
39
- "@types/node": "^26.6.4",
40
- "typescript": "^7.0.2"
39
+ "@types/node": "^26.6.4"
41
40
  },
42
41
  "dependencies": {
43
- "ajv": "^8.20.0"
42
+ "ajv": "^8.20.0",
43
+ "eslint": "10.14.0",
44
+ "eslint-plugin-vue": "10.12.0",
45
+ "typescript": "6.0.3",
46
+ "typescript-eslint": "8.72.1",
47
+ "vue-eslint-parser": "10.4.1"
44
48
  }
45
49
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shinaps",
3
3
  "description": "Shinaps AIの開発ワークフロー。プロジェクトの指示から条件を読み、調査・設計・実装・簡素化・レビュー・コミット・PR・リリースとLinearでの開発進行を支援する。",
4
- "version": "0.43.0",
4
+ "version": "0.44.0",
5
5
  "author": {
6
6
  "name": "shinaps"
7
7
  },
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 検証CLIと契約版
6
6
 
7
- 利用中のskillとCLIの版を確認し、0.43.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.43.0`)。pluginだけにはCLIは含まれない。導入・更新が必要ならその権限を確認し、検証のたびに `npx latest` を取得しない。
7
+ 利用中のskillとCLIの版を確認し、0.44.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.44.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
 
@@ -0,0 +1,34 @@
1
+ # 規約の機械検査(Shinaps lint)
2
+
3
+ Shinaps AIの規約は、機械検査(`checks`)を持てる。検査は配布CLI `shinaps-skills lint` が、プロジェクトのlint設定・script・依存を使わずに手元で実行する。プロジェクトの変更は要らない。対象のコード・差分・結果はサーバーへ送らない。CLIの入出力と同梱ルールの追加手順は、配布パッケージの `docs/lint.md` にある。
4
+
5
+ 検査で照合を省いてよいのは、静的に保証できた組(規約×ファイル)だけ。未実行・対象外・エラー・一部だけの検査は合格にせず、AI照合に回す。
6
+
7
+ ## 照合で使う
8
+
9
+ [Shinaps規約の取得](rules.md)で全文を取得した後、照合の担当へ渡す前に行う。
10
+
11
+ 1. 全文の `checks` を持つ規約を、`{ "rules": [{ "id": "<規約id>", "checks": [<全文のchecksのまま>] }] }` の形で作業記録の下(例: `.shinaps/work/<作業>/lint-checks.json`)に書く。checksを書き換えない。持つ規約が無ければこの節を省く。
12
+ 2. 対象ファイル(照合の対象として確定したもの。削除されたファイルを除く)を付けて1回実行する。
13
+
14
+ ```sh
15
+ shinaps-skills lint --checks <lint-checks.json> --root <リポジトリのルート> --out <lint-result.json> -- <対象ファイル...>
16
+ ```
17
+
18
+ コマンドが無い、終了コードが0でない、または結果を読めないときは、全ての規約をAI照合に回し、理由を報告する。CLIは利用中のスキルと同じ版を使う。
19
+ 3. 結果の `pairs` は規約×ファイルの組ごとに状態を持つ。
20
+ - `covered`: 全体を検査する検査(coverage `full`)が実行され、違反が無い。この組は担当の照合から外す。
21
+ - `violation`: 全体を検査する検査が実行され、指摘がある(同じ規約の一部だけを検査する検査の指摘を含む)。照合から外し、`findings` を指摘にする。
22
+ - `partial`: 一部だけを検査する検査だけが実行された。`findings` を指摘にし、規約はそのファイルの照合に残す(担当にはlintの指摘も渡す)。
23
+ - `not-run`: 実行していない(対象外、この版に無いルール、ESLintが設定を拒否した、型が得られない、致命的な報告など)。照合に残し、`reason` を記録する。
24
+ 4. lint由来の指摘は、根拠に規約idとルールID(`findings[].rule`)を付けて集約に入れる。誤報告がありうるので自動修正にせず、メインがその位置を読んで確認してから扱う。
25
+ 5. 報告に、状態ごとの組の数、`not-run` の理由、`units[].error` を書く。照合から外した組を「AIが照合した」と書かない。
26
+
27
+ ## 検査を登録・更新する
28
+
29
+ [Shinaps規約の書き方](rule-writing.md)で書き込む規約に、検査を付けられるかを判断する。
30
+
31
+ - **使えるルール**: ESLint core、`@typescript-eslint/*`(型情報を使うものを含む)、`vue/*`、同梱の `shinaps/*`(`docs/lint.md` の一覧)。版によって違うので、同じ版のCLIで確かめる。使えるルールが無い規約には検査を付けない(AI照合のまま)。同梱ルールの追加は `docs/lint.md` の手順で別の作業として提案する。
32
+ - **coverageの判断**: 規約の全文(scope・labelsを含み、更新では更新後の全文)と、検査の `rule`・`options`・`files` を照らす。`files` の範囲で、規約が求めることへの違反をこのルールが全て報告すると言えるときだけ `full`、見逃しが一つでもありうれば `partial`。lintが規約より広く報告する(例外も報告する)ことは `full` の妨げにならない(指摘はメインが確認する)。例で通ることは根拠にならない。
33
+ - **補助の確認**: 規約のgood・badの例を一時ディレクトリにファイルとして書き、同じ入口で実行して、badで報告され、goodで報告されないことを確かめる。対象プロジェクトのコードを写さない。
34
+ - **書き込み**: `record_rule`・`update_rule` の `checks` に渡す。一括の `record_rules` は `checks` を受け取らない(付けると呼出し全体が入力エラーになる)ので、検査は登録後に `update_rule` で付ける。`update_rule` は配列ごと置換し、`[]` で外す。検査を持つ規約の内容(出典以外)を変える更新では、更新後の全文で検査を見直し、同じ要求で `checks` を渡す。渡さないと `CHECKS_REQUIRED` で保存されない。見直した結果が同じなら同じ配列を渡す。
@@ -34,6 +34,10 @@ Shinaps AIへ個人規約を登録・更新するときの書き方。短さそ
34
34
 
35
35
  結果は候補ごとに、比べた既存の規約のidまたは相手の候補、未確定ならその旨、取得の条件、未完了の範囲と返らない範囲を添えて報告する。
36
36
 
37
+ ## 機械検査
38
+
39
+ coding の規約は、使えるlintのルールで検査できるかを判断し、できるものには `checks` を付ける。coverageの決め方・補助の確認・書き込みと、検査を持つ規約の内容を変えるときの見直しは[規約の機械検査](lint.md)の「検査を登録・更新する」に従う。
40
+
37
41
  ## 項目の役割
38
42
 
39
43
  各項目は役割が重ならないようにし、同じ内容を言い換えて繰り返さない。
@@ -70,7 +70,7 @@ Shinaps AIのMCPから規約を取得する手順。取得は呼出し元(メ
70
70
 
71
71
  ## 担当へ渡す
72
72
 
73
- 全文は取得した応答の単位で、続きのバッチとして渡す。1回の依頼に全件をまとめない。担当には読み終えた規約のidを報告させ、報告の無い範囲は同じ担当へ戻す。
73
+ `checks` を持つ規約がある照合では、渡す前に[規約の機械検査](lint.md)の「照合で使う」を行い、照合から外す組を決める。全文は取得した応答の単位で、続きのバッチとして渡す。1回の依頼に全件をまとめない。担当には読み終えた規約のidを報告させ、報告の無い範囲は同じ担当へ戻す。
74
74
 
75
75
  ## 照合結果に書くこと
76
76
 
@@ -33,7 +33,7 @@ shinaps-ai サーバは git / file / AI 推論を一切呼ばない。すべて
33
33
  メインスレッド (あなた) は **オーケストレータ** に徹する:
34
34
 
35
35
  1. 対象ファイル集合の確定、規約カタログの集約 (Step 0-1)
36
- 2. 規約・対象ファイルを担当へ渡せる形に整理する (Step 2)
36
+ 2. 機械検査で静的に保証できた組を照合から外し、規約・対象ファイルを担当へ渡せる形に整理する (Step 2)
37
37
  3. Opus / medium 1体を起動し、全対象を照合する (Step 3)
38
38
  4. 返ってきた findings を集約 → 変更箇所 / 既存箇所で振り分け (Step 4-5)
39
39
 
@@ -68,6 +68,8 @@ shinaps-ai サーバは git / file / AI 推論を一切呼ばない。すべて
68
68
 
69
69
  ## Step 2. 共通の照合対象を準備する
70
70
 
71
+ 取得した rule 全文に `checks` を持つ規約があれば、先に [規約の機械検査](../../references/lint.md) の「照合で使う」を行う。`covered`・`violation` の組(規約×ファイル)はそのファイルについて担当の照合対象から外し、`violation`・`partial` の findings は lint 由来の指摘として Step 4 に加える。担当には、ファイルごとに照合から外した規約の id を伝え、`partial` の組には lint の指摘を添える。
72
+
71
73
  取得した rule 全文と関連する AGENTS.md / CLAUDE.md / docs、対象ファイル、変更範囲を担当へ渡す。対象が大きい場合は関連するファイルをまとめてバッチに分け、対象全体の一覧を維持する。カテゴリ数やバッチ数を起動するエージェント数に置き換えない。
72
74
 
73
75
  ## Step 3. Opus 1体で照合する
@@ -139,6 +141,7 @@ shinaps-ai サーバは git / file / AI 推論を一切呼ばない。すべて
139
141
  - **scope 整合**: 同じ箇所の `scope` がバッチ間で異なる場合は実際の変更範囲を確認して判断し、不明なら `pre-existing` とする
140
142
  - **優先度整列**: Violation → Improvement → Question の順に並べ、Violation 内は影響範囲が大きいものから
141
143
  - **注意**: Info レベルは元々含まれない前提だが、agent 出力に混じっていたら破棄する
144
+ - **lint 由来の指摘**: 根拠は規約 id とルール ID。メインが該当位置を読んで違反か確かめてから他の finding と同じ基準で扱う。誤報告と判断したものは理由を記録して除く
142
145
 
143
146
  集約結果の総数と内訳を 1 行で報告してから Step 5 に進む (`changed` / `pre-existing` の件数も併記する)。
144
147
 
@@ -153,7 +156,7 @@ shinaps-ai サーバは git / file / AI 推論を一切呼ばない。すべて
153
156
 
154
157
  ### 自明な修正 (auto-apply、`changed` のみ)
155
158
 
156
- 修正方法が一意で設計判断を伴わない finding は、ユーザーに確認せず `Edit` / `Write` で即修正する:
159
+ lint 由来の指摘は auto-apply にしない (ルールは構文で判定するので、意図した例外も報告しうる)。修正方法が一意で設計判断を伴わない finding は、ユーザーに確認せず `Edit` / `Write` で即修正する:
157
160
 
158
161
  - コメント / 文言 / typo の修正 (規約が指す直し方が一つしかない)
159
162
  - 既存パターンへの機械的な追従 (隣接コードと同じ流儀に揃えるだけ)
@@ -185,6 +188,7 @@ shinaps-ai サーバは git / file / AI 推論を一切呼ばない。すべて
185
188
 
186
189
  - 対象ファイル数と、その決め方 (`git diff HEAD` の変更ファイル / PR / commit range / path 指定のどれか) を冒頭に明示
187
190
  - [Shinaps規約の取得](../../references/rules.md) の「照合結果に書くこと」
191
+ - 機械検査を使った場合は、状態ごとの組の数 (照合から外した `covered`・`violation` を含む)、`not-run` の理由。使えなかった場合はその理由
188
192
  - 実際の担当数・バッチ数と各担当の finding 件数、照合済みファイル数、非該当・未完了のファイルと理由
189
193
  - 検出 / 処理した finding の総数と内訳 (自動修正 X 件 / 確認の上修正 Y 件 / 許容 Z 件 / rule 更新 W 件 / 後回し V 件)
190
194
  - **`changed` と `pre-existing` の内訳** (既存箇所の指摘が何件あり、そのうち何件を今回直したか。今回の変更に無関係な修正がどれだけ混ざったかをユーザーが把握できるように)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shinaps",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Shinaps AIのCodex向け開発ワークフロー",
5
5
  "skills": "./skills/"
6
6
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 検証CLIと契約版
6
6
 
7
- 利用中のskillとCLIの版を確認し、0.43.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.43.0`)。pluginだけにはCLIは含まれない。導入・更新が必要ならその権限を確認し、検証のたびに `npx latest` を取得しない。
7
+ 利用中のskillとCLIの版を確認し、0.44.0のskillには同じ版のnpm CLIを導入する(`npm install -g @shinaps/skills@0.44.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
 
@@ -0,0 +1,34 @@
1
+ # 規約の機械検査(Shinaps lint)
2
+
3
+ Shinaps AIの規約は、機械検査(`checks`)を持てる。検査は配布CLI `shinaps-skills lint` が、プロジェクトのlint設定・script・依存を使わずに手元で実行する。プロジェクトの変更は要らない。対象のコード・差分・結果はサーバーへ送らない。CLIの入出力と同梱ルールの追加手順は、配布パッケージの `docs/lint.md` にある。
4
+
5
+ 検査で照合を省いてよいのは、静的に保証できた組(規約×ファイル)だけ。未実行・対象外・エラー・一部だけの検査は合格にせず、AI照合に回す。
6
+
7
+ ## 照合で使う
8
+
9
+ [Shinaps規約の取得](rules.md)で全文を取得した後、照合の担当へ渡す前に行う。
10
+
11
+ 1. 全文の `checks` を持つ規約を、`{ "rules": [{ "id": "<規約id>", "checks": [<全文のchecksのまま>] }] }` の形で作業記録の下(例: `.shinaps/work/<作業>/lint-checks.json`)に書く。checksを書き換えない。持つ規約が無ければこの節を省く。
12
+ 2. 対象ファイル(照合の対象として確定したもの。削除されたファイルを除く)を付けて1回実行する。
13
+
14
+ ```sh
15
+ shinaps-skills lint --checks <lint-checks.json> --root <リポジトリのルート> --out <lint-result.json> -- <対象ファイル...>
16
+ ```
17
+
18
+ コマンドが無い、終了コードが0でない、または結果を読めないときは、全ての規約をAI照合に回し、理由を報告する。CLIは利用中のスキルと同じ版を使う。
19
+ 3. 結果の `pairs` は規約×ファイルの組ごとに状態を持つ。
20
+ - `covered`: 全体を検査する検査(coverage `full`)が実行され、違反が無い。この組は担当の照合から外す。
21
+ - `violation`: 全体を検査する検査が実行され、指摘がある(同じ規約の一部だけを検査する検査の指摘を含む)。照合から外し、`findings` を指摘にする。
22
+ - `partial`: 一部だけを検査する検査だけが実行された。`findings` を指摘にし、規約はそのファイルの照合に残す(担当にはlintの指摘も渡す)。
23
+ - `not-run`: 実行していない(対象外、この版に無いルール、ESLintが設定を拒否した、型が得られない、致命的な報告など)。照合に残し、`reason` を記録する。
24
+ 4. lint由来の指摘は、根拠に規約idとルールID(`findings[].rule`)を付けて集約に入れる。誤報告がありうるので自動修正にせず、メインがその位置を読んで確認してから扱う。
25
+ 5. 報告に、状態ごとの組の数、`not-run` の理由、`units[].error` を書く。照合から外した組を「AIが照合した」と書かない。
26
+
27
+ ## 検査を登録・更新する
28
+
29
+ [Shinaps規約の書き方](rule-writing.md)で書き込む規約に、検査を付けられるかを判断する。
30
+
31
+ - **使えるルール**: ESLint core、`@typescript-eslint/*`(型情報を使うものを含む)、`vue/*`、同梱の `shinaps/*`(`docs/lint.md` の一覧)。版によって違うので、同じ版のCLIで確かめる。使えるルールが無い規約には検査を付けない(AI照合のまま)。同梱ルールの追加は `docs/lint.md` の手順で別の作業として提案する。
32
+ - **coverageの判断**: 規約の全文(scope・labelsを含み、更新では更新後の全文)と、検査の `rule`・`options`・`files` を照らす。`files` の範囲で、規約が求めることへの違反をこのルールが全て報告すると言えるときだけ `full`、見逃しが一つでもありうれば `partial`。lintが規約より広く報告する(例外も報告する)ことは `full` の妨げにならない(指摘はメインが確認する)。例で通ることは根拠にならない。
33
+ - **補助の確認**: 規約のgood・badの例を一時ディレクトリにファイルとして書き、同じ入口で実行して、badで報告され、goodで報告されないことを確かめる。対象プロジェクトのコードを写さない。
34
+ - **書き込み**: `record_rule`・`update_rule` の `checks` に渡す。一括の `record_rules` は `checks` を受け取らない(付けると呼出し全体が入力エラーになる)ので、検査は登録後に `update_rule` で付ける。`update_rule` は配列ごと置換し、`[]` で外す。検査を持つ規約の内容(出典以外)を変える更新では、更新後の全文で検査を見直し、同じ要求で `checks` を渡す。渡さないと `CHECKS_REQUIRED` で保存されない。見直した結果が同じなら同じ配列を渡す。
@@ -34,6 +34,10 @@ Shinaps AIへ個人規約を登録・更新するときの書き方。短さそ
34
34
 
35
35
  結果は候補ごとに、比べた既存の規約のidまたは相手の候補、未確定ならその旨、取得の条件、未完了の範囲と返らない範囲を添えて報告する。
36
36
 
37
+ ## 機械検査
38
+
39
+ coding の規約は、使えるlintのルールで検査できるかを判断し、できるものには `checks` を付ける。coverageの決め方・補助の確認・書き込みと、検査を持つ規約の内容を変えるときの見直しは[規約の機械検査](lint.md)の「検査を登録・更新する」に従う。
40
+
37
41
  ## 項目の役割
38
42
 
39
43
  各項目は役割が重ならないようにし、同じ内容を言い換えて繰り返さない。
@@ -70,7 +70,7 @@ Shinaps AIのMCPから規約を取得する手順。取得は呼出し元(メ
70
70
 
71
71
  ## 担当へ渡す
72
72
 
73
- 全文は取得した応答の単位で、続きのバッチとして渡す。1回の依頼に全件をまとめない。担当には読み終えた規約のidを報告させ、報告の無い範囲は同じ担当へ戻す。
73
+ `checks` を持つ規約がある照合では、渡す前に[規約の機械検査](lint.md)の「照合で使う」を行い、照合から外す組を決める。全文は取得した応答の単位で、続きのバッチとして渡す。1回の依頼に全件をまとめない。担当には読み終えた規約のidを報告させ、報告の無い範囲は同じ担当へ戻す。
74
74
 
75
75
  ## 照合結果に書くこと
76
76
 
@@ -7,6 +7,8 @@ description: 対象ファイルとShinaps AIのルールを照合し、違反・
7
7
 
8
8
  指定されたパス・PR・コミット範囲から対象を決める。作業中の変更ならstaged、unstaged、関連するuntrackedを含める。適用されるプロジェクトの指示を読み、[Shinaps規約の取得](../../references/rules.md)に従って、対象ファイルについて規約全文を取得する。MCPが使えない場合は照合を実施できないと報告し、推測でルールを作らない。アカウント作成・接続は利用者の標準認証フローを案内する。
9
9
 
10
+ 取得した規約に `checks` があれば、照合の前に[規約の機械検査](../../references/lint.md)の「照合で使う」に従い、静的に保証できた組(`covered`・`violation`)をその規約の照合から外し、lintの指摘は位置を読んで確かめてから扱う(自動修正しない)。照合から外した組と `not-run` の理由を報告する。
11
+
10
12
  対象ファイルの現状全文を読み、ルールごとに適用条件と実際のコードを比較する。指摘には根拠となるルール、該当箇所、影響と修正案を付ける。違反・改善・仕様判断を区別し、今回の変更と元からある問題を分ける。テストに関わる指摘と修正の判断は [テスト方針](../../references/testing.md) に従う。
11
13
 
12
14
  委託を許可されている場合は登録済みのshinaps-refine-agentへ重ならない範囲とルール全文を渡せる。担当の起動・継続は[担当の起動](../../references/dev/launch.md)に従う。結果は現在のコードで検証し重複をまとめる。独立検証をしなければそのように報告する。