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
@@ -10,6 +10,9 @@
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
15
+ // @work-item-id WI-220
13
16
 
14
17
  import {
15
18
  access,
@@ -29,7 +32,8 @@ import { DeploymentEntry } from "../../domain/deployment-entry.js";
29
32
  import { DeploymentManifest } from "../../domain/deployment-manifest.js";
30
33
  import type { ManagedBlockInput } from "../../domain/managed-block.js";
31
34
  import type { RepairMode } from "../../domain/repair-mode.js";
32
- import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
35
+ import { getBundledSkillsForSet, resolveInstalledSkillSet, type SkillSet } from "../bundled-skill-selection.js";
36
+ import { mergeNamedHookJson } from "../named-hook-json.js";
33
37
  import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
34
38
  import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
35
39
  import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
@@ -37,10 +41,12 @@ import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
37
41
  type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse" | "prune";
38
42
  type StrategyType =
39
43
  | "json"
44
+ | "json-named"
40
45
  | "shell"
41
46
  | "yaml-add"
42
47
  | "package-json"
43
48
  | "markdown-managed"
49
+ | "copy"
44
50
  | "copy-dir"
45
51
  | "symlink"
46
52
  | "unknown";
@@ -96,7 +102,7 @@ const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
96
102
  const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
97
103
  const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
98
104
  const HARNESS_VERSION_BASENAME = ".harness-version";
99
- const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
105
+ const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills", ".agents/skills"] as const;
100
106
 
101
107
  function isRecord(value: unknown): value is Record<string, unknown> {
102
108
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -322,7 +328,7 @@ function reconcileManagedMarkdown(existing: string | null, incoming: string): st
322
328
  return next.replace(/\s*$/, "\n");
323
329
  }
324
330
 
325
- function renderAgentContextTemplate(template: string): string {
331
+ function renderAgentContextTemplate(template: string, skillSet: SkillSet): string {
326
332
  const commands = [
327
333
  "phasegate doctor",
328
334
  "phasegate phasegate:check-ready",
@@ -334,12 +340,12 @@ function renderAgentContextTemplate(template: string): string {
334
340
  .join("\n");
335
341
  return template
336
342
  .replaceAll("{{PHASEGATE_AGENT}}", "both")
337
- .replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
343
+ .replaceAll("{{PHASEGATE_SKILLS_MODE}}", skillSet)
338
344
  .replaceAll("{{PHASEGATE_WORKFLOW}}", "standard")
339
345
  .replaceAll("{{PHASEGATE_HUSKY_STATE}}", "managed")
340
346
  .replaceAll("{{PHASEGATE_CI_STATE}}", "managed")
341
347
  .replaceAll("{{PHASEGATE_COMMANDS}}", commands)
342
- .replaceAll("{{PHASEGATE_SKILLS}}", "- `all bundled skills`")
348
+ .replaceAll("{{PHASEGATE_SKILLS}}", skillSet === "all" ? "- `all bundled skills`" : `- \`${skillSet} skills\``)
343
349
  .replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
344
350
  .replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
345
351
  }
@@ -387,7 +393,9 @@ export class RunReconcileUseCase {
387
393
 
388
394
  const includeHusky = input.includeHusky ?? manifest.installationFlags?.includeHusky ?? true;
389
395
  const includeCi = input.includeCi ?? manifest.installationFlags?.includeCi ?? true;
390
- const targets = this.createTargets({ includeHusky, includeCi });
396
+ const includeAntigravity = manifest.findEntry(".agents/hooks.json") !== null;
397
+ const includeClaudeScripts = manifest.findEntry(".claude/settings.json") !== null;
398
+ const targets = this.createTargets({ includeHusky, includeCi, includeAntigravity, includeClaudeScripts });
391
399
  const targetsByPath = new Map(targets.map((target) => [target.path, target]));
392
400
  let nextManifest = DeploymentManifest.reconstitute({
393
401
  version: input.phasegateVersion,
@@ -400,11 +408,13 @@ export class RunReconcileUseCase {
400
408
  const changed: ReconcilePlanItem[] = [];
401
409
  const backupStamp = `reconcile-${new Date().toISOString().replace(/[:.]/g, "-")}`;
402
410
  let backupDir: string | null = null;
411
+ let repairedMetadata = false;
403
412
 
404
413
  const outcomes: Array<{
405
414
  readonly item: ReconcilePlanItem;
406
415
  readonly needsBackup: boolean;
407
416
  readonly prune?: boolean;
417
+ readonly recoveredContent?: string;
408
418
  readonly apply: () => Promise<string | null>;
409
419
  }> = [];
410
420
 
@@ -424,7 +434,7 @@ export class RunReconcileUseCase {
424
434
  plan.push(item);
425
435
  continue;
426
436
  }
427
- const outcome = await this.planManagedEntry(input, entry, target);
437
+ const outcome = await this.planManagedEntry(input, entry, target, manifest.version);
428
438
  outcomes.push(outcome);
429
439
  plan.push(outcome.item);
430
440
  if (
@@ -453,16 +463,22 @@ export class RunReconcileUseCase {
453
463
  }
454
464
 
455
465
  const personalInstall = this.isPersonalManifest(manifest);
466
+ const skillSets = new Map<string, SkillSet>();
467
+ for (const root of personalInstall ? this.personalSkillPaths(manifest) : ["skills"]) {
468
+ skillSets.set(root, resolveInstalledSkillSet(await readTextOrNull(join(input.projectRoot, root, HARNESS_VERSION_BASENAME))));
469
+ }
456
470
  if (!personalInstall && this.manifestIntendsSharedSkills(manifest)) {
457
- const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
458
- const outcome = await this.planSharedSkills(input, manifest, sharedSkills, "all");
471
+ const skillSet = skillSets.get("skills") ?? "all";
472
+ const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
473
+ const outcome = await this.planSharedSkills(input, manifest, sharedSkills, skillSet);
459
474
  outcomes.push(outcome);
460
475
  plan.push(outcome.item);
461
476
  }
462
477
  if (personalInstall) {
463
- const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
464
478
  for (const skillPath of this.personalSkillPaths(manifest)) {
465
- const outcome = await this.planPersonalSkills(input, manifest, skillPath, personalSkills, "all");
479
+ const skillSet = skillSets.get(skillPath) ?? "all";
480
+ const personalSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
481
+ const outcome = await this.planPersonalSkills(input, manifest, skillPath, personalSkills, skillSet);
466
482
  outcomes.push(outcome);
467
483
  plan.push(outcome.item);
468
484
  }
@@ -478,7 +494,17 @@ export class RunReconcileUseCase {
478
494
  }
479
495
 
480
496
  for (const outcome of outcomes) {
481
- if (!outcome.item.changed) continue;
497
+ if (!outcome.item.changed) {
498
+ const previous = nextManifest.findEntry(outcome.item.path);
499
+ if (previous !== null && outcome.recoveredContent !== undefined) {
500
+ nextManifest = nextManifest.addEntry(DeploymentEntry.create({
501
+ ...previous,
502
+ hash: this.hashCalculator.compute(outcome.recoveredContent),
503
+ }));
504
+ repairedMetadata = true;
505
+ }
506
+ continue;
507
+ }
482
508
  if (outcome.prune === true) {
483
509
  await outcome.apply();
484
510
  nextManifest = nextManifest.removeEntry(outcome.item.path);
@@ -493,34 +519,38 @@ export class RunReconcileUseCase {
493
519
  changed.push(outcome.item);
494
520
  if (hashContent !== null) {
495
521
  if (outcome.item.strategy === "copy-dir" && outcome.item.path === "skills") {
496
- const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
522
+ const skillSet = skillSets.get("skills") ?? "all";
523
+ const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
497
524
  nextManifest = nextManifest.addEntry(
498
525
  this.createdEntry(
499
526
  SHARED_SKILLS_VERSION_PATH,
500
- this.sharedSkillsVersionHashInput(input.phasegateVersion, "all", sharedSkills),
527
+ this.sharedSkillsVersionHashInput(input.phasegateVersion, skillSet, sharedSkills),
501
528
  ),
502
529
  );
503
530
  for (const skill of sharedSkills) {
504
531
  nextManifest = nextManifest.addEntry(
505
- this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, "all")),
532
+ this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, skillSet)),
506
533
  );
507
534
  }
508
535
  } else if (
509
536
  outcome.item.strategy === "copy-dir" &&
510
- (outcome.item.path === ".claude/skills" || outcome.item.path === ".codex/skills")
537
+ (outcome.item.path === ".claude/skills" ||
538
+ outcome.item.path === ".codex/skills" ||
539
+ outcome.item.path === ".agents/skills")
511
540
  ) {
512
- const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
541
+ const skillSet = skillSets.get(outcome.item.path) ?? "all";
542
+ const personalSkills = await listSelectedBundledSkills(input.harnessRoot, skillSet);
513
543
  nextManifest = nextManifest.addEntry(
514
544
  this.createdEntry(
515
545
  `${outcome.item.path}/.harness-version`,
516
- this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, "all", personalSkills),
546
+ this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, skillSet, personalSkills),
517
547
  ),
518
548
  );
519
549
  for (const skill of personalSkills) {
520
550
  nextManifest = nextManifest.addEntry(
521
551
  this.createdEntry(
522
552
  `${outcome.item.path}/${skill}`,
523
- this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, "all"),
553
+ this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, skillSet),
524
554
  ),
525
555
  );
526
556
  }
@@ -544,13 +574,13 @@ export class RunReconcileUseCase {
544
574
  }
545
575
  }
546
576
 
547
- if (changed.length > 0 || manifest.version !== input.phasegateVersion) {
577
+ if (changed.length > 0 || repairedMetadata || manifest.version !== input.phasegateVersion) {
548
578
  await this.manifestRepository.save(input.projectRoot, nextManifest);
549
579
  }
550
580
  return { plan, refused, changed, backupDir };
551
581
  }
552
582
 
553
- private async planManagedEntry(input: RunReconcileInput, entry: DeploymentEntry, target: ReconcileTarget) {
583
+ private async planManagedEntry(input: RunReconcileInput, entry: DeploymentEntry, target: ReconcileTarget, previousVersion: string) {
554
584
  if (target.strategy === "symlink") return this.planSymlink(input.projectRoot, target.path);
555
585
  const absolutePath = this.resolveProjectPath(input.projectRoot, entry.path);
556
586
  const before = await readTextOrNull(absolutePath);
@@ -558,12 +588,25 @@ export class RunReconcileUseCase {
558
588
  const currentHash = this.hashCalculator.compute(before);
559
589
  const matchesManifest = currentHash.equals(entry.hash);
560
590
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
561
- const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
591
+ const template = target.strategy === "markdown-managed"
592
+ ? renderAgentContextTemplate(rawTemplate, await this.contextSkillSet(input.projectRoot, target.path)) : rawTemplate;
562
593
  const next =
563
- entry.mode === "created" && target.strategy !== "package-json" && target.strategy !== "markdown-managed"
594
+ entry.mode === "created" &&
595
+ target.strategy !== "package-json" &&
596
+ target.strategy !== "markdown-managed" &&
597
+ target.strategy !== "json-named"
564
598
  ? template
565
599
  : this.reconcileContent(target, before, template, input.phasegateVersion);
566
600
  const changed = before !== next;
601
+ // A no-op merge alone cannot attest user-edited content. Recover only a
602
+ // complete canonical output, or a package version-only change whose prior
603
+ // bytes can be reconstructed and matched against the recorded hash.
604
+ const recoveredContent = !changed && !matchesManifest && (
605
+ target.strategy === "package-json"
606
+ ? this.hashCalculator.compute(this.reconcileContent(target, before, template, previousVersion)).equals(entry.hash)
607
+ : before === (entry.mode === "created" && target.strategy !== "markdown-managed" && target.strategy !== "json-named"
608
+ ? template : this.reconcileContent(target, null, template, input.phasegateVersion))
609
+ ) ? before : undefined;
567
610
  const repairMode: RepairMode = matchesManifest ? "mechanical" : "ai-assisted";
568
611
  return {
569
612
  item: this.item(
@@ -572,11 +615,13 @@ export class RunReconcileUseCase {
572
615
  repairMode,
573
616
  target.strategy,
574
617
  changed,
575
- changed ? `${entry.path}: update managed portion` : `${entry.path}: already up to date`,
618
+ changed ? `${entry.path}: update managed portion` : recoveredContent !== undefined
619
+ ? `${entry.path}: content already current; recover interrupted manifest hash` : `${entry.path}: already up to date`,
576
620
  this.diffSummary(before, next),
577
621
  repairMode === "ai-assisted" ? SKILL_HINT : null,
578
622
  ),
579
623
  needsBackup: !matchesManifest || input.force,
624
+ recoveredContent,
580
625
  apply: async () => {
581
626
  await mkdir(dirname(absolutePath), { recursive: true });
582
627
  await writeFile(absolutePath, next, "utf8");
@@ -586,12 +631,20 @@ export class RunReconcileUseCase {
586
631
  };
587
632
  }
588
633
 
634
+ private async contextSkillSet(projectRoot: string, contextPath: string): Promise<SkillSet> {
635
+ const agentRoot = contextPath.includes("CLAUDE") ? ".claude/skills" : ".codex/skills";
636
+ const metadata = await readTextOrNull(join(projectRoot, agentRoot, HARNESS_VERSION_BASENAME))
637
+ ?? await readTextOrNull(join(projectRoot, SHARED_SKILLS_VERSION_PATH));
638
+ return resolveInstalledSkillSet(metadata);
639
+ }
640
+
589
641
  private async planMissingTarget(input: RunReconcileInput, target: ReconcileTarget) {
590
642
  if (target.strategy === "symlink") return this.planSymlink(input.projectRoot, target.path);
591
643
  const absolutePath = this.resolveProjectPath(input.projectRoot, target.path);
592
644
  const before = await readTextOrNull(absolutePath);
593
645
  const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
594
- const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
646
+ const template = target.strategy === "markdown-managed"
647
+ ? renderAgentContextTemplate(rawTemplate, await this.contextSkillSet(input.projectRoot, target.path)) : rawTemplate;
595
648
  const next =
596
649
  before === null && target.strategy !== "package-json"
597
650
  ? template
@@ -681,6 +734,7 @@ export class RunReconcileUseCase {
681
734
  return (
682
735
  manifest.findEntry(".claude/skills") !== null ||
683
736
  manifest.findEntry(".codex/skills") !== null ||
737
+ manifest.findEntry(".agents/skills") !== null ||
684
738
  manifest.entries.some((entry) => entry.path.startsWith("skills/"))
685
739
  );
686
740
  }
@@ -692,16 +746,23 @@ export class RunReconcileUseCase {
692
746
  return (
693
747
  manifest.findEntry(".phasegate-local/phasegate.config.json") !== null ||
694
748
  manifest.entries.some(
695
- (entry) => (entry.path === ".claude/skills" || entry.path === ".codex/skills") && entry.mode === "created",
749
+ (entry) =>
750
+ (entry.path === ".claude/skills" || entry.path === ".codex/skills" || entry.path === ".agents/skills") &&
751
+ entry.mode === "created",
696
752
  ) ||
697
753
  manifest.entries.some(
698
- (entry) => entry.path.startsWith(".claude/skills/") || entry.path.startsWith(".codex/skills/"),
754
+ (entry) =>
755
+ entry.path.startsWith(".claude/skills/") ||
756
+ entry.path.startsWith(".codex/skills/") ||
757
+ entry.path.startsWith(".agents/skills/"),
699
758
  )
700
759
  );
701
760
  }
702
761
 
703
- private personalSkillPaths(manifest: DeploymentManifest): Array<".claude/skills" | ".codex/skills"> {
704
- const paths = new Set<".claude/skills" | ".codex/skills">();
762
+ private personalSkillPaths(
763
+ manifest: DeploymentManifest,
764
+ ): Array<".claude/skills" | ".codex/skills" | ".agents/skills"> {
765
+ const paths = new Set<".claude/skills" | ".codex/skills" | ".agents/skills">();
705
766
  if (
706
767
  manifest.findEntry(".claude/skills") !== null ||
707
768
  manifest.entries.some((entry) => entry.path.startsWith(".claude/skills/"))
@@ -712,6 +773,11 @@ export class RunReconcileUseCase {
712
773
  manifest.entries.some((entry) => entry.path.startsWith(".codex/skills/"))
713
774
  )
714
775
  paths.add(".codex/skills");
776
+ if (
777
+ manifest.findEntry(".agents/skills") !== null ||
778
+ manifest.entries.some((entry) => entry.path.startsWith(".agents/skills/"))
779
+ )
780
+ paths.add(".agents/skills");
715
781
  return [...paths].sort();
716
782
  }
717
783
 
@@ -783,7 +849,7 @@ export class RunReconcileUseCase {
783
849
  private async planPersonalSkills(
784
850
  input: RunReconcileInput,
785
851
  manifest: DeploymentManifest,
786
- relativePath: ".claude/skills" | ".codex/skills",
852
+ relativePath: ".claude/skills" | ".codex/skills" | ".agents/skills",
787
853
  skills: readonly string[],
788
854
  skillSet: SkillSet,
789
855
  ) {
@@ -936,21 +1002,61 @@ export class RunReconcileUseCase {
936
1002
  if (target.strategy === "yaml-add") return template;
937
1003
  if (target.strategy === "shell") return reconcileShell(before, template);
938
1004
  if (target.strategy === "markdown-managed") return reconcileManagedMarkdown(before, template);
1005
+ if (target.strategy === "copy") return before ?? template;
939
1006
  if (target.strategy === "package-json") {
940
1007
  const existing = before === null ? {} : (JSON.parse(before) as unknown);
941
1008
  return `${JSON.stringify(reconcilePackageJson(isRecord(existing) ? existing : {}, version), null, 2)}\n`;
942
1009
  }
943
1010
  const existing = before === null ? {} : (JSON.parse(before) as unknown);
944
1011
  const incoming = JSON.parse(template) as unknown;
1012
+ if (target.strategy === "json-named") {
1013
+ const merged = mergeNamedHookJson(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {});
1014
+ return `${JSON.stringify(merged, null, 2)}\n`;
1015
+ }
945
1016
  return `${JSON.stringify(reconcileJsonObject(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {}), null, 2)}\n`;
946
1017
  }
947
1018
 
948
1019
  private createTargets(options: {
949
1020
  readonly includeHusky: boolean;
950
1021
  readonly includeCi: boolean;
1022
+ readonly includeAntigravity: boolean;
1023
+ readonly includeClaudeScripts: boolean;
951
1024
  }): readonly ReconcileTarget[] {
952
1025
  return [
953
1026
  { path: ".claude/settings.json", strategy: "json", templatePath: "templates/.claude/settings.json" },
1027
+ ...(options.includeClaudeScripts
1028
+ ? ([
1029
+ {
1030
+ path: ".claude/scripts/deny-check.sh",
1031
+ strategy: "shell",
1032
+ templatePath: "templates/.claude/scripts/deny-check.sh",
1033
+ executable: true,
1034
+ },
1035
+ {
1036
+ path: ".claude/scripts/format-settings-hook.sh",
1037
+ strategy: "shell",
1038
+ templatePath: "templates/.claude/scripts/format-settings-hook.sh",
1039
+ executable: true,
1040
+ },
1041
+ {
1042
+ path: ".claude/scripts/format-typescript-hook.sh",
1043
+ strategy: "shell",
1044
+ templatePath: "templates/.claude/scripts/format-typescript-hook.sh",
1045
+ executable: true,
1046
+ },
1047
+ {
1048
+ path: ".claude/scripts/analyze-errors-hook.sh",
1049
+ strategy: "shell",
1050
+ templatePath: "templates/.claude/scripts/analyze-errors-hook.sh",
1051
+ executable: true,
1052
+ },
1053
+ {
1054
+ path: ".claude/scripts/hook-config.json",
1055
+ strategy: "copy",
1056
+ templatePath: "templates/.claude/scripts/hook-config.json",
1057
+ },
1058
+ ] as const satisfies readonly ReconcileTarget[])
1059
+ : []),
954
1060
  {
955
1061
  path: "CLAUDE.md",
956
1062
  strategy: "markdown-managed",
@@ -958,6 +1064,11 @@ export class RunReconcileUseCase {
958
1064
  block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
959
1065
  },
960
1066
  { path: ".codex/hooks.json", strategy: "json", templatePath: "templates/.codex/hooks.json" },
1067
+ ...(options.includeAntigravity
1068
+ ? ([
1069
+ { path: ".agents/hooks.json", strategy: "json-named", templatePath: "templates/.agents/hooks.json" },
1070
+ ] as const)
1071
+ : []),
961
1072
  {
962
1073
  path: "AGENTS.md",
963
1074
  strategy: "markdown-managed",
@@ -1001,6 +1112,7 @@ export class RunReconcileUseCase {
1001
1112
  { path: "package.json", strategy: "package-json", templatePath: "package.json" },
1002
1113
  { path: ".claude/skills", strategy: "symlink" },
1003
1114
  { path: ".codex/skills", strategy: "symlink" },
1115
+ ...(options.includeAntigravity ? ([{ path: ".agents/skills", strategy: "symlink" }] as const) : []),
1004
1116
  ];
1005
1117
  }
1006
1118
 
@@ -1014,7 +1126,12 @@ export class RunReconcileUseCase {
1014
1126
 
1015
1127
  private managedBlockFor(path: string, strategy: StrategyType): ManagedBlockInput | null {
1016
1128
  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") {
1129
+ if (
1130
+ strategy === "json" ||
1131
+ strategy === "json-named" ||
1132
+ strategy === "package-json" ||
1133
+ strategy === "markdown-managed"
1134
+ ) {
1018
1135
  return { start: "phasegate structured merge", end: "phasegate structured merge", content: `${strategy}:${path}` };
1019
1136
  }
1020
1137
  return null;