phasegate 0.315.0 → 0.340.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.ja.md +22 -9
  3. package/README.md +28 -17
  4. package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
  5. package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
  6. package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
  7. package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
  8. package/docs/guide/antigravity-integration.md +26 -0
  9. package/docs/guide/cli-reference.md +93 -11
  10. package/docs/guide/codex-integration.md +44 -107
  11. package/docs/guide/configuration.md +43 -2
  12. package/docs/guide/grok-integration.md +26 -0
  13. package/docs/guide/hooks-integration.md +27 -4
  14. package/docs/guide/installation.md +4 -2
  15. package/docs/guide/quick-vs-full-mode.md +36 -1
  16. package/docs/guide/setup-artifacts.md +4 -4
  17. package/docs/guide/skills-overview.md +1 -1
  18. package/package.json +2 -2
  19. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +6 -2
  20. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  21. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +17 -9
  22. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  23. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  24. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +196 -54
  25. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
  26. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +39 -17
  27. package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
  28. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  29. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
  30. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +129 -73
  31. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  32. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  33. package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
  34. package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
  35. package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
  36. package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
  37. package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
  38. package/scripts/harness/ci-governance/composition-root.ts +55 -3
  39. package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
  40. package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
  41. package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
  42. package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
  43. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  44. package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
  45. package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
  46. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
  47. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
  48. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
  49. package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
  50. package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
  51. package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
  52. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +2 -2
  53. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +2 -2
  54. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +2 -2
  55. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
  56. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
  57. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
  58. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  59. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  60. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  61. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  62. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  63. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  64. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  65. package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
  66. package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
  67. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  68. package/scripts/harness/installation/composition-root.ts +10 -0
  69. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  70. package/scripts/harness/installation/domain/check-id.ts +5 -0
  71. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  72. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  73. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  74. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  75. package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
  76. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  77. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  78. package/scripts/harness/main.ts +202 -51
  79. package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
  80. package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
  81. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +4 -1
  82. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
  83. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  84. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  85. package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
  86. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +59 -19
  87. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  88. package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
  89. package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
  90. package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
  91. package/scripts/harness/quick-mode/index.ts +7 -1
  92. package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
  93. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  94. package/skills/product-architect/SKILL.md +19 -0
  95. package/skills/story-mapper/SKILL.md +11 -0
  96. package/skills/story-writer/SKILL.md +11 -0
  97. package/skills/unit-designer/SKILL.md +11 -0
  98. package/templates/.agents/hooks.json +16 -0
  99. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  100. package/templates/.claude/settings.json +5 -3
  101. package/templates/.codex/hooks.json +2 -2
  102. package/templates/product_overview.template.md +85 -0
  103. package/templates/product_overview_plan.template.md +55 -0
  104. package/templates/story_mapping_plan.template.md +61 -0
  105. package/templates/story_writer_plan.template.md +61 -0
  106. package/templates/unit_design_plan.template.md +63 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @layer domain
3
3
  * @unit agent-integration
4
+ * @work-item-id WI-390
4
5
  *
5
6
  * ProtectedFileList 値オブジェクト
6
7
  * 変更をブロックすべきファイルのパターンリスト
@@ -14,19 +15,42 @@ export class ProtectedFileListEmptyError extends Error {
14
15
  }
15
16
  }
16
17
 
