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
@@ -5,6 +5,20 @@
5
5
  "required": [
6
6
  "project"
7
7
  ],
8
+ "$defs": {
9
+ "changeCategory": {
10
+ "type": "string",
11
+ "enum": ["bugfix", "docs", "test", "config", "feature", "domain", "api"]
12
+ },
13
+ "categoryOverrideGlobs": {
14
+ "type": "array",
15
+ "items": {
16
+ "type": "string",
17
+ "minLength": 1
18
+ },
19
+ "uniqueItems": true
20
+ }
21
+ },
8
22
  "properties": {
9
23
  "project": {
10
24
  "type": "object",
@@ -160,9 +174,10 @@
160
174
  "additionalProperties": false,
161
175
  "properties": {
162
176
  "allowedCategories": {
177
+ "description": "Quick Mode を通過させる ChangeCategory。7 値の enum で、未知値は設定エラーとして拒否される。",
163
178
  "type": "array",
164
179
  "items": {
165
- "type": "string"
180
+ "$ref": "#/$defs/changeCategory"
166
181
  },
167
182
  "uniqueItems": true
168
183
  },
@@ -194,6 +209,34 @@
194
209
  "type": "boolean"
195
210
  }
196
211
  }
212
+ },
213
+ "categoryOverrides": {
214
+ "description": "プロジェクト固有パスを任意の ChangeCategory へ割り当てる glob ルール。",
215
+ "type": "object",
216
+ "additionalProperties": false,
217
+ "properties": {
218
+ "bugfix": {
219
+ "$ref": "#/$defs/categoryOverrideGlobs"
220
+ },
221
+ "docs": {
222
+ "$ref": "#/$defs/categoryOverrideGlobs"
223
+ },
224
+ "test": {
225
+ "$ref": "#/$defs/categoryOverrideGlobs"
226
+ },
227
+ "config": {
228
+ "$ref": "#/$defs/categoryOverrideGlobs"
229
+ },
230
+ "feature": {
231
+ "$ref": "#/$defs/categoryOverrideGlobs"
232
+ },
233
+ "domain": {
234
+ "$ref": "#/$defs/categoryOverrideGlobs"
235
+ },
236
+ "api": {
237
+ "$ref": "#/$defs/categoryOverrideGlobs"
238
+ }
239
+ }
197
240
  }
198
241
  }
199
242
  },
@@ -13,6 +13,18 @@
13
13
  "decisionSignal": {
14
14
  "type": "string",
15
15
  "enum": ["business-rule-branch", "validation-rule", "error-construction", "state-transition", "policy-selection"]
16
+ },
17
+ "changeCategory": {
18
+ "type": "string",
19
+ "enum": ["bugfix", "docs", "test", "config", "feature", "domain", "api"]
20
+ },
21
+ "categoryOverrideGlobs": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "string",
25
+ "minLength": 1
26
+ },
27
+ "uniqueItems": true
16
28
  }
17
29
  },
18
30
  "properties": {
@@ -170,9 +182,10 @@
170
182
  "additionalProperties": false,
171
183
  "properties": {
172
184
  "allowedCategories": {
185
+ "description": "Quick Mode を通過させる ChangeCategory。7 値の enum で、未知値は設定エラーとして拒否される。",
173
186
  "type": "array",
174
187
  "items": {
175
- "type": "string"
188
+ "$ref": "#/$defs/changeCategory"
176
189
  },
177
190
  "uniqueItems": true
178
191
  },
@@ -204,6 +217,34 @@
204
217
  "type": "boolean"
205
218
  }
206
219
  }
