phasegate 0.119.0 → 0.121.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 +25 -0
- package/docs/guide/hooks-integration.md +18 -0
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +7 -4
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +8 -0
- package/scripts/harness/main.ts +14 -1
- package/scripts/harness/setup/skill-deployer.ts +178 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.121.0] - 2026-05-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **WI-087 finding #3 + WI-086 docs: PreToolUse の Quick Mode 通過時の visibility 改善 + hook 責務分離をドキュメント化** — 外部レポーター nakataj-mti / junpei-9898 が GitHub Issue [#3](https://github.com/junpei-9898/phasegate/issues/3) finding #3 / [#2](https://github.com/junpei-9898/phasegate/issues/2) で報告した「Quick Mode で write が allow されるとき hook が完全 silent で、初見ユーザーが "hook が走っていない" と誤認する」問題と「pre-tool-use が L1 lint をしないことが暗黙仕様化していてレポーター期待と齟齬している」問題を解消。
|
|
15
|
+
- **Quick Mode visibility notice** (`scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts`): `HandlePreToolUseOutput.quickModeAllowed` がセットされた場合、stderr に `phasegate: write allowed (Quick Mode, category=<dominantCategory>)` を出力。exit code は 0 維持で **semantics は不変**、可視性のみ向上。
|
|
16
|
+
- **DTO 拡張** (`scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts`): `quickModeAllowed?: { dominantCategory?: string }` を追加。`HandlePreToolUseUseCase.execute` 内で `WRITE_TOOLS + fullModeRequirementQueryPort + requiresFullMode=false` の経路で populate。block 経路や WRITE_TOOLS 外 (`Bash` 等) では出力されず、後方互換維持。
|
|
17
|
+
- **テスト追加** (`scripts/harness/__tests__/integration/agent-integration/handle-pre-tool-use-usecase.test.ts`): 4 ケース — `quickModeAllowed` が `dominantCategory` 付きで返る / `dominantCategory` 未設定で返る / `fullModeRequirementQueryPort` 未指定時は出力なし / WRITE_TOOLS 外 (`Bash`) では出力なし。全 49 ケース (前回 45 + 新規 4) グリーン。
|
|
18
|
+
- **責務分離のドキュメント明文化** (`docs/guide/hooks-integration.md`): "Responsibility Separation" セクションを冒頭に追加し、pre = フェーズゲート / post = lint / Stop = complete-check の役割分担表と「pre-tool-use は意図的に lint を実行しない (lint は書き込み後の content が必要)」旨を明記。WI-086 で指摘された「`pre-tool-use` で違反 Write を exit 2 でブロックしてほしい」期待が現行設計と乖離する理由を ユーザー視点で説明。
|
|
19
|
+
- **互換性**: 既存テスト全てグリーン(`expect(actual).toEqual({ shouldBlock: false })` 形式の既存 assert は `quickModeAllowed: undefined` を含む結果でも一致)。`HandlePreToolUseOutput` への field 追加は外部 API ではなく application/presentation 内部 DTO のみで、外部消費者なし。
|
|
20
|
+
- **スコープ外** (Phase C-2 で対応予定): WI-087 finding #4 — Stop hook `--enforce` flag (`agentIntegration.stopHook.enforce` config 追加で Complete Check 失敗時に exit 2 + decision JSON `"deny"` を返す strict mode)。config schema 拡張のため story-implementor で別リリース予定。
|
|
21
|
+
|
|
22
|
+
## [0.120.0] - 2026-05-07
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **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 になる」問題を修正。
|
|
27
|
+
- **モノレポ 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'` 行抽出のみ)。
|
|
28
|
+
- **formatter 自動検出** (`detectFormatter`): `package.json.devDependencies` (および `dependencies`) を読み、`@biomejs/biome` 存在 → `biome` + `["check","--write"]`、不在 + `prettier` 存在 → `eslint-prettier`、どちらも不在 → `null` (formatter フィールド省略で `format-typescript-hook.sh` の case 文 default フォールスルー、registry pull を回避)。
|
|
29
|
+
- **`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: ...)` 行を追加。
|
|
30
|
+
- **`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 / 手動編集ルートを継続。
|
|
31
|
+
- **テスト追加**: `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 が出ないこと)。
|
|
32
|
+
- **互換性**: 既存 v2 config を読む load-resolved-config-use-case 側は変更なし。既存 `.claude/scripts/hook-config.json` をカスタマイズ済みのプロジェクトで `phasegate init` を再実行しても上書きされない (write-back ガード)。
|
|
33
|
+
- **スコープ外** (Phase C で対応予定): Quick Mode 通過時の stderr notice (finding #3) / Stop hook `--enforce` flag (finding #4) / `pre-tool-use` の責務範囲ドキュメント明文化 (WI-086)。
|
|
34
|
+
|
|
10
35
|
## [0.119.0] - 2026-05-07
|
|
11
36
|
|
|
12
37
|
### Fixed
|
|
@@ -42,9 +42,27 @@ Add the following to `.claude/settings.json`:
|
|
|
42
42
|
|
|
43
43
|
## Hook Behaviors
|
|
44
44
|
|
|
45
|
+
### Responsibility Separation (important)
|
|
46
|
+
|
|
47
|
+
Phasegate's hooks are **strictly partitioned by phase**:
|
|
48
|
+
|
|
49
|
+
| Hook | What it checks | What it does NOT check |
|
|
50
|
+
|------|----------------|------------------------|
|
|
51
|
+
| **PreToolUse** | Phase Gate, protected files, Quick Mode category gating, story-reflection | L1 lint (`@unit` / `@layer` / `any` abuse) — these are post-write concerns |
|
|
52
|
+
| **PostToolUse** | L1 Biome AST rules, formatter, type-check (via `analyze-errors-hook.sh`) | Phase Gate (already checked before write) |
|
|
53
|
+
| **Stop** | Complete-check (L2-L4 full validation suite) | Per-edit lint (already done by PostToolUse) |
|
|
54
|
+
|
|
55
|
+
This separation is intentional:
|
|
56
|
+
- **Pre = "should this write happen at all?"** — concerns the caller's intent and design state.
|
|
57
|
+
- **Post = "is what was written valid?"** — concerns the resulting code's quality.
|
|
58
|
+
- **Stop = "is the session ready to end?"** — concerns the cumulative state across the session.
|
|
59
|
+
|
|
60
|
+
If you expect L1 lint (e.g., missing `@unit` annotation) to **block** a Write before it happens, that is by design **not** the case. The PreToolUse hook intentionally does not run lint, because lint requires the resulting file content (which only exists after the write). Lint violations surface as **PostToolUse** decision JSON (`decision: "block"`) and trigger Claude Code to retry.
|
|
61
|
+
|
|
45
62
|
### PreToolUse (before file write)
|
|
46
63
|
- Enforces Phase Gate: blocks writing to source files if required design documents don't exist
|
|
47
64
|
- Blocks writes to protected files (package.json, biome.json, tsconfig.json)
|
|
65
|
+
- Quick Mode: when a write is allowed because the change is classified within `quickMode.allowedCategories`, an informational notice is emitted to stderr (`phasegate: write allowed (Quick Mode, category=<...>)`) — exit code remains 0
|
|
48
66
|
- Returns actionable error messages with:
|
|
49
67
|
- Violation reason
|
|
50
68
|
- Missing artifacts
|
package/package.json
CHANGED
|
@@ -19,4 +19,9 @@ export interface HandlePreToolUseOutput {
|
|
|
19
19
|
fullModeRejectionRule?: 'MIXED_CHANGES' | 'NEW_DOMAIN' | 'API_CONTRACT';
|
|
20
20
|
fullModeDominantCategory?: string;
|
|
21
21
|
nextAction?: string;
|
|
22
|
+
// Quick Mode が write を許可した際に dominant カテゴリを presentation 層に伝える。
|
|
23
|
+
// shouldBlock=false のときのみセットされる。WI-087 finding #3。
|
|
24
|
+
quickModeAllowed?: {
|
|
25
|
+
dominantCategory?: string;
|
|
26
|
+
};
|
|
22
27
|
}
|
package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts
CHANGED
|
@@ -134,6 +134,7 @@ export class HandlePreToolUseUseCase {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
let quickModeAllowed: HandlePreToolUseOutput["quickModeAllowed"];
|
|
137
138
|
if (
|
|
138
139
|
HandlePreToolUseUseCase.WRITE_TOOLS.has(input.toolName) &&
|
|
139
140
|
this.fullModeRequirementQueryPort !== undefined &&
|
|
@@ -157,29 +158,31 @@ export class HandlePreToolUseUseCase {
|
|
|
157
158
|
unitIdForGuidance,
|
|
158
159
|
);
|
|
159
160
|
}
|
|
161
|
+
} else {
|
|
162
|
+
quickModeAllowed = { dominantCategory: fullModeResult.dominantCategory };
|
|
160
163
|
}
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
const scope = this.resolveStoryReflectionScope(input);
|
|
165
168
|
if (scope === null || this.storyReflectionQueryPort === undefined) {
|
|
166
|
-
return { shouldBlock: false };
|
|
169
|
+
return { shouldBlock: false, quickModeAllowed };
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
if (grandfather.allGrandfathered) {
|
|
170
173
|
this.grandfatherLogger("story-reflection", input.targetFilePaths);
|
|
171
|
-
return { shouldBlock: false };
|
|
174
|
+
return { shouldBlock: false, quickModeAllowed };
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
const unitId = scope.unitId;
|
|
175
178
|
if (unitId === undefined) {
|
|
176
|
-
return { shouldBlock: false };
|
|
179
|
+
return { shouldBlock: false, quickModeAllowed };
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
const reflectionResult = await this.storyReflectionQueryPort.checkReflection(unitId);
|
|
180
183
|
|
|
181
184
|
if (reflectionResult.skipped || reflectionResult.passed) {
|
|
182
|
-
return { shouldBlock: false };
|
|
185
|
+
return { shouldBlock: false, quickModeAllowed };
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
return HandlePreToolUseUseCase.buildStoryReflectionBlockOutput(
|
|
@@ -150,6 +150,14 @@ async function main(): Promise<void> {
|
|
|
150
150
|
process.exit(2);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Quick Mode が write を許可した場合に visibility を上げる informational notice。
|
|
154
|
+
// exit 0 は維持し semantics は変えない。WI-087 finding #3。
|
|
155
|
+
if (output.quickModeAllowed !== undefined) {
|
|
156
|
+
const cat = output.quickModeAllowed.dominantCategory;
|
|
157
|
+
const suffix = cat !== undefined && cat !== '' ? `, category=${cat}` : '';
|
|
158
|
+
process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
process.exit(0);
|
|
154
162
|
} catch (error) {
|
|
155
163
|
process.stderr.write(`実行エラー: ${String(error)}\n`);
|
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: {
|