@ryusenkei/conventions 0.1.0 → 0.2.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
@@ -46,28 +46,47 @@ The PreToolUse hook blocks the call and tells Claude why, so Claude can fix it a
46
46
  | Rule | Enforcement |
47
47
  | --- | --- |
48
48
  | pnpm only | Blocks `npm install` (and other npm changes) and `yarn` |
49
- | Branch names `<type>/<name>` | Checks `git switch -c`, `git checkout -b`, `git branch`, `git worktree add -b` |
49
+ | Branch names `<type>/<name>` | Checks `git switch -c`, `git checkout -b`, `git branch`, `git worktree add -b` (also allows `dev/<issue>-<name>` with the epic flow) |
50
50
  | No skipped Git hooks | Blocks `git commit --no-verify` / `-n` and `git push --no-verify` |
51
+ | Stage files by path | Blocks `git add -A` / `--all` / `.` / `-u` / `--update` and `git commit -a` / `--all` |
52
+ | No force push to `main` / `master` | Blocks `git push --force` / `-f` / `--force-with-lease` and `+<refspec>` targeting `main` or `master` (also the current branch when no refspec is given) |
51
53
  | PR title = squash commit message | Checks `gh pr create` / `gh pr edit` titles with prose-lint's commit rules |
52
54
  | No AI signature | `attribution` is empty, and the hook blocks PR bodies with an AI signature |
53
55
  | Squash merge only | Blocks `gh pr merge --merge` / `--rebase` |
