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.
Files changed (59) 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/041-non-excludable-agent-trust-roots.md +68 -0
  6. package/docs/guide/antigravity-integration.md +26 -0
  7. package/docs/guide/cli-reference.md +4 -4
  8. package/docs/guide/codex-integration.md +44 -107
  9. package/docs/guide/grok-integration.md +26 -0
  10. package/docs/guide/hooks-integration.md +27 -4
  11. package/docs/guide/installation.md +4 -2
  12. package/docs/guide/setup-artifacts.md +4 -4
  13. package/docs/guide/skills-overview.md +1 -1
  14. package/package.json +1 -1
  15. package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +1 -1
  16. package/scripts/harness/agent-integration/application/dto/normalized-pre-tool-use-request.ts +32 -0
  17. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +2 -2
  18. package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
  19. package/scripts/harness/agent-integration/domain/services/apply-patch-write-target-extractor.ts +98 -0
  20. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +6 -52
  21. package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +1 -1
  22. package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
  23. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
  24. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +1 -0
  25. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +123 -70
  26. package/scripts/harness/agent-integration/presentation/pre-tool-use-payload-normalizer.ts +228 -0
  27. package/scripts/harness/agent-integration/presentation/pre-tool-use-response-renderer.ts +38 -0
  28. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
  29. package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
  30. package/scripts/harness/installation/application/checks/codex-hook-missing-check.ts +70 -6
  31. package/scripts/harness/installation/application/checks/grok-hook-missing-check.ts +79 -0
  32. package/scripts/harness/installation/application/checks/husky-runtime-inactive-check.ts +33 -0
  33. package/scripts/harness/installation/application/named-hook-json.ts +17 -0
  34. package/scripts/harness/installation/application/operator-notice.ts +42 -0
  35. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +18 -7
  36. package/scripts/harness/installation/application/usecases/run-install.ts +152 -52
  37. package/scripts/harness/installation/application/usecases/run-reconcile.ts +86 -10
  38. package/scripts/harness/installation/application/usecases/run-uninstall.ts +149 -25
  39. package/scripts/harness/installation/composition-root.ts +10 -0
  40. package/scripts/harness/installation/domain/agent-target.ts +39 -0
  41. package/scripts/harness/installation/domain/check-id.ts +5 -0
  42. package/scripts/harness/installation/domain/husky-runtime-state.ts +34 -0
  43. package/scripts/harness/installation/domain/ports/git-hooks-runtime-probe.ts +9 -0
  44. package/scripts/harness/installation/domain/repair-table.ts +5 -0
  45. package/scripts/harness/installation/infrastructure/adapters/git-hooks-runtime-probe-adapter.ts +62 -0
  46. package/scripts/harness/installation/presentation/cli/install-handler.ts +23 -4
  47. package/scripts/harness/installation/presentation/cli/reconcile-handler.ts +7 -1
  48. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +27 -0
  49. package/scripts/harness/main.ts +100 -39
  50. package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +1 -1
  51. package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
  52. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
  53. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +13 -2
  54. package/scripts/harness/quick-mode/domain/types/rejection-rule.ts +1 -1
  55. package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
  56. package/templates/.agents/hooks.json +16 -0
  57. package/templates/.claude/scripts/analyze-errors-hook.sh +2 -2
  58. package/templates/.claude/settings.json +5 -3
  59. package/templates/.codex/hooks.json +2 -2
@@ -19,6 +19,8 @@
19
19
  // @work-item-id WI-315
20
20
  // @work-item-id WI-326
21
21
  // @work-item-id WI-331
22
+ // @work-item-id WI-385
23
+ // @work-item-id WI-387
22
24
 
