phasegate 0.335.0 → 0.341.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 (111) hide show
  1. package/CHANGELOG.md +16 -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/041-non-excludable-agent-trust-roots.md +68 -0
  6. package/docs/folder_management_rules.md +10 -1
  7. package/docs/guide/antigravity-integration.md +26 -0
  8. package/docs/guide/cli-reference.md +27 -7
  9. package/docs/guide/codex-integration.md +44 -107
  10. package/docs/guide/configuration.md +28 -2
  11. package/docs/guide/grok-integration.md +26 -0
  12. package/docs/guide/hooks-integration.md +34 -5
  13. package/docs/guide/installation.md +4 -2
  14. package/docs/guide/setup-artifacts.md +4 -4
  15. package/docs/guide/skills-overview.md +4 -1
  16. package/package.json +2 -1
  17. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
  18. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  19. package/scripts/harness/agent-integration/application/usecases/handle-post-tool-use-usecase.ts +4 -0
  20. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
  21. package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
  22. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  23. package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
  24. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  25. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
  26. package/scripts/harness/agent-integration/domain/services/hook-to-cli-translator.ts +8 -5
  27. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
  28. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
  29. package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
  30. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
  31. package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
  32. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
  33. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
  34. package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
  35. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  36. package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
  37. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
  38. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -70
  39. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  40. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  41. package/scripts/harness/agent-integration/presentation/stop-hook.ts +1 -3
  42. package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
  43. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
  44. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  45. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
  46. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
  47. package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
  48. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
  49. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
  50. package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
  51. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  52. package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
  53. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  54. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  55. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  56. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  57. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  58. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  59. package/scripts/harness/installation/application/usecases/run-install.ts +157 -57
  60. package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
  61. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  62. package/scripts/harness/installation/composition-root.ts +10 -0
  63. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  64. package/scripts/harness/installation/domain/check-id.ts +5 -0
  65. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  66. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  67. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  68. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  69. package/scripts/harness/installation/presentation/cli/install-handler.ts +25 -5
  70. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  71. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  72. package/scripts/harness/main.ts +227 -128
  73. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
  74. package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
  75. package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
  76. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
  77. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
  78. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
  79. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  80. package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
  81. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  82. package/scripts/harness/quick-mode/composition-root.ts +5 -1
  83. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
  84. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  85. package/scripts/harness/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
  86. package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
  87. package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
  88. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  89. package/scripts/harness/setup/skill-deployer.ts +3 -45
  90. package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
  91. package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
  92. package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
  93. package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
  94. package/scripts/harness/skill-quality/composition-root.ts +14 -7
  95. package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
  96. package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
  97. package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
  98. package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
  99. package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
  100. package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
  101. package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
  102. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
  103. package/skills/README.md +7 -0
  104. package/skills/cascade-updater/SKILL.md +37 -109
  105. package/skills/cascade-updater/references//345/261/244/345/210/245/345/210/244/345/256/232/343/202/254/343/202/244/343/203/211.md +5 -3
  106. package/skills/release-publisher/SKILL.md +3 -1
  107. package/skills/skill-creator/SKILL.md +8 -9
  108. package/templates/.agents/hooks.json +16 -0
  109. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  110. package/templates/.claude/settings.json +5 -3
  111. package/templates/.codex/hooks.json +2 -2
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { accessSync, constants, statSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
7
9
  import type { FilePath } from '../../domain/value-objects/file-path.js';
8
10
  import type { BiomeExecutorPort } from '../../domain/ports/biome-executor-port.js';
9
11
 
@@ -34,7 +36,25 @@ export class BiomeCliExecutorAdapter implements BiomeExecutorPort {
34
36
 
35
37
  constructor(deps: BiomeCliExecutorAdapterDeps) {
36
38
  this.cwd = deps.cwd;
37
- this.biomeBin = deps.biomeBin ?? 'npx';
39
+ this.biomeBin = deps.biomeBin ?? this.findLocalBin(deps.cwd) ?? 'npx';
40
+ }
41
+
42
+ private findLocalBin(cwd: string): string | undefined {
43
+ // .cmd execution has different quoting/shell requirements; retain its old route.
44
+ if (process.platform === 'win32') return undefined;
45
+ let directory = resolve(cwd);
46
+ while (true) {
47
+ const candidate = join(directory, 'node_modules', '.bin', 'biome');
48
+ try {
49
+ accessSync(candidate, constants.X_OK);
50
+ if (statSync(candidate).isFile()) return candidate;
51
+ } catch {
52
+ // Match local npm-bin lookup from the working directory toward the root.
53
+ }
54
+ const parent = dirname(directory);
55
+ if (parent === directory) return undefined;
56
+ directory = parent;
57
+ }
38
58
  }
39
59
 
40
60
  async executeCheck(files: readonly FilePath[]): Promise<void> {
@@ -2,6 +2,7 @@
2
2
  * @layer infrastructure
3
3
  * @unit biome-ast-engine
4
4
  * @work-item-id WI-109
5
+ * @work-item-id WI-220
5
6
  */
6
7
 
7
8
  import * as ts from 'typescript';
@@ -74,6 +75,10 @@ export class TypeScriptSourceModuleAnalyzerAdapter implements SourceModuleAnalyz
74
75
  moduleResolution: ts.ModuleResolutionKind.Node16,
75
76
  allowJs: false,
76
77
  noEmit: true,
78
+ // Snapshots use syntax only. The workspace scanner already supplies every
79
+ // graph node; libraries and transitive imports are not snapshot inputs.
80
+ noLib: true,
81
+ noResolve: true,
77
82
  });
78
83
 
79
84
  const snapshots: SourceModuleSnapshot[] = [];
@@ -1,5 +1,6 @@
1
1
  // @unit ci-governance
2
2
  // @layer domain
3
+ // @work-item-id WI-384
3
4
 
4
5
  export interface IntegrityTargetProps {
5
6
  readonly include: readonly string[];
@@ -31,6 +32,7 @@ export class IntegrityTarget {
31
32
  "skills/*/SKILL.md",
32
33
  ".claude/settings.json",
33
34
  ".claude/scripts/*.sh",
35
+ ".codex/hooks.json",
34
36
  ".husky/*",
35
37
  "docs/templates/agent-context/**",
36
38
  ],
@@ -685,6 +685,21 @@
685
685
  "type": "object",
686
686
  "additionalProperties": false,
687
687
  "properties": {
688
+ "preToolUse": {
689
+ "type": "object",
690
+ "additionalProperties": false,
691
+ "properties": {
692
+ "enabled": { "type": "boolean" },
693
+ "dependencyReflection": { "type": "string", "enum": ["advisory", "enforce"] }
694
+ }
695
+ },
696
+ "postToolUse": {
697
+ "type": "object",
698
+ "additionalProperties": false,
699
+ "properties": {
700
+ "enabled": { "type": "boolean" }
701
+ }
702
+ },
688
703
  "stopHook": {
689
704
  "type": "object",
690
705
  "additionalProperties": false,
@@ -109,13 +109,16 @@ function toHarnessError(error: ErrorObject): HarnessErrorWithPath {
109
109
  });
110
110
  }
111
111
 
112
- const validateSchemaV2 = createValidateFunction(SCHEMA_V2_PATH);
113
- const validateSchemaV3 = createValidateFunction(SCHEMA_V3_PATH);
112
+ // Compile only the selected schema, once per module. Failed compilation is not cached.
113
+ let validateSchemaV2: ValidateFunction | undefined;
114
+ let validateSchemaV3: ValidateFunction | undefined;
114
115
 
115
116
  export class AjvConfigSchemaValidator implements ConfigSchemaValidatorPort {
116
117
  validate(document: unknown): readonly HarnessError[] {
117
118
  const schemaVersion = detectSchemaVersion(document);
118
- const validateSchema = schemaVersion === 'v3' ? validateSchemaV3 : validateSchemaV2;
119
+ const validateSchema = schemaVersion === 'v3'
120
+ ? (validateSchemaV3 ??= createValidateFunction(SCHEMA_V3_PATH))
121
+ : (validateSchemaV2 ??= createValidateFunction(SCHEMA_V2_PATH));
119
122
  const valid = validateSchema(document);
120
123
 
121
124
  if (valid) {
@@ -1,10 +1,11 @@
1
+ // @unit harness-api
1
2
  // @layer domain
2
3
  // biome-lint-port.ts
3
4
 
4
5
  import type { HarnessError } from '../value-objects/harness-api-response.js';
5
6
 
6
7
  export interface BiomeLintPort {
7
- runLint(): Promise<{
8
+ runLint(reportTargets?: readonly string[]): Promise<{
8
9
  passed: boolean;
9
10
  errors: HarnessError[];
10
11
  warnings: HarnessError[];
@@ -312,7 +312,18 @@ export class CommandDispatchService {
312
312
  }
313
313
 
314
314
  case "phasegate:lint": {
315
- const lintResult = await this.ports.biomeLintPort.runLint();
315
+ const target = typeof _flags.target === 'string' && _flags.target.length > 0 ? _flags.target : undefined;
316
+ let reportTargets: readonly string[] | undefined = target === undefined ? undefined : [target];
317
+ if (args.reportTargets !== undefined) {
318
+ const parsed: unknown = JSON.parse(args.reportTargets);
319
+ if (!Array.isArray(parsed) || !parsed.every((item): item is string => typeof item === 'string' && item.length > 0)) {
320
+ throw new Error('lint reportTargets must be an array of non-empty paths');
321
+ }
322
+ reportTargets = parsed;
323
+ }
324
+ const lintResult = reportTargets === undefined
325
+ ? await this.ports.biomeLintPort.runLint()
326
+ : await this.ports.biomeLintPort.runLint(reportTargets);
316
327
  if (lintResult.passed) {
317
328
  const r = HarnessApiResponse.pass({ ...summary, passed: 1 });
318
329
  return { status: "pass", errors: [], summary: r.summary, data: undefined, exitCode: 0 };
@@ -1,3 +1,4 @@
1
+ // @unit harness-api
1
2
  // @layer infrastructure
2
3
  // biome-ast-engine-lint-adapter.ts — BiomeAstEngineLintAdapter
3
4
  // Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
@@ -5,6 +6,7 @@
5
6
 
6
7
  import type { BiomeLintPort } from '../../domain/ports/biome-lint-port.js';
7
8
  import type { HarnessError } from '../../domain/value-objects/harness-api-response.js';
9
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
8
10
 
9
11
  interface RuleViolation {
10
12
  filePath: string;
@@ -37,7 +39,7 @@ function violationToHarnessError(v: RuleViolation): HarnessError {
37
39
  export class BiomeAstEngineLintAdapter implements BiomeLintPort {
38
40
  private readonly stub: IBiomeAstEngineStub;
39
41
 
40
- constructor(stub?: IBiomeAstEngineStub, rootDir = process.cwd()) {
42
+ constructor(stub?: IBiomeAstEngineStub, private readonly rootDir = process.cwd()) {
41
43
  this.stub = stub ?? BiomeAstEngineLintAdapter.createRealImpl(rootDir);
42
44
  }
43
45
 
@@ -62,12 +64,18 @@ export class BiomeAstEngineLintAdapter implements BiomeLintPort {
62
64
  };
63
65
  }
64
66
 
65
- async runLint(): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
67
+ async runLint(reportTargets?: readonly string[]): Promise<{ passed: boolean; errors: HarnessError[]; warnings: HarnessError[] }> {
68
+ // Always analyze the complete graph; targets restrict reporting, never analysis.
66
69
  const result = await this.stub.runLint();
70
+ const targets = reportTargets?.map((target) => resolve(this.rootDir, target)) ?? [];
67
71
  const errors: HarnessError[] = [];
68
72
  const warnings: HarnessError[] = [];
69
73
 
70
74
  for (const v of result.violations) {
75
+ if (v.filePath && targets.length > 0 && !targets.some((target) => {
76
+ const rel = relative(target, resolve(this.rootDir, v.filePath));
77
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
78
+ })) continue;
71
79
  const harnessError = violationToHarnessError(v);
72
80
  if (v.severity === 'warning') {
73
81
  warnings.push(harnessError);
@@ -1,8 +1,10 @@
1
1
  // @unit installation
2
2
  // @layer application
3
3
  // @work-item-id WI-216
4
+ // @work-item-id WI-223
4
5
 
5
- export type SkillSet = "core" | "all";
6
+ export type SkillSet = "core" | "all" | "consumer";
7
+ export type SkillCategory = "core" | "aidlc" | "utility" | "guidance";
6
8
 
7
9
  const CORE_SKILLS = [
8
10
  "cascade-updater",
@@ -37,7 +39,31 @@ const AIDLC_SKILLS = [
37
39
  const UTILITY_SKILLS = ["codex-delegator", "skill-creator"] as const;
38
40
  const GUIDANCE_SKILLS = ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"] as const;
39
41
 
42
+ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
43
+ core: CORE_SKILLS,
44
+ aidlc: AIDLC_SKILLS,
45
+ utility: UTILITY_SKILLS,
46
+ guidance: GUIDANCE_SKILLS,
47
+ };
48
+
49
+ export function isSkillSet(value: unknown): value is SkillSet {
50
+ return value === "core" || value === "all" || value === "consumer";
51
+ }
52
+
53
+ export function resolveInstalledSkillSet(metadata: string | null): SkillSet {
54
+ try {
55
+ const value: unknown = JSON.parse(metadata ?? "null");
56
+ if (typeof value === "object" && value !== null && "skillSet" in value && isSkillSet(value.skillSet)) {
57
+ return value.skillSet;
58
+ }
59
+ } catch { /* Legacy installations without readable metadata retain the all set. */ }
60
+ return "all";
61
+ }
62
+
40
63
  export function getBundledSkillsForSet(skillSet: SkillSet): string[] {
41
64
  if (skillSet === "core") return [...CORE_SKILLS];
42
- return [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
65
+ const all = [...CORE_SKILLS, ...AIDLC_SKILLS, ...UTILITY_SKILLS, ...GUIDANCE_SKILLS];
66
+ return skillSet === "consumer"
67
+ ? all.filter(name => name !== "release-publisher" && name !== "skill-creator")
68
+ : all;
43
69
  }
@@ -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
+ }
@@ -6,7 +6,7 @@
6
6
  // @work-item-id WI-216
7
7
 
8
8
  import { join } from "node:path";
9
- import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
9
+ import { getBundledSkillsForSet, resolveInstalledSkillSet } from "../bundled-skill-selection.js";
10
10
  import type { CheckId } from "../../domain/check-id.js";
11
11
  import { DiagnosticFinding, type DiagnosticSeverity } from "../../domain/diagnostic-finding.js";
12
12
  import type { RepairMode } from "../../domain/repair-mode.js";
@@ -73,7 +73,7 @@ export function skillDirectoryLooksValid(files: readonly string[]): boolean {
73
73
  }
74
74
 
75
75
  export function phasegateSkillDirectoryLooksComplete(files: readonly string[], metadata: string | null): boolean {
76
- const skillSet: SkillSet = metadata?.includes('"skillSet": "core"') ? "core" : "all";
76
+ const skillSet = resolveInstalledSkillSet(metadata);
77
77
  const expectedSkills = getBundledSkillsForSet(skillSet);
78
78
  return expectedSkills.every((skill) => files.some((file) => {
79
79
  const normalized = file.replaceAll("\\", "/");
@@ -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
+ });