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.
- package/CHANGELOG.md +16 -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/folder_management_rules.md +10 -1
- package/docs/guide/antigravity-integration.md +26 -0
- package/docs/guide/cli-reference.md +27 -7
- package/docs/guide/codex-integration.md +44 -107
- package/docs/guide/configuration.md +28 -2
- package/docs/guide/grok-integration.md +26 -0
- package/docs/guide/hooks-integration.md +34 -5
- package/docs/guide/installation.md +4 -2
- package/docs/guide/setup-artifacts.md +4 -4
- package/docs/guide/skills-overview.md +4 -1
- package/package.json +2 -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-post-tool-use-usecase.ts +4 -0
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +38 -3
- package/scripts/harness/agent-integration/application/usecases/handle-stop-usecase.ts +3 -9
- package/scripts/harness/agent-integration/domain/ports/full-mode-requirement-query-port.ts +3 -1
- package/scripts/harness/agent-integration/domain/ports/story-reflection-query-port.ts +2 -0
- 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/services/hook-to-cli-translator.ts +8 -5
- package/scripts/harness/agent-integration/domain/value-objects/hook-translation-result.ts +2 -2
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +35 -18
- package/scripts/harness/agent-integration/domain/value-objects/story-reflection-query-result.ts +10 -2
- package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +12 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/child-process-cli-executor-adapter.ts +64 -13
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +108 -2
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +8 -6
- package/scripts/harness/agent-integration/presentation/hook-skip-event-recorder.ts +3 -0
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +1 -1
- package/scripts/harness/agent-integration/presentation/post-tool-use-feedback.ts +39 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +21 -27
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +126 -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/agent-integration/presentation/stop-hook.ts +1 -3
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/biome-cli-executor-adapter.ts +21 -1
- package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +5 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +2 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +15 -0
- package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +6 -3
- package/scripts/harness/harness-api/domain/ports/biome-lint-port.ts +2 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -1
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +10 -2
- package/scripts/harness/installation/application/bundled-skill-selection.ts +28 -2
- package/scripts/harness/installation/application/checks/antigravity-hook-missing-check.ts +79 -0
- package/scripts/harness/installation/application/checks/check-utils.ts +2 -2
- 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 +157 -57
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +148 -31
- 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 +25 -5
- 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 +227 -128
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +48 -5
- package/scripts/harness/phase-dependency-model/domain/services/work-item-reflection-scope-resolver.ts +64 -0
- package/scripts/harness/phase-dependency-model/domain/values/story-reflection-mapping.ts +8 -2
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +6 -1
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-work-item-dependency-catalog.ts +73 -0
- package/scripts/harness/quick-mode/application/dto/change-category-classification-contract.ts +5 -1
- package/scripts/harness/quick-mode/application/dto/quick-mode-eligibility-contract.ts +1 -1
- package/scripts/harness/quick-mode/application/ports/change-risk-advisory-port.ts +16 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +3 -1
- package/scripts/harness/quick-mode/composition-root.ts +5 -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/quick-mode/infrastructure/adapters/snapshot-risk-advisory-adapter.ts +118 -0
- package/scripts/harness/quick-mode/presentation/formatters/change-category-formatter.ts +7 -0
- package/scripts/harness/quick-mode/presentation/handlers/check-change-category-handler.ts +12 -1
- package/scripts/harness/regression-suite/infrastructure/adapters/vitest-test-runner-adapter.ts +13 -3
- package/scripts/harness/setup/skill-deployer.ts +3 -45
- package/scripts/harness/skill-quality/application/dto/run-plan-checker-loop-output.ts +1 -0
- package/scripts/harness/skill-quality/application/usecases/apply-cascade-update-usecase.ts +9 -4
- package/scripts/harness/skill-quality/application/usecases/execute-tdd-cycle-usecase.ts +2 -1
- package/scripts/harness/skill-quality/application/usecases/run-plan-checker-loop-usecase.ts +8 -0
- package/scripts/harness/skill-quality/composition-root.ts +14 -7
- package/scripts/harness/skill-quality/domain/ports/plan-check-executor-port.ts +2 -0
- package/scripts/harness/skill-quality/domain/value-objects/cascade-update-target.ts +15 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l1-biome-validator-adapter.ts +9 -1
- package/scripts/harness/skill-quality/infrastructure/adapters/l2-validator-system-adapter.ts +18 -2
- package/scripts/harness/skill-quality/presentation/handlers/apply-cascade-update-handler.ts +8 -2
- package/scripts/harness/skill-quality/presentation/handlers/execute-tdd-cycle-handler.ts +4 -3
- package/scripts/harness/skill-quality/presentation/handlers/run-plan-checker-loop-handler.ts +11 -4
- package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +41 -0
- package/skills/README.md +7 -0
- package/skills/cascade-updater/SKILL.md +37 -109
- 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
- package/skills/release-publisher/SKILL.md +3 -1
- package/skills/skill-creator/SKILL.md +8 -9
- 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
|
@@ -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}}",
|
|
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
|
|
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
|
|
458
|
-
const
|
|
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
|
|
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)
|
|
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
|
|
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,
|
|
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,
|
|
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" ||
|
|
537
|
+
(outcome.item.path === ".claude/skills" ||
|
|
538
|
+
outcome.item.path === ".codex/skills" ||
|
|
539
|
+
outcome.item.path === ".agents/skills")
|
|
511
540
|
) {
|
|
512
|
-
const
|
|
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,
|
|
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,
|
|
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"
|
|
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" &&
|
|
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` :
|
|
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"
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
704
|
-
|
|
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 (
|
|
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;
|