phasegate 0.335.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.
- package/CHANGELOG.md +4 -0
- package/README.ja.md +22 -9
- package/README.md +28 -17
- package/docs/ADR/038-config-state-operation-permission-policy.md +5 -2
- package/docs/ADR/041-non-excludable-agent-trust-roots.md +68 -0
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +4 -4
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +27 -4
- package/docs/guide/installation.md +4 -2
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +1 -1
- package/package.json +1 -1
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
- package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +2 -2
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +123 -70
- package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
- package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
- package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
- package/scripts/harness/installation/application/named-hook-json.ts +17 -0
- package/scripts/harness/installation/application/operator-notice.ts +42 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
- package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
- package/scripts/harness/installation/composition-root.ts +10 -0
- package/scripts/harness/installation/domain/agent-target.ts +39 -0
- package/scripts/harness/installation/domain/check-id.ts +5 -0
- package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
- package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
- package/scripts/harness/installation/domain/repair-table.ts +5 -0
- package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
- package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
- package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
- package/scripts/harness/main.ts +100 -39
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
- package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/templates/.agents/hooks.json +16 -0
- package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
- package/templates/.claude/settings.json +5 -3
- package/templates/.codex/hooks.json +2 -2
|
@@ -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 {
|
|
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 (
|
|
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:
|
|
41
|
-
repairMode:
|
|
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 =
|
|
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 (
|
|
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[];
|