56
+ | Merge only after CI passes | Blocks `gh pr merge` unless every check of the PR has passed (`gh pr checks`). Pending, failed, missing, or unreadable checks are blocked, also with `--auto` |
57
+ | Epic flow (opt-in) | See [Epic flow](#epic-flow) |
54
58
  | Do not edit `node_modules` | Blocks Edit and Write under `node_modules` |
55
59
  | Secrets | `permissions.deny` blocks reading `.env` and `.dev.vars` with Claude's file tools (not Bash) |
56
60
  | Definition of done (`--stop`) | The Stop hook runs `pnpm lint` and `pnpm test` when the working tree has changes, and sends Claude back once on failure |
57
61
 
58
- Commit messages are checked by `.githooks/commit-msg`, and edited files by prose-lint's PostToolUse hook.
62
+ Commit messages are checked by `.githooks/commit-msg`, and edited files by prose-lint's PostToolUse hook. `.githooks/pre-push` runs `pnpm run --if-present check`, so a repository that defines a `check` script cannot push until it passes. It applies to people as well as Claude.
63
+
64
+ ### Epic flow
65
+
66
+ Opt in per repository in the root `package.json`:
67
+
68
+ ```json
69
+ {
70
+ "ryusenkei": { "flow": "epic" }
71
+ }
72
+ ```
73
+
74
+ - Each epic gets an integration branch `dev/<issue>-<name>` (e.g. `dev/12-search`). Sub-issue PRs target it, and only a `dev/*` branch is merged into `main` (on deploy).
75
+ - `gh pr create` to `main` (`--base main`, or no `--base`) is blocked unless the head (`--head`, or the current branch) is `dev/*`.
76
+ - A PR from any other branch must target a `dev/*` base. `gh pr edit` is checked only when it changes `--base`.
77
+ - Without the opt-in, none of this applies and `dev/*` branch names are rejected.
59
78
 
60
79
  ## Synced files
61
80
 
62
81
  `sync` writes these files and `check` compares them, ignoring line endings:
63
82
 
64
- - `.editorconfig`, `.gitattributes`, `.gitignore`, `.gitleaks.toml`, `mise.toml`, `.github/dependabot.yaml`, `.githooks/commit-msg`
83
+ - `.editorconfig`, `.gitattributes`, `.gitignore`, `.gitleaks.toml`, `mise.toml`, `.github/dependabot.yaml`, `.githooks/commit-msg`, `.githooks/pre-push`
65
84
  - `.claude/settings.json`: only the hooks, `attribution`, and `permissions.deny` are managed. Other settings and hooks are kept
66
85
  - `pnpm-workspace.yaml`: `minimumReleaseAge` and `minimumReleaseAgeExclude`, matching the Dependabot `cooldown`
67
86
 
68
87
  `check` also reports a missing `rules.md` import in `CLAUDE.md`.
69
88
 
70
- Updates arrive as Dependabot PRs. The repository's `cooldown` excludes `@ryusenkei/*`, so new versions are picked up the next day. After an update, `check` fails until `sync` is run.
89
+ Dependabot runs monthly with `rebase-strategy: disabled` to save GitHub Actions minutes, so rebase a Dependabot PR (`@dependabot rebase`) before merging it. The `cooldown` excludes `@ryusenkei/*`. To pick up a new version sooner, run `pnpm update "@ryusenkei/*"`. After an update, `check` fails until `sync` is run.
71
90
 
72
91
  ## Versioning
73
92
 
package/dist/files.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- /** CLAUDE.md の共通の規約の import */
1
+ /** CLAUDE.md で共通の規約を読み込む import */
2
2
  export declare const IMPORT = "@node_modules/@ryusenkei/conventions/rules.md";
3
- /** 差分のあるファイル。`expected` が `undefined` = 自動で修正できない(`message` に対処) */
3
+ /** 差分のあるファイル(`expected` が `undefined` = 自動で修正できない → `message` に対処) */
4
4
  export type Drift = {
5
5
  path: string;
6
6
  expected?: string;
package/dist/files.js CHANGED
@@ -1,4 +1,4 @@
1
- // 利用側のリポの共通ファイルの同期・検査
1
+ // 利用側のリポにある共通ファイルの同期・検査
2
2
  // - マスタ: パッケージの `files/`(npm は `.gitignore` を同梱しない → 先頭の `.` を除いた名前で格納)
3
3
  // - 比較は改行コードを LF に正規化(Windows の作業ツリーは CRLF になりうる)
4
4
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -12,10 +12,11 @@ const FILES = [
12
12
  { from: 'mise.toml', to: 'mise.toml' },
13
13
  { from: 'dependabot.yaml', to: '.github/dependabot.yaml' },
14
14
  { from: 'commit-msg', to: '.githooks/commit-msg', mode: 0o755 },
15
+ { from: 'pre-push', to: '.githooks/pre-push', mode: 0o755 },
15
16
  ];
16
17
  const SETTINGS = '.claude/settings.json';
17
18
  const WORKSPACE = 'pnpm-workspace.yaml';
18
- /** CLAUDE.md の共通の規約の import */
19
+ /** CLAUDE.md で共通の規約を読み込む import */
19
20
  export const IMPORT = '@node_modules/@ryusenkei/conventions/rules.md';
20
21
  // `.github/dependabot.yaml` の `cooldown` と同値
21
22
  const RELEASE_AGE = [
package/dist/hook.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Claude Code の hook(`.claude/settings.json` から `node` で直接実行)
3
3
  // - PreToolUse: 規約に反するツールの実行を拒否 → 終了コード2・理由は stderr
4
4
  // - Stop: 変更があれば `pnpm lint`・`pnpm test` を実行し、失敗時は完了を差し戻し
5
- // - hook 自体の失敗は終了コード1(拒否にしない → 作業を止めない)
5
+ // - hook 自体の失敗は終了コード1(拒否にしない → 作業を継続)
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
8
8
  import { tmpdir } from 'node:os';
@@ -14,7 +14,7 @@ const root = process.env.CLAUDE_PROJECT_DIR ?? input.cwd ?? process.cwd();
14
14
  // Windows の `pnpm` は `.cmd` → シェル経由でのみ起動可能
15
15
  const shell = process.platform === 'win32';
16
16
  if (input.hook_event_name === 'PreToolUse') {
17
- const reason = denyReason(input, { titleIssues, readFile });
17
+ const reason = denyReason(input, { root, titleIssues, readFile, currentBranch, prHead, prChecks });
18
18
  if (reason) {
19
19
  process.stderr.write(`${reason}\n`);
20
20
  process.exit(2);
@@ -45,6 +45,33 @@ function readFile(path) {
45
45
  return undefined;
46
46
  }
47
47
  }
48
+ function currentBranch(cwd) {
49
+ const r = spawnSync('git', ['branch', '--show-current'], { cwd: cwd ?? root, encoding: 'utf8' });
50
+ return (r.status === 0 && r.stdout.trim()) || undefined;
51
+ }
52
+ function prHead(pr, cwd) {
53
+ const r = spawnSync('gh', ['pr', 'view', ...(pr ? [pr] : []), '--json', 'headRefName', '--jq', '.headRefName'], { cwd: cwd ?? root, encoding: 'utf8' });
54
+ return (r.status === 0 && r.stdout.trim()) || undefined;
55
+ }
56
+ // `gh pr checks` は実行中・失敗で終了コードが0以外(8・1)→ 終了コードではなく出力で判定
57
+ function prChecks(pr, cwd) {
58
+ const r = spawnSync('gh', ['pr', 'checks', ...(pr ? [pr] : []), '--json', 'bucket'], { cwd: cwd ?? root, encoding: 'utf8' });
59
+ let buckets;
60
+ try {
61
+ buckets = JSON.parse(r.stdout).map((c) => c.bucket);
62
+ }
63
+ catch {
64
+ return /no checks reported/i.test(r.stderr ?? '') ? 'none' : 'unknown';
65
+ }
66
+ if (buckets.some((b) => b === 'fail' || b === 'cancel'))
67
+ return 'fail';
68
+ if (buckets.some((b) => b === 'pending'))
69
+ return 'pending';
70
+ // すべてスキップ = 成功したチェックがない
71
+ if (!buckets.includes('pass'))
72
+ return 'none';
73
+ return buckets.every((b) => b === 'pass' || b === 'skipping') ? 'pass' : 'unknown';
74
+ }
48
75
  // 差し戻しは1回まで(`stop_hook_active` = 差し戻し後の再実行)→ 解消できない失敗で停止しない状態を防止
49
76
  function stop() {
50
77
  if (input.stop_hook_active)
package/dist/policy.d.ts CHANGED
@@ -4,12 +4,22 @@ export type ToolUse = {
4
4
  tool_input: Record<string, unknown>;
5
5
  cwd?: string;
6
6
  };
7
+ /** PR のチェックの集計(`unknown` = 取得に失敗) */
8
+ export type Checks = 'pass' | 'pending' | 'fail' | 'none' | 'unknown';
7
9
  /** 判定に必要な外部の処理 */
8
10
  export type Context = {
11
+ /** リポのルート(`package.json` の opt-in の読み込み元) */
12
+ root: string;
9
13
  /** PR のタイトルの指摘(なければ空配列) */
10
14
  titleIssues: (title: string) => string[];
11
- /** ファイルの内容(読めなければ `undefined`) */
15
+ /** ファイルの内容(読み込みの失敗時は `undefined`) */
12
16
  readFile: (path: string) => string | undefined;
17
+ /** 現在のブランチ(detached HEAD・取得の失敗時は `undefined`) */
18
+ currentBranch: (cwd: string | undefined) => string | undefined;
19
+ /** PR の head のブランチ(`pr` の省略時は現在のブランチの PR) */
20
+ prHead: (pr: string | undefined, cwd: string | undefined) => string | undefined;
21
+ /** PR のチェックの集計(`pr` の省略時は現在のブランチの PR) */
22
+ prChecks: (pr: string | undefined, cwd: string | undefined) => Checks;
13
23
  };
14
- /** 拒否の理由。許可する場合は `undefined` */
24
+ /** 拒否の理由(許可する場合は `undefined`) */
15
25
  export declare function denyReason(use: ToolUse, ctx: Context): string | undefined;
package/dist/policy.js CHANGED
@@ -1,10 +1,13 @@
1
1
  // PreToolUse で拒否するツールの実行
2
- // - 判定は純粋な関数 → 外部の処理(タイトルの検査・ファイルの読み込み)は `Context` で注入
2
+ // - 判定は純粋な関数 → 外部の処理(タイトルの検査・ファイルの読み込み・git・gh)は `Context` で注入
3
3
  // - 拒否の理由は Claude に返却 → Claude が修正して再実行
4
4
  import { isAbsolute, join } from 'node:path';
5
5
  import { commands, option } from "./shell.js";
6
6
  const TYPES = ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'];
7
7
  const BRANCH = new RegExp(`^(${TYPES.join('|')})/[a-z0-9][a-z0-9._-]*$`);
8
+ // epic のフローの統合ブランチ(`dev/<issue の番号>-<内容>`)
9
+ const DEV_BRANCH = /^dev\/\d+-[a-z0-9][a-z0-9._-]*$/;
10
+ const PROTECTED = ['main', 'master'];
8
11
  const EDIT_TOOLS = ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'];
9
12
  // pnpm 以外のパッケージ管理の操作(`npm view`・`npm pack`・`npx` などの参照・実行は対象外)
10
13
  const NPM_CHANGES = ['install', 'i', 'add', 'ci', 'uninstall', 'un', 'remove', 'rm', 'update', 'up', 'link'];
@@ -13,7 +16,24 @@ const AI_TRAILER = /^[\w-]+:.*\b(claude|anthropic|openai|chatgpt|gpt-\d|copilot|
13
16
  const AI_NOTE = /generated (with|by) \[?(claude|chatgpt|copilot|gemini|codex|cursor)|🤖/i;
14
17
  // `git branch` の作成以外の操作
15
18
  const BRANCH_QUERY = ['-d', '-D', '--delete', '-l', '--list', '-a', '-r', '--all', '--remotes', '-v', '-vv', '--verbose', '--show-current', '--contains', '--no-contains', '--merged', '--no-merged', '-u', '--set-upstream-to', '--unset-upstream', '--edit-description'];
16
- /** 拒否の理由。許可する場合は `undefined` */
19
+ // 作業ツリー全体を対象にする pathspec
20
+ const ALL_PATHS = ['.', './', ':/', ':', '*'];
21
+ // 値を取るオプション(位置引数・短いオプションの束の判定で値を除外)
22
+ const COMMIT_VALUE_SHORT = 'mFCctS';
23
+ const ADD_VALUE = ['--chmod', '--pathspec-from-file'];
24
+ const PUSH_VALUE = ['-o', '--push-option', '--repo', '--receive-pack', '--exec'];
25
+ const MERGE_VALUE = ['-t', '--subject', '-b', '--body', '-F', '--body-file', '-A', '--author-email', '--match-head-commit', '-R', '--repo'];
26
+ const EDIT_VALUE = ['-t', '--title', '-b', '--body', '-F', '--body-file', '-B', '--base', '-m', '--milestone', '-R', '--repo', '--add-assignee', '--remove-assignee', '--add-label', '--remove-label', '--add-project', '--remove-project', '--add-reviewer', '--remove-reviewer'];
27
+ const ASK_USER = '必要な場合はユーザーに実行を依頼';
28
+ const BULK_STAGING = '一括の add(`git add -A`・`.`・`-u`、`git commit -a`)は禁止。変更・追加したファイルをパスで指定して add(`git status --short` で確認)';
29
+ const BRANCH_RULE = `\`<type>/<内容>\`(type = ${TYPES.join('・')}、内容 = 英小文字・数字・\`-\`)`;
30
+ const CHECKS_REASON = {
31
+ pending: 'CI が実行中。全チェックの成功を確認してからマージ(`gh pr checks --watch`)',
32
+ fail: 'CI が失敗。修正して全チェックが成功してからマージ',
33
+ none: 'PR にチェックがない。CI の成功を確認できない PR はマージしない',
34
+ unknown: 'CI の結果を取得できない(`gh pr checks` が失敗)。全チェックの成功を確認してからマージ',
35
+ };
36
+ /** 拒否の理由(許可する場合は `undefined`) */
17
37
  export function denyReason(use, ctx) {
18
38
  if (EDIT_TOOLS.includes(use.tool_name)) {
19
39
  const path = String(use.tool_input.file_path ?? use.tool_input.notebook_path ?? '');
@@ -31,11 +51,23 @@ function commandReasons(words, cwd, ctx) {
31
51
  if (cmd === 'yarn' || (cmd === 'npm' && NPM_CHANGES.includes(args[0] ?? '')))
32
52
  return ['パッケージ管理は pnpm のみ(`pnpm add`・`pnpm install` など)'];
33
53
  if (cmd === 'git')
34
- return gitReasons(gitArgs(args));
54
+ return gitReasons(gitArgs(args), cwd, ctx);
35
55
  if (cmd === 'gh' && args[0] === 'pr')
36
56
  return prReasons(args[1], args.slice(2), cwd, ctx);
37
57
  return [];
38
58
  }
59
+ // epic のフローの opt-in: ルートの `package.json` の `"ryusenkei": { "flow": "epic" }`
60
+ function epicFlow(ctx) {
61
+ const raw = ctx.readFile(join(ctx.root, 'package.json'));
62
+ if (raw === undefined)
63
+ return false;
64
+ try {
65
+ return JSON.parse(raw).ryusenkei?.flow === 'epic';
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
39
71
  // git の全体のオプション(`-C <path>`・`-c <name=value>` など)を除去
40
72
  function gitArgs(args) {
41
73
  let i = 0;
@@ -43,18 +75,87 @@ function gitArgs(args) {
43
75
  i += args[i] === '-C' || args[i] === '-c' ? 2 : 1;
44
76
  return args.slice(i);
45
77
  }
46
- function gitReasons([sub, ...args]) {
78
+ // 位置引数(`--` 以降はすべて位置引数)
79
+ function positionals(args, valued) {
80
+ const out = [];
81
+ for (let i = 0; i < args.length; i++) {
82
+ const a = args[i];
83
+ if (a === '--')
84
+ return [...out, ...args.slice(i + 1)];
85
+ if (valued.includes(a))
86
+ i++;
87
+ else if (!a.startsWith('-'))
88
+ out.push(a);
89
+ }
90
+ return out;
91
+ }
92
+ // 短いオプションの束(`-am` など)の `flag` の有無
93
+ // - `valued` の文字の後ろは値
94
+ function hasShort(args, flag, valued = '') {
95
+ for (let i = 0; i < args.length; i++) {
96
+ const a = args[i];
97
+ if (a === '--')
98
+ return false;
99
+ if (!/^-[a-zA-Z]/.test(a))
100
+ continue;
101
+ for (const c of a.slice(1)) {
102
+ if (c === flag)
103
+ return true;
104
+ if (valued.includes(c)) {
105
+ if (a.endsWith(c))
106
+ i++;
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+ function gitReasons([sub, ...args], cwd, ctx) {
47
114
  if ((sub === 'commit' || sub === 'push') && args.includes('--no-verify'))
48
115
  return ['フックの省略(`--no-verify`)は禁止'];
49
116
  // `git commit -n` = `--no-verify`(`git push -n` は dry run)
50
- if (sub === 'commit' && args.some((a) => /^-[a-zA-Z]*n[a-zA-Z]*$/.test(a)))
117
+ if (sub === 'commit' && hasShort(args, 'n', COMMIT_VALUE_SHORT))
51
118
  return ['フックの省略(`-n`)は禁止'];
119
+ if (sub === 'commit' && (args.includes('--all') || hasShort(args, 'a', COMMIT_VALUE_SHORT)))
120
+ return [BULK_STAGING];
121
+ if (sub === 'add' && bulkAdd(args))
122
+ return [BULK_STAGING];
123
+ if (sub === 'push')
124
+ return pushReasons(args, cwd, ctx);
52
125
  const name = newBranch(sub, args);
53
- if (name !== undefined && !BRANCH.test(name))
54
- return [`ブランチ名は \`<type>/<内容>\`(type = ${TYPES.join('・')}、内容 = 英小文字・数字・\`-\`): \`${name}\``];
55
- return [];
126
+ if (name === undefined || BRANCH.test(name))
127
+ return [];
128
+ if (!epicFlow(ctx))
129
+ return [`ブランチ名は ${BRANCH_RULE}: \`${name}\``];
130
+ if (DEV_BRANCH.test(name))
131
+ return [];
132
+ return [`ブランチ名は ${BRANCH_RULE}、epic は \`dev/<issue の番号>-<内容>\`: \`${name}\``];
56
133
  }
57
- // 作成・改名するブランチの名前
134
+ function bulkAdd(args) {
135
+ if (args.some((a) => a === '--all' || a === '--update'))
136
+ return true;
137
+ if (args.some((a) => /^-[a-zA-Z]*[Au][a-zA-Z]*$/.test(a)))
138
+ return true;
139
+ return positionals(args, ADD_VALUE).some((p) => ALL_PATHS.includes(p));
140
+ }
141
+ // main・master への force push を拒否
142
+ function pushReasons(args, cwd, ctx) {
143
+ const forceAll = args.some((a) => a === '--force' || a.startsWith('--force-with-lease')) || hasShort(args, 'f', 'o');
144
+ if (args.includes('--all') || args.includes('--mirror'))
145
+ return forceAll ? [`\`--all\`・\`--mirror\` の force push は禁止(${PROTECTED.join('・')} を含む)。${ASK_USER}`] : [];
146
+ const [, ...refspecs] = positionals(args, PUSH_VALUE);
147
+ const targets = refspecs.length
148
+ ? refspecs.map((r) => ({ force: forceAll || r.startsWith('+'), branch: pushTarget(r.replace(/^\+/, ''), cwd, ctx) }))
149
+ : [{ force: forceAll, branch: ctx.currentBranch(cwd) }];
150
+ const hit = targets.find((t) => t.force && t.branch !== undefined && PROTECTED.includes(t.branch));
151
+ return hit ? [`\`${hit.branch}\` への force push は禁止。${ASK_USER}`] : [];
152
+ }
153
+ function pushTarget(refspec, cwd, ctx) {
154
+ const dst = refspec.includes(':') ? refspec.slice(refspec.indexOf(':') + 1) : refspec;
155
+ const name = dst.replace(/^refs\/heads\//, '');
156
+ return name === 'HEAD' ? ctx.currentBranch(cwd) : name;
157
+ }
158
+ // 作成・リネームするブランチの名前
58
159
  function newBranch(sub, args) {
59
160
  const positionals = args.filter((a) => !a.startsWith('-'));
60
161
  switch (sub) {
@@ -75,8 +176,8 @@ function newBranch(sub, args) {
75
176
  }
76
177
  }
77
178
  function prReasons(sub, args, cwd, ctx) {
78
- if (sub === 'merge' && args.some((a) => a === '--merge' || a === '--rebase' || a === '-m' || a === '-r'))
79
- return ['マージは squash のみ(`--squash`)'];
179
+ if (sub === 'merge')
180
+ return mergeReasons(args, cwd, ctx);
80
181
  if (sub !== 'create' && sub !== 'edit')
81
182
  return [];
82
183
  const reasons = [];
@@ -89,5 +190,31 @@ function prReasons(sub, args, cwd, ctx) {
89
190
  const body = [option(args, ['--body', '-b']), file && file !== '-' ? ctx.readFile(isAbsolute(file) || !cwd ? file : join(cwd, file)) : undefined].join('\n');
90
191
  if (AI_TRAILER.test(body) || AI_NOTE.test(body))
91
192
  reasons.push('PR の本文に AI の署名を付けない');
193
+ if (epicFlow(ctx))
194
+ reasons.push(...epicReasons(sub, args, cwd, ctx));
92
195
  return reasons;
93
196
  }
197
+ // `--auto` も対象(必須のチェックがないリポでは即時にマージされる)
198
+ function mergeReasons(args, cwd, ctx) {
199
+ if (args.some((a) => a === '--merge' || a === '--rebase' || a === '-m' || a === '-r'))
200
+ return ['マージは squash のみ(`--squash`)'];
201
+ const checks = ctx.prChecks(positionals(args, MERGE_VALUE)[0], cwd);
202
+ return checks === 'pass' ? [] : [CHECKS_REASON[checks]];
203
+ }
204
+ // epic のフロー: main への PR は dev/* から、それ以外のブランチの PR は dev/* へ
205
+ // - `gh pr create` の base の省略 = main(既定のブランチ)
206
+ // - `gh pr edit` は base を変更する場合のみ
207
+ function epicReasons(sub, args, cwd, ctx) {
208
+ const explicit = option(args, ['--base', '-B']);
209
+ if (sub === 'edit' && explicit === undefined)
210
+ return [];
211
+ const base = explicit ?? 'main';
212
+ const head = sub === 'create' ? (option(args, ['--head', '-H'])?.replace(/^.*:/, '') ?? ctx.currentBranch(cwd)) : ctx.prHead(positionals(args, EDIT_VALUE)[0], cwd);
213
+ if (head !== undefined && DEV_BRANCH.test(head))
214
+ return [];
215
+ if (PROTECTED.includes(base))
216
+ return [`\`${base}\` への PR は epic の \`dev/<issue の番号>-<内容>\` からのみ(デプロイのとき)。sub-issue の PR は \`--base dev/...\` で epic のブランチへ`];
217
+ if (!DEV_BRANCH.test(base))
218
+ return [`PR の base は epic の \`dev/<issue の番号>-<内容>\`: \`${base}\``];
219
+ return [];
220
+ }
package/dist/shell.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** コマンド文字列をコマンドごとの語の配列に分解 */
2
2
  export declare function commands(line: string): string[][];
3
- /** オプションの値(`--name value`・`--name=value`・`-n value`)。指定がなければ `undefined` */
3
+ /** オプションの値(`--name value`・`--name=value`・`-n value`)→ 指定がなければ `undefined` */
4
4
  export declare function option(words: string[], names: string[]): string | undefined;
package/dist/shell.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // シェルのコマンド文字列の簡易な分解(hook の判定用)
2
2
  // - 区切り: `&&`・`||`・`;`・`|`・`&`・改行 → コマンドごとの語の配列
3
- // - 解釈: 引用符(`'`・`"`)とバックスラッシュ。変数・コマンド置換は展開しない
3
+ // - 解釈: 引用符(`'`・`"`)とバックスラッシュ → 変数・コマンド置換は展開しない
4
4
  // - 先頭の `NAME=value`(環境変数の指定)は除去
5
5
  /** コマンド文字列をコマンドごとの語の配列に分解 */
6
6
  export function commands(line) {
@@ -61,7 +61,7 @@ export function commands(line) {
61
61
  endCommand();
62
62
  return out;
63
63
  }
64
- /** オプションの値(`--name value`・`--name=value`・`-n value`)。指定がなければ `undefined` */
64
+ /** オプションの値(`--name value`・`--name=value`・`-n value`)→ 指定がなければ `undefined` */
65
65
  export function option(words, names) {
66
66
  for (let i = 0; i < words.length; i++) {
67
67
  const w = words[i];
@@ -3,9 +3,11 @@ updates:
3
3
  # npm(pnpm ワークスペース)。ルートの1件で apps/*・packages/* を横断
4
4
  - package-ecosystem: npm
5
5
  directory: "/"
6
- # 毎日(自社のパッケージの新しいバージョンをすぐ取り込むため)
6
+ # 月1回(PR ごとの CI による GitHub Actions の消費を抑制)。自社のパッケージを急ぐ場合は手動で `pnpm update "@ryusenkei/*"`
7
7
  schedule:
8
- interval: daily
8
+ interval: monthly
9
+ # 自動の rebase なし(main の更新ごとの CI の再実行を防止)→ マージの前に PR で `@dependabot rebase`
10
+ rebase-strategy: disabled
9
11
  # 公開直後のバージョンは配布ファイルの反映待ちでインストールに失敗しうる → 2日待つ。自社のパッケージは待たない
10
12
  cooldown:
11
13
  default-days: 2
@@ -30,7 +32,8 @@ updates:
30
32
  - package-ecosystem: github-actions
31
33
  directory: "/"
32
34
  schedule:
33
- interval: weekly
35
+ interval: monthly
36
+ rebase-strategy: disabled
34
37
  commit-message:
35
38
  prefix: ci
36
39
  include: scope
package/files/pre-push ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ # push の前の検査(`check` のスクリプトを持つリポのみ。CI の失敗・Actions の消費を手元で防止)
3
+ exec pnpm run --if-present check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryusenkei/conventions",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "RYUSENKEI のモノリポの共通の規約(Claude Code の規約・hook、共通ファイルの同期)",
5
5
  "license": "MIT",
6
6
  "author": "RYUSENKEI",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public",
34
- "provenance": false
34
+ "provenance": true
35
35
  },
36
36
  "dependencies": {
37
37
  "@ryusenkei/prose-lint": "^0.1.0"
package/rules.md CHANGED
@@ -6,7 +6,7 @@
6
6
  - TypeScript: ビルドせず Node で直接実行(`node scripts/x.ts`)
7
7
  - テスト: Vitest。対象と同じディレクトリに `*.test.ts`
8
8
  - 子プロセス: `scripts/lib/exec.ts` の `exec`(Windows の `.cmd` に対応)
9
- - 依存の更新: Dependabot の PR で実施
9
+ - 依存の更新: Dependabot の PR で実施(月1回・自動の rebase なし → マージの前に手で rebase)
10
10
 
11
11
  ## 設定・秘密値
12
12
 
@@ -31,9 +31,15 @@
31
31
  - ドメインの概念・説明・表示文字列は日本語
32
32
  - 文体の指摘(`prose-lint`)がドメインの用語に該当する場合: `prose-lint.json` の `allow`・`vocabulary` に追加
33
33
 
34
- ## PR
34
+ ## Git・PR
35
35
 
36
- - 本文: 背景・変更・検証
36
+ - コミット: 変更・追加したファイルをパスで指定して add(一括の add は禁止、フックで強制)
37
+ - push の前: `pnpm check`(`check` を持つリポは pre-push で強制)
38
+ - PR の本文: 背景・変更・検証
39
+ - マージ: CI の全チェックの成功を確認してから(フックで強制)
40
+ - epic のフロー(有効にしたリポのみ)
41
+ - epic ごとに `dev/<issue の番号>-<内容>` を作成し、sub-issue の PR は dev のブランチへ
42
+ - `main` へのマージはデプロイのときだけ dev のブランチから
37
43
 
38
44
  ## 完了の条件
39
45