220
+ },
221
+ "categoryOverrides": {
222
+ "description": "プロジェクト固有パスを任意の ChangeCategory へ割り当てる glob ルール。組み込み分類より先に評価されるが、組み込みが domain / api のファイルは降格できない。",
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "properties": {
226
+ "bugfix": {
227
+ "$ref": "#/$defs/categoryOverrideGlobs"
228
+ },
229
+ "docs": {
230
+ "$ref": "#/$defs/categoryOverrideGlobs"
231
+ },
232
+ "test": {
233
+ "$ref": "#/$defs/categoryOverrideGlobs"
234
+ },
235
+ "config": {
236
+ "$ref": "#/$defs/categoryOverrideGlobs"
237
+ },
238
+ "feature": {
239
+ "$ref": "#/$defs/categoryOverrideGlobs"
240
+ },
241
+ "domain": {
242
+ "$ref": "#/$defs/categoryOverrideGlobs"
243
+ },
244
+ "api": {
245
+ "$ref": "#/$defs/categoryOverrideGlobs"
246
+ }
247
+ }
207
248
  }
208
249
  }
209
250
  },
@@ -1,6 +1,6 @@
1
1
  // @unit harness-api
2
2
  // @layer domain
3
- // @work-item-id WI-250, WI-291, WI-296
3
+ // @work-item-id WI-250, WI-291, WI-296, WI-367, WI-368
4
4
  // known-harness-commands.ts — CLI 実サーフェスの canonical 既知コマンド一覧
5
5
  //
6
6
  // main.ts の CLI dispatch(`switch (command)`)が受理する全トップレベルコマンド名の