17
- /** デフォルトの保護対象ファイルパターン */
18
- const DEFAULT_PATTERNS = [
18
+ /**
19
+ * 設定から除外できない agent trust roots。
20
+ * これらを protectedFiles.exclude で解除できると、防御機構そのものを agent が
21
+ * 無効化してから書き換えられるため、通常の保護対象とは別の集合で保持する。
22
+ */
23
+ const NON_EXCLUDABLE_PATTERNS = [
24
+ 'phasegate.config.json',
25
+ '.phasegate-local/phasegate.config.json',
26
+ '**/phasegate.config.json',
27
+ // baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
28
+ // grandfather bypass を防ぐため、書き込み自体を保護対象とする。
29
+ '.phasegate/baseline.json',
30
+ '**/.phasegate/baseline.json',
31
+ // WI-363: .husky/ 配下は L0 runtime の実施点(pre-commit / commit-msg / pre-push)。
32
+ // WI-352 で config カテゴリに分類されるようになり Quick Mode の書き込み許可対象に
33
+ // 入ったため、防御機構そのものの書き換えを protected file として明示的に止める。
34
+ '.husky/**',
35
+ '**/.husky/**',
36
+ // Root agent instructions determine the permissions and operating procedure
37
+ // used by coding agents, so direct agent writes must remain blocked.
38
+ 'CLAUDE.md',
39
+ 'AGENTS.md',
40
+ 'GEMINI.md',
41
+ ];
42
+
43
+ /** 利用者設定で除外可能な通常のデフォルト保護対象。 */
44
+ const EXCLUDABLE_DEFAULT_PATTERNS = [
19
45
  'biome.json',
20
46
  '.biome.json',
21
47
  'tsconfig.json',
22
48
  'package.json',
23
49
  'package-lock.json',
24
- // baseline.json は grandfather 判定の信頼基盤。手動追記による protected file の
25
- // grandfather bypass を防ぐため、書き込み自体を保護対象とする。
26
- '.phasegate/baseline.json',
27
- '**/.phasegate/baseline.json',
28
50
  ];
29
51
 
52
+ const DEFAULT_PATTERNS = [...NON_EXCLUDABLE_PATTERNS, ...EXCLUDABLE_DEFAULT_PATTERNS];
53
+
30
54
  /**
31
55
  * glob パターンのシンプルなマッチング実装
32
56
  * micromatch なしで基本的な glob をサポート
@@ -74,23 +98,21 @@ export class ProtectedFileList {
74
98
  }
75
99
 
76
100
  static createWithExclusions(exclusions: string[]): ProtectedFileList {
77
- const filtered = DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
78
- if (filtered.length === 0) {
79
- return new ProtectedFileList([...DEFAULT_PATTERNS]);
80
- }
81
- return new ProtectedFileList(filtered);
101
+ const excludable = EXCLUDABLE_DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
102
+ return new ProtectedFileList([...NON_EXCLUDABLE_PATTERNS, ...excludable]);
82
103
  }
83
104
 
84
105
  static createWithAdditionalAndExclusions(
85
106
  additionalPatterns: string[],
86
107
  exclusions: string[],
87
108
  ): ProtectedFileList {
88
- const base = DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
89
- const allPatterns = [...base, ...additionalPatterns];
90
- if (allPatterns.length === 0) {
91
- return new ProtectedFileList([...DEFAULT_PATTERNS]);
92
- }
93
- return new ProtectedFileList(allPatterns);
109
+ const excludableDefaults = EXCLUDABLE_DEFAULT_PATTERNS.filter((p) => !exclusions.includes(p));
110
+ const excludableAdditional = additionalPatterns.filter((p) => !exclusions.includes(p));
111
+ return new ProtectedFileList([
112
+ ...NON_EXCLUDABLE_PATTERNS,
113
+ ...excludableDefaults,
114
+ ...excludableAdditional,
115
+ ]);
94
116
  }
95
117
 
96
118
  matches(filePath: string): boolean {
@@ -1,5 +1,6 @@
1
1
  // @unit agent-integration
2
2
  // @layer infrastructure
3
+ // @work-item-id WI-373
3
4
 
4
5
  import type {
5
6
  FullModeRequirementQueryPort,
@@ -7,6 +8,7 @@ import type {
7
8
  FullModeTargetChange,
8
9
  } from '../../domain/ports/full-mode-requirement-query-port.js';
9
10
  import type { ClassifyChangeCategoryUseCase } from '../../../quick-mode/application/usecases/classify-change-category-usecase.js';
11
+ import { QuickModeConfigError } from '../../../quick-mode/domain/errors/quick-mode-config-error.js';
10
12
 
11
13
  export interface QuickModeFullModeRequirementAdapterDeps {
12
14
  classifyUseCaseFactory: () => ClassifyChangeCategoryUseCase;
@@ -45,7 +47,19 @@ export class QuickModeFullModeRequirementAdapter implements FullModeRequirementQ
45
47
  rejectionReason: contract.rejectionReason,
46
48
  dominantCategory: contract.dominantCategory ?? undefined,
47
49
  };
48
- } catch {
50
+ } catch (error) {
51
+ // WI-373: quickMode 設定そのものが不正な場合は fail-closed。
52
+ // allowedCategories / categoryOverrides の enum 検証が入ったことで
53
+ // config の typo が例外になるため、ここで一律 fail-open のままだと
54
+ // 「設定を壊すと全書き込みが素通りする」という最悪の穴になる。
55
+ // config 不在・IO エラー等その他の例外は WI-333 の fail-open を維持する。
56
+ if (error instanceof QuickModeConfigError) {
57
+ return {
58
+ requiresFullMode: true,
59
+ rejectionRule: 'MIXED_CHANGES',
60
+ rejectionReason: `quickMode 設定が不正なため Quick Mode 判定を実行できません: ${error.message}`,
61
+ };
62
+ }
49
63
  return { requiresFullMode: false };
50
64
  }
51
65
  }
@@ -243,7 +243,7 @@ export function buildSessionStartContext(status: PhasegateStatus): string {
243
243
  "",
244
244
  "- Do NOT write to protected files without going through `/quick-implementor` skill.",
245
245
  '- Do NOT create/structurally modify source files under units listed as "blocked" below — the required design docs (logical_design.md / domain_model.md) are missing, and pre-tool-use hooks will block writes.',
246
- "- Prefer the native `apply_patch` tool for edits, BUT note that Codex's apply_patch bypasses pre-edit hooks. Violations surface at pre-commit time.",
246
+ "- Native `apply_patch` edits are phase-gated at PreToolUse; the Codex matcher is `Bash|apply_patch`. L2 pre-commit remains the backstop for skipped or untrusted hooks.",
247
247
  "",
248
248
  ];
249
249
 
@@ -3,6 +3,7 @@
3
3
  * @unit agent-integration
4
4
  * @work-item-id WI-208
5
5
  * @work-item-id WI-323
6
+ * @work-item-id WI-384
6
7
  *
7
8
  * PostToolUse Hook Adapter
8
9
  * Claude Code の PostToolUse Hook エントリポイント
@@ -6,45 +6,50 @@
6
6
  * @work-item-id WI-208
7
7
  * @work-item-id WI-345
8
8
  * @work-item-id WI-347
9
+ * @work-item-id WI-376
10
+ * @work-item-id WI-384
11
+ * @work-item-id WI-385
12
+ * @work-item-id WI-386
9
13
  *
10
14
  * PreToolUse Hook Adapter
11
15
  * Claude Code の PreToolUse Hook エントリポイント
12
16
  * stdin からJSON を読み取り、HandlePreToolUseUseCase を呼び出す
13
17
  */
14
18
 
15
- import { HandlePreToolUseUseCase } from '../application/usecases/handle-pre-tool-use-usecase.js';
16
- import { BashWriteTargetExtractor } from '../domain/services/bash-write-target-extractor.js';
17
- import { HarnessConfigConfigQueryAdapter } from '../infrastructure/adapters/harness-config-config-query-adapter.js';
18
- import { PhaseGateQueryAdapter } from '../infrastructure/adapters/phase-gate-query-adapter.js';
19
- import { FileSystemStoryReflectionQueryAdapter } from '../infrastructure/adapters/file-system-story-reflection-query-adapter.js';
20
- import { QuickModeFullModeRequirementAdapter } from '../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js';
21
- import { CiGovernanceBaselineGrandfatherAdapter } from '../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js';
22
- import { HarnessErrorGuidanceAdapter } from '../infrastructure/adapters/harness-error-guidance-adapter.js';
23
- import { FileSystemFullModeSessionQueryAdapter } from '../infrastructure/adapters/file-system-full-mode-session-query-adapter.js';
24
- import { createQuickModeCompositionRoot } from '../../quick-mode/composition-root.js';
25
- import * as path from 'node:path';
26
- import * as fs from 'node:fs/promises';
19
+ import * as fs from "node:fs/promises";
20
+ import * as path from "node:path";
21
+ import { createQuickModeCompositionRoot } from "../../quick-mode/composition-root.js";
22
+ import type {
23
+ CanonicalPreToolUseInput,
24
+ HookResponseProfile,
25
+ } from "../application/dto/normalized-pre-tool-use-request.js";
26
+ import { HandlePreToolUseUseCase } from "../application/usecases/handle-pre-tool-use-usecase.js";
27
+ import { ApplyPatchWriteTargetExtractor } from "../domain/services/apply-patch-write-target-extractor.js";
28
+ import { BashWriteTargetExtractor } from "../domain/services/bash-write-target-extractor.js";
29
+ import { CiGovernanceBaselineGrandfatherAdapter } from "../infrastructure/adapters/ci-governance-baseline-grandfather-adapter.js";
30
+ import { FileSystemFullModeSessionQueryAdapter } from "../infrastructure/adapters/file-system-full-mode-session-query-adapter.js";
31
+ import { FileSystemStoryReflectionQueryAdapter } from "../infrastructure/adapters/file-system-story-reflection-query-adapter.js";
32
+ import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
33
+ import { HarnessErrorGuidanceAdapter } from "../infrastructure/adapters/harness-error-guidance-adapter.js";
34
+ import { PhaseGateQueryAdapter } from "../infrastructure/adapters/phase-gate-query-adapter.js";
35
+ import { QuickModeFullModeRequirementAdapter } from "../infrastructure/adapters/quick-mode-full-mode-requirement-adapter.js";
36
+ import { PreToolUsePayloadNormalizer } from "./pre-tool-use-payload-normalizer.js";
37
+ import { PreToolUseResponseRenderer } from "./pre-tool-use-response-renderer.js";
27
38
 
39
+ /**
40
+ * WI-376 (ADR-039): 呼び出し元 skill 名を受け取るフィールド(caller_skill)は持たない。
41
+ * Claude Code の PreToolUse payload に skill 情報は無く、エージェントの自己申告値は
42
+ * authorization / guidance の入力にしない。未知キーが来ても単に無視される。
43
+ */
28
44
  interface PreToolUseHookInput {
29
45
  cwd?: string;
30
- tool_name?: string;
31
- caller_skill?: string;
32
- tool_input?: {
33
- path?: string;
34
- file_path?: string;
35
- paths?: string[];
36
- command?: string;
37
- content?: string;
38
- old_string?: string;
39
- new_string?: string;
40
- old_str?: string;
41
- new_str?: string;
42
- [key: string]: unknown;
43
- };
46
+ tool_name: string;
47
+ tool_input: CanonicalPreToolUseInput;
44
48
  }
45
49
 
46
50
  interface TargetChange {
47
51
  filePath: string;
52
+ changeKind?: "CREATE" | "MODIFY" | "DELETE";
48
53
  beforeContent?: string | null;
49
54
  afterContent?: string | null;
50
55
  }
@@ -54,15 +59,15 @@ async function readStdin(): Promise<string> {
54
59
  for await (const chunk of process.stdin) {
55
60
  chunks.push(chunk as Buffer);
56
61
  }
57
- return Buffer.concat(chunks).toString('utf8');
62
+ return Buffer.concat(chunks).toString("utf8");
58
63
  }
59
64
 
60
65
  async function findConfigPath(startDir: string): Promise<string> {
61
66
  let dir = startDir;
62
67
  while (true) {
63
68
  const candidates = [
64
- path.join(dir, 'phasegate.config.json'),
65
- path.join(dir, '.phasegate-local', 'phasegate.config.json'),
69
+ path.join(dir, "phasegate.config.json"),
70
+ path.join(dir, ".phasegate-local", "phasegate.config.json"),
66
71
  ];
67
72
  for (const candidate of candidates) {
68
73
  try {
@@ -74,18 +79,22 @@ async function findConfigPath(startDir: string): Promise<string> {
74
79
  if (parent === dir) break;
75
80
  dir = parent;
76
81
  }
77
- return path.join(startDir, 'phasegate.config.json');
82
+ return path.join(startDir, "phasegate.config.json");
78
83
  }
79
84
 
80
85
  function projectRootForConfig(configPath: string): string {
81
86
  const configDir = path.dirname(configPath);
82
- return path.basename(configDir) === '.phasegate-local' ? path.dirname(configDir) : configDir;
87
+ return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
83
88
  }
84
89
 
85
90
  function isProjectExternalPath(filePath: string, cwd: string, projectRoot: string): boolean {
86
91
  const resolvedPath = path.resolve(cwd, filePath);
87
92
  const relativePath = path.relative(projectRoot, resolvedPath);
88
- return relativePath === '..' || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
93
+ return relativePath === ".." || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath);
94
+ }
95
+
96
+ function normalizeProjectPath(filePath: string, cwd: string, projectRoot: string): string {
97
+ return path.relative(projectRoot, path.resolve(cwd, filePath)).split(path.sep).join("/");
89
98
  }
90
99
 
91
100
  async function main(): Promise<void> {
@@ -93,29 +102,41 @@ async function main(): Promise<void> {
93
102
  try {
94
103
  raw = await readStdin();
95
104
  } catch {
96
- process.stderr.write('stdin読み取りエラー\n');
105
+ process.stderr.write("stdin読み取りエラー\n");
97
106
  process.exit(2);
98
107
  }
99
108
 
100
- let input: PreToolUseHookInput;
109
+ let parsed: unknown;
101
110
  try {
102
- input = JSON.parse(raw) as PreToolUseHookInput;
111
+ parsed = JSON.parse(raw) as unknown;
103
112
  } catch {
104
113
  process.stderr.write(`不正なJSONです: ${raw}\n`);
105
114
  process.exit(2);
106
115
  }
107
116
 
108
- const toolName = input.tool_name;
109
- if (!toolName) {
110
- process.stderr.write('tool_nameフィールドが必要です\n');
111
- process.exit(2);
117
+ const normalizer = new PreToolUsePayloadNormalizer();
118
+ const normalized = normalizer.normalize(parsed);
119
+ if (!normalized.ok) {
120
+ if (normalized.responseProfile === undefined) {
121
+ process.stderr.write(`${normalized.reason}\n`);
122
+ process.exit(2);
123
+ }
124
+ exitWithRenderedResponse(new PreToolUseResponseRenderer().deny(normalized.responseProfile, normalized.reason));
112
125
  }
126
+ const request = normalized.request;
127
+ const responseProfile = request.responseProfile;
128
+ const input: PreToolUseHookInput = {
129
+ cwd: request.cwd,
130
+ tool_name: request.toolName,
131
+ tool_input: request.toolInput,
132
+ };
133
+ const toolName = input.tool_name;
113
134
 
114
135
  const cwd = path.resolve(input.cwd ?? process.cwd());
115
136
  const toRelative = (p: string): string => {
116
137
  if (path.isAbsolute(p)) {
117
138
  const rel = path.relative(cwd, p);
118
- return rel.startsWith('..') ? p : rel;
139
+ return rel.startsWith("..") ? p : rel;
119
140
  }
120
141
  return p;
121
142
  };
@@ -140,7 +161,26 @@ async function main(): Promise<void> {
140
161
  // translator の WRITE_TOOLS チェックを通過させる(Bash のままではフェーズゲートが
141
162
  // スキップされるため)。
142
163
  let effectiveToolName = toolName;
143
- if (toolName === 'Bash' && typeof input.tool_input?.command === 'string') {
164
+ if (toolName === "apply_patch") {
165
+ const command = input.tool_input?.command;
166
+ if (typeof command !== "string" || command.length === 0) {
167
+ deny(responseProfile, "apply_patch の tool_input.command が必要です");
168
+ }
169
+ const extractor = new ApplyPatchWriteTargetExtractor();
170
+ const patchTargets = extractor.extract(command);
171
+ if (patchTargets.length === 0) {
172
+ deny(responseProfile, "apply_patch command から書き込み対象を抽出できませんでした");
173
+ }
174
+ targetFilePaths.push(...patchTargets.map((target) => toRelative(target.filePath)));
175
+ targetChanges.push(
176
+ ...patchTargets.map((target) => ({
177
+ filePath: toRelative(target.filePath),
178
+ changeKind: target.changeKind,
179
+ })),
180
+ );
181
+ effectiveToolName = "Write";
182
+ }
183
+ if (toolName === "Bash" && typeof input.tool_input?.command === "string") {
144
184
  const extractor = new BashWriteTargetExtractor();
145
185
  const bashTargets = extractor.extract(input.tool_input.command);
146
186
  if (bashTargets.length > 0) {
@@ -155,19 +195,22 @@ async function main(): Promise<void> {
155
195
  recordedTargetPaths.add(change.filePath);
156
196
  }
157
197
  }
158
- effectiveToolName = 'Write';
198
+ effectiveToolName = "Write";
159
199
  }
160
200
  }
161
201
 
162
202
  try {
163
203
  const configPath = await findConfigPath(cwd);
164
204
  const projectRoot = projectRootForConfig(configPath);
165
- const projectTargetFilePaths = targetFilePaths.filter(
166
- (filePath) => !isProjectExternalPath(filePath, cwd, projectRoot),
167
- );
168
- const projectTargetChanges = targetChanges.filter(
169
- (change) => !isProjectExternalPath(change.filePath, cwd, projectRoot),
170
- );
205
+ const projectTargetFilePaths = targetFilePaths
206
+ .filter((filePath) => !isProjectExternalPath(filePath, cwd, projectRoot))
207
+ .map((filePath) => normalizeProjectPath(filePath, cwd, projectRoot));
208
+ const projectTargetChanges = targetChanges
209
+ .filter((change) => !isProjectExternalPath(change.filePath, cwd, projectRoot))
210
+ .map((change) => ({
211
+ ...change,
212
+ filePath: normalizeProjectPath(change.filePath, cwd, projectRoot),
213
+ }));
171
214
  const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
172
215
  const phaseGateQueryPort = new PhaseGateQueryAdapter();
173
216
  const storyReflectionQueryPort = new FileSystemStoryReflectionQueryAdapter({
@@ -199,49 +242,60 @@ async function main(): Promise<void> {
199
242
  fullModeSessionQueryPort,
200
243
  });
201
244
 
202
- const callerSkill = input.caller_skill ?? process.env.PHASEGATE_CALLER_SKILL;
203
245
  const output = await useCase.execute({
204
246
  toolName: effectiveToolName,
205
247
  targetFilePaths: projectTargetFilePaths,
206
- callerSkill,
207
248
  targetChanges: projectTargetChanges,
208
249
  });
209
250
 
210
251
  if (output.shouldBlock) {
211
- const msg = output.error?.message
212
- ?? `ファイル保護によりブロックされました: ${output.blockedFilePath ?? '不明なファイル'}`;
213
- process.stderr.write(`${msg}\n`);
214
- process.exit(2);
252
+ const msg =
253
+ output.error?.message ?? `ファイル保護によりブロックされました: ${output.blockedFilePath ?? "不明なファイル"}`;
254
+ deny(responseProfile, msg);
215
255
  }
216
256
 
217
257
  // Quick Mode が write を許可した場合に visibility を上げる informational notice。
218
258
  // exit 0 は維持し semantics は変えない。WI-087 finding #3。
219
259
  if (output.quickModeAllowed !== undefined) {
220
260
  const cat = output.quickModeAllowed.dominantCategory;
221
- const suffix = cat !== undefined && cat !== '' ? `, category=${cat}` : '';
261
+ const suffix = cat !== undefined && cat !== "" ? `, category=${cat}` : "";
222
262
  process.stderr.write(`phasegate: write allowed (Quick Mode${suffix})\n`);
223
263
  }
224
264
  if (output.fullModeSessionAllowed !== undefined) {
225
265
  const session = output.fullModeSessionAllowed;
226
- const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : '';
227
- const unit = session.unit !== undefined ? `, unit=${session.unit}` : '';
266
+ const workItem = session.workItemId !== undefined ? `, workItem=${session.workItemId}` : "";
267
+ const unit = session.unit !== undefined ? `, unit=${session.unit}` : "";
228
268
  process.stderr.write(`phasegate: write allowed (Full Mode session${workItem}${unit})\n`);
229
269
  }
230
270
 
231
271
  process.exit(0);
232
272
  } catch (error) {
233
- process.stderr.write(`実行エラー: ${String(error)}\n`);
234
- process.exit(2);
273
+ deny(responseProfile, `実行エラー: ${String(error)}`);
235
274
  }
236
275
  }
237
276
 
277
+ function exitWithRenderedResponse(response: {
278
+ readonly stdout: string;
279
+ readonly stderr: string;
280
+ readonly exitCode: 0 | 2;
281
+ }): never {
282
+ if (response.stdout.length > 0) process.stdout.write(response.stdout);
283
+ if (response.stderr.length > 0) process.stderr.write(response.stderr);
284
+ process.exit(response.exitCode);
285
+ throw new Error("process.exit returned unexpectedly");
286
+ }
287
+
288
+ function deny(profile: HookResponseProfile, reason: string): never {
289
+ exitWithRenderedResponse(new PreToolUseResponseRenderer().deny(profile, reason));
290
+ }
291
+
238
292
  async function buildTargetChanges(
239
293
  input: PreToolUseHookInput,
240
294
  cwd: string,
241
295
  toRelative: (p: string) => string,
242
296
  ): Promise<TargetChange[]> {
243
297
  const toolInput = input.tool_input;
244
- if (toolInput == null || typeof toolInput !== 'object') {
298
+ if (toolInput == null || typeof toolInput !== "object") {
245
299
  return [];
246
300
  }
247
301
 
@@ -251,18 +305,20 @@ async function buildTargetChanges(
251
305
  }
252
306
 
253
307
  const filePath = toRelative(rawPath);
254
- const oldString = typeof toolInput.old_string === 'string' ? toolInput.old_string : toolInput.old_str;
255
- const newString = typeof toolInput.new_string === 'string' ? toolInput.new_string : toolInput.new_str;
256
- if (typeof oldString === 'string' && typeof newString === 'string') {
308
+ const oldString = toolInput.old_string;
309
+ const newString = toolInput.new_string;
310
+ if (typeof oldString === "string" && typeof newString === "string") {
257
311
  return [{ filePath, beforeContent: oldString, afterContent: newString }];
258
312
  }
259
313
 
260
- if (typeof toolInput.content === 'string') {
261
- return [{
262
- filePath,
263
- beforeContent: await readExistingContent(cwd, rawPath),
264
- afterContent: toolInput.content,
265
- }];
314
+ if (typeof toolInput.content === "string") {
315
+ return [
316
+ {
317
+ filePath,
318
+ beforeContent: await readExistingContent(cwd, rawPath),
319
+ afterContent: toolInput.content,
320
+ },
321
+ ];
266
322
  }
267
323
 
268
324
  return [];
@@ -271,7 +327,7 @@ async function buildTargetChanges(
271
327
  async function readExistingContent(cwd: string, filePath: string): Promise<string | null> {
272
328
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
273
329
  try {
274
- return await fs.readFile(absolutePath, 'utf8');
330
+ return await fs.readFile(absolutePath, "utf8");
275
331
  } catch {
276
332
  return null;
277
333
  }
@@ -283,7 +339,7 @@ async function buildBashTargetChange(
283
339
  toRelative: (p: string) => string,
284
340
  ): Promise<TargetChange> {
285
341
  const filePath = toRelative(rawPath);
286
- if (rawPath.includes('$') || rawPath.includes('`') || rawPath.startsWith('~')) {
342
+ if (rawPath.includes("$") || rawPath.includes("`") || rawPath.startsWith("~")) {
287
343
  // hook ではシェル展開後の実パスを解決できないため、存在チェックを避けて MODIFY 既定にする。
288
344
  return { filePath };
289
345
  }
@@ -293,10 +349,10 @@ async function buildBashTargetChange(
293
349
  return { filePath };
294
350
  } catch (error) {
295
351
  const code = (error as NodeJS.ErrnoException).code;
296
- if (code === 'ENOENT' || code === 'ENOTDIR') {
352
+ if (code === "ENOENT" || code === "ENOTDIR") {
297
353
  // Bash では afterContent を取得できないため、空文字を CREATE 判定用の
298
354
  // 「変更後内容あり」sentinel として渡す。実ファイルへの書き込みは行わない。
299
- return { filePath, beforeContent: null, afterContent: '' };
355
+ return { filePath, beforeContent: null, afterContent: "" };
300
356
  }
301
357
  // 権限エラー等で存在を確認できない場合は従来どおり MODIFY 既定(安全側)。
302
358
  return { filePath };