23
25
  import {
24
26
  access,
@@ -34,11 +36,13 @@ import {
34
36
  writeFile,
35
37
  } from "node:fs/promises";
36
38
  import { dirname, join } from "node:path";
39
+ import { type AgentTarget, resolveAgentTarget } from "../../domain/agent-target.js";
37
40
  import { DeploymentEntry } from "../../domain/deployment-entry.js";
38
41
  import { DeploymentManifest, type InstallationFlags } from "../../domain/deployment-manifest.js";
39
42
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
40
43
  import type { RepairMode } from "../../domain/repair-mode.js";
41
44
  import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
45
+ import { mergeNamedHookJson } from "../named-hook-json.js";
42
46
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
43
47
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
44
48
  import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
@@ -46,6 +50,7 @@ import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
46
50
  type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
47
51
  type StrategyType =
48
52
  | "json"
53
+ | "json-named"
49
54
  | "shell"
50
55
  | "yaml-add"
51
56
  | "package-json"
@@ -83,7 +88,7 @@ export interface RunInstallInput {
83
88
  readonly includeCi?: boolean;
84
89
  readonly skillSet?: "core" | "all";
85
90
  readonly workflow?: "standard" | "strict";
86
- readonly agent?: "claude" | "codex" | "both";
91
+ readonly agent?: AgentTarget;
87
92
  readonly personal?: boolean;
88
93
  }
89
94
 
@@ -130,6 +135,7 @@ const PERSONAL_AGENT_RUNTIME_FILES = new Set([
130
135
  ".claude/settings.json",
131
136
  "AGENTS.md",
132
137
  ".codex/hooks.json",
138
+ ".agents/hooks.json",
133
139
  ]);
134
140
  const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
135
141
  const PERSONAL_PRINCIPLES_DOCS = ".phasegate-local/docs/principles";
@@ -361,7 +367,7 @@ function mergeManagedMarkdown(existing: string | null, incoming: string): string
361
367
  function renderAgentContextTemplate(
362
368
  template: string,
363
369
  options: {
364
- readonly agent: "claude" | "codex" | "both";
370
+ readonly agent: AgentTarget;
365
371
  readonly skillSet: "core" | "all";
366
372
  readonly workflow: "standard" | "strict";
367
373
  readonly includeHusky: boolean;
@@ -409,20 +415,6 @@ function mergePackageJson(existing: Record<string, unknown>, version: string): R
409
415
  };
410
416
  }
411
417
 
412
- function hasCustomJson(content: string | null): boolean {
413
- if (content === null) return false;
414
- try {
415
- const parsed = JSON.parse(content) as unknown;
416
- if (!isRecord(parsed)) return true;
417
- const withoutEmptyHooks = { ...parsed };
418
- if (isRecord(withoutEmptyHooks.hooks) && Object.keys(withoutEmptyHooks.hooks).length === 0)
419
- delete withoutEmptyHooks.hooks;
420
- return Object.keys(withoutEmptyHooks).length > 0;
421
- } catch {
422
- return true;
423
- }
424
- }
425
-
426
418
  function errorCode(error: unknown): string {
427
419
  if (isRecord(error) && typeof error.code === "string") return error.code;
428
420
  return "UNKNOWN";
@@ -471,17 +463,19 @@ export class RunInstallUseCase {
471
463
  ) {}
472
464
 
473
465
  async execute(input: RunInstallInput): Promise<RunInstallResult> {
474
- const includeClaude = input.includeClaude ?? true;
475
- const includeCodex = input.includeCodex ?? true;
466
+ const legacyIncludeClaude = input.includeClaude ?? true;
467
+ const legacyIncludeCodex = input.includeCodex ?? true;
476
468
  const includeHusky = input.includeHusky ?? true;
477
469
  const includeCi = input.includeCi ?? true;
478
470
  const skillSet = input.skillSet ?? "all";
479
471
  const workflow = input.workflow ?? "standard";
480
- const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
472
+ const agent =
473
+ input.agent ?? (legacyIncludeClaude && legacyIncludeCodex ? "both" : legacyIncludeCodex ? "codex" : "claude");
474
+ const selection = resolveAgentTarget(agent);
481
475
  const personal = input.personal ?? false;
482
476
  const targets = personal
483
- ? this.createPersonalTargets({ includeClaude, includeCodex })
484
- : this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
477
+ ? this.createPersonalTargets(selection)
478
+ : this.createTargets({ ...selection, includeHusky, includeCi });
485
479
  // Persist the effective opt-in state so a later reconcile can honor the
486
480
  // original install options. Personal installs never deploy Husky/CI
487
481
  // targets, so their effective state is recorded as false regardless of
@@ -584,7 +578,7 @@ export class RunInstallUseCase {
584
578
  const mode = before === null ? "created" : "merged";
585
579
  const hash = this.hashCalculator.compute(next);
586
580
  const manifestEntry = baseManifest.findEntry(target.path);
587
- if (manifestEntry !== null && manifestEntry.hash.equals(hash)) {
581
+ if (manifestEntry?.hash.equals(hash)) {
588
582
  manifest = manifest.addEntry(manifestEntry);
589
583
  } else {
590
584
  manifest = manifest.addEntry(
@@ -600,7 +594,11 @@ export class RunInstallUseCase {
600
594
  }
601
595
 
602
596
  const personalSkillTargets = input.personal
603
- ? [...(includeClaude ? [".claude/skills"] : []), ...(includeCodex ? [".codex/skills"] : [])]
597
+ ? [
598
+ ...(selection.claudeSkills ? [".claude/skills"] : []),
599
+ ...(selection.codexSkills ? [".codex/skills"] : []),
600
+ ...(selection.antigravitySkills ? [".agents/skills"] : []),
601
+ ]
604
602
  : [];
605
603
  const selectedPersonalSkills = input.personal ? await listSelectedBundledSkills(input.harnessRoot, skillSet) : [];
606
604
  for (const skillPath of personalSkillTargets) {
@@ -644,7 +642,7 @@ export class RunInstallUseCase {
644
642
  }
645
643
  }
646
644
 
647
- if (!input.personal && (includeClaude || includeCodex)) {
645
+ if (!input.personal && (selection.claudeSkills || selection.codexSkills || selection.antigravitySkills)) {
648
646
  const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
649
647
  const item = await this.planSharedSkillDirectory(input, sharedSkills, baseManifest);
650
648
  plan.push(item);
@@ -673,8 +671,9 @@ export class RunInstallUseCase {
673
671
  const linkSpecs = input.personal
674
672
  ? []
675
673
  : [
676
- ...(includeClaude ? [{ path: ".claude/skills", target: "../skills" }] : []),
677
- ...(includeCodex ? [{ path: ".codex/skills", target: "../skills" }] : []),
674
+ ...(selection.claudeSkills ? [{ path: ".claude/skills", target: "../skills" }] : []),
675
+ ...(selection.codexSkills ? [{ path: ".codex/skills", target: "../skills" }] : []),
676
+ ...(selection.antigravitySkills ? [{ path: ".agents/skills", target: "../skills" }] : []),
678
677
  ];
679
678
  for (const linkSpec of linkSpecs) {
680
679
  const item = await this.planSkillLink(input.projectRoot, linkSpec.path, linkSpec.target);
@@ -703,7 +702,7 @@ export class RunInstallUseCase {
703
702
  });
704
703
  }
705
704
 
706
- if (input.personal && includeCodex) {
705
+ if (input.personal && selection.codexHook) {
707
706
  plan.push({
708
707
  path: "~/.codex/config.toml",
709
708
  action: "will-skip",
@@ -770,8 +769,11 @@ export class RunInstallUseCase {
770
769
  }
771
770
 
772
771
  private createTargets(options: {
773
- readonly includeClaude: boolean;
774
- readonly includeCodex: boolean;
772
+ readonly claudeHook: boolean;
773
+ readonly claudeContext: boolean;
774
+ readonly codexHook: boolean;
775
+ readonly agentsContext: boolean;
776
+ readonly antigravityHook: boolean;
775
777
  readonly includeHusky: boolean;
776
778
  readonly includeCi: boolean;
777
779
  }): readonly InstallTarget[] {
@@ -781,7 +783,7 @@ export class RunInstallUseCase {
781
783
  strategy: "copy" as const,
782
784
  templatePath: "docs/templates/project/phasegate.config.json",
783
785
  },
784
- ...(options.includeClaude
786
+ ...(options.claudeHook
785
787
  ? [
786
788
  {
787
789
  path: ".claude/settings.json",
@@ -789,16 +791,60 @@ export class RunInstallUseCase {
789
791
  templatePath: "templates/.claude/settings.json",
790
792
  },
791
793
  {
792
- path: "CLAUDE.md",
793
- strategy: "markdown-managed" as const,
794
- templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
795
- block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
794
+ path: ".claude/scripts/deny-check.sh",
795
+ strategy: "shell" as const,
796
+ templatePath: "templates/.claude/scripts/deny-check.sh",
797
+ executable: true,
798
+ },
799
+ {
800
+ path: ".claude/scripts/format-settings-hook.sh",
801
+ strategy: "shell" as const,
802
+ templatePath: "templates/.claude/scripts/format-settings-hook.sh",
803
+ executable: true,
804
+ },
805
+ {
806
+ path: ".claude/scripts/format-typescript-hook.sh",
807
+ strategy: "shell" as const,
808
+ templatePath: "templates/.claude/scripts/format-typescript-hook.sh",
809
+ executable: true,
796
810
  },
811
+ {
812
+ path: ".claude/scripts/analyze-errors-hook.sh",
813
+ strategy: "shell" as const,
814
+ templatePath: "templates/.claude/scripts/analyze-errors-hook.sh",
815
+ executable: true,
816
+ },
817
+ {
818
+ path: ".claude/scripts/hook-config.json",
819
+ strategy: "copy" as const,
820
+ templatePath: "templates/.claude/scripts/hook-config.json",
821
+ },
822
+ ...(options.claudeContext
823
+ ? [
824
+ {
825
+ path: "CLAUDE.md",
826
+ strategy: "markdown-managed" as const,
827
+ templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
828
+ block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
829
+ },
830
+ ]
831
+ : []),
797
832
  ]
798
833
  : []),
799
- ...(options.includeCodex
834
+ ...(options.codexHook
835
+ ? [{ path: ".codex/hooks.json", strategy: "json" as const, templatePath: "templates/.codex/hooks.json" }]
836
+ : []),
837
+ ...(options.antigravityHook
838
+ ? [
839
+ {
840
+ path: ".agents/hooks.json",
841
+ strategy: "json-named" as const,
842
+ templatePath: "templates/.agents/hooks.json",
843
+ },
844
+ ]
845
+ : []),
846
+ ...(options.agentsContext
800
847
  ? [
801
- { path: ".codex/hooks.json", strategy: "json" as const, templatePath: "templates/.codex/hooks.json" },
802
848
  {
803
849
  path: "AGENTS.md",
804
850
  strategy: "markdown-managed" as const,
@@ -846,8 +892,11 @@ export class RunInstallUseCase {
846
892
  }
847
893
 
848
894
  private createPersonalTargets(options: {
849
- readonly includeClaude: boolean;
850
- readonly includeCodex: boolean;
895
+ readonly claudeHook: boolean;
896
+ readonly claudeContext: boolean;
897
+ readonly codexHook: boolean;
898
+ readonly agentsContext: boolean;
899
+ readonly antigravityHook: boolean;
851
900
  }): readonly InstallTarget[] {
852
901
  return [
853
902
  {
@@ -855,27 +904,60 @@ export class RunInstallUseCase {
855
904
  strategy: "copy" as const,
856
905
  templatePath: "docs/templates/personal/phasegate-local-config.json",
857
906
  },
858
- ...(options.includeClaude
907
+ ...(options.claudeHook
859
908
  ? [
860
- {
861
- path: ".claude/CLAUDE.md",
862
- strategy: "markdown-managed" as const,
863
- templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
864
- block: {
865
- start: MARKDOWN_BEGIN,
866
- end: MARKDOWN_END,
867
- content: "phasegate personal .claude/CLAUDE.md managed section",
868
- },
869
- personalManualIfUnmanaged: true,
870
- },
909
+ ...(options.claudeContext
910
+ ? [
911
+ {
912
+ path: ".claude/CLAUDE.md",
913
+ strategy: "markdown-managed" as const,
914
+ templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
915
+ block: {
916
+ start: MARKDOWN_BEGIN,
917
+ end: MARKDOWN_END,
918
+ content: "phasegate personal .claude/CLAUDE.md managed section",
919
+ },
920
+ personalManualIfUnmanaged: true,
921
+ },
922
+ ]
923
+ : []),
871
924
  {
872
925
  path: ".claude/settings.json",
873
926
  strategy: "copy" as const,
874
927
  templatePath: "templates/.claude/settings.json",
875
928
  },
929
+ {
930
+ path: ".claude/scripts/deny-check.sh",
931
+ strategy: "shell" as const,
932
+ templatePath: "templates/.claude/scripts/deny-check.sh",
933
+ executable: true,
934
+ },
935
+ {
936
+ path: ".claude/scripts/format-settings-hook.sh",
937
+ strategy: "shell" as const,
938
+ templatePath: "templates/.claude/scripts/format-settings-hook.sh",
939
+ executable: true,
940
+ },
941
+ {
942
+ path: ".claude/scripts/format-typescript-hook.sh",
943
+ strategy: "shell" as const,
944
+ templatePath: "templates/.claude/scripts/format-typescript-hook.sh",
945
+ executable: true,
946
+ },
947
+ {
948
+ path: ".claude/scripts/analyze-errors-hook.sh",
949
+ strategy: "shell" as const,
950
+ templatePath: "templates/.claude/scripts/analyze-errors-hook.sh",
951
+ executable: true,
952
+ },
953
+ {
954
+ path: ".claude/scripts/hook-config.json",
955
+ strategy: "copy" as const,
956
+ templatePath: "templates/.claude/scripts/hook-config.json",
957
+ },
876
958
  ]
877
959
  : []),
878
- ...(options.includeCodex
960
+ ...(options.agentsContext
879
961
  ? [
880
962
  {
881
963
  path: "AGENTS.md",
@@ -888,6 +970,10 @@ export class RunInstallUseCase {
888
970
  },
889
971
  personalManualIfUnmanaged: true,
890
972
  },
973
+ ]
974
+ : []),
975
+ ...(options.codexHook
976
+ ? [
891
977
  {
892
978
  path: ".codex/hooks.json",
893
979
  strategy: "copy" as const,
@@ -895,6 +981,15 @@ export class RunInstallUseCase {
895
981
  },
896
982
  ]
897
983
  : []),
984
+ ...(options.antigravityHook
985
+ ? [
986
+ {
987
+ path: ".agents/hooks.json",
988
+ strategy: "json-named" as const,
989
+ templatePath: "templates/.agents/hooks.json",
990
+ },
991
+ ]
992
+ : []),
898
993
  {
899
994
  path: ".git/hooks/pre-commit",
900
995
  strategy: "copy" as const,
@@ -943,6 +1038,7 @@ export class RunInstallUseCase {
943
1038
  if (target.strategy === "copy-dir") return "mechanical";
944
1039
  if (target.strategy === "symlink") return "mechanical";
945
1040
  if (target.strategy === "json") return jsonRepairMode(before);
1041
+ if (target.strategy === "json-named") return jsonRepairMode(before);
946
1042
  if (target.strategy === "markdown-managed") return "mechanical";
947
1043
  return "mechanical";
948
1044
  }
@@ -966,6 +1062,10 @@ export class RunInstallUseCase {
966
1062
  }
967
1063
  const existing = before === null ? {} : (JSON.parse(before) as unknown);
968
1064
  const incoming = JSON.parse(template) as unknown;
1065
+ if (target.strategy === "json-named") {
1066
+ const merged = mergeNamedHookJson(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {});
1067
+ return `${JSON.stringify(merged, null, 2)}\n`;
1068
+ }
969
1069
  const merged = mergeJsonObject(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {});
970
1070
  return `${JSON.stringify(merged, null, 2)}\n`;
971
1071
  }
@@ -1124,7 +1224,7 @@ export class RunInstallUseCase {
1124
1224
  ): DeploymentManifest {
1125
1225
  const hash = this.hashCalculator.compute(input.contentForHash);
1126
1226
  const existingEntry = baseManifest.findEntry(input.path);
1127
- if (existingEntry !== null && existingEntry.hash.equals(hash)) {
1227
+ if (existingEntry?.hash.equals(hash)) {
1128
1228
  return manifest.addEntry(existingEntry);
1129
1229
  }
1130
1230
  return manifest.addEntry(
@@ -10,6 +10,8 @@
10
10
  // @work-item-id WI-315
11
11
  // @work-item-id WI-326
12
12
  // @work-item-id WI-331
13
+ // @work-item-id WI-385
14
+ // @work-item-id WI-387
13
15
 
14
16
  import {
15
17
  access,
@@ -30,6 +32,7 @@ import { DeploymentManifest } from "../../domain/deployment-manifest.js";
30
32
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
31
33
  import type { RepairMode } from "../../domain/repair-mode.js";
32
34
  import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
35
+ import { mergeNamedHookJson } from "../named-hook-json.js";
33
36
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
34
37
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
35
38
  import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
@@ -37,10 +40,12 @@ import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
37
40
  type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse" | "prune";
38
41
  type StrategyType =
39
42
  | "json"
43
+ | "json-named"
40
44
  | "shell"
41
45
  | "yaml-add"
42
46
  | "package-json"
43
47
  | "markdown-managed"
48
+ | "copy"
44
49
  | "copy-dir"
45
50
  | "symlink"
46
51
  | "unknown";
@@ -96,7 +101,7 @@ const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
96
101
  const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
97
102
  const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
98
103
  const HARNESS_VERSION_BASENAME = ".harness-version";
99
- const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
104
+ const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills", ".agents/skills"] as const;
100
105
 
101
106
  function isRecord(value: unknown): value is Record<string, unknown> {
102
107
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -387,7 +392,9 @@ export class RunReconcileUseCase {
387
392
 
388
393
  const includeHusky = input.includeHusky ?? manifest.installationFlags?.includeHusky ?? true;
389
394
  const includeCi = input.includeCi ?? manifest.installationFlags?.includeCi ?? true;
390
- const targets = this.createTargets({ includeHusky, includeCi });
395
+ const includeAntigravity = manifest.findEntry(".agents/hooks.json") !== null;
396
+ const includeClaudeScripts = manifest.findEntry(".claude/settings.json") !== null;
397
+ const targets = this.createTargets({ includeHusky, includeCi, includeAntigravity, includeClaudeScripts });
391
398
  const targetsByPath = new Map(targets.map((target) => [target.path, target]));
392
399
  let nextManifest = DeploymentManifest.reconstitute({
393
400
  version: input.phasegateVersion,
@@ -507,7 +514,9 @@ export class RunReconcileUseCase {
507
514
  }
508
515
  } else if (
509
516
  outcome.item.strategy === "copy-dir" &&
510
- (outcome.item.path === ".claude/skills" || outcome.item.path === ".codex/skills")
517
+ (outcome.item.path === ".claude/skills" ||
518
+ outcome.item.path === ".codex/skills" ||
519
+ outcome.item.path === ".agents/skills")
511
520
  ) {
512
521
  const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
513
522
  nextManifest = nextManifest.addEntry(
@@ -560,7 +569,10 @@ export class RunReconcileUseCase {
560
569
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
561
570
  const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
562
571
  const next =
563
- entry.mode === "created" && target.strategy !== "package-json" && target.strategy !== "markdown-managed"
572
+ entry.mode === "created" &&
573
+ target.strategy !== "package-json" &&
574
+ target.strategy !== "markdown-managed" &&
575
+ target.strategy !== "json-named"
564
576
  ? template
565
577
  : this.reconcileContent(target, before, template, input.phasegateVersion);
566
578
  const changed = before !== next;
@@ -681,6 +693,7 @@ export class RunReconcileUseCase {
681
693
  return (
682
694
  manifest.findEntry(".claude/skills") !== null ||
683
695
  manifest.findEntry(".codex/skills") !== null ||
696
+ manifest.findEntry(".agents/skills") !== null ||
684
697
  manifest.entries.some((entry) => entry.path.startsWith("skills/"))
685
698
  );
686
699
  }
@@ -692,16 +705,23 @@ export class RunReconcileUseCase {
692
705
  return (
693
706
  manifest.findEntry(".phasegate-local/phasegate.config.json") !== null ||
694
707
  manifest.entries.some(
695
- (entry) => (entry.path === ".claude/skills" || entry.path === ".codex/skills") && entry.mode === "created",
708
+ (entry) =>
709
+ (entry.path === ".claude/skills" || entry.path === ".codex/skills" || entry.path === ".agents/skills") &&
710
+ entry.mode === "created",
696
711
  ) ||
697
712
  manifest.entries.some(
698
- (entry) => entry.path.startsWith(".claude/skills/") || entry.path.startsWith(".codex/skills/"),
713
+ (entry) =>
714
+ entry.path.startsWith(".claude/skills/") ||
715
+ entry.path.startsWith(".codex/skills/") ||
716
+ entry.path.startsWith(".agents/skills/"),
699
717
  )
700
718
  );
701
719
  }
702
720
 
703
- private personalSkillPaths(manifest: DeploymentManifest): Array<".claude/skills" | ".codex/skills"> {
704
- const paths = new Set<".claude/skills" | ".codex/skills">();
721
+ private personalSkillPaths(
722
+ manifest: DeploymentManifest,
723
+ ): Array<".claude/skills" | ".codex/skills" | ".agents/skills"> {
724
+ const paths = new Set<".claude/skills" | ".codex/skills" | ".agents/skills">();
705
725
  if (
706
726
  manifest.findEntry(".claude/skills") !== null ||
707
727
  manifest.entries.some((entry) => entry.path.startsWith(".claude/skills/"))
@@ -712,6 +732,11 @@ export class RunReconcileUseCase {
712
732
  manifest.entries.some((entry) => entry.path.startsWith(".codex/skills/"))
713
733
  )
714
734
  paths.add(".codex/skills");
735
+ if (
736
+ manifest.findEntry(".agents/skills") !== null ||
737
+ manifest.entries.some((entry) => entry.path.startsWith(".agents/skills/"))
738
+ )
739
+ paths.add(".agents/skills");
715
740
  return [...paths].sort();
716
741
  }
717
742
 
@@ -783,7 +808,7 @@ export class RunReconcileUseCase {
783
808
  private async planPersonalSkills(
784
809
  input: RunReconcileInput,
785
810
  manifest: DeploymentManifest,
786
- relativePath: ".claude/skills" | ".codex/skills",
811
+ relativePath: ".claude/skills" | ".codex/skills" | ".agents/skills",
787
812
  skills: readonly string[],
788
813
  skillSet: SkillSet,
789
814
  ) {
@@ -936,21 +961,61 @@ export class RunReconcileUseCase {
936
961
  if (target.strategy === "yaml-add") return template;
937
962
  if (target.strategy === "shell") return reconcileShell(before, template);
938
963
  if (target.strategy === "markdown-managed") return reconcileManagedMarkdown(before, template);
964
+ if (target.strategy === "copy") return before ?? template;
939
965
  if (target.strategy === "package-json") {
940
966
  const existing = before === null ? {} : (JSON.parse(before) as unknown);
941
967
  return `${JSON.stringify(reconcilePackageJson(isRecord(existing) ? existing : {}, version), null, 2)}\n`;
942
968
  }
943
969
  const existing = before === null ? {} : (JSON.parse(before) as unknown);
944
970
  const incoming = JSON.parse(template) as unknown;
971
+ if (target.strategy === "json-named") {
972
+ const merged = mergeNamedHookJson(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {});
973
+ return `${JSON.stringify(merged, null, 2)}\n`;
974
+ }
945
975
  return `${JSON.stringify(reconcileJsonObject(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {}), null, 2)}\n`;
946
976
  }
947
977
 
948
978
  private createTargets(options: {
949
979
  readonly includeHusky: boolean;
950
980
  readonly includeCi: boolean;
981
+ readonly includeAntigravity: boolean;
982
+ readonly includeClaudeScripts: boolean;
951
983
  }): readonly ReconcileTarget[] {
952
984
  return [
953
985
  { path: ".claude/settings.json", strategy: "json", templatePath: "templates/.claude/settings.json" },
986
+ ...(options.includeClaudeScripts
987
+ ? ([
988
+ {
989
+ path: ".claude/scripts/deny-check.sh",
990
+ strategy: "shell",
991
+ templatePath: "templates/.claude/scripts/deny-check.sh",
992
+ executable: true,
993
+ },
994
+ {
995
+ path: ".claude/scripts/format-settings-hook.sh",
996
+ strategy: "shell",
997
+ templatePath: "templates/.claude/scripts/format-settings-hook.sh",
998
+ executable: true,
999
+ },
1000
+ {
1001
+ path: ".claude/scripts/format-typescript-hook.sh",
1002
+ strategy: "shell",
1003
+ templatePath: "templates/.claude/scripts/format-typescript-hook.sh",
1004
+ executable: true,
1005
+ },
1006
+ {
1007
+ path: ".claude/scripts/analyze-errors-hook.sh",
1008
+ strategy: "shell",
1009
+ templatePath: "templates/.claude/scripts/analyze-errors-hook.sh",
1010
+ executable: true,
1011
+ },
1012
+ {
1013
+ path: ".claude/scripts/hook-config.json",
1014
+ strategy: "copy",
1015
+ templatePath: "templates/.claude/scripts/hook-config.json",
1016
+ },
1017
+ ] as const satisfies readonly ReconcileTarget[])
1018
+ : []),
954
1019
  {
955
1020
  path: "CLAUDE.md",
956
1021
  strategy: "markdown-managed",
@@ -958,6 +1023,11 @@ export class RunReconcileUseCase {
958
1023
  block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
959
1024
  },
960
1025
  { path: ".codex/hooks.json", strategy: "json", templatePath: "templates/.codex/hooks.json" },
1026
+ ...(options.includeAntigravity
1027
+ ? ([
1028
+ { path: ".agents/hooks.json", strategy: "json-named", templatePath: "templates/.agents/hooks.json" },
1029
+ ] as const)
1030
+ : []),
961
1031
  {
962
1032
  path: "AGENTS.md",
963
1033
  strategy: "markdown-managed",
@@ -1001,6 +1071,7 @@ export class RunReconcileUseCase {
1001
1071
  { path: "package.json", strategy: "package-json", templatePath: "package.json" },
1002
1072
  { path: ".claude/skills", strategy: "symlink" },
1003
1073
  { path: ".codex/skills", strategy: "symlink" },
1074
+ ...(options.includeAntigravity ? ([{ path: ".agents/skills", strategy: "symlink" }] as const) : []),
1004
1075
  ];
1005
1076
  }
1006
1077
 
@@ -1014,7 +1085,12 @@ export class RunReconcileUseCase {
1014
1085
 
1015
1086
  private managedBlockFor(path: string, strategy: StrategyType): ManagedBlockInput | null {
1016
1087
  if (strategy === "shell") return { start: SHELL_BEGIN, end: SHELL_END, content: `phasegate ${path} managed block` };
1017
- if (strategy === "json" || strategy === "package-json" || strategy === "markdown-managed") {
1088
+ if (
1089
+ strategy === "json" ||
1090
+ strategy === "json-named" ||
1091
+ strategy === "package-json" ||
1092
+ strategy === "markdown-managed"
1093
+ ) {
1018
1094
  return { start: "phasegate structured merge", end: "phasegate structured merge", content: `${strategy}:${path}` };
1019
1095
  }
1020
1096
  return null;