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
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
// @work-item-id WI-216
|
|
8
8
|
// @work-item-id WI-219
|
|
9
9
|
// @work-item-id WI-264
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// @work-item-id WI-315
|
|
11
|
+
// @work-item-id WI-326
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
access,
|
|
15
|
+
chmod,
|
|
16
|
+
copyFile,
|
|
17
|
+
lstat,
|
|
18
|
+
mkdir,
|
|
19
|
+
readdir,
|
|
20
|
+
readFile,
|
|
21
|
+
readlink,
|
|
22
|
+
rm,
|
|
23
|
+
symlink,
|
|
24
|
+
writeFile,
|
|
25
|
+
} from "node:fs/promises";
|
|
12
26
|
import { dirname, join, relative, resolve } from "node:path";
|
|
13
27
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
14
28
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
@@ -20,7 +34,15 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
20
34
|
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
21
35
|
|
|
22
36
|
type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse" | "prune";
|
|
23
|
-
type StrategyType =
|
|
37
|
+
type StrategyType =
|
|
38
|
+
| "json"
|
|
39
|
+
| "shell"
|
|
40
|
+
| "yaml-add"
|
|
41
|
+
| "package-json"
|
|
42
|
+
| "markdown-managed"
|
|
43
|
+
| "copy-dir"
|
|
44
|
+
| "symlink"
|
|
45
|
+
| "unknown";
|
|
24
46
|
|
|
25
47
|
export interface ReconcilePlanItem {
|
|
26
48
|
readonly path: string;
|
|
@@ -40,6 +62,12 @@ export interface RunReconcileInput {
|
|
|
40
62
|
readonly dryRun: boolean;
|
|
41
63
|
readonly apply: boolean;
|
|
42
64
|
readonly force: boolean;
|
|
65
|
+
// Explicit per-run overrides. When omitted, the flags recorded in the
|
|
66
|
+
// manifest at install time (installationFlags) are used; manifests written
|
|
67
|
+
// before WI-326 carry no flags and fall back to the legacy behavior of
|
|
68
|
+
// reconciling every bundled target.
|
|
69
|
+
readonly includeHusky?: boolean;
|
|
70
|
+
readonly includeCi?: boolean;
|
|
43
71
|
}
|
|
44
72
|
|
|
45
73
|
export interface RunReconcileResult {
|
|
@@ -62,6 +90,8 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
62
90
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
63
91
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
64
92
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
93
|
+
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
94
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
65
95
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
66
96
|
const HARNESS_VERSION_BASENAME = ".harness-version";
|
|
67
97
|
const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
|
|
@@ -177,7 +207,10 @@ function replaceHookArrays(existing: unknown, incoming: unknown): unknown[] {
|
|
|
177
207
|
return result;
|
|
178
208
|
}
|
|
179
209
|
|
|
180
|
-
function reconcileJsonObject(
|
|
210
|
+
function reconcileJsonObject(
|
|
211
|
+
existing: Record<string, unknown>,
|
|
212
|
+
incoming: Record<string, unknown>,
|
|
213
|
+
): Record<string, unknown> {
|
|
181
214
|
const result: Record<string, unknown> = { ...existing };
|
|
182
215
|
const existingHooks = isRecord(existing.hooks) ? existing.hooks : {};
|
|
183
216
|
const incomingHooks = isRecord(incoming.hooks) ? incoming.hooks : {};
|
|
@@ -220,11 +253,39 @@ function managedMarkdownBlock(content: string): string {
|
|
|
220
253
|
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
221
254
|
}
|
|
222
255
|
|
|
256
|
+
// Extracts the user-authored body between the user-section markers. Returns
|
|
257
|
+
// null when the markers are absent or the body is blank, so callers fall back
|
|
258
|
+
// to the template placeholder.
|
|
259
|
+
function extractUserSectionBody(content: string | null): string | null {
|
|
260
|
+
if (content === null) return null;
|
|
261
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
262
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
263
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
264
|
+
const body = content.slice(start + USER_SECTION_BEGIN.length, end).trim();
|
|
265
|
+
return body.length === 0 ? null : body;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The CLAUDE.md template nests the user-section inside the managed section, so
|
|
269
|
+
// replacing the managed block wholesale would wipe user-authored instructions
|
|
270
|
+
// with the template placeholder. Re-inject the existing user-section body into
|
|
271
|
+
// the incoming block. Templates whose managed block carries no user-section
|
|
272
|
+
// markers (e.g. AGENTS.md) are returned unchanged.
|
|
273
|
+
function restoreUserSection(block: string, existing: string | null): string {
|
|
274
|
+
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
275
|
+
const end = block.indexOf(USER_SECTION_END);
|
|
276
|
+
if (start === -1 || end === -1 || end < start) return block;
|
|
277
|
+
const preserved = extractUserSectionBody(existing);
|
|
278
|
+
if (preserved === null) return block;
|
|
279
|
+
return `${block.slice(0, start + USER_SECTION_BEGIN.length)}\n${preserved}\n${block.slice(end)}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
223
282
|
function reconcileManagedMarkdown(existing: string | null, incoming: string): string {
|
|
224
|
-
const block = managedMarkdownBlock(incoming);
|
|
225
283
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
284
|
+
const block = restoreUserSection(managedMarkdownBlock(incoming), existing);
|
|
226
285
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
227
|
-
|
|
286
|
+
// Replacer function keeps user-authored text (now part of the block) from
|
|
287
|
+
// being interpreted as `$`-substitution patterns by String.replace.
|
|
288
|
+
if (pattern.test(existing)) return existing.replace(pattern, () => block).replace(/\s*$/, "\n");
|
|
228
289
|
return `${block}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
229
290
|
}
|
|
230
291
|
|
|
@@ -235,7 +296,9 @@ function renderAgentContextTemplate(template: string): string {
|
|
|
235
296
|
"phasegate validate --layer L2 --format human",
|
|
236
297
|
"phasegate setup:agent --dry-run",
|
|
237
298
|
"phasegate config:plan --intent l4-strict --dry-run",
|
|
238
|
-
]
|
|
299
|
+
]
|
|
300
|
+
.map((command) => `- \`${command}\``)
|
|
301
|
+
.join("\n");
|
|
239
302
|
return template
|
|
240
303
|
.replaceAll("{{PHASEGATE_AGENT}}", "both")
|
|
241
304
|
.replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
|
|
@@ -289,12 +352,15 @@ export class RunReconcileUseCase {
|
|
|
289
352
|
return { plan: [item], refused: [], changed: [], backupDir: null };
|
|
290
353
|
}
|
|
291
354
|
|
|
292
|
-
const
|
|
355
|
+
const includeHusky = input.includeHusky ?? manifest.installationFlags?.includeHusky ?? true;
|
|
356
|
+
const includeCi = input.includeCi ?? manifest.installationFlags?.includeCi ?? true;
|
|
357
|
+
const targets = this.createTargets({ includeHusky, includeCi });
|
|
293
358
|
const targetsByPath = new Map(targets.map((target) => [target.path, target]));
|
|
294
359
|
let nextManifest = DeploymentManifest.reconstitute({
|
|
295
360
|
version: input.phasegateVersion,
|
|
296
361
|
installedAt: manifest.installedAt,
|
|
297
362
|
entries: manifest.entries,
|
|
363
|
+
installationFlags: manifest.installationFlags,
|
|
298
364
|
});
|
|
299
365
|
const plan: ReconcilePlanItem[] = [];
|
|
300
366
|
const refused: ReconcilePlanItem[] = [];
|
|
@@ -312,14 +378,28 @@ export class RunReconcileUseCase {
|
|
|
312
378
|
for (const entry of manifest.entries) {
|
|
313
379
|
const target = targetsByPath.get(entry.path);
|
|
314
380
|
if (target === undefined) {
|
|
315
|
-
const item = this.item(
|
|
381
|
+
const item = this.item(
|
|
382
|
+
entry.path,
|
|
383
|
+
"skip",
|
|
384
|
+
"manual",
|
|
385
|
+
"unknown",
|
|
386
|
+
false,
|
|
387
|
+
`${entry.path}: no bundled template`,
|
|
388
|
+
"manual review required",
|
|
389
|
+
SKILL_HINT,
|
|
390
|
+
);
|
|
316
391
|
plan.push(item);
|
|
317
392
|
continue;
|
|
318
393
|
}
|
|
319
394
|
const outcome = await this.planManagedEntry(input, entry, target);
|
|
320
395
|
outcomes.push(outcome);
|
|
321
396
|
plan.push(outcome.item);
|
|
322
|
-
if (
|
|
397
|
+
if (
|
|
398
|
+
input.apply &&
|
|
399
|
+
outcome.item.changed &&
|
|
400
|
+
(outcome.item.repairMode === "ai-assisted" || outcome.item.repairMode === "manual") &&
|
|
401
|
+
!input.force
|
|
402
|
+
) {
|
|
323
403
|
refused.push({ ...outcome.item, action: "refuse" });
|
|
324
404
|
}
|
|
325
405
|
}
|
|
@@ -329,7 +409,12 @@ export class RunReconcileUseCase {
|
|
|
329
409
|
const outcome = await this.planMissingTarget(input, target);
|
|
330
410
|
outcomes.push(outcome);
|
|
331
411
|
plan.push(outcome.item);
|
|
332
|
-
if (
|
|
412
|
+
if (
|
|
413
|
+
input.apply &&
|
|
414
|
+
outcome.item.changed &&
|
|
415
|
+
(outcome.item.repairMode === "ai-assisted" || outcome.item.repairMode === "manual") &&
|
|
416
|
+
!input.force
|
|
417
|
+
) {
|
|
333
418
|
refused.push({ ...outcome.item, action: "refuse" });
|
|
334
419
|
}
|
|
335
420
|
}
|
|
@@ -376,18 +461,43 @@ export class RunReconcileUseCase {
|
|
|
376
461
|
if (hashContent !== null) {
|
|
377
462
|
if (outcome.item.strategy === "copy-dir" && outcome.item.path === "skills") {
|
|
378
463
|
const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
379
|
-
nextManifest = nextManifest.addEntry(
|
|
464
|
+
nextManifest = nextManifest.addEntry(
|
|
465
|
+
this.createdEntry(
|
|
466
|
+
SHARED_SKILLS_VERSION_PATH,
|
|
467
|
+
this.sharedSkillsVersionHashInput(input.phasegateVersion, "all", sharedSkills),
|
|
468
|
+
),
|
|
469
|
+
);
|
|
380
470
|
for (const skill of sharedSkills) {
|
|
381
|
-
nextManifest = nextManifest.addEntry(
|
|
471
|
+
nextManifest = nextManifest.addEntry(
|
|
472
|
+
this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, "all")),
|
|
473
|
+
);
|
|
382
474
|
}
|
|
383
|
-
} else if (
|
|
475
|
+
} else if (
|
|
476
|
+
outcome.item.strategy === "copy-dir" &&
|
|
477
|
+
(outcome.item.path === ".claude/skills" || outcome.item.path === ".codex/skills")
|
|
478
|
+
) {
|
|
384
479
|
const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
385
|
-
nextManifest = nextManifest.addEntry(
|
|
480
|
+
nextManifest = nextManifest.addEntry(
|
|
481
|
+
this.createdEntry(
|
|
482
|
+
`${outcome.item.path}/.harness-version`,
|
|
483
|
+
this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, "all", personalSkills),
|
|
484
|
+
),
|
|
485
|
+
);
|
|
386
486
|
for (const skill of personalSkills) {
|
|
387
|
-
nextManifest = nextManifest.addEntry(
|
|
487
|
+
nextManifest = nextManifest.addEntry(
|
|
488
|
+
this.createdEntry(
|
|
489
|
+
`${outcome.item.path}/${skill}`,
|
|
490
|
+
this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, "all"),
|
|
491
|
+
),
|
|
492
|
+
);
|
|
388
493
|
}
|
|
389
494
|
} else {
|
|
390
|
-
const mode =
|
|
495
|
+
const mode =
|
|
496
|
+
outcome.item.strategy === "symlink"
|
|
497
|
+
? "symlink"
|
|
498
|
+
: outcome.item.action === "add"
|
|
499
|
+
? "created"
|
|
500
|
+
: (manifest.findEntry(outcome.item.path)?.mode ?? "merged");
|
|
391
501
|
nextManifest = nextManifest.addEntry(
|
|
392
502
|
DeploymentEntry.create({
|
|
393
503
|
path: outcome.item.path,
|
|
@@ -416,9 +526,10 @@ export class RunReconcileUseCase {
|
|
|
416
526
|
const matchesManifest = currentHash.equals(entry.hash);
|
|
417
527
|
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
418
528
|
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
419
|
-
const next =
|
|
420
|
-
|
|
421
|
-
|
|
529
|
+
const next =
|
|
530
|
+
entry.mode === "created" && target.strategy !== "package-json" && target.strategy !== "markdown-managed"
|
|
531
|
+
? template
|
|
532
|
+
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
422
533
|
const changed = before !== next;
|
|
423
534
|
const repairMode: RepairMode = matchesManifest ? "mechanical" : "ai-assisted";
|
|
424
535
|
return {
|
|
@@ -448,11 +559,13 @@ export class RunReconcileUseCase {
|
|
|
448
559
|
const before = await readTextOrNull(absolutePath);
|
|
449
560
|
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
450
561
|
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
451
|
-
const next =
|
|
452
|
-
|
|
453
|
-
|
|
562
|
+
const next =
|
|
563
|
+
before === null && target.strategy !== "package-json"
|
|
564
|
+
? template
|
|
565
|
+
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
454
566
|
const changed = before !== next;
|
|
455
|
-
const repairMode: RepairMode =
|
|
567
|
+
const repairMode: RepairMode =
|
|
568
|
+
target.strategy === "shell" && before !== null && !before.includes(SHELL_BEGIN) ? "ai-assisted" : "mechanical";
|
|
456
569
|
return {
|
|
457
570
|
item: this.item(
|
|
458
571
|
target.path,
|
|
@@ -480,19 +593,46 @@ export class RunReconcileUseCase {
|
|
|
480
593
|
const stat = await lstat(absolutePath);
|
|
481
594
|
if (stat.isSymbolicLink() && (await readlink(absolutePath)) === "../skills") {
|
|
482
595
|
return {
|
|
483
|
-
item: this.item(
|
|
596
|
+
item: this.item(
|
|
597
|
+
relativePath,
|
|
598
|
+
"skip",
|
|
599
|
+
"mechanical",
|
|
600
|
+
"symlink",
|
|
601
|
+
false,
|
|
602
|
+
`${relativePath}: already linked`,
|
|
603
|
+
"no changes",
|
|
604
|
+
null,
|
|
605
|
+
),
|
|
484
606
|
needsBackup: false,
|
|
485
607
|
apply: async () => "../skills",
|
|
486
608
|
};
|
|
487
609
|
}
|
|
488
610
|
return {
|
|
489
|
-
item: this.item(
|
|
611
|
+
item: this.item(
|
|
612
|
+
relativePath,
|
|
613
|
+
"skip",
|
|
614
|
+
"manual",
|
|
615
|
+
"symlink",
|
|
616
|
+
false,
|
|
617
|
+
`${relativePath}: existing non-phasegate path requires manual review`,
|
|
618
|
+
"manual review required",
|
|
619
|
+
SKILL_HINT,
|
|
620
|
+
),
|
|
490
621
|
needsBackup: false,
|
|
491
622
|
apply: async () => null,
|
|
492
623
|
};
|
|
493
624
|
} catch {
|
|
494
625
|
return {
|
|
495
|
-
item: this.item(
|
|
626
|
+
item: this.item(
|
|
627
|
+
relativePath,
|
|
628
|
+
"link",
|
|
629
|
+
"mechanical",
|
|
630
|
+
"symlink",
|
|
631
|
+
true,
|
|
632
|
+
`${relativePath}: create symlink`,
|
|
633
|
+
"+ symlink ../skills",
|
|
634
|
+
null,
|
|
635
|
+
),
|
|
496
636
|
needsBackup: false,
|
|
497
637
|
apply: async () => {
|
|
498
638
|
await mkdir(join(projectRoot, "skills"), { recursive: true });
|
|
@@ -505,21 +645,40 @@ export class RunReconcileUseCase {
|
|
|
505
645
|
}
|
|
506
646
|
|
|
507
647
|
private manifestIntendsSharedSkills(manifest: DeploymentManifest): boolean {
|
|
508
|
-
return
|
|
509
|
-
|
|
510
|
-
|
|
648
|
+
return (
|
|
649
|
+
manifest.findEntry(".claude/skills") !== null ||
|
|
650
|
+
manifest.findEntry(".codex/skills") !== null ||
|
|
651
|
+
manifest.entries.some((entry) => entry.path.startsWith("skills/"))
|
|
652
|
+
);
|
|
511
653
|
}
|
|
512
654
|
|
|
513
655
|
private isPersonalManifest(manifest: DeploymentManifest): boolean {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
656
|
+
// Manifests written since WI-326 record the install mode explicitly;
|
|
657
|
+
// older manifests fall back to the legacy entry-shape heuristic.
|
|
658
|
+
if (manifest.installationFlags !== undefined) return manifest.installationFlags.personal;
|
|
659
|
+
return (
|
|
660
|
+
manifest.findEntry(".phasegate-local/phasegate.config.json") !== null ||
|
|
661
|
+
manifest.entries.some(
|
|
662
|
+
(entry) => (entry.path === ".claude/skills" || entry.path === ".codex/skills") && entry.mode === "created",
|
|
663
|
+
) ||
|
|
664
|
+
manifest.entries.some(
|
|
665
|
+
(entry) => entry.path.startsWith(".claude/skills/") || entry.path.startsWith(".codex/skills/"),
|
|
666
|
+
)
|
|
667
|
+
);
|
|
517
668
|
}
|
|
518
669
|
|
|
519
670
|
private personalSkillPaths(manifest: DeploymentManifest): Array<".claude/skills" | ".codex/skills"> {
|
|
520
671
|
const paths = new Set<".claude/skills" | ".codex/skills">();
|
|
521
|
-
if (
|
|
522
|
-
|
|
672
|
+
if (
|
|
673
|
+
manifest.findEntry(".claude/skills") !== null ||
|
|
674
|
+
manifest.entries.some((entry) => entry.path.startsWith(".claude/skills/"))
|
|
675
|
+
)
|
|
676
|
+
paths.add(".claude/skills");
|
|
677
|
+
if (
|
|
678
|
+
manifest.findEntry(".codex/skills") !== null ||
|
|
679
|
+
manifest.entries.some((entry) => entry.path.startsWith(".codex/skills/"))
|
|
680
|
+
)
|
|
681
|
+
paths.add(".codex/skills");
|
|
523
682
|
return [...paths].sort();
|
|
524
683
|
}
|
|
525
684
|
|
|
@@ -538,8 +697,9 @@ export class RunReconcileUseCase {
|
|
|
538
697
|
break;
|
|
539
698
|
}
|
|
540
699
|
}
|
|
541
|
-
const missingManifest =
|
|
542
|
-
|
|
700
|
+
const missingManifest =
|
|
701
|
+
manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null ||
|
|
702
|
+
skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
|
|
543
703
|
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
544
704
|
this.modelDelegation,
|
|
545
705
|
input.harnessRoot,
|
|
@@ -547,7 +707,12 @@ export class RunReconcileUseCase {
|
|
|
547
707
|
this.resolveProjectPath(input.projectRoot, "skills"),
|
|
548
708
|
skills,
|
|
549
709
|
);
|
|
550
|
-
const changed =
|
|
710
|
+
const changed =
|
|
711
|
+
versionContent === null ||
|
|
712
|
+
!versionContent.includes(expectedVersion) ||
|
|
713
|
+
missingSkill ||
|
|
714
|
+
missingManifest ||
|
|
715
|
+
renderedSkillDrift;
|
|
551
716
|
return {
|
|
552
717
|
item: this.item(
|
|
553
718
|
"skills",
|
|
@@ -568,7 +733,11 @@ export class RunReconcileUseCase {
|
|
|
568
733
|
};
|
|
569
734
|
}
|
|
570
735
|
|
|
571
|
-
private async deploySharedSkills(
|
|
736
|
+
private async deploySharedSkills(
|
|
737
|
+
input: RunReconcileInput,
|
|
738
|
+
skills: readonly string[],
|
|
739
|
+
skillSet: SkillSet,
|
|
740
|
+
): Promise<void> {
|
|
572
741
|
const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
|
|
573
742
|
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
574
743
|
await writeFile(
|
|
@@ -595,8 +764,9 @@ export class RunReconcileUseCase {
|
|
|
595
764
|
break;
|
|
596
765
|
}
|
|
597
766
|
}
|
|
598
|
-
const missingManifest =
|
|
599
|
-
|
|
767
|
+
const missingManifest =
|
|
768
|
+
manifest.findEntry(`${relativePath}/.harness-version`) === null ||
|
|
769
|
+
skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
600
770
|
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
601
771
|
this.modelDelegation,
|
|
602
772
|
input.harnessRoot,
|
|
@@ -604,7 +774,12 @@ export class RunReconcileUseCase {
|
|
|
604
774
|
this.resolveProjectPath(input.projectRoot, relativePath),
|
|
605
775
|
skills,
|
|
606
776
|
);
|
|
607
|
-
const changed =
|
|
777
|
+
const changed =
|
|
778
|
+
versionContent === null ||
|
|
779
|
+
!versionContent.includes(expectedVersion) ||
|
|
780
|
+
missingSkill ||
|
|
781
|
+
missingManifest ||
|
|
782
|
+
renderedSkillDrift;
|
|
608
783
|
return {
|
|
609
784
|
item: this.item(
|
|
610
785
|
relativePath,
|
|
@@ -620,7 +795,13 @@ export class RunReconcileUseCase {
|
|
|
620
795
|
apply: async () => {
|
|
621
796
|
if (!changed) return null;
|
|
622
797
|
const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
|
|
623
|
-
await copySelectedSkillDirectories(
|
|
798
|
+
await copySelectedSkillDirectories(
|
|
799
|
+
this.modelDelegation,
|
|
800
|
+
input.harnessRoot,
|
|
801
|
+
input.projectRoot,
|
|
802
|
+
targetRoot,
|
|
803
|
+
skills,
|
|
804
|
+
);
|
|
624
805
|
await writeFile(
|
|
625
806
|
join(targetRoot, ".harness-version"),
|
|
626
807
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -631,7 +812,10 @@ export class RunReconcileUseCase {
|
|
|
631
812
|
};
|
|
632
813
|
}
|
|
633
814
|
|
|
634
|
-
private planOrphanSkills(
|
|
815
|
+
private planOrphanSkills(
|
|
816
|
+
input: RunReconcileInput,
|
|
817
|
+
manifest: DeploymentManifest,
|
|
818
|
+
): Array<{
|
|
635
819
|
readonly item: ReconcilePlanItem;
|
|
636
820
|
readonly needsBackup: boolean;
|
|
637
821
|
readonly prune: boolean;
|
|
@@ -702,7 +886,12 @@ export class RunReconcileUseCase {
|
|
|
702
886
|
return `shared-skill:${skill}:${version}:${skillSet}`;
|
|
703
887
|
}
|
|
704
888
|
|
|
705
|
-
private personalSkillsVersionHashInput(
|
|
889
|
+
private personalSkillsVersionHashInput(
|
|
890
|
+
path: string,
|
|
891
|
+
version: string,
|
|
892
|
+
skillSet: SkillSet,
|
|
893
|
+
skills: readonly string[],
|
|
894
|
+
): string {
|
|
706
895
|
return `personal-skills-version:${path}:${version}:${skillSet}:${skills.join(",")}`;
|
|
707
896
|
}
|
|
708
897
|
|
|
@@ -723,7 +912,10 @@ export class RunReconcileUseCase {
|
|
|
723
912
|
return `${JSON.stringify(reconcileJsonObject(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {}), null, 2)}\n`;
|
|
724
913
|
}
|
|
725
914
|
|
|
726
|
-
private createTargets(
|
|
915
|
+
private createTargets(options: {
|
|
916
|
+
readonly includeHusky: boolean;
|
|
917
|
+
readonly includeCi: boolean;
|
|
918
|
+
}): readonly ReconcileTarget[] {
|
|
727
919
|
return [
|
|
728
920
|
{ path: ".claude/settings.json", strategy: "json", templatePath: "templates/.claude/settings.json" },
|
|
729
921
|
{
|
|
@@ -739,28 +931,40 @@ export class RunReconcileUseCase {
|
|
|
739
931
|
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
740
932
|
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate AGENTS.md managed section" },
|
|
741
933
|
},
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
934
|
+
...(options.includeHusky
|
|
935
|
+
? ([
|
|
936
|
+
{
|
|
937
|
+
path: ".husky/pre-commit",
|
|
938
|
+
strategy: "shell",
|
|
939
|
+
templatePath: "docs/templates/hooks/pre-commit",
|
|
940
|
+
executable: true,
|
|
941
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate pre-commit managed block" },
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
path: ".husky/commit-msg",
|
|
945
|
+
strategy: "shell",
|
|
946
|
+
templatePath: "docs/templates/hooks/commit-msg",
|
|
947
|
+
executable: true,
|
|
948
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate commit-msg managed block" },
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
path: ".husky/pre-push",
|
|
952
|
+
strategy: "shell",
|
|
953
|
+
templatePath: "docs/templates/hooks/pre-push",
|
|
954
|
+
executable: true,
|
|
955
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate pre-push managed block" },
|
|
956
|
+
},
|
|
957
|
+
] as const satisfies readonly ReconcileTarget[])
|
|
958
|
+
: []),
|
|
959
|
+
...(options.includeCi
|
|
960
|
+
? ([
|
|
961
|
+
{
|
|
962
|
+
path: ".github/workflows/phasegate-aidlc-gate.yml",
|
|
963
|
+
strategy: "yaml-add",
|
|
964
|
+
templatePath: "docs/templates/ci/aidlc-gate.yml",
|
|
965
|
+
},
|
|
966
|
+
] as const satisfies readonly ReconcileTarget[])
|
|
967
|
+
: []),
|
|
764
968
|
{ path: "package.json", strategy: "package-json", templatePath: "package.json" },
|
|
765
969
|
{ path: ".claude/skills", strategy: "symlink" },
|
|
766
970
|
{ path: ".codex/skills", strategy: "symlink" },
|
|
@@ -786,7 +990,11 @@ export class RunReconcileUseCase {
|
|
|
786
990
|
private resolveProjectPath(projectRoot: string, relativePath: string): string {
|
|
787
991
|
const absolutePath = resolve(projectRoot, relativePath);
|
|
788
992
|
const root = resolve(projectRoot);
|
|
789
|
-
if (
|
|
993
|
+
if (
|
|
994
|
+
absolutePath !== root &&
|
|
995
|
+
!absolutePath.startsWith(`${root}/`) &&
|
|
996
|
+
relative(root, absolutePath).startsWith("..")
|
|
997
|
+
) {
|
|
790
998
|
throw new Error(`Manifest entry escapes project root: ${relativePath}`);
|
|
791
999
|
}
|
|
792
1000
|
return absolutePath;
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer domain
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-326
|
|
4
5
|
|
|
5
6
|
import { DeploymentEntry, type DeploymentEntryJson } from "./deployment-entry.js";
|
|
6
7
|
|
|
8
|
+
// Records which install-time options produced this manifest so a later
|
|
9
|
+
// reconcile can honor the original opt-in state instead of assuming defaults.
|
|
10
|
+
// Optional for backward compatibility: manifests written before WI-326 carry
|
|
11
|
+
// no flags, and consumers must fall back to legacy behavior in that case.
|
|
12
|
+
export interface InstallationFlags {
|
|
13
|
+
readonly includeHusky: boolean;
|
|
14
|
+
readonly includeCi: boolean;
|
|
15
|
+
readonly personal: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
export interface DeploymentManifestInput {
|
|
8
19
|
readonly version: string;
|
|
9
20
|
readonly installedAt: string;
|
|
10
21
|
readonly entries: readonly DeploymentEntry[];
|
|
22
|
+
readonly installationFlags?: InstallationFlags;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export interface DeploymentManifestJson {
|
|
14
26
|
readonly version: string;
|
|
15
27
|
readonly installedAt: string;
|
|
16
28
|
readonly entries: readonly DeploymentEntryJson[];
|
|
29
|
+
readonly installationFlags?: InstallationFlags;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export class DeploymentManifest {
|
|
@@ -22,6 +35,7 @@ export class DeploymentManifest {
|
|
|
22
35
|
readonly version: string;
|
|
23
36
|
readonly installedAt: string;
|
|
24
37
|
readonly entries: readonly DeploymentEntry[];
|
|
38
|
+
readonly installationFlags?: InstallationFlags;
|
|
25
39
|
|
|
26
40
|
private constructor(input: DeploymentManifestInput) {
|
|
27
41
|
if (!DeploymentManifest.SEMVER_PATTERN.test(input.version)) {
|
|
@@ -30,6 +44,12 @@ export class DeploymentManifest {
|
|
|
30
44
|
if (!Number.isFinite(Date.parse(input.installedAt))) {
|
|
31
45
|
throw new Error("DeploymentManifest installedAt must be ISO8601-compatible");
|
|
32
46
|
}
|
|
47
|
+
if (input.installationFlags !== undefined) {
|
|
48
|
+
const { includeHusky, includeCi, personal } = input.installationFlags;
|
|
49
|
+
if (typeof includeHusky !== "boolean" || typeof includeCi !== "boolean" || typeof personal !== "boolean") {
|
|
50
|
+
throw new Error("DeploymentManifest installationFlags must contain boolean includeHusky/includeCi/personal");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
33
53
|
const paths = new Set<string>();
|
|
34
54
|
for (const entry of input.entries) {
|
|
35
55
|
if (paths.has(entry.path)) {
|
|
@@ -40,6 +60,14 @@ export class DeploymentManifest {
|
|
|
40
60
|
this.version = input.version;
|
|
41
61
|
this.installedAt = input.installedAt;
|
|
42
62
|
this.entries = Object.freeze([...input.entries]);
|
|
63
|
+
this.installationFlags =
|
|
64
|
+
input.installationFlags === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: Object.freeze({
|
|
67
|
+
includeHusky: input.installationFlags.includeHusky,
|
|
68
|
+
includeCi: input.installationFlags.includeCi,
|
|
69
|
+
personal: input.installationFlags.personal,
|
|
70
|
+
});
|
|
43
71
|
Object.freeze(this);
|
|
44
72
|
}
|
|
45
73
|
|
|
@@ -56,6 +84,7 @@ export class DeploymentManifest {
|
|
|
56
84
|
version: input.version,
|
|
57
85
|
installedAt: input.installedAt,
|
|
58
86
|
entries: input.entries.map((entry) => DeploymentEntry.fromJSON(entry)),
|
|
87
|
+
installationFlags: input.installationFlags,
|
|
59
88
|
});
|
|
60
89
|
}
|
|
61
90
|
|
|
@@ -65,6 +94,7 @@ export class DeploymentManifest {
|
|
|
65
94
|
version: this.version,
|
|
66
95
|
installedAt: this.installedAt,
|
|
67
96
|
entries: [...entries, entry],
|
|
97
|
+
installationFlags: this.installationFlags,
|
|
68
98
|
});
|
|
69
99
|
}
|
|
70
100
|
|
|
@@ -73,6 +103,16 @@ export class DeploymentManifest {
|
|
|
73
103
|
version: this.version,
|
|
74
104
|
installedAt: this.installedAt,
|
|
75
105
|
entries: this.entries.filter((entry) => entry.path !== path),
|
|
106
|
+
installationFlags: this.installationFlags,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
withInstallationFlags(installationFlags: InstallationFlags): DeploymentManifest {
|
|
111
|
+
return DeploymentManifest.reconstitute({
|
|
112
|
+
version: this.version,
|
|
113
|
+
installedAt: this.installedAt,
|
|
114
|
+
entries: this.entries,
|
|
115
|
+
installationFlags,
|
|
76
116
|
});
|
|
77
117
|
}
|
|
78
118
|
|
|
@@ -85,6 +125,9 @@ export class DeploymentManifest {
|
|
|
85
125
|
version: this.version,
|
|
86
126
|
installedAt: this.installedAt,
|
|
87
127
|
entries: this.entries.map((entry) => entry.toJSON()),
|
|
128
|
+
// Omit the key entirely for legacy manifests so serialized output stays
|
|
129
|
+
// byte-compatible with pre-WI-326 manifest.json files.
|
|
130
|
+
...(this.installationFlags === undefined ? {} : { installationFlags: this.installationFlags }),
|
|
88
131
|
};
|
|
89
132
|
}
|
|
90
133
|
}
|