@@ -70,6 +70,7 @@ export const KNOWN_HARNESS_COMMANDS: readonly string[] = Object.freeze([
70
70
  "regression:run-k14-k15",
71
71
  "render-errors",
72
72
  "scaffold-design",
73
+ "scaffold-inception",
73
74
  "scaffold-wi",
74
75
  "session",
75
76
  "setup:agent",
@@ -80,6 +81,7 @@ export const KNOWN_HARNESS_COMMANDS: readonly string[] = Object.freeze([
80
81
  "skill:validate-structure",
81
82
  "skills",
82
83
  "status",
84
+ "templates",
83
85
  "uninstall",
84
86
  "update-skills",
85
87
  "validate",
@@ -0,0 +1,79 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-385
4
+
5
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
8
+ import { createFinding, projectPath } from "./check-utils.js";
9
+
10
+ const REQUIRED_TOOLS = ["write_to_file", "replace_file_content", "multi_replace_file_content", "run_command"] as const;
11
+ const REPAIR_HINT =
12
+ "Run npx phasegate reconcile --apply. Antigravity hard blocking is supported for the agy CLI surface; IDE/desktop must rely on the L2 pre-commit backstop.";
13
+
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ function matches(matcher: unknown, tool: string): boolean {
19
+ if (typeof matcher !== "string") return false;
20
+ try {
21
+ return new RegExp(matcher).test(tool);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function phasegateCommands(entry: Record<string, unknown>): Record<string, unknown>[] {
28
+ if (!Array.isArray(entry.hooks)) return [];
29
+ return entry.hooks.filter(
30
+ (hook): hook is Record<string, unknown> =>
31
+ isRecord(hook) && typeof hook.command === "string" && hook.command.includes("phasegate hook pre-tool-use"),
32
+ );
33
+ }
34
+
35
+ export class AntigravityHookMissingCheck implements HeuristicCheck {
36
+ readonly checkId = "antigravity-hook-missing" as const;
37
+ private readonly target = ".agents/hooks.json";
38
+
39
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
40
+ const absolutePath = projectPath(projectRoot, this.target);
41
+ if (!(await inspector.exists(absolutePath))) return this.finding("named definition missing", "mechanical");
42
+ const json = await inspector.readJson(absolutePath);
43
+ if (json === null) return this.finding("JSON parse failed", "manual");
44
+ const named = isRecord(json) && isRecord(json["phasegate-gate"]) ? json["phasegate-gate"] : undefined;
45
+ const entries = named !== undefined && Array.isArray(named.PreToolUse) ? named.PreToolUse : [];
46
+ const phasegateEntries = entries.filter(
47
+ (entry): entry is Record<string, unknown> => isRecord(entry) && phasegateCommands(entry).length > 0,
48
+ );
49
+ const missing = REQUIRED_TOOLS.filter(
50
+ (tool) => !phasegateEntries.some((entry) => matches(entry.matcher, tool)),
51
+ );
52
+ const invalidType = phasegateEntries.some((entry) =>
53
+ phasegateCommands(entry).some((hook) => hook.type !== "command"),
54
+ );
55
+ const invalidTimeout = phasegateEntries.some((entry) =>
56
+ phasegateCommands(entry).some((hook) => hook.timeout !== 30),
57
+ );
58
+ if (missing.length === 0 && phasegateEntries.length > 0 && !invalidType && !invalidTimeout) return null;
59
+ const details = [
60
+ ...missing.map((tool) => `matcher:${tool}`),
61
+ ...(phasegateEntries.length === 0 ? ["command"] : []),
62
+ ...(invalidType ? ["type=command"] : []),
63
+ ...(invalidTimeout ? ["timeout=30"] : []),
64
+ ];
65
+ const hasUserDefinitions = isRecord(json) && Object.keys(json).some((key) => key !== "phasegate-gate");
66
+ return this.finding(details.join(", "), hasUserDefinitions ? "ai-assisted" : "mechanical");
67
+ }
68
+
69
+ private finding(detail: string, repairMode: "mechanical" | "manual" | "ai-assisted"): DiagnosticFinding {
70
+ return createFinding({
71
+ checkId: this.checkId,
72
+ severity: "red",
73
+ target: this.target,
74
+ message: `.agents/hooks.json の Antigravity hook wiring が古いか不足しています: ${detail}`,
75
+ repairMode,
76
+ repairHint: REPAIR_HINT,
77
+ });
78
+ }
79
+ }
@@ -1,11 +1,60 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-145
4
+ // @work-item-id WI-384
4
5
 
5
- import type { FileInspectorPort } from "../ports/file-inspector-port.js";
6
- import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
6
  import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
8
- import { containsPhasegateHook, createFinding, hasUserCustomization, projectPath } from "./check-utils.js";
7
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
8
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
9
+ import { containsPhasegateHook, createFinding, projectPath } from "./check-utils.js";
10
+
11
+ const REQUIRED_MATCHER_TOKENS = ["Bash", "apply_patch"] as const;
12
+ const REPAIR_HINT =
13
+ "Run npx phasegate reconcile --apply, then open Codex /hooks and trust the updated hook definition hash (Codex CLI >= 0.124.0).";
14
+
15
+ function isRecord(value: unknown): value is Record<string, unknown> {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+
19
+ function phasegateCommandFor(entry: unknown, command: "pre-tool-use" | "post-tool-use"): boolean {
20
+ if (!isRecord(entry) || !Array.isArray(entry.hooks)) return false;
21
+ return entry.hooks.some((hook) => {
22
+ if (!isRecord(hook) || typeof hook.command !== "string") return false;
23
+ return hook.command.includes(`phasegate hook ${command}`);
24
+ });
25
+ }
26
+
27
+ function matcherTokens(entry: unknown): ReadonlySet<string> {
28
+ if (!isRecord(entry) || typeof entry.matcher !== "string") return new Set();
29
+ return new Set(entry.matcher.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? []);
30
+ }
31
+
32
+ function missingForEvent(
33
+ hooks: Record<string, unknown>,
34
+ event: "PreToolUse" | "PostToolUse",
35
+ command: "pre-tool-use" | "post-tool-use",
36
+ ): string[] {
37
+ const entries = Array.isArray(hooks[event]) ? hooks[event] : [];
38
+ const phasegateEntries = entries.filter((entry) => phasegateCommandFor(entry, command));
39
+ if (phasegateEntries.length === 0) return [`${event}:phasegate-command`];
40
+ const missing: string[] = [];
41
+ for (const token of REQUIRED_MATCHER_TOKENS) {
42
+ if (!phasegateEntries.some((entry) => matcherTokens(entry).has(token))) {
43
+ missing.push(`${event}:${token}`);
44
+ }
45
+ }
46
+ return missing;
47
+ }
48
+
49
+ function hasUserHookEntry(json: Record<string, unknown>): boolean {
50
+ if (Array.isArray(json.hooks)) {
51
+ return json.hooks.some((entry) => !containsPhasegateHook(entry));
52
+ }
53
+ if (!isRecord(json.hooks)) return false;
54
+ return Object.values(json.hooks).some(
55
+ (entries) => Array.isArray(entries) && entries.some((entry) => !containsPhasegateHook(entry)),
56
+ );
57
+ }
9
58
 
10
59
  export class CodexHookMissingCheck implements HeuristicCheck {
11
60
  readonly checkId = "codex-hook-missing" as const;
@@ -32,13 +81,28 @@ export class CodexHookMissingCheck implements HeuristicCheck {
32
81
  repairMode: "manual",
33
82
  });
34
83
  }
35
- if (containsPhasegateHook(json)) return null;
84
+ if (!isRecord(json) || !isRecord(json.hooks)) {
85
+ return createFinding({
86
+ checkId: this.checkId,
87
+ severity: "red",
88
+ target: this.target,
89
+ message: ".codex/hooks.json に phasegate hook が登録されていません",
90
+ repairMode: isRecord(json) && hasUserHookEntry(json) ? "ai-assisted" : "mechanical",
91
+ repairHint: REPAIR_HINT,
92
+ });
93
+ }
94
+ const missing = [
95
+ ...missingForEvent(json.hooks, "PreToolUse", "pre-tool-use"),
96
+ ...missingForEvent(json.hooks, "PostToolUse", "post-tool-use"),
97
+ ];
98
+ if (missing.length === 0) return null;
36
99
  return createFinding({
37
100
  checkId: this.checkId,
38
101
  severity: "red",
39
102
  target: this.target,
40
- message: ".codex/hooks.json phasegate hook が登録されていません",
41
- repairMode: hasUserCustomization(json) ? "ai-assisted" : "mechanical",
103
+ message: `.codex/hooks.json Codex hook wiring が古いか不足しています: ${missing.join(", ")}`,
104
+ repairMode: hasUserHookEntry(json) ? "ai-assisted" : "mechanical",
105
+ repairHint: REPAIR_HINT,
42
106
  });
43
107
  }
44
108
  }
@@ -0,0 +1,79 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-385
4
+
5
+ import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
6
+ import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
7
+ import type { FileInspectorPort } from "../ports/file-inspector-port.js";
8
+ import { containsPhasegateHook, createFinding, projectPath } from "./check-utils.js";
9
+
10
+ const REQUIRED_TOOLS = ["Bash", "Write", "Edit", "apply_patch"] as const;
11
+ const REPAIR_HINT =
12
+ "Run npx phasegate reconcile --apply, confirm every phasegate PreToolUse command has timeout 30, then use grok inspect and /hooks to verify trust (--trust or /hooks-trust).";
13
+
14
+ function isRecord(value: unknown): value is Record<string, unknown> {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+
18
+ function matcherMatches(entry: Record<string, unknown>, tool: string): boolean {
19
+ if (typeof entry.matcher !== "string") return false;
20
+ try {
21
+ return new RegExp(`^(?:${entry.matcher})$`).test(tool);
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function phasegateCommands(entry: Record<string, unknown>): Record<string, unknown>[] {
28
+ if (!Array.isArray(entry.hooks)) return [];
29
+ return entry.hooks.filter(
30
+ (hook): hook is Record<string, unknown> =>
31
+ isRecord(hook) && typeof hook.command === "string" && hook.command.includes("phasegate hook pre-tool-use"),
32
+ );
33
+ }
34
+
35
+ function hasUserHook(entries: readonly unknown[]): boolean {
36
+ return entries.some((entry) => !containsPhasegateHook(entry));
37
+ }
38
+
39
+ export class GrokHookMissingCheck implements HeuristicCheck {
40
+ readonly checkId = "grok-hook-missing" as const;
41
+ private readonly target = ".claude/settings.json";
42
+
43
+ async run(projectRoot: string, inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
44
+ const absolutePath = projectPath(projectRoot, this.target);
45
+ if (!(await inspector.exists(absolutePath))) return this.finding("compatible settings missing", "mechanical");
46
+ const json = await inspector.readJson(absolutePath);
47
+ if (json === null) return this.finding("JSON parse failed", "manual");
48
+ const hooks = isRecord(json) && isRecord(json.hooks) ? json.hooks : undefined;
49
+ const entries = hooks !== undefined && Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
50
+ const phasegateEntries = entries.filter(
51
+ (entry): entry is Record<string, unknown> => isRecord(entry) && phasegateCommands(entry).length > 0,
52
+ );
53
+ const missing = REQUIRED_TOOLS.filter((tool) => !phasegateEntries.some((entry) => matcherMatches(entry, tool)));
54
+ const invalidType = phasegateEntries.some((entry) =>
55
+ phasegateCommands(entry).some((hook) => hook.type !== "command"),
56
+ );
57
+ const invalidTimeout = phasegateEntries.some((entry) =>
58
+ phasegateCommands(entry).some((hook) => hook.timeout !== 30),
59
+ );
60
+ if (missing.length === 0 && !invalidType && !invalidTimeout) return null;
61
+ const details = [
62
+ ...missing.map((tool) => `matcher:${tool}`),
63
+ ...(invalidType ? ["type=command"] : []),
64
+ ...(invalidTimeout ? ["timeout=30"] : []),
65
+ ];
66
+ return this.finding(details.join(", "), hasUserHook(entries) ? "ai-assisted" : "mechanical");
67
+ }
68
+
69
+ private finding(detail: string, repairMode: "mechanical" | "manual" | "ai-assisted"): DiagnosticFinding {
70
+ return createFinding({
71
+ checkId: this.checkId,
72
+ severity: "red",
73
+ target: this.target,
74
+ message: `.claude/settings.json の Grok-compatible hook wiring が古いか不足しています: ${detail}`,
75
+ repairMode,
76
+ repairHint: REPAIR_HINT,
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,33 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-390
4
+
5
+ import type { DiagnosticFinding } from '../../domain/diagnostic-finding.js';
6
+ import type { GitHooksRuntimeProbe } from '../../domain/ports/git-hooks-runtime-probe.js';
7
+ import type { FileInspector } from '../../domain/ports/file-inspector.js';
8
+ import type { HeuristicCheck } from '../../domain/ports/heuristic-check.js';
9
+ import { createFinding } from './check-utils.js';
10
+
11
+ export class HuskyRuntimeInactiveCheck implements HeuristicCheck {
12
+ readonly checkId = 'husky-runtime-inactive' as const;
13
+
14
+ constructor(private readonly runtimeProbe: GitHooksRuntimeProbe) {}
15
+
16
+ async run(projectRoot: string, _inspector: FileInspector): Promise<DiagnosticFinding | null> {
17
+ const state = await this.runtimeProbe.probe(projectRoot);
18
+ if (state.isActive()) return null;
19
+
20
+ const detail =
21
+ state.kind === 'unavailable'
22
+ ? `Git hooks runtime を確認できませんでした: ${state.detail ?? 'unknown error'}`
23
+ : `Husky runtime が無効です: reason=${state.reason}, core.hooksPath=${state.hooksPath ?? 'unset'}`;
24
+ return createFinding({
25
+ checkId: this.checkId,
26
+ severity: 'red',
27
+ target: 'core.hooksPath',
28
+ message: detail,
29
+ repairMode: 'mechanical',
30
+ repairHint: 'npx phasegate setup:agent --apply --with-husky',
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,17 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-385
4
+
5
+ export const PHASEGATE_NAMED_HOOK = "phasegate-gate";
6
+
7
+ export function mergeNamedHookJson(
8
+ existing: Readonly<Record<string, unknown>>,
9
+ incoming: Readonly<Record<string, unknown>>,
10
+ ): Record<string, unknown> {
11
+ const owned = incoming[PHASEGATE_NAMED_HOOK];
12
+ return owned === undefined ? { ...existing } : { ...existing, [PHASEGATE_NAMED_HOOK]: owned };
13
+ }
14
+
15
+ export function removeNamedHookJson(existing: Readonly<Record<string, unknown>>): Record<string, unknown> {
16
+ return Object.fromEntries(Object.entries(existing).filter(([key]) => key !== PHASEGATE_NAMED_HOOK));
17
+ }
@@ -0,0 +1,42 @@
1
+ // @unit installation
2
+ // @layer application
3
+ // @work-item-id WI-384
4
+ // @work-item-id WI-385
5
+
6
+ export interface OperatorNotice {
7
+ readonly code:
8
+ | "CODEX_HOOK_TRUST_REQUIRED"
9
+ | "CODEX_HOOK_TRUST_UNVERIFIABLE"
10
+ | "GROK_HOOK_TRUST_UNVERIFIABLE"
11
+ | "ANTIGRAVITY_CLI_ONLY";
12
+ readonly level: "info";
13
+ readonly message: string;
14
+ }
15
+
16
+ export const CODEX_HOOK_TRUST_REQUIRED_NOTICE: OperatorNotice = Object.freeze({
17
+ code: "CODEX_HOOK_TRUST_REQUIRED",
18
+ level: "info",
19
+ message:
20
+ "Codex CLI >= 0.124.0 is required for native apply_patch hooks. The hook definition hash changed; open /hooks and trust the updated definition before relying on the edit-time gate.",
21
+ });
22
+
23
+ export const CODEX_HOOK_TRUST_UNVERIFIABLE_NOTICE: OperatorNotice = Object.freeze({
24
+ code: "CODEX_HOOK_TRUST_UNVERIFIABLE",
25
+ level: "info",
26
+ message:
27
+ "Codex CLI >= 0.124.0 is required for native apply_patch hooks. Phasegate cannot verify the external trust store; open /hooks and confirm the current hook definition is trusted.",
28
+ });
29
+
30
+ export const GROK_HOOK_TRUST_UNVERIFIABLE_NOTICE: OperatorNotice = Object.freeze({
31
+ code: "GROK_HOOK_TRUST_UNVERIFIABLE",
32
+ level: "info",
33
+ message:
34
+ "Phasegate cannot verify Grok's project-hook trust state. Run grok inspect, open /hooks, and approve with --trust or /hooks-trust before relying on the edit-time gate; keep the L2 pre-commit backstop enabled.",
35
+ });
36
+
37
+ export const ANTIGRAVITY_CLI_ONLY_NOTICE: OperatorNotice = Object.freeze({
38
+ code: "ANTIGRAVITY_CLI_ONLY",
39
+ level: "info",
40
+ message:
41
+ "Antigravity hard blocking is supported for the agy CLI surface only. IDE/desktop hook execution is not guaranteed; the L2 pre-commit gate remains the primary backstop there.",
42
+ });
@@ -6,7 +6,10 @@
6
6
  // @work-item-id WI-215
7
7
  // @work-item-id WI-330
8
8
  // @work-item-id WI-343
9
+ // @work-item-id WI-385
10
+ // @work-item-id WI-390
9
11
 
12
+ import type { AgentTarget } from "../../domain/agent-target.js";
10
13
  import type { CheckId } from "../../domain/check-id.js";
11
14
  import type { ConfigStatus } from "../../domain/config-status.js";
12
15
  import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
@@ -16,7 +19,7 @@ import type { ConfigStatusProbePort } from "../ports/config-status-probe-port.js
16
19
  import type { FileInspectorPort } from "../ports/file-inspector-port.js";
17
20
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
18
21
 
19
- export type DoctorAgentScope = "claude" | "codex" | "both";
22
+ export type DoctorAgentScope = AgentTarget;
20
23
 
21
24
  export interface ScopedOutDiagnosticFinding {
22
25
  readonly finding: DiagnosticFinding;
@@ -40,10 +43,13 @@ export interface RunDoctorDiagnosticsOutput {
40
43
 
41
44
  const CLAUDE_ONLY_CHECKS = new Set<CheckId>(["claude-hook-missing", "claude-context-missing", "claude-skills-symlink"]);
42
45
  const CODEX_ONLY_CHECKS = new Set<CheckId>(["codex-hook-missing", "codex-context-missing", "codex-skills-symlink"]);
46
+ const CLAUDE_COMPATIBLE_CHECKS = new Set<CheckId>(["grok-hook-missing"]);
47
+ const ANTIGRAVITY_ONLY_CHECKS = new Set<CheckId>(["antigravity-hook-missing"]);
43
48
  const PERSONAL_SCOPED_OUT_CHECKS = new Set<CheckId>([
44
49
  "husky-pre-commit-missing",
45
50
  "husky-commit-msg-missing",
46
51
  "husky-pre-push-missing",
52
+ "husky-runtime-inactive",
47
53
  "ci-workflow-missing",
48
54
  "package-json-devdep-missing",
49
55
  ]);
@@ -65,9 +71,7 @@ export class RunDoctorDiagnosticsUseCase {
65
71
  : "project";
66
72
  const configStatus = (await this.configStatusProbe.probe(input.projectRoot)).status;
67
73
  const rawFindings = (
68
- await Promise.all(
69
- this.checks.map((check) => check.run(input.projectRoot, this.inspector, { installationMode })),
70
- )
74
+ await Promise.all(this.checks.map((check) => check.run(input.projectRoot, this.inspector, { installationMode })))
71
75
  ).filter((finding) => finding !== null);
72
76
  const { findings, scopedOutFindings } = this.applyPersonalScope(
73
77
  this.applyAgentScope(rawFindings, agent),
@@ -91,12 +95,10 @@ export class RunDoctorDiagnosticsUseCase {
91
95
  readonly findings: readonly DiagnosticFinding[];
92
96
  readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
93
97
  } {
94
- if (agent === "both") return { findings, scopedOutFindings: [] };
95
- const scopedOutChecks = agent === "claude" ? CODEX_ONLY_CHECKS : CLAUDE_ONLY_CHECKS;
96
98
  const applicable: DiagnosticFinding[] = [];
97
99
  const scopedOut: ScopedOutDiagnosticFinding[] = [];
98
100
  for (const finding of findings) {
99
- if (scopedOutChecks.has(finding.checkId)) {
101
+ if (!this.isApplicableAgentCheck(finding.checkId, agent)) {
100
102
  scopedOut.push({
101
103
  finding,
102
104
  scopeReason: `${finding.checkId} belongs to an unselected agent for doctor --agent ${agent}.`,
@@ -108,6 +110,15 @@ export class RunDoctorDiagnosticsUseCase {
108
110
  return { findings: applicable, scopedOutFindings: scopedOut };
109
111
  }
110
112
 
113
+ private isApplicableAgentCheck(checkId: CheckId, agent: DoctorAgentScope): boolean {
114
+ if (CLAUDE_ONLY_CHECKS.has(checkId)) return agent === "claude" || agent === "both" || agent === "all";
115
+ if (CODEX_ONLY_CHECKS.has(checkId)) return agent === "codex" || agent === "both" || agent === "all";
116
+ if (CLAUDE_COMPATIBLE_CHECKS.has(checkId))
117
+ return agent === "claude" || agent === "both" || agent === "grok" || agent === "all";
118
+ if (ANTIGRAVITY_ONLY_CHECKS.has(checkId)) return agent === "antigravity" || agent === "all";
119
+ return true;
120
+ }
121
+
111
122
  private applyPersonalScope(
112
123
  scoped: {
113
124
  readonly findings: readonly DiagnosticFinding[];