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
|
@@ -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?:
|
|
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:
|
|
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
|
|
475
|
-
const
|
|
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 =
|
|
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(
|
|
484
|
-
: this.createTargets({
|
|
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
|
|
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
|
-
? [
|
|
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 && (
|
|
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
|
-
...(
|
|
677
|
-
...(
|
|
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 &&
|
|
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
|
|
774
|
-
readonly
|
|
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.
|
|
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: "
|
|
793
|
-
strategy: "
|
|
794
|
-
templatePath: "
|
|
795
|
-
|
|
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.
|
|
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
|
|
850
|
-
readonly
|
|
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.
|
|
907
|
+
...(options.claudeHook
|
|
859
908
|
? [
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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.
|
|
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
|
|
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
|
|
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" ||
|
|
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" &&
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
704
|
-
|
|
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 (
|
|
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;
|