phasegate 0.118.0 → 0.120.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.120.0] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-087 Phase B + WI-086 finding #1 統合対応: `phasegate init` のデフォルト値を実プロジェクト構成に追従させる** — 外部レポーター nakataj-mti / junpei-9898 が GitHub Issue [#3](https://github.com/junpei-9898/phasegate/issues/3) / [#2](https://github.com/junpei-9898/phasegate/issues/2) で報告した「デプロイされる `hook-config.json` のデフォルト値が単一パッケージ構成(`targetDirs:["src"]` / `formatter:"biome"`)固定で、モノレポ構成や biome 不在環境ではユーザーが明示的に書き換えない限り hook が silent no-op になる」問題を修正。
|
|
15
|
+
- **モノレポ workspace 自動検出** (`scripts/harness/setup/skill-deployer.ts:detectWorkspaceTargetDirs`): `pnpm-workspace.yaml` の `packages:` 配列 → `package.json.workspaces` (配列形式 / `{packages:[...]}` オブジェクト形式の両対応) → `lerna.json.packages` の優先順位で検出。検出した workspace glob (`pkg/*` 等) を実 FS 上で展開し、`<workspace>/src` ディレクトリが存在するもののみを `targetDirs` に採用。pnpm-workspace.yaml は依存追加を避けるため line-based の最小 parser で対応 (`packages:` ブロック検出 + `- 'pattern'` 行抽出のみ)。
|
|
16
|
+
- **formatter 自動検出** (`detectFormatter`): `package.json.devDependencies` (および `dependencies`) を読み、`@biomejs/biome` 存在 → `biome` + `["check","--write"]`、不在 + `prettier` 存在 → `eslint-prettier`、どちらも不在 → `null` (formatter フィールド省略で `format-typescript-hook.sh` の case 文 default フォールスルー、registry pull を回避)。
|
|
17
|
+
- **`deployHookScripts` 拡張**: `copyDirectory` 直前に既存 `.claude/scripts/hook-config.json` を捕捉し、(a) 既存あり → 元の内容を書き戻して **ユーザーカスタマイズ尊重**、(b) 既存なし → 検出結果を反映した新規 hook-config.json を生成。`DeployHooksResult` に `hookConfigGenerated` / `detectedTargetDirs` / `detectedFormatter` を追加し、`scripts/harness/main.ts` の init 出力に `✓ hook-config.json generated (targetDirs: ...; formatter: ...)` 行を追加。
|
|
18
|
+
- **`initHarnessConfig` テンプレートに `architecture: { preset: "clean" }` を追加**: 新規プロジェクトで生成される `phasegate.config.json` が schemaVersion = 'v3' と判定されるようにし、`init` 直後の v2 schema warning を解消(GitHub Issue #2 の "v2 schema warning が phasegate init 直後でも出る" を解消)。既存 v2 config を持つプロジェクトは `migrate-schema` CLI / 手動編集ルートを継続。
|
|
19
|
+
- **テスト追加**: `scripts/harness/__tests__/integration/setup/init-hook-config-detection.integration.test.ts` (14 ケース) — workspace 検出 (pnpm/npm/yarn 配列/yarn オブジェクト/lerna/未定義/src 不在/全 src 不在)、formatter 検出 (biome/prettier/null)、hook-config.json 生成 (既存尊重 / 新規生成)、schema v3 化 (architecture フィールド存在 / `phasegate:status` の stderr に v2 warning が出ないこと)。
|
|
20
|
+
- **互換性**: 既存 v2 config を読む load-resolved-config-use-case 側は変更なし。既存 `.claude/scripts/hook-config.json` をカスタマイズ済みのプロジェクトで `phasegate init` を再実行しても上書きされない (write-back ガード)。
|
|
21
|
+
- **スコープ外** (Phase C で対応予定): Quick Mode 通過時の stderr notice (finding #3) / Stop hook `--enforce` flag (finding #4) / `pre-tool-use` の責務範囲ドキュメント明文化 (WI-086)。
|
|
22
|
+
|
|
23
|
+
## [0.119.0] - 2026-05-07
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **WI-087 Phase A: `phasegate init` がデプロイする hook スクリプトが macOS 標準 bash 3.2 で silent no-op になる問題を修正** — 外部レポーター nakataj-mti が GitHub Issue [#3](https://github.com/junpei-9898/phasegate/issues/3) で報告。`templates/.claude/scripts/format-typescript-hook.sh` および `analyze-errors-hook.sh` の `mapfile -t TARGET_DIRS < <(jq ...)` が bash 4+ builtin であり、macOS 標準 `/bin/bash` (3.2.57) では `mapfile: command not found` エラーで `TARGET_DIRS` 配列が空になり、後続の `${#TARGET_DIRS[@]} -eq 0` 判定で hook が exit 0 で抜けていた。結果としてユーザーには何も表示されず、format / lint hook が deliberately quiet と誤認される状態だった。
|
|
28
|
+
- `mapfile -t ARRAY < <(...)` を bash 3.2 互換の `while IFS= read -r line; do ARRAY+=("$line"); done < <(...)` idiom に置換(`format-typescript-hook.sh` 内 2 箇所、`analyze-errors-hook.sh` 内 1 箇所)。
|
|
29
|
+
- shebang は `#!/bin/bash` を維持(追加インストール不要、既存ユーザーへの影響なし)。
|
|
30
|
+
- 検証: macOS bash 3.2.57 で `targetDirs` を含む hook-config.json をロード後、対象ディレクトリ下のファイルに対して `format-typescript-hook.sh` が `Formatted: ...` を出力、`analyze-errors-hook.sh` が `{"decision": "approve", ...}` を返すことを確認。
|
|
31
|
+
- 関連: WI-086(GitHub Issue [#2](https://github.com/junpei-9898/phasegate/issues/2), reporter: junpei-9898)の "post-tool-use がセッション中に呼ばれても通知されない" 症状の主因も同じ `mapfile` であり、本修正で副次的に解消する。
|
|
32
|
+
- スコープ外(後続フェーズで対応予定): `phasegate init` の monorepo 自動検出(WI-087 Phase B)、Quick Mode 通過時の stderr notice / Stop hook `--enforce` flag(WI-087 Phase C)、`phasegate init` の formatter 自動検出(WI-087 Phase B)。
|
|
33
|
+
|
|
10
34
|
## [0.118.0] - 2026-05-07
|
|
11
35
|
|
|
12
36
|
### Fixed
|
package/package.json
CHANGED
package/scripts/harness/main.ts
CHANGED
|
@@ -469,7 +469,13 @@ async function main(): Promise<void> {
|
|
|
469
469
|
const configResult = await initHarnessConfig(rootDir, projectName, phasePreset);
|
|
470
470
|
const hooksResult = deployClaude
|
|
471
471
|
? await deployHookScripts(harnessRoot, rootDir)
|
|
472
|
-
: {
|
|
472
|
+
: {
|
|
473
|
+
scriptsDeployed: 0,
|
|
474
|
+
settingsCreated: false,
|
|
475
|
+
hookConfigGenerated: false,
|
|
476
|
+
detectedTargetDirs: [] as string[],
|
|
477
|
+
detectedFormatter: null,
|
|
478
|
+
};
|
|
473
479
|
const codexResult = deployCodex ? await deployCodexHooks(harnessRoot, rootDir) : null;
|
|
474
480
|
const designDocsResult = await deployDesignDocs(harnessRoot, rootDir);
|
|
475
481
|
const withHusky = hasFlag(args, "--with-husky");
|
|
@@ -486,6 +492,13 @@ async function main(): Promise<void> {
|
|
|
486
492
|
if (hooksResult.scriptsDeployed > 0) {
|
|
487
493
|
console.log(`✓ Hook scripts deployed to .claude/scripts/ (${hooksResult.scriptsDeployed} files)`);
|
|
488
494
|
}
|
|
495
|
+
if (hooksResult.hookConfigGenerated) {
|
|
496
|
+
const dirsLabel = hooksResult.detectedTargetDirs.join(", ");
|
|
497
|
+
const formatterLabel = hooksResult.detectedFormatter ?? "none";
|
|
498
|
+
console.log(
|
|
499
|
+
`✓ hook-config.json generated (targetDirs: ${dirsLabel}; formatter: ${formatterLabel})`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
489
502
|
if (hooksResult.settingsCreated) {
|
|
490
503
|
console.log(`✓ .claude/settings.json created`);
|
|
491
504
|
} else if (hooksResult.scriptsDeployed > 0) {
|
|
@@ -197,15 +197,159 @@ export async function deployAgentSkillLinks(
|
|
|
197
197
|
export interface DeployHooksResult {
|
|
198
198
|
scriptsDeployed: number;
|
|
199
199
|
settingsCreated: boolean;
|
|
200
|
+
hookConfigGenerated: boolean;
|
|
201
|
+
detectedTargetDirs: string[];
|
|
202
|
+
detectedFormatter: string | null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// pnpm-workspace.yaml の packages: 配列を line-based でパースする最小実装。
|
|
206
|
+
// 依存追加を避けつつ pnpm の workspace 定義を扱う。
|
|
207
|
+
function parsePnpmWorkspacePackages(yaml: string): string[] {
|
|
208
|
+
const lines = yaml.split(/\r?\n/);
|
|
209
|
+
const packages: string[] = [];
|
|
210
|
+
let inPackages = false;
|
|
211
|
+
let packagesIndent = -1;
|
|
212
|
+
for (const rawLine of lines) {
|
|
213
|
+
const line = rawLine.replace(/#.*$/, "").replace(/\s+$/, "");
|
|
214
|
+
if (line.length === 0) continue;
|
|
215
|
+
const indent = line.length - line.trimStart().length;
|
|
216
|
+
if (!inPackages) {
|
|
217
|
+
if (/^packages\s*:\s*$/.test(line)) {
|
|
218
|
+
inPackages = true;
|
|
219
|
+
packagesIndent = indent;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (indent <= packagesIndent) {
|
|
224
|
+
// packages: ブロックを抜けた
|
|
225
|
+
inPackages = false;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const itemMatch = line.match(/^\s*-\s*['"]?([^'"#]+?)['"]?\s*$/);
|
|
229
|
+
if (itemMatch) {
|
|
230
|
+
packages.push(itemMatch[1].trim());
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return packages;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 単純な末尾 `*` glob (`pkg/*`) のみを展開する。`**` 等のネストパターンは対象外。
|
|
237
|
+
async function expandWorkspaceGlobs(projectRoot: string, patterns: string[]): Promise<string[]> {
|
|
238
|
+
const expanded = new Set<string>();
|
|
239
|
+
for (const pattern of patterns) {
|
|
240
|
+
if (pattern.includes("**")) {
|
|
241
|
+
// 安全のため、再帰 glob は無視(無限ループ・性能を避ける)
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (pattern.endsWith("/*")) {
|
|
245
|
+
const baseDir = pattern.slice(0, -2);
|
|
246
|
+
try {
|
|
247
|
+
const entries = await fs.readdir(join(projectRoot, baseDir), { withFileTypes: true });
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
250
|
+
expanded.add(`${baseDir}/${entry.name}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {}
|
|
254
|
+
} else {
|
|
255
|
+
// glob なし: そのまま採用(ディレクトリ存在チェックは後段で実施)
|
|
256
|
+
expanded.add(pattern);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return [...expanded];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function detectWorkspaceTargetDirs(projectRoot: string): Promise<string[]> {
|
|
263
|
+
let patterns: string[] = [];
|
|
264
|
+
|
|
265
|
+
// 1. pnpm-workspace.yaml
|
|
266
|
+
try {
|
|
267
|
+
const yamlContent = await fs.readFile(join(projectRoot, "pnpm-workspace.yaml"), "utf-8");
|
|
268
|
+
patterns = parsePnpmWorkspacePackages(yamlContent);
|
|
269
|
+
} catch {}
|
|
270
|
+
|
|
271
|
+
// 2. package.json の workspaces (npm/yarn)
|
|
272
|
+
if (patterns.length === 0) {
|
|
273
|
+
try {
|
|
274
|
+
const pkgRaw = await fs.readFile(join(projectRoot, "package.json"), "utf-8");
|
|
275
|
+
const pkg = JSON.parse(pkgRaw) as { workspaces?: string[] | { packages?: string[] } };
|
|
276
|
+
const workspaces = pkg.workspaces;
|
|
277
|
+
if (Array.isArray(workspaces)) {
|
|
278
|
+
patterns = workspaces;
|
|
279
|
+
} else if (workspaces && Array.isArray(workspaces.packages)) {
|
|
280
|
+
patterns = workspaces.packages;
|
|
281
|
+
}
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 3. lerna.json
|
|
286
|
+
if (patterns.length === 0) {
|
|
287
|
+
try {
|
|
288
|
+
const lernaRaw = await fs.readFile(join(projectRoot, "lerna.json"), "utf-8");
|
|
289
|
+
const lerna = JSON.parse(lernaRaw) as { packages?: string[] };
|
|
290
|
+
if (Array.isArray(lerna.packages)) {
|
|
291
|
+
patterns = lerna.packages;
|
|
292
|
+
}
|
|
293
|
+
} catch {}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (patterns.length === 0) {
|
|
297
|
+
return ["src"];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const workspaceDirs = await expandWorkspaceGlobs(projectRoot, patterns);
|
|
301
|
+
const targetDirs: string[] = [];
|
|
302
|
+
for (const ws of workspaceDirs) {
|
|
303
|
+
try {
|
|
304
|
+
const stats = await fs.stat(join(projectRoot, ws, "src"));
|
|
305
|
+
if (stats.isDirectory()) {
|
|
306
|
+
targetDirs.push(`${ws}/src`);
|
|
307
|
+
}
|
|
308
|
+
} catch {}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// workspace 定義は見つかったが src/ を持つものが一切ない場合は ["src"] にフォールバック
|
|
312
|
+
return targetDirs.length > 0 ? targetDirs.sort() : ["src"];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export interface DetectedFormatter {
|
|
316
|
+
formatter: string | null;
|
|
317
|
+
formatterArgs: string[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export async function detectFormatter(projectRoot: string): Promise<DetectedFormatter> {
|
|
321
|
+
try {
|
|
322
|
+
const pkgRaw = await fs.readFile(join(projectRoot, "package.json"), "utf-8");
|
|
323
|
+
const pkg = JSON.parse(pkgRaw) as {
|
|
324
|
+
devDependencies?: Record<string, string>;
|
|
325
|
+
dependencies?: Record<string, string>;
|
|
326
|
+
};
|
|
327
|
+
const allDeps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
328
|
+
if (allDeps["@biomejs/biome"]) {
|
|
329
|
+
return { formatter: "biome", formatterArgs: ["check", "--write"] };
|
|
330
|
+
}
|
|
331
|
+
if (allDeps["prettier"]) {
|
|
332
|
+
return { formatter: "eslint-prettier", formatterArgs: [] };
|
|
333
|
+
}
|
|
334
|
+
} catch {}
|
|
335
|
+
return { formatter: null, formatterArgs: [] };
|
|
200
336
|
}
|
|
201
337
|
|
|
202
338
|
export async function deployHookScripts(harnessRoot: string, projectRoot: string): Promise<DeployHooksResult> {
|
|
203
339
|
const templateDir = join(harnessRoot, HOOKS_TEMPLATE_DIR);
|
|
204
340
|
const targetDir = join(projectRoot, HOOKS_TARGET_DIR);
|
|
205
341
|
|
|
206
|
-
// scripts/ ディレクトリをコピー
|
|
207
342
|
const scriptsSource = join(templateDir, "scripts");
|
|
208
343
|
const scriptsTarget = join(targetDir, "scripts");
|
|
344
|
+
const hookConfigPath = join(scriptsTarget, "hook-config.json");
|
|
345
|
+
|
|
346
|
+
// copyDirectory で全 scripts/ を上書きするため、ユーザーカスタマイズを尊重するには
|
|
347
|
+
// 事前に hook-config.json の既存内容を捕捉して後で復元する。
|
|
348
|
+
let preexistingHookConfig: string | null = null;
|
|
349
|
+
try {
|
|
350
|
+
preexistingHookConfig = await fs.readFile(hookConfigPath, "utf-8");
|
|
351
|
+
} catch {}
|
|
352
|
+
|
|
209
353
|
let scriptsDeployed = 0;
|
|
210
354
|
|
|
211
355
|
try {
|
|
@@ -221,6 +365,29 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
|
|
|
221
365
|
}
|
|
222
366
|
} catch {}
|
|
223
367
|
|
|
368
|
+
// hook-config.json の決定:
|
|
369
|
+
// 既存あり → ユーザーカスタマイズ尊重で元の内容を書き戻し
|
|
370
|
+
// 既存なし → 検出結果を反映して新規生成
|
|
371
|
+
const detectedTargetDirs = await detectWorkspaceTargetDirs(projectRoot);
|
|
372
|
+
const { formatter, formatterArgs } = await detectFormatter(projectRoot);
|
|
373
|
+
let hookConfigGenerated = false;
|
|
374
|
+
|
|
375
|
+
if (scriptsDeployed > 0) {
|
|
376
|
+
if (preexistingHookConfig !== null) {
|
|
377
|
+
await fs.writeFile(hookConfigPath, preexistingHookConfig, "utf-8");
|
|
378
|
+
} else {
|
|
379
|
+
const hookConfig: Record<string, unknown> = {
|
|
380
|
+
targetDirs: detectedTargetDirs,
|
|
381
|
+
};
|
|
382
|
+
if (formatter !== null) {
|
|
383
|
+
hookConfig.formatter = formatter;
|
|
384
|
+
hookConfig.formatterArgs = formatterArgs;
|
|
385
|
+
}
|
|
386
|
+
await fs.writeFile(hookConfigPath, JSON.stringify(hookConfig, null, 2) + "\n", "utf-8");
|
|
387
|
+
hookConfigGenerated = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
224
391
|
// settings.json を作成(既存があればスキップ)
|
|
225
392
|
const settingsSource = join(templateDir, "settings.json");
|
|
226
393
|
const settingsTarget = join(targetDir, "settings.json");
|
|
@@ -238,7 +405,13 @@ export async function deployHookScripts(harnessRoot: string, projectRoot: string
|
|
|
238
405
|
} catch {}
|
|
239
406
|
}
|
|
240
407
|
|
|
241
|
-
return {
|
|
408
|
+
return {
|
|
409
|
+
scriptsDeployed,
|
|
410
|
+
settingsCreated,
|
|
411
|
+
hookConfigGenerated,
|
|
412
|
+
detectedTargetDirs,
|
|
413
|
+
detectedFormatter: formatter,
|
|
414
|
+
};
|
|
242
415
|
}
|
|
243
416
|
|
|
244
417
|
export async function initHarnessConfig(
|
|
@@ -259,6 +432,9 @@ export async function initHarnessConfig(
|
|
|
259
432
|
name: projectName,
|
|
260
433
|
preset: "standard",
|
|
261
434
|
},
|
|
435
|
+
architecture: {
|
|
436
|
+
preset: "clean",
|
|
437
|
+
},
|
|
262
438
|
layers: {},
|
|
263
439
|
quickMode: {},
|
|
264
440
|
phaseDependencies: {
|
|
@@ -15,7 +15,10 @@ CONFIG_FILE="$SCRIPT_DIR/hook-config.json"
|
|
|
15
15
|
TARGET_DIRS=()
|
|
16
16
|
|
|
17
17
|
if [[ -f "$CONFIG_FILE" ]] && command -v jq >/dev/null 2>&1; then
|
|
18
|
-
|
|
18
|
+
# bash 3.2 (macOS default) has no mapfile; use portable while-read.
|
|
19
|
+
while IFS= read -r line; do
|
|
20
|
+
[[ -n "$line" ]] && TARGET_DIRS+=("$line")
|
|
21
|
+
done < <(jq -r '.targetDirs[]' "$CONFIG_FILE" 2>/dev/null)
|
|
19
22
|
fi
|
|
20
23
|
|
|
21
24
|
if [[ ${#TARGET_DIRS[@]} -eq 0 ]]; then
|
|
@@ -17,9 +17,14 @@ FORMATTER="biome"
|
|
|
17
17
|
FORMATTER_ARGS=()
|
|
18
18
|
|
|
19
19
|
if [[ -f "$CONFIG_FILE" ]] && command -v jq &> /dev/null; then
|
|
20
|
-
|
|
20
|
+
# bash 3.2 (macOS default) has no mapfile; use portable while-read.
|
|
21
|
+
while IFS= read -r line; do
|
|
22
|
+
[[ -n "$line" ]] && TARGET_DIRS+=("$line")
|
|
23
|
+
done < <(jq -r '.targetDirs[]' "$CONFIG_FILE" 2>/dev/null)
|
|
21
24
|
FORMATTER=$(jq -r '.formatter // "biome"' "$CONFIG_FILE" 2>/dev/null)
|
|
22
|
-
|
|
25
|
+
while IFS= read -r line; do
|
|
26
|
+
[[ -n "$line" ]] && FORMATTER_ARGS+=("$line")
|
|
27
|
+
done < <(jq -r '.formatterArgs[]' "$CONFIG_FILE" 2>/dev/null)
|
|
23
28
|
fi
|
|
24
29
|
|
|
25
30
|
if [[ ${#TARGET_DIRS[@]} -eq 0 ]]; then
|