phasegate 0.264.0 → 0.315.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 +54 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/guide/installation.md +1 -1
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +28 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/ci-governance/composition-root.ts +2 -2
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- 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 +1 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -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 +135 -87
- 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 +2 -22
- 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/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +83 -15
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
- package/scripts/harness/validator-system/composition-root.ts +4 -1
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/quick-implementor/SKILL.md +19 -0
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
// @work-item-id WI-216
|
|
8
8
|
// @work-item-id WI-219
|
|
9
9
|
// @work-item-id WI-264
|
|
10
|
+
// @work-item-id WI-315
|
|
11
|
+
// @work-item-id WI-326
|
|
12
|
+
// @work-item-id WI-331
|
|
10
13
|
|
|
11
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
access,
|
|
16
|
+
chmod,
|
|
17
|
+
copyFile,
|
|
18
|
+
lstat,
|
|
19
|
+
mkdir,
|
|
20
|
+
readdir,
|
|
21
|
+
readFile,
|
|
22
|
+
readlink,
|
|
23
|
+
rm,
|
|
24
|
+
symlink,
|
|
25
|
+
writeFile,
|
|
26
|
+
} from "node:fs/promises";
|
|
12
27
|
import { dirname, join, relative, resolve } from "node:path";
|
|
13
28
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
14
29
|
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
@@ -20,7 +35,15 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
20
35
|
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
21
36
|
|
|
22
37
|
type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse" | "prune";
|
|
23
|
-
type StrategyType =
|
|
38
|
+
type StrategyType =
|
|
39
|
+
| "json"
|
|
40
|
+
| "shell"
|
|
41
|
+
| "yaml-add"
|
|
42
|
+
| "package-json"
|
|
43
|
+
| "markdown-managed"
|
|
44
|
+
| "copy-dir"
|
|
45
|
+
| "symlink"
|
|
46
|
+
| "unknown";
|
|
24
47
|
|
|
25
48
|
export interface ReconcilePlanItem {
|
|
26
49
|
readonly path: string;
|
|
@@ -40,6 +63,12 @@ export interface RunReconcileInput {
|
|
|
40
63
|
readonly dryRun: boolean;
|
|
41
64
|
readonly apply: boolean;
|
|
42
65
|
readonly force: boolean;
|
|
66
|
+
// Explicit per-run overrides. When omitted, the flags recorded in the
|
|
67
|
+
// manifest at install time (installationFlags) are used; manifests written
|
|
68
|
+
// before WI-326 carry no flags and fall back to the legacy behavior of
|
|
69
|
+
// reconciling every bundled target.
|
|
70
|
+
readonly includeHusky?: boolean;
|
|
71
|
+
readonly includeCi?: boolean;
|
|
43
72
|
}
|
|
44
73
|
|
|
45
74
|
export interface RunReconcileResult {
|
|
@@ -62,6 +91,9 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
62
91
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
63
92
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
64
93
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
94
|
+
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
95
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
96
|
+
const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
|
|
65
97
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
66
98
|
const HARNESS_VERSION_BASENAME = ".harness-version";
|
|
67
99
|
const SKILL_ROOT_PREFIXES = ["skills", ".claude/skills", ".codex/skills"] as const;
|
|
@@ -177,7 +209,10 @@ function replaceHookArrays(existing: unknown, incoming: unknown): unknown[] {
|
|
|
177
209
|
return result;
|
|
178
210
|
}
|
|
179
211
|
|
|
180
|
-
function reconcileJsonObject(
|
|
212
|
+
function reconcileJsonObject(
|
|
213
|
+
existing: Record<string, unknown>,
|
|
214
|
+
incoming: Record<string, unknown>,
|
|
215
|
+
): Record<string, unknown> {
|
|
181
216
|
const result: Record<string, unknown> = { ...existing };
|
|
182
217
|
const existingHooks = isRecord(existing.hooks) ? existing.hooks : {};
|
|
183
218
|
const incomingHooks = isRecord(incoming.hooks) ? incoming.hooks : {};
|
|
@@ -220,12 +255,71 @@ function managedMarkdownBlock(content: string): string {
|
|
|
220
255
|
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
221
256
|
}
|
|
222
257
|
|
|
258
|
+
// Extracts the user-authored body between the user-section markers. Returns
|
|
259
|
+
// null when the markers are absent or the body is blank, so callers fall back
|
|
260
|
+
// to the template placeholder.
|
|
261
|
+
function extractUserSectionBody(content: string | null): string | null {
|
|
262
|
+
if (content === null) return null;
|
|
263
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
264
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
265
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
266
|
+
const body = content.slice(start + USER_SECTION_BEGIN.length, end).trim();
|
|
267
|
+
return body.length === 0 ? null : body;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function hasUserSectionMarkers(content: string): boolean {
|
|
271
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
272
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
273
|
+
return start !== -1 && end !== -1 && start < end;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Legacy (pre-WI-331) templates nested the user-section inside the managed
|
|
277
|
+
// section, so replacing the managed block wholesale would wipe user-authored
|
|
278
|
+
// instructions with the template placeholder. Re-inject the existing
|
|
279
|
+
// user-section body into the incoming block. Blocks without user-section
|
|
280
|
+
// markers (all current templates) are returned unchanged; the outside-of-block
|
|
281
|
+
// migration for those is handled in reconcileManagedMarkdown.
|
|
282
|
+
function restoreUserSection(block: string, existing: string | null): string {
|
|
283
|
+
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
284
|
+
const end = block.indexOf(USER_SECTION_END);
|
|
285
|
+
if (start === -1 || end === -1 || end < start) return block;
|
|
286
|
+
const preserved = extractUserSectionBody(existing);
|
|
287
|
+
if (preserved === null) return block;
|
|
288
|
+
return `${block.slice(0, start + USER_SECTION_BEGIN.length)}\n${preserved}\n${block.slice(end)}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
223
291
|
function reconcileManagedMarkdown(existing: string | null, incoming: string): string {
|
|
224
|
-
const block = managedMarkdownBlock(incoming);
|
|
225
292
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
293
|
+
const incomingBlock = managedMarkdownBlock(incoming);
|
|
226
294
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
227
|
-
if (pattern.test(existing)) return existing.replace(
|
|
228
|
-
|
|
295
|
+
if (!pattern.test(existing)) return `${incomingBlock}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
296
|
+
|
|
297
|
+
const blockStart = existing.indexOf(MARKDOWN_BEGIN);
|
|
298
|
+
const blockEnd = existing.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
299
|
+
const existingBlock = existing.slice(blockStart, blockEnd);
|
|
300
|
+
const outsideBlock = existing.slice(0, blockStart) + existing.slice(blockEnd);
|
|
301
|
+
const block = restoreUserSection(incomingBlock, existing);
|
|
302
|
+
// Replacer function keeps user-authored text (possibly part of the block)
|
|
303
|
+
// from being interpreted as `$`-substitution patterns by String.replace.
|
|
304
|
+
let next = existing.replace(pattern, () => block);
|
|
305
|
+
|
|
306
|
+
// Structure migration (WI-331): the existing file nests the user-section
|
|
307
|
+
// inside the managed block (old template shape) while the incoming block
|
|
308
|
+
// keeps it outside. Replacing the block would drop the user-authored body,
|
|
309
|
+
// so relocate it to just after the managed block — the position the current
|
|
310
|
+
// template uses. When a user-section already exists outside the block, keep
|
|
311
|
+
// that one and do not duplicate.
|
|
312
|
+
if (
|
|
313
|
+
hasUserSectionMarkers(existingBlock) &&
|
|
314
|
+
!hasUserSectionMarkers(incomingBlock) &&
|
|
315
|
+
!hasUserSectionMarkers(outsideBlock)
|
|
316
|
+
) {
|
|
317
|
+
const body = extractUserSectionBody(existingBlock) ?? USER_SECTION_PLACEHOLDER;
|
|
318
|
+
const insertAt = next.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
319
|
+
const relocated = `\n\n## User Section\n\n${USER_SECTION_BEGIN}\n${body}\n${USER_SECTION_END}`;
|
|
320
|
+
next = `${next.slice(0, insertAt)}${relocated}${next.slice(insertAt)}`;
|
|
321
|
+
}
|
|
322
|
+
return next.replace(/\s*$/, "\n");
|
|
229
323
|
}
|
|
230
324
|
|
|
231
325
|
function renderAgentContextTemplate(template: string): string {
|
|
@@ -235,7 +329,9 @@ function renderAgentContextTemplate(template: string): string {
|
|
|
235
329
|
"phasegate validate --layer L2 --format human",
|
|
236
330
|
"phasegate setup:agent --dry-run",
|
|
237
331
|
"phasegate config:plan --intent l4-strict --dry-run",
|
|
238
|
-
]
|
|
332
|
+
]
|
|
333
|
+
.map((command) => `- \`${command}\``)
|
|
334
|
+
.join("\n");
|
|
239
335
|
return template
|
|
240
336
|
.replaceAll("{{PHASEGATE_AGENT}}", "both")
|
|
241
337
|
.replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
|
|
@@ -245,7 +341,7 @@ function renderAgentContextTemplate(template: string): string {
|
|
|
245
341
|
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
246
342
|
.replaceAll("{{PHASEGATE_SKILLS}}", "- `all bundled skills`")
|
|
247
343
|
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
248
|
-
.replaceAll("{{PHASEGATE_USER_SECTION}}",
|
|
344
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
|
|
249
345
|
}
|
|
250
346
|
|
|
251
347
|
function reconcilePackageJson(existing: Record<string, unknown>, version: string): Record<string, unknown> {
|
|
@@ -289,12 +385,15 @@ export class RunReconcileUseCase {
|
|
|
289
385
|
return { plan: [item], refused: [], changed: [], backupDir: null };
|
|
290
386
|
}
|
|
291
387
|
|
|
292
|
-
const
|
|
388
|
+
const includeHusky = input.includeHusky ?? manifest.installationFlags?.includeHusky ?? true;
|
|
389
|
+
const includeCi = input.includeCi ?? manifest.installationFlags?.includeCi ?? true;
|
|
390
|
+
const targets = this.createTargets({ includeHusky, includeCi });
|
|
293
391
|
const targetsByPath = new Map(targets.map((target) => [target.path, target]));
|
|
294
392
|
let nextManifest = DeploymentManifest.reconstitute({
|
|
295
393
|
version: input.phasegateVersion,
|
|
296
394
|
installedAt: manifest.installedAt,
|
|
297
395
|
entries: manifest.entries,
|
|
396
|
+
installationFlags: manifest.installationFlags,
|
|
298
397
|
});
|
|
299
398
|
const plan: ReconcilePlanItem[] = [];
|
|
300
399
|
const refused: ReconcilePlanItem[] = [];
|
|
@@ -312,14 +411,28 @@ export class RunReconcileUseCase {
|
|
|
312
411
|
for (const entry of manifest.entries) {
|
|
313
412
|
const target = targetsByPath.get(entry.path);
|
|
314
413
|
if (target === undefined) {
|
|
315
|
-
const item = this.item(
|
|
414
|
+
const item = this.item(
|
|
415
|
+
entry.path,
|
|
416
|
+
"skip",
|
|
417
|
+
"manual",
|
|
418
|
+
"unknown",
|
|
419
|
+
false,
|
|
420
|
+
`${entry.path}: no bundled template`,
|
|
421
|
+
"manual review required",
|
|
422
|
+
SKILL_HINT,
|
|
423
|
+
);
|
|
316
424
|
plan.push(item);
|
|
317
425
|
continue;
|
|
318
426
|
}
|
|
319
427
|
const outcome = await this.planManagedEntry(input, entry, target);
|
|
320
428
|
outcomes.push(outcome);
|
|
321
429
|
plan.push(outcome.item);
|
|
322
|
-
if (
|
|
430
|
+
if (
|
|
431
|
+
input.apply &&
|
|
432
|
+
outcome.item.changed &&
|
|
433
|
+
(outcome.item.repairMode === "ai-assisted" || outcome.item.repairMode === "manual") &&
|
|
434
|
+
!input.force
|
|
435
|
+
) {
|
|
323
436
|
refused.push({ ...outcome.item, action: "refuse" });
|
|
324
437
|
}
|
|
325
438
|
}
|
|
@@ -329,7 +442,12 @@ export class RunReconcileUseCase {
|
|
|
329
442
|
const outcome = await this.planMissingTarget(input, target);
|
|
330
443
|
outcomes.push(outcome);
|
|
331
444
|
plan.push(outcome.item);
|
|
332
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
input.apply &&
|
|
447
|
+
outcome.item.changed &&
|
|
448
|
+
(outcome.item.repairMode === "ai-assisted" || outcome.item.repairMode === "manual") &&
|
|
449
|
+
!input.force
|
|
450
|
+
) {
|
|
333
451
|
refused.push({ ...outcome.item, action: "refuse" });
|
|
334
452
|
}
|
|
335
453
|
}
|
|
@@ -376,18 +494,43 @@ export class RunReconcileUseCase {
|
|
|
376
494
|
if (hashContent !== null) {
|
|
377
495
|
if (outcome.item.strategy === "copy-dir" && outcome.item.path === "skills") {
|
|
378
496
|
const sharedSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
379
|
-
nextManifest = nextManifest.addEntry(
|
|
497
|
+
nextManifest = nextManifest.addEntry(
|
|
498
|
+
this.createdEntry(
|
|
499
|
+
SHARED_SKILLS_VERSION_PATH,
|
|
500
|
+
this.sharedSkillsVersionHashInput(input.phasegateVersion, "all", sharedSkills),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
380
503
|
for (const skill of sharedSkills) {
|
|
381
|
-
nextManifest = nextManifest.addEntry(
|
|
504
|
+
nextManifest = nextManifest.addEntry(
|
|
505
|
+
this.createdEntry(`skills/${skill}`, this.sharedSkillHashInput(skill, input.phasegateVersion, "all")),
|
|
506
|
+
);
|
|
382
507
|
}
|
|
383
|
-
} else if (
|
|
508
|
+
} else if (
|
|
509
|
+
outcome.item.strategy === "copy-dir" &&
|
|
510
|
+
(outcome.item.path === ".claude/skills" || outcome.item.path === ".codex/skills")
|
|
511
|
+
) {
|
|
384
512
|
const personalSkills = await listSelectedBundledSkills(input.harnessRoot, "all");
|
|
385
|
-
nextManifest = nextManifest.addEntry(
|
|
513
|
+
nextManifest = nextManifest.addEntry(
|
|
514
|
+
this.createdEntry(
|
|
515
|
+
`${outcome.item.path}/.harness-version`,
|
|
516
|
+
this.personalSkillsVersionHashInput(outcome.item.path, input.phasegateVersion, "all", personalSkills),
|
|
517
|
+
),
|
|
518
|
+
);
|
|
386
519
|
for (const skill of personalSkills) {
|
|
387
|
-
nextManifest = nextManifest.addEntry(
|
|
520
|
+
nextManifest = nextManifest.addEntry(
|
|
521
|
+
this.createdEntry(
|
|
522
|
+
`${outcome.item.path}/${skill}`,
|
|
523
|
+
this.personalSkillHashInput(outcome.item.path, skill, input.phasegateVersion, "all"),
|
|
524
|
+
),
|
|
525
|
+
);
|
|
388
526
|
}
|
|
389
527
|
} else {
|
|
390
|
-
const mode =
|
|
528
|
+
const mode =
|
|
529
|
+
outcome.item.strategy === "symlink"
|
|
530
|
+
? "symlink"
|
|
531
|
+
: outcome.item.action === "add"
|
|
532
|
+
? "created"
|
|
533
|
+
: (manifest.findEntry(outcome.item.path)?.mode ?? "merged");
|
|
391
534
|
nextManifest = nextManifest.addEntry(
|
|
392
535
|
DeploymentEntry.create({
|
|
393
536
|
path: outcome.item.path,
|
|
@@ -416,9 +559,10 @@ export class RunReconcileUseCase {
|
|
|
416
559
|
const matchesManifest = currentHash.equals(entry.hash);
|
|
417
560
|
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
418
561
|
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
419
|
-
const next =
|
|
420
|
-
|
|
421
|
-
|
|
562
|
+
const next =
|
|
563
|
+
entry.mode === "created" && target.strategy !== "package-json" && target.strategy !== "markdown-managed"
|
|
564
|
+
? template
|
|
565
|
+
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
422
566
|
const changed = before !== next;
|
|
423
567
|
const repairMode: RepairMode = matchesManifest ? "mechanical" : "ai-assisted";
|
|
424
568
|
return {
|
|
@@ -448,11 +592,13 @@ export class RunReconcileUseCase {
|
|
|
448
592
|
const before = await readTextOrNull(absolutePath);
|
|
449
593
|
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
450
594
|
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
451
|
-
const next =
|
|
452
|
-
|
|
453
|
-
|
|
595
|
+
const next =
|
|
596
|
+
before === null && target.strategy !== "package-json"
|
|
597
|
+
? template
|
|
598
|
+
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
454
599
|
const changed = before !== next;
|
|
455
|
-
const repairMode: RepairMode =
|
|
600
|
+
const repairMode: RepairMode =
|
|
601
|
+
target.strategy === "shell" && before !== null && !before.includes(SHELL_BEGIN) ? "ai-assisted" : "mechanical";
|
|
456
602
|
return {
|
|
457
603
|
item: this.item(
|
|
458
604
|
target.path,
|
|
@@ -480,19 +626,46 @@ export class RunReconcileUseCase {
|
|
|
480
626
|
const stat = await lstat(absolutePath);
|
|
481
627
|
if (stat.isSymbolicLink() && (await readlink(absolutePath)) === "../skills") {
|
|
482
628
|
return {
|
|
483
|
-
item: this.item(
|
|
629
|
+
item: this.item(
|
|
630
|
+
relativePath,
|
|
631
|
+
"skip",
|
|
632
|
+
"mechanical",
|
|
633
|
+
"symlink",
|
|
634
|
+
false,
|
|
635
|
+
`${relativePath}: already linked`,
|
|
636
|
+
"no changes",
|
|
637
|
+
null,
|
|
638
|
+
),
|
|
484
639
|
needsBackup: false,
|
|
485
640
|
apply: async () => "../skills",
|
|
486
641
|
};
|
|
487
642
|
}
|
|
488
643
|
return {
|
|
489
|
-
item: this.item(
|
|
644
|
+
item: this.item(
|
|
645
|
+
relativePath,
|
|
646
|
+
"skip",
|
|
647
|
+
"manual",
|
|
648
|
+
"symlink",
|
|
649
|
+
false,
|
|
650
|
+
`${relativePath}: existing non-phasegate path requires manual review`,
|
|
651
|
+
"manual review required",
|
|
652
|
+
SKILL_HINT,
|
|
653
|
+
),
|
|
490
654
|
needsBackup: false,
|
|
491
655
|
apply: async () => null,
|
|
492
656
|
};
|
|
493
657
|
} catch {
|
|
494
658
|
return {
|
|
495
|
-
item: this.item(
|
|
659
|
+
item: this.item(
|
|
660
|
+
relativePath,
|
|
661
|
+
"link",
|
|
662
|
+
"mechanical",
|
|
663
|
+
"symlink",
|
|
664
|
+
true,
|
|
665
|
+
`${relativePath}: create symlink`,
|
|
666
|
+
"+ symlink ../skills",
|
|
667
|
+
null,
|
|
668
|
+
),
|
|
496
669
|
needsBackup: false,
|
|
497
670
|
apply: async () => {
|
|
498
671
|
await mkdir(join(projectRoot, "skills"), { recursive: true });
|
|
@@ -505,21 +678,40 @@ export class RunReconcileUseCase {
|
|
|
505
678
|
}
|
|
506
679
|
|
|
507
680
|
private manifestIntendsSharedSkills(manifest: DeploymentManifest): boolean {
|
|
508
|
-
return
|
|
509
|
-
|
|
510
|
-
|
|
681
|
+
return (
|
|
682
|
+
manifest.findEntry(".claude/skills") !== null ||
|
|
683
|
+
manifest.findEntry(".codex/skills") !== null ||
|
|
684
|
+
manifest.entries.some((entry) => entry.path.startsWith("skills/"))
|
|
685
|
+
);
|
|
511
686
|
}
|
|
512
687
|
|
|
513
688
|
private isPersonalManifest(manifest: DeploymentManifest): boolean {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
689
|
+
// Manifests written since WI-326 record the install mode explicitly;
|
|
690
|
+
// older manifests fall back to the legacy entry-shape heuristic.
|
|
691
|
+
if (manifest.installationFlags !== undefined) return manifest.installationFlags.personal;
|
|
692
|
+
return (
|
|
693
|
+
manifest.findEntry(".phasegate-local/phasegate.config.json") !== null ||
|
|
694
|
+
manifest.entries.some(
|
|
695
|
+
(entry) => (entry.path === ".claude/skills" || entry.path === ".codex/skills") && entry.mode === "created",
|
|
696
|
+
) ||
|
|
697
|
+
manifest.entries.some(
|
|
698
|
+
(entry) => entry.path.startsWith(".claude/skills/") || entry.path.startsWith(".codex/skills/"),
|
|
699
|
+
)
|
|
700
|
+
);
|
|
517
701
|
}
|
|
518
702
|
|
|
519
703
|
private personalSkillPaths(manifest: DeploymentManifest): Array<".claude/skills" | ".codex/skills"> {
|
|
520
704
|
const paths = new Set<".claude/skills" | ".codex/skills">();
|
|
521
|
-
if (
|
|
522
|
-
|
|
705
|
+
if (
|
|
706
|
+
manifest.findEntry(".claude/skills") !== null ||
|
|
707
|
+
manifest.entries.some((entry) => entry.path.startsWith(".claude/skills/"))
|
|
708
|
+
)
|
|
709
|
+
paths.add(".claude/skills");
|
|
710
|
+
if (
|
|
711
|
+
manifest.findEntry(".codex/skills") !== null ||
|
|
712
|
+
manifest.entries.some((entry) => entry.path.startsWith(".codex/skills/"))
|
|
713
|
+
)
|
|
714
|
+
paths.add(".codex/skills");
|
|
523
715
|
return [...paths].sort();
|
|
524
716
|
}
|
|
525
717
|
|
|
@@ -538,8 +730,9 @@ export class RunReconcileUseCase {
|
|
|
538
730
|
break;
|
|
539
731
|
}
|
|
540
732
|
}
|
|
541
|
-
const missingManifest =
|
|
542
|
-
|
|
733
|
+
const missingManifest =
|
|
734
|
+
manifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null ||
|
|
735
|
+
skills.some((skill) => manifest.findEntry(`skills/${skill}`) === null);
|
|
543
736
|
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
544
737
|
this.modelDelegation,
|
|
545
738
|
input.harnessRoot,
|
|
@@ -547,7 +740,12 @@ export class RunReconcileUseCase {
|
|
|
547
740
|
this.resolveProjectPath(input.projectRoot, "skills"),
|
|
548
741
|
skills,
|
|
549
742
|
);
|
|
550
|
-
const changed =
|
|
743
|
+
const changed =
|
|
744
|
+
versionContent === null ||
|
|
745
|
+
!versionContent.includes(expectedVersion) ||
|
|
746
|
+
missingSkill ||
|
|
747
|
+
missingManifest ||
|
|
748
|
+
renderedSkillDrift;
|
|
551
749
|
return {
|
|
552
750
|
item: this.item(
|
|
553
751
|
"skills",
|
|
@@ -568,7 +766,11 @@ export class RunReconcileUseCase {
|
|
|
568
766
|
};
|
|
569
767
|
}
|
|
570
768
|
|
|
571
|
-
private async deploySharedSkills(
|
|
769
|
+
private async deploySharedSkills(
|
|
770
|
+
input: RunReconcileInput,
|
|
771
|
+
skills: readonly string[],
|
|
772
|
+
skillSet: SkillSet,
|
|
773
|
+
): Promise<void> {
|
|
572
774
|
const targetRoot = this.resolveProjectPath(input.projectRoot, "skills");
|
|
573
775
|
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
574
776
|
await writeFile(
|
|
@@ -595,8 +797,9 @@ export class RunReconcileUseCase {
|
|
|
595
797
|
break;
|
|
596
798
|
}
|
|
597
799
|
}
|
|
598
|
-
const missingManifest =
|
|
599
|
-
|
|
800
|
+
const missingManifest =
|
|
801
|
+
manifest.findEntry(`${relativePath}/.harness-version`) === null ||
|
|
802
|
+
skills.some((skill) => manifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
600
803
|
const renderedSkillDrift = await hasRenderedSkillDrift(
|
|
601
804
|
this.modelDelegation,
|
|
602
805
|
input.harnessRoot,
|
|
@@ -604,7 +807,12 @@ export class RunReconcileUseCase {
|
|
|
604
807
|
this.resolveProjectPath(input.projectRoot, relativePath),
|
|
605
808
|
skills,
|
|
606
809
|
);
|
|
607
|
-
const changed =
|
|
810
|
+
const changed =
|
|
811
|
+
versionContent === null ||
|
|
812
|
+
!versionContent.includes(expectedVersion) ||
|
|
813
|
+
missingSkill ||
|
|
814
|
+
missingManifest ||
|
|
815
|
+
renderedSkillDrift;
|
|
608
816
|
return {
|
|
609
817
|
item: this.item(
|
|
610
818
|
relativePath,
|
|
@@ -620,7 +828,13 @@ export class RunReconcileUseCase {
|
|
|
620
828
|
apply: async () => {
|
|
621
829
|
if (!changed) return null;
|
|
622
830
|
const targetRoot = this.resolveProjectPath(input.projectRoot, relativePath);
|
|
623
|
-
await copySelectedSkillDirectories(
|
|
831
|
+
await copySelectedSkillDirectories(
|
|
832
|
+
this.modelDelegation,
|
|
833
|
+
input.harnessRoot,
|
|
834
|
+
input.projectRoot,
|
|
835
|
+
targetRoot,
|
|
836
|
+
skills,
|
|
837
|
+
);
|
|
624
838
|
await writeFile(
|
|
625
839
|
join(targetRoot, ".harness-version"),
|
|
626
840
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -631,7 +845,10 @@ export class RunReconcileUseCase {
|
|
|
631
845
|
};
|
|
632
846
|
}
|
|
633
847
|
|
|
634
|
-
private planOrphanSkills(
|
|
848
|
+
private planOrphanSkills(
|
|
849
|
+
input: RunReconcileInput,
|
|
850
|
+
manifest: DeploymentManifest,
|
|
851
|
+
): Array<{
|
|
635
852
|
readonly item: ReconcilePlanItem;
|
|
636
853
|
readonly needsBackup: boolean;
|
|
637
854
|
readonly prune: boolean;
|
|
@@ -702,7 +919,12 @@ export class RunReconcileUseCase {
|
|
|
702
919
|
return `shared-skill:${skill}:${version}:${skillSet}`;
|
|
703
920
|
}
|
|
704
921
|
|
|
705
|
-
private personalSkillsVersionHashInput(
|
|
922
|
+
private personalSkillsVersionHashInput(
|
|
923
|
+
path: string,
|
|
924
|
+
version: string,
|
|
925
|
+
skillSet: SkillSet,
|
|
926
|
+
skills: readonly string[],
|
|
927
|
+
): string {
|
|
706
928
|
return `personal-skills-version:${path}:${version}:${skillSet}:${skills.join(",")}`;
|
|
707
929
|
}
|
|
708
930
|
|
|
@@ -723,7 +945,10 @@ export class RunReconcileUseCase {
|
|
|
723
945
|
return `${JSON.stringify(reconcileJsonObject(isRecord(existing) ? existing : {}, isRecord(incoming) ? incoming : {}), null, 2)}\n`;
|
|
724
946
|
}
|
|
725
947
|
|
|
726
|
-
private createTargets(
|
|
948
|
+
private createTargets(options: {
|
|
949
|
+
readonly includeHusky: boolean;
|
|
950
|
+
readonly includeCi: boolean;
|
|
951
|
+
}): readonly ReconcileTarget[] {
|
|
727
952
|
return [
|
|
728
953
|
{ path: ".claude/settings.json", strategy: "json", templatePath: "templates/.claude/settings.json" },
|
|
729
954
|
{
|
|
@@ -739,28 +964,40 @@ export class RunReconcileUseCase {
|
|
|
739
964
|
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
740
965
|
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate AGENTS.md managed section" },
|
|
741
966
|
},
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
967
|
+
...(options.includeHusky
|
|
968
|
+
? ([
|
|
969
|
+
{
|
|
970
|
+
path: ".husky/pre-commit",
|
|
971
|
+
strategy: "shell",
|
|
972
|
+
templatePath: "docs/templates/hooks/pre-commit",
|
|
973
|
+
executable: true,
|
|
974
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate pre-commit managed block" },
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
path: ".husky/commit-msg",
|
|
978
|
+
strategy: "shell",
|
|
979
|
+
templatePath: "docs/templates/hooks/commit-msg",
|
|
980
|
+
executable: true,
|
|
981
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate commit-msg managed block" },
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
path: ".husky/pre-push",
|
|
985
|
+
strategy: "shell",
|
|
986
|
+
templatePath: "docs/templates/hooks/pre-push",
|
|
987
|
+
executable: true,
|
|
988
|
+
block: { start: SHELL_BEGIN, end: SHELL_END, content: "phasegate pre-push managed block" },
|
|
989
|
+
},
|
|
990
|
+
] as const satisfies readonly ReconcileTarget[])
|
|
991
|
+
: []),
|
|
992
|
+
...(options.includeCi
|
|
993
|
+
? ([
|
|
994
|
+
{
|
|
995
|
+
path: ".github/workflows/phasegate-aidlc-gate.yml",
|
|
996
|
+
strategy: "yaml-add",
|
|
997
|
+
templatePath: "docs/templates/ci/aidlc-gate.yml",
|
|
998
|
+
},
|
|
999
|
+
] as const satisfies readonly ReconcileTarget[])
|
|
1000
|
+
: []),
|
|
764
1001
|
{ path: "package.json", strategy: "package-json", templatePath: "package.json" },
|
|
765
1002
|
{ path: ".claude/skills", strategy: "symlink" },
|
|
766
1003
|
{ path: ".codex/skills", strategy: "symlink" },
|
|
@@ -786,7 +1023,11 @@ export class RunReconcileUseCase {
|
|
|
786
1023
|
private resolveProjectPath(projectRoot: string, relativePath: string): string {
|
|
787
1024
|
const absolutePath = resolve(projectRoot, relativePath);
|
|
788
1025
|
const root = resolve(projectRoot);
|
|
789
|
-
if (
|
|
1026
|
+
if (
|
|
1027
|
+
absolutePath !== root &&
|
|
1028
|
+
!absolutePath.startsWith(`${root}/`) &&
|
|
1029
|
+
relative(root, absolutePath).startsWith("..")
|
|
1030
|
+
) {
|
|
790
1031
|
throw new Error(`Manifest entry escapes project root: ${relativePath}`);
|
|
791
1032
|
}
|
|
792
1033
|
return absolutePath;
|
|
@@ -3,26 +3,29 @@
|
|
|
3
3
|
// @work-item-id WI-145
|
|
4
4
|
// @work-item-id WI-148
|
|
5
5
|
// @work-item-id WI-215
|
|
6
|
+
// @work-item-id WI-330
|
|
6
7
|
|
|
8
|
+
import { CiWorkflowMissingCheck } from "./application/checks/ci-workflow-missing-check.js";
|
|
7
9
|
import { ClaudeContextMissingCheck } from "./application/checks/claude-context-missing-check.js";
|
|
8
10
|
import { ClaudeHookMissingCheck } from "./application/checks/claude-hook-missing-check.js";
|
|
9
11
|
import { ClaudeSkillsSymlinkCheck } from "./application/checks/claude-skills-symlink-check.js";
|
|
10
12
|
import { CodexContextMissingCheck } from "./application/checks/codex-context-missing-check.js";
|
|
11
13
|
import { CodexHookMissingCheck } from "./application/checks/codex-hook-missing-check.js";
|
|
12
14
|
import { CodexSkillsSymlinkCheck } from "./application/checks/codex-skills-symlink-check.js";
|
|
13
|
-
import {
|
|
15
|
+
import { ConfigStatusCheck } from "./application/checks/config-status-check.js";
|
|
14
16
|
import { HuskyCommitMsgMissingCheck } from "./application/checks/husky-commit-msg-missing-check.js";
|
|
15
17
|
import { HuskyPreCommitMissingCheck } from "./application/checks/husky-pre-commit-missing-check.js";
|
|
16
18
|
import { HuskyPrePushMissingCheck } from "./application/checks/husky-pre-push-missing-check.js";
|
|
17
19
|
import { PackageJsonDevdepMissingCheck } from "./application/checks/package-json-devdep-missing-check.js";
|
|
18
20
|
import { WiWorkflowDriftCheck } from "./application/checks/wi-workflow-drift-check.js";
|
|
21
|
+
import { RunDoctorDiagnosticsUseCase } from "./application/usecases/run-doctor-diagnostics.js";
|
|
19
22
|
import { RunInstallUseCase } from "./application/usecases/run-install.js";
|
|
20
23
|
import { RunReconcileUseCase } from "./application/usecases/run-reconcile.js";
|
|
21
24
|
import { RunUninstallUseCase } from "./application/usecases/run-uninstall.js";
|
|
22
|
-
import { RunDoctorDiagnosticsUseCase } from "./application/usecases/run-doctor-diagnostics.js";
|
|
23
25
|
import type { MergeStrategy } from "./domain/ports/merge-strategy.js";
|
|
24
26
|
import type { ReconcileStrategy } from "./domain/ports/reconcile-strategy.js";
|
|
25
27
|
import type { UninstallReverseStrategy } from "./domain/ports/uninstall-reverse-strategy.js";
|
|
28
|
+
import { ConfigStatusProbeAdapter } from "./infrastructure/adapters/config-status-probe-adapter.js";
|
|
26
29
|
import { FileSystemManifestRepositoryAdapter } from "./infrastructure/adapters/file-system-manifest-repository-adapter.js";
|
|
27
30
|
import { NodeCryptoHashAdapter } from "./infrastructure/adapters/node-crypto-hash-adapter.js";
|
|
28
31
|
import { NodeFsFileInspectorAdapter } from "./infrastructure/adapters/node-fs-file-inspector-adapter.js";
|
|
@@ -45,6 +48,7 @@ export function createInstallationModule() {
|
|
|
45
48
|
const manifestRepository = new FileSystemManifestRepositoryAdapter();
|
|
46
49
|
const hashCalculator = new NodeCryptoHashAdapter();
|
|
47
50
|
const modelDelegation = new SkillDeployerModelDelegationAdapter();
|
|
51
|
+
const configStatusProbe = new ConfigStatusProbeAdapter();
|
|
48
52
|
const checks = [
|
|
49
53
|
new ClaudeHookMissingCheck(),
|
|
50
54
|
new ClaudeContextMissingCheck(),
|
|
@@ -58,8 +62,14 @@ export function createInstallationModule() {
|
|
|
58
62
|
new ClaudeSkillsSymlinkCheck(),
|
|
59
63
|
new CodexSkillsSymlinkCheck(),
|
|
60
64
|
new WiWorkflowDriftCheck(),
|
|
65
|
+
new ConfigStatusCheck(configStatusProbe),
|
|
61
66
|
];
|
|
62
|
-
const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(
|
|
67
|
+
const runDoctorDiagnosticsUseCase = new RunDoctorDiagnosticsUseCase(
|
|
68
|
+
checks,
|
|
69
|
+
inspector,
|
|
70
|
+
manifestRepository,
|
|
71
|
+
configStatusProbe,
|
|
72
|
+
);
|
|
63
73
|
const runInstallUseCase = new RunInstallUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
64
74
|
const runReconcileUseCase = new RunReconcileUseCase(manifestRepository, hashCalculator, modelDelegation);
|
|
65
75
|
const runUninstallUseCase = new RunUninstallUseCase(manifestRepository, hashCalculator);
|