phasegate 0.254.0 → 0.283.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 +60 -0
- package/README.ja.md +6 -3
- package/README.md +6 -3
- package/docs/contracts/attestation-v2.schema.json +110 -0
- package/docs/guide/cli-reference.md +12 -1
- package/docs/guide/configuration.md +56 -0
- package/docs/guide/installation.md +1 -1
- package/docs/templates/ci/aidlc-gate.yml +60 -2
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
- package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
- package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
- package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
- package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
- package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
- package/scripts/harness/attestation/composition-root.ts +5 -0
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
- package/scripts/harness/attestation/index.ts +7 -1
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
- package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
- package/scripts/harness/config-foundation/index.ts +14 -15
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +84 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/integrations/pre-commit.ts +169 -27
- package/scripts/harness/main.ts +200 -136
- package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
- package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
- package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
- package/scripts/harness/traceability-model/composition-root.ts +5 -0
- package/scripts/harness/traceability-model/index.ts +9 -0
- package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
- package/scripts/harness/validator-system/composition-root.ts +25 -7
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
- package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
- package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
- package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
- package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
- package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
- package/scripts/harness/world-model/composition-root.ts +62 -33
- package/scripts/harness/world-model/index.ts +11 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
- package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +6 -2
|
@@ -16,20 +16,43 @@
|
|
|
16
16
|
// @work-item-id WI-215
|
|
17
17
|
// @work-item-id WI-216
|
|
18
18
|
// @work-item-id WI-219
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// @work-item-id WI-315
|
|
20
|
+
// @work-item-id WI-326
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
access,
|
|
24
|
+
chmod,
|
|
25
|
+
copyFile,
|
|
26
|
+
lstat,
|
|
27
|
+
mkdir,
|
|
28
|
+
readdir,
|
|
29
|
+
readFile,
|
|
30
|
+
readlink,
|
|
31
|
+
rm,
|
|
32
|
+
symlink,
|
|
33
|
+
writeFile,
|
|
34
|
+
} from "node:fs/promises";
|
|
21
35
|
import { dirname, join } from "node:path";
|
|
22
36
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
23
|
-
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
37
|
+
import { DeploymentManifest, type InstallationFlags } from "../../domain/deployment-manifest.js";
|
|
24
38
|
import type { ManagedBlockInput } from "../../domain/managed-block.js";
|
|
25
39
|
import type { RepairMode } from "../../domain/repair-mode.js";
|
|
26
40
|
import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
|
|
27
|
-
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
28
41
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
42
|
+
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
29
43
|
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
30
44
|
|
|
31
45
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
32
|
-
type StrategyType =
|
|
46
|
+
type StrategyType =
|
|
47
|
+
| "json"
|
|
48
|
+
| "shell"
|
|
49
|
+
| "yaml-add"
|
|
50
|
+
| "package-json"
|
|
51
|
+
| "markdown-managed"
|
|
52
|
+
| "text-managed"
|
|
53
|
+
| "copy"
|
|
54
|
+
| "copy-dir"
|
|
55
|
+
| "symlink";
|
|
33
56
|
|
|
34
57
|
export interface InstallPlanItem {
|
|
35
58
|
readonly path: string;
|
|
@@ -96,9 +119,16 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
96
119
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
97
120
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
98
121
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
122
|
+
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
123
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
99
124
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
100
125
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
101
|
-
const PERSONAL_AGENT_RUNTIME_FILES = new Set([
|
|
126
|
+
const PERSONAL_AGENT_RUNTIME_FILES = new Set([
|
|
127
|
+
".claude/CLAUDE.md",
|
|
128
|
+
".claude/settings.json",
|
|
129
|
+
"AGENTS.md",
|
|
130
|
+
".codex/hooks.json",
|
|
131
|
+
]);
|
|
102
132
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
103
133
|
const PERSONAL_PRINCIPLES_DOCS = ".phasegate-local/docs/principles";
|
|
104
134
|
const PERSONAL_FOLDER_RULES_DOC = ".phasegate-local/docs/folder_management_rules.md";
|
|
@@ -207,7 +237,10 @@ function mergeHookArrays(existing: unknown, incoming: unknown): unknown[] {
|
|
|
207
237
|
return result;
|
|
208
238
|
}
|
|
209
239
|
|
|
210
|
-
function mergeJsonObject(
|
|
240
|
+
function mergeJsonObject(
|
|
241
|
+
existing: Record<string, unknown>,
|
|
242
|
+
incoming: Record<string, unknown>,
|
|
243
|
+
): Record<string, unknown> {
|
|
211
244
|
const result: Record<string, unknown> = { ...existing };
|
|
212
245
|
const existingHooks = isRecord(existing.hooks) ? existing.hooks : {};
|
|
213
246
|
const incomingHooks = isRecord(incoming.hooks) ? incoming.hooks : {};
|
|
@@ -256,11 +289,39 @@ function managedMarkdownBlock(content: string): string {
|
|
|
256
289
|
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
257
290
|
}
|
|
258
291
|
|
|
292
|
+
// Extracts the user-authored body between the user-section markers. Returns
|
|
293
|
+
// null when the markers are absent or the body is blank, so callers fall back
|
|
294
|
+
// to the template placeholder.
|
|
295
|
+
function extractUserSectionBody(content: string | null): string | null {
|
|
296
|
+
if (content === null) return null;
|
|
297
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
298
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
299
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
300
|
+
const body = content.slice(start + USER_SECTION_BEGIN.length, end).trim();
|
|
301
|
+
return body.length === 0 ? null : body;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The CLAUDE.md template nests the user-section inside the managed section, so
|
|
305
|
+
// replacing the managed block wholesale would wipe user-authored instructions
|
|
306
|
+
// with the template placeholder. Re-inject the existing user-section body into
|
|
307
|
+
// the incoming block. Templates whose managed block carries no user-section
|
|
308
|
+
// markers (e.g. AGENTS.md) are returned unchanged.
|
|
309
|
+
function restoreUserSection(block: string, existing: string | null): string {
|
|
310
|
+
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
311
|
+
const end = block.indexOf(USER_SECTION_END);
|
|
312
|
+
if (start === -1 || end === -1 || end < start) return block;
|
|
313
|
+
const preserved = extractUserSectionBody(existing);
|
|
314
|
+
if (preserved === null) return block;
|
|
315
|
+
return `${block.slice(0, start + USER_SECTION_BEGIN.length)}\n${preserved}\n${block.slice(end)}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
259
318
|
function mergeManagedMarkdown(existing: string | null, incoming: string): string {
|
|
260
|
-
const block = managedMarkdownBlock(incoming);
|
|
261
319
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
320
|
+
const block = restoreUserSection(managedMarkdownBlock(incoming), existing);
|
|
262
321
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
263
|
-
|
|
322
|
+
// Replacer function keeps user-authored text (now part of the block) from
|
|
323
|
+
// being interpreted as `$`-substitution patterns by String.replace.
|
|
324
|
+
if (pattern.test(existing)) return existing.replace(pattern, () => block).replace(/\s*$/, "\n");
|
|
264
325
|
return `${block}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
265
326
|
}
|
|
266
327
|
|
|
@@ -321,7 +382,8 @@ function hasCustomJson(content: string | null): boolean {
|
|
|
321
382
|
const parsed = JSON.parse(content) as unknown;
|
|
322
383
|
if (!isRecord(parsed)) return true;
|
|
323
384
|
const withoutEmptyHooks = { ...parsed };
|
|
324
|
-
if (isRecord(withoutEmptyHooks.hooks) && Object.keys(withoutEmptyHooks.hooks).length === 0)
|
|
385
|
+
if (isRecord(withoutEmptyHooks.hooks) && Object.keys(withoutEmptyHooks.hooks).length === 0)
|
|
386
|
+
delete withoutEmptyHooks.hooks;
|
|
325
387
|
return Object.keys(withoutEmptyHooks).length > 0;
|
|
326
388
|
} catch {
|
|
327
389
|
return true;
|
|
@@ -343,10 +405,13 @@ function likelyCauseFor(code: string): string {
|
|
|
343
405
|
}
|
|
344
406
|
|
|
345
407
|
function recoveryFor(code: string, target: string): string {
|
|
346
|
-
if (code === "EPERM")
|
|
408
|
+
if (code === "EPERM")
|
|
409
|
+
return `Review sandbox or filesystem permissions for ${target}, ask the user for write access when needed, then rerun phasegate setup:agent --apply or phasegate install --apply.`;
|
|
347
410
|
if (code === "EACCES") return `Fix ownership or permissions for ${target}, then rerun phasegate install --apply.`;
|
|
348
|
-
if (code === "EROFS")
|
|
349
|
-
|
|
411
|
+
if (code === "EROFS")
|
|
412
|
+
return `Move the project to a writable filesystem or rerun in a writable workspace before applying ${target}.`;
|
|
413
|
+
if (code === "EEXIST" || code === "ENOTDIR")
|
|
414
|
+
return `Inspect the parent path for ${target}; if it is user-owned, rename or move it before rerunning phasegate install --dry-run --json and then --apply.`;
|
|
350
415
|
return `Inspect ${target}, run phasegate install --dry-run --json, then rerun with --apply after resolving the filesystem issue.`;
|
|
351
416
|
}
|
|
352
417
|
|
|
@@ -380,12 +445,22 @@ export class RunInstallUseCase {
|
|
|
380
445
|
const skillSet = input.skillSet ?? "all";
|
|
381
446
|
const workflow = input.workflow ?? "standard";
|
|
382
447
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
383
|
-
const
|
|
448
|
+
const personal = input.personal ?? false;
|
|
449
|
+
const targets = personal
|
|
384
450
|
? this.createPersonalTargets({ includeClaude, includeCodex })
|
|
385
451
|
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
452
|
+
// Persist the effective opt-in state so a later reconcile can honor the
|
|
453
|
+
// original install options. Personal installs never deploy Husky/CI
|
|
454
|
+
// targets, so their effective state is recorded as false regardless of
|
|
455
|
+
// input.
|
|
456
|
+
const installationFlags: InstallationFlags = {
|
|
457
|
+
includeHusky: personal ? false : includeHusky,
|
|
458
|
+
includeCi: personal ? false : includeCi,
|
|
459
|
+
personal,
|
|
460
|
+
};
|
|
386
461
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
387
462
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
388
|
-
let manifest = baseManifest;
|
|
463
|
+
let manifest = baseManifest.withInstallationFlags(installationFlags);
|
|
389
464
|
const plan: InstallPlanItem[] = [];
|
|
390
465
|
const refused: InstallPlanItem[] = [];
|
|
391
466
|
const changed: InstallPlanItem[] = [];
|
|
@@ -396,16 +471,18 @@ export class RunInstallUseCase {
|
|
|
396
471
|
const absolutePath = join(input.projectRoot, target.path);
|
|
397
472
|
const before = await readTextOrNull(absolutePath);
|
|
398
473
|
const rawTemplate = await readFile(join(input.harnessRoot, target.templatePath), "utf8");
|
|
399
|
-
const template =
|
|
400
|
-
|
|
401
|
-
|
|
474
|
+
const template =
|
|
475
|
+
target.strategy === "markdown-managed"
|
|
476
|
+
? renderAgentContextTemplate(rawTemplate, { agent, skillSet, workflow, includeHusky, includeCi })
|
|
477
|
+
: rawTemplate;
|
|
402
478
|
const existingEntry = baseManifest.findEntry(target.path);
|
|
403
479
|
const beforeHash = before === null ? null : this.hashCalculator.compute(before);
|
|
404
|
-
const unmanagedPersonalRuntimeFile =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
&&
|
|
408
|
-
|
|
480
|
+
const unmanagedPersonalRuntimeFile =
|
|
481
|
+
input.personal &&
|
|
482
|
+
(PERSONAL_AGENT_RUNTIME_FILES.has(target.path) || target.personalManualIfUnmanaged === true) &&
|
|
483
|
+
before !== null &&
|
|
484
|
+
!before.includes(MARKDOWN_BEGIN) &&
|
|
485
|
+
(existingEntry === null || beforeHash === null || !beforeHash.equals(existingEntry.hash));
|
|
409
486
|
const repairMode = unmanagedPersonalRuntimeFile ? "manual" : this.repairMode(target, before);
|
|
410
487
|
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
411
488
|
const didChange = before !== next;
|
|
@@ -414,11 +491,11 @@ export class RunInstallUseCase {
|
|
|
414
491
|
// (merge returns `before`). For executable git hooks this means the
|
|
415
492
|
// phasegate hook is NOT wired in — warn instead of skipping silently.
|
|
416
493
|
const preservedExistingHook =
|
|
417
|
-
target.strategy === "copy"
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
494
|
+
target.strategy === "copy" &&
|
|
495
|
+
target.executable === true &&
|
|
496
|
+
before !== null &&
|
|
497
|
+
before.trim() !== template.trim() &&
|
|
498
|
+
!input.force;
|
|
422
499
|
const warning = preservedExistingHook
|
|
423
500
|
? `${target.path}: an existing hook was found and left unchanged; phasegate's checks are NOT wired in. Merge the phasegate hook manually (or rerun with --force to back up and overwrite).`
|
|
424
501
|
: null;
|
|
@@ -432,7 +509,9 @@ export class RunInstallUseCase {
|
|
|
432
509
|
? `${target.path}: existing non-phasegate runtime path requires manual review`
|
|
433
510
|
: warning !== null
|
|
434
511
|
? warning
|
|
435
|
-
: didChange
|
|
512
|
+
: didChange
|
|
513
|
+
? `${target.path}: ${action}`
|
|
514
|
+
: `${target.path}: already up to date`,
|
|
436
515
|
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
437
516
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
438
517
|
warning,
|
|
@@ -488,10 +567,7 @@ export class RunInstallUseCase {
|
|
|
488
567
|
}
|
|
489
568
|
|
|
490
569
|
const personalSkillTargets = input.personal
|
|
491
|
-
? [
|
|
492
|
-
...(includeClaude ? [".claude/skills"] : []),
|
|
493
|
-
...(includeCodex ? [".codex/skills"] : []),
|
|
494
|
-
]
|
|
570
|
+
? [...(includeClaude ? [".claude/skills"] : []), ...(includeCodex ? [".codex/skills"] : [])]
|
|
495
571
|
: [];
|
|
496
572
|
const selectedPersonalSkills = input.personal ? await listSelectedBundledSkills(input.harnessRoot, skillSet) : [];
|
|
497
573
|
for (const skillPath of personalSkillTargets) {
|
|
@@ -499,7 +575,13 @@ export class RunInstallUseCase {
|
|
|
499
575
|
plan.push(item);
|
|
500
576
|
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
501
577
|
try {
|
|
502
|
-
await copySelectedSkillDirectories(
|
|
578
|
+
await copySelectedSkillDirectories(
|
|
579
|
+
this.modelDelegation,
|
|
580
|
+
input.harnessRoot,
|
|
581
|
+
input.projectRoot,
|
|
582
|
+
join(input.projectRoot, skillPath),
|
|
583
|
+
selectedPersonalSkills,
|
|
584
|
+
);
|
|
503
585
|
await writeFile(
|
|
504
586
|
join(input.projectRoot, skillPath, ".harness-version"),
|
|
505
587
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -512,7 +594,12 @@ export class RunInstallUseCase {
|
|
|
512
594
|
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
513
595
|
path: `${item.path}/.harness-version`,
|
|
514
596
|
mode: "created",
|
|
515
|
-
contentForHash: this.personalSkillsVersionHashInput(
|
|
597
|
+
contentForHash: this.personalSkillsVersionHashInput(
|
|
598
|
+
item.path,
|
|
599
|
+
input.phasegateVersion,
|
|
600
|
+
skillSet,
|
|
601
|
+
selectedPersonalSkills,
|
|
602
|
+
),
|
|
516
603
|
});
|
|
517
604
|
for (const skill of selectedPersonalSkills) {
|
|
518
605
|
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
@@ -567,7 +654,11 @@ export class RunInstallUseCase {
|
|
|
567
654
|
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "mkdir", error);
|
|
568
655
|
}
|
|
569
656
|
try {
|
|
570
|
-
await symlink(
|
|
657
|
+
await symlink(
|
|
658
|
+
linkSpec.target,
|
|
659
|
+
join(input.projectRoot, linkSpec.path),
|
|
660
|
+
process.platform === "win32" ? "junction" : "dir",
|
|
661
|
+
);
|
|
571
662
|
} catch (error) {
|
|
572
663
|
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "symlink", error);
|
|
573
664
|
}
|
|
@@ -586,24 +677,45 @@ export class RunInstallUseCase {
|
|
|
586
677
|
repairMode: "manual",
|
|
587
678
|
strategy: "json",
|
|
588
679
|
changed: false,
|
|
589
|
-
summary:
|
|
680
|
+
summary:
|
|
681
|
+
"~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
|
|
590
682
|
diff: "manual Codex hooks feature enablement may be required",
|
|
591
683
|
skillHint: null,
|
|
592
684
|
warning: null,
|
|
593
685
|
});
|
|
594
686
|
}
|
|
595
687
|
|
|
596
|
-
|
|
688
|
+
// Re-save on apply when the recorded install flags drift from a previous
|
|
689
|
+
// manifest (e.g. re-install with different --with-husky/--with-ci), even
|
|
690
|
+
// if no managed file content changed. A missing manifest with zero changes
|
|
691
|
+
// keeps the legacy behavior of not creating one.
|
|
692
|
+
const flagsDrifted =
|
|
693
|
+
existingManifest !== null && !this.installationFlagsEqual(existingManifest.installationFlags, installationFlags);
|
|
694
|
+
if (input.apply && (changed.length > 0 || flagsDrifted)) {
|
|
597
695
|
try {
|
|
598
696
|
await this.manifestRepository.save(input.projectRoot, manifest);
|
|
599
697
|
} catch (error) {
|
|
600
|
-
return this.withApplyError(
|
|
698
|
+
return this.withApplyError(
|
|
699
|
+
{ plan, refused, changed, backupDir },
|
|
700
|
+
".phasegate/manifest.json",
|
|
701
|
+
"manifest-save",
|
|
702
|
+
error,
|
|
703
|
+
);
|
|
601
704
|
}
|
|
602
705
|
}
|
|
603
706
|
|
|
604
707
|
return { plan, refused, changed, backupDir };
|
|
605
708
|
}
|
|
606
709
|
|
|
710
|
+
private installationFlagsEqual(existing: InstallationFlags | undefined, next: InstallationFlags): boolean {
|
|
711
|
+
return (
|
|
712
|
+
existing !== undefined &&
|
|
713
|
+
existing.includeHusky === next.includeHusky &&
|
|
714
|
+
existing.includeCi === next.includeCi &&
|
|
715
|
+
existing.personal === next.personal
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
607
719
|
private withApplyError(
|
|
608
720
|
result: RunInstallResult,
|
|
609
721
|
target: string,
|
|
@@ -638,7 +750,11 @@ export class RunInstallUseCase {
|
|
|
638
750
|
},
|
|
639
751
|
...(options.includeClaude
|
|
640
752
|
? [
|
|
641
|
-
{
|
|
753
|
+
{
|
|
754
|
+
path: ".claude/settings.json",
|
|
755
|
+
strategy: "json" as const,
|
|
756
|
+
templatePath: "templates/.claude/settings.json",
|
|
757
|
+
},
|
|
642
758
|
{
|
|
643
759
|
path: "CLAUDE.md",
|
|
644
760
|
strategy: "markdown-managed" as const,
|
|
@@ -696,7 +812,10 @@ export class RunInstallUseCase {
|
|
|
696
812
|
];
|
|
697
813
|
}
|
|
698
814
|
|
|
699
|
-
private createPersonalTargets(options: {
|
|
815
|
+
private createPersonalTargets(options: {
|
|
816
|
+
readonly includeClaude: boolean;
|
|
817
|
+
readonly includeCodex: boolean;
|
|
818
|
+
}): readonly InstallTarget[] {
|
|
700
819
|
return [
|
|
701
820
|
{
|
|
702
821
|
path: ".phasegate-local/phasegate.config.json",
|
|
@@ -709,7 +828,11 @@ export class RunInstallUseCase {
|
|
|
709
828
|
path: ".claude/CLAUDE.md",
|
|
710
829
|
strategy: "markdown-managed" as const,
|
|
711
830
|
templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
|
|
712
|
-
block: {
|
|
831
|
+
block: {
|
|
832
|
+
start: MARKDOWN_BEGIN,
|
|
833
|
+
end: MARKDOWN_END,
|
|
834
|
+
content: "phasegate personal .claude/CLAUDE.md managed section",
|
|
835
|
+
},
|
|
713
836
|
personalManualIfUnmanaged: true,
|
|
714
837
|
},
|
|
715
838
|
{
|
|
@@ -725,7 +848,11 @@ export class RunInstallUseCase {
|
|
|
725
848
|
path: "AGENTS.md",
|
|
726
849
|
strategy: "markdown-managed" as const,
|
|
727
850
|
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
728
|
-
block: {
|
|
851
|
+
block: {
|
|
852
|
+
start: MARKDOWN_BEGIN,
|
|
853
|
+
end: MARKDOWN_END,
|
|
854
|
+
content: "phasegate personal AGENTS.md managed section",
|
|
855
|
+
},
|
|
729
856
|
personalManualIfUnmanaged: true,
|
|
730
857
|
},
|
|
731
858
|
{
|
|
@@ -861,9 +988,15 @@ export class RunInstallUseCase {
|
|
|
861
988
|
break;
|
|
862
989
|
}
|
|
863
990
|
}
|
|
864
|
-
const missingManifest =
|
|
865
|
-
|
|
866
|
-
|
|
991
|
+
const missingManifest =
|
|
992
|
+
baseManifest.findEntry(`${relativePath}/.harness-version`) === null ||
|
|
993
|
+
skills.some((skill) => baseManifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
994
|
+
const changed =
|
|
995
|
+
current === null ||
|
|
996
|
+
!current.includes(expectedVersion) ||
|
|
997
|
+
!current.includes(expectedSkillSet) ||
|
|
998
|
+
missingSkill ||
|
|
999
|
+
missingManifest;
|
|
867
1000
|
return {
|
|
868
1001
|
path: relativePath,
|
|
869
1002
|
action: changed ? "missing" : "will-skip",
|
|
@@ -877,7 +1010,12 @@ export class RunInstallUseCase {
|
|
|
877
1010
|
};
|
|
878
1011
|
}
|
|
879
1012
|
|
|
880
|
-
private personalSkillsVersionHashInput(
|
|
1013
|
+
private personalSkillsVersionHashInput(
|
|
1014
|
+
path: string,
|
|
1015
|
+
version: string,
|
|
1016
|
+
skillSet: "core" | "all",
|
|
1017
|
+
skills: readonly string[],
|
|
1018
|
+
): string {
|
|
881
1019
|
return `personal-skills-version:${path}:${version}:${skillSet}:${skills.join(",")}`;
|
|
882
1020
|
}
|
|
883
1021
|
|
|
@@ -902,9 +1040,15 @@ export class RunInstallUseCase {
|
|
|
902
1040
|
break;
|
|
903
1041
|
}
|
|
904
1042
|
}
|
|
905
|
-
const missingManifest =
|
|
906
|
-
|
|
907
|
-
|
|
1043
|
+
const missingManifest =
|
|
1044
|
+
baseManifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null ||
|
|
1045
|
+
skills.some((skill) => baseManifest.findEntry(`skills/${skill}`) === null);
|
|
1046
|
+
const changed =
|
|
1047
|
+
current === null ||
|
|
1048
|
+
!current.includes(expectedVersion) ||
|
|
1049
|
+
!current.includes(expectedSkillSet) ||
|
|
1050
|
+
missingSkill ||
|
|
1051
|
+
missingManifest;
|
|
908
1052
|
return {
|
|
909
1053
|
path: "skills",
|
|
910
1054
|
action: changed ? "missing" : "will-skip",
|
|
@@ -918,7 +1062,11 @@ export class RunInstallUseCase {
|
|
|
918
1062
|
};
|
|
919
1063
|
}
|
|
920
1064
|
|
|
921
|
-
private async deploySharedSkills(
|
|
1065
|
+
private async deploySharedSkills(
|
|
1066
|
+
input: RunInstallInput,
|
|
1067
|
+
skills: readonly string[],
|
|
1068
|
+
skillSet: SkillSet,
|
|
1069
|
+
): Promise<void> {
|
|
922
1070
|
const targetRoot = join(input.projectRoot, "skills");
|
|
923
1071
|
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
924
1072
|
await writeFile(
|