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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
6
|
+
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
7
|
+
import type { ConfigStatusProbePort } from "../ports/config-status-probe-port.js";
|
|
8
|
+
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
9
|
+
import { createFinding } from "./check-utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* WI-330 (GitHub #40 恒久化): phasegate.config.json の存在・妥当性を doctor で可視化する。
|
|
13
|
+
* - missing → warn(既定設定の fail-open で動作していることをユーザーに知らせる)
|
|
14
|
+
* - invalid-json / invalid-schema → red(意図した設定が適用されていない)
|
|
15
|
+
* - valid → finding なし
|
|
16
|
+
*/
|
|
17
|
+
export class ConfigStatusCheck implements HeuristicCheck {
|
|
18
|
+
readonly checkId = "config-status" as const;
|
|
19
|
+
|
|
20
|
+
constructor(private readonly probe: ConfigStatusProbePort) {}
|
|
21
|
+
|
|
22
|
+
async run(projectRoot: string, _inspector: FileInspectorPort): Promise<DiagnosticFinding | null> {
|
|
23
|
+
const result = await this.probe.probe(projectRoot);
|
|
24
|
+
if (result.status === "valid") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (result.status === "missing") {
|
|
28
|
+
return createFinding({
|
|
29
|
+
checkId: this.checkId,
|
|
30
|
+
severity: "warn",
|
|
31
|
+
target: result.configPath,
|
|
32
|
+
message:
|
|
33
|
+
"phasegate.config.json が存在しません(既定設定の fail-open モードで動作中)。`phasegate init` で生成できます",
|
|
34
|
+
repairMode: "mechanical",
|
|
35
|
+
repairHint: "npx phasegate init",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const reason = result.status === "invalid-json" ? "JSON 構文エラー" : "スキーマ違反";
|
|
39
|
+
const detail = result.detail !== null && result.detail.length > 0 ? ` — ${result.detail}` : "";
|
|
40
|
+
return createFinding({
|
|
41
|
+
checkId: this.checkId,
|
|
42
|
+
severity: "red",
|
|
43
|
+
target: result.configPath,
|
|
44
|
+
message: `phasegate.config.json が${reason}で壊れており、ユーザーの意図した設定が適用されていません(既定設定の fail-open モードで動作中)${detail}`,
|
|
45
|
+
repairMode: "manual",
|
|
46
|
+
repairHint:
|
|
47
|
+
result.status === "invalid-json"
|
|
48
|
+
? "phasegate.config.json の JSON 構文エラーを修正するか、version control から復元してください"
|
|
49
|
+
: "報告されたパス・型のスキーマ違反を phasegate.config.json 上で修正するか、version control から復元してください",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-145
|
|
4
|
+
// @work-item-id WI-340
|
|
4
5
|
|
|
5
6
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
6
7
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
@@ -22,6 +23,11 @@ export class HuskyPreCommitMissingCheck implements HeuristicCheck {
|
|
|
22
23
|
"main.ts check-phase-gate",
|
|
23
24
|
"$HARNESS_CMD lint",
|
|
24
25
|
"$HARNESS_CMD check-phase-gate",
|
|
26
|
+
"phasegate pre-commit",
|
|
27
|
+
"main.ts pre-commit",
|
|
28
|
+
"$PHASEGATE_CMD lint",
|
|
29
|
+
"$PHASEGATE_CMD check-phase-gate",
|
|
30
|
+
"$PHASEGATE_CMD pre-commit",
|
|
25
31
|
])
|
|
26
32
|
) {
|
|
27
33
|
return null;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @unit installation
|
|
2
|
+
// @layer application
|
|
3
|
+
// @work-item-id WI-330
|
|
4
|
+
|
|
5
|
+
import type { ConfigStatusProbeResult } from "../../domain/config-status.js";
|
|
6
|
+
|
|
7
|
+
export interface ConfigStatusProbePort {
|
|
8
|
+
probe(projectRoot: string): Promise<ConfigStatusProbeResult>;
|
|
9
|
+
}
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
// @work-item-id WI-178
|
|
5
5
|
// @work-item-id WI-208
|
|
6
6
|
// @work-item-id WI-215
|
|
7
|
+
// @work-item-id WI-330
|
|
8
|
+
// @work-item-id WI-343
|
|
7
9
|
|
|
8
|
-
import { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
9
10
|
import type { CheckId } from "../../domain/check-id.js";
|
|
11
|
+
import type { ConfigStatus } from "../../domain/config-status.js";
|
|
10
12
|
import type { DiagnosticFinding } from "../../domain/diagnostic-finding.js";
|
|
13
|
+
import { DiagnosticReport } from "../../domain/diagnostic-report.js";
|
|
11
14
|
import type { HeuristicCheck } from "../../domain/ports/heuristic-check.js";
|
|
15
|
+
import type { ConfigStatusProbePort } from "../ports/config-status-probe-port.js";
|
|
12
16
|
import type { FileInspectorPort } from "../ports/file-inspector-port.js";
|
|
13
17
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
14
18
|
|
|
@@ -29,6 +33,7 @@ export interface RunDoctorDiagnosticsOutput {
|
|
|
29
33
|
readonly report: DiagnosticReport;
|
|
30
34
|
readonly agent: DoctorAgentScope;
|
|
31
35
|
readonly installationMode: "project" | "personal";
|
|
36
|
+
readonly configStatus: ConfigStatus;
|
|
32
37
|
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
33
38
|
readonly exitCode: number;
|
|
34
39
|
}
|
|
@@ -48,15 +53,22 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
48
53
|
private readonly checks: readonly HeuristicCheck[],
|
|
49
54
|
private readonly inspector: FileInspectorPort,
|
|
50
55
|
private readonly manifestRepository: ManifestRepositoryPort,
|
|
56
|
+
private readonly configStatusProbe: ConfigStatusProbePort,
|
|
51
57
|
) {}
|
|
52
58
|
|
|
53
59
|
async execute(input: RunDoctorDiagnosticsInput): Promise<RunDoctorDiagnosticsOutput> {
|
|
54
60
|
const agent = input.agent ?? "both";
|
|
55
61
|
const manifest = await this.manifestRepository.load(input.projectRoot).catch(() => null);
|
|
56
|
-
const installationMode =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const installationMode =
|
|
63
|
+
manifest?.findEntry(".phasegate-local/phasegate.config.json") !== null && manifest !== null
|
|
64
|
+
? "personal"
|
|
65
|
+
: "project";
|
|
66
|
+
const configStatus = (await this.configStatusProbe.probe(input.projectRoot)).status;
|
|
67
|
+
const rawFindings = (
|
|
68
|
+
await Promise.all(
|
|
69
|
+
this.checks.map((check) => check.run(input.projectRoot, this.inspector, { installationMode })),
|
|
70
|
+
)
|
|
71
|
+
).filter((finding) => finding !== null);
|
|
60
72
|
const { findings, scopedOutFindings } = this.applyPersonalScope(
|
|
61
73
|
this.applyAgentScope(rawFindings, agent),
|
|
62
74
|
installationMode,
|
|
@@ -66,6 +78,7 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
66
78
|
report,
|
|
67
79
|
agent,
|
|
68
80
|
installationMode,
|
|
81
|
+
configStatus,
|
|
69
82
|
scopedOutFindings,
|
|
70
83
|
exitCode: this.decideExitCode(report, input.strict),
|
|
71
84
|
};
|
|
@@ -74,7 +87,10 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
74
87
|
private applyAgentScope(
|
|
75
88
|
findings: readonly DiagnosticFinding[],
|
|
76
89
|
agent: DoctorAgentScope,
|
|
77
|
-
): {
|
|
90
|
+
): {
|
|
91
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
92
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
93
|
+
} {
|
|
78
94
|
if (agent === "both") return { findings, scopedOutFindings: [] };
|
|
79
95
|
const scopedOutChecks = agent === "claude" ? CODEX_ONLY_CHECKS : CLAUDE_ONLY_CHECKS;
|
|
80
96
|
const applicable: DiagnosticFinding[] = [];
|
|
@@ -93,9 +109,15 @@ export class RunDoctorDiagnosticsUseCase {
|
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
private applyPersonalScope(
|
|
96
|
-
scoped: {
|
|
112
|
+
scoped: {
|
|
113
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
114
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
115
|
+
},
|
|
97
116
|
installationMode: "project" | "personal",
|
|
98
|
-
): {
|
|
117
|
+
): {
|
|
118
|
+
readonly findings: readonly DiagnosticFinding[];
|
|
119
|
+
readonly scopedOutFindings: readonly ScopedOutDiagnosticFinding[];
|
|
120
|
+
} {
|
|
99
121
|
if (installationMode !== "personal") return scoped;
|
|
100
122
|
const applicable: DiagnosticFinding[] = [];
|
|
101
123
|
const scopedOut: ScopedOutDiagnosticFinding[] = [...scoped.scopedOutFindings];
|
|
@@ -16,20 +16,44 @@
|
|
|
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
|
+
// @work-item-id WI-331
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
access,
|
|
25
|
+
chmod,
|
|
26
|
+
copyFile,
|
|
27
|
+
lstat,
|
|
28
|
+
mkdir,
|
|
29
|
+
readdir,
|
|
30
|
+
readFile,
|
|
31
|
+
readlink,
|
|
32
|
+
rm,
|
|
33
|
+
symlink,
|
|
34
|
+
writeFile,
|
|
35
|
+
} from "node:fs/promises";
|
|
21
36
|
import { dirname, join } from "node:path";
|
|
22
37
|
import { DeploymentEntry } from "../../domain/deployment-entry.js";
|
|
23
|
-
import { DeploymentManifest } from "../../domain/deployment-manifest.js";
|
|
38
|
+
import { DeploymentManifest, type InstallationFlags } from "../../domain/deployment-manifest.js";
|
|
24
39
|
import type { ManagedBlockInput } from "../../domain/managed-block.js";
|
|
25
40
|
import type { RepairMode } from "../../domain/repair-mode.js";
|
|
26
41
|
import { getBundledSkillsForSet, type SkillSet } from "../bundled-skill-selection.js";
|
|
27
|
-
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
28
42
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
43
|
+
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
29
44
|
import type { ModelDelegationPort } from "../ports/model-delegation-port.js";
|
|
30
45
|
|
|
31
46
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
32
|
-
type StrategyType =
|
|
47
|
+
type StrategyType =
|
|
48
|
+
| "json"
|
|
49
|
+
| "shell"
|
|
50
|
+
| "yaml-add"
|
|
51
|
+
| "package-json"
|
|
52
|
+
| "markdown-managed"
|
|
53
|
+
| "text-managed"
|
|
54
|
+
| "copy"
|
|
55
|
+
| "copy-dir"
|
|
56
|
+
| "symlink";
|
|
33
57
|
|
|
34
58
|
export interface InstallPlanItem {
|
|
35
59
|
readonly path: string;
|
|
@@ -96,9 +120,17 @@ const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
|
96
120
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
97
121
|
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
98
122
|
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
123
|
+
const USER_SECTION_BEGIN = "<!-- phasegate:user-section:start -->";
|
|
124
|
+
const USER_SECTION_END = "<!-- phasegate:user-section:end -->";
|
|
125
|
+
const USER_SECTION_PLACEHOLDER = "Project-specific agent instructions go here.";
|
|
99
126
|
const TEXT_BEGIN = "# phasegate personal install exclude (BEGIN)";
|
|
100
127
|
const TEXT_END = "# phasegate personal install exclude (END)";
|
|
101
|
-
const PERSONAL_AGENT_RUNTIME_FILES = new Set([
|
|
128
|
+
const PERSONAL_AGENT_RUNTIME_FILES = new Set([
|
|
129
|
+
".claude/CLAUDE.md",
|
|
130
|
+
".claude/settings.json",
|
|
131
|
+
"AGENTS.md",
|
|
132
|
+
".codex/hooks.json",
|
|
133
|
+
]);
|
|
102
134
|
const SHARED_SKILLS_VERSION_PATH = "skills/.harness-version";
|
|
103
135
|
const PERSONAL_PRINCIPLES_DOCS = ".phasegate-local/docs/principles";
|
|
104
136
|
const PERSONAL_FOLDER_RULES_DOC = ".phasegate-local/docs/folder_management_rules.md";
|
|
@@ -207,7 +239,10 @@ function mergeHookArrays(existing: unknown, incoming: unknown): unknown[] {
|
|
|
207
239
|
return result;
|
|
208
240
|
}
|
|
209
241
|
|
|
210
|
-
function mergeJsonObject(
|
|
242
|
+
function mergeJsonObject(
|
|
243
|
+
existing: Record<string, unknown>,
|
|
244
|
+
incoming: Record<string, unknown>,
|
|
245
|
+
): Record<string, unknown> {
|
|
211
246
|
const result: Record<string, unknown> = { ...existing };
|
|
212
247
|
const existingHooks = isRecord(existing.hooks) ? existing.hooks : {};
|
|
213
248
|
const incomingHooks = isRecord(incoming.hooks) ? incoming.hooks : {};
|
|
@@ -256,12 +291,71 @@ function managedMarkdownBlock(content: string): string {
|
|
|
256
291
|
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
257
292
|
}
|
|
258
293
|
|
|
294
|
+
// Extracts the user-authored body between the user-section markers. Returns
|
|
295
|
+
// null when the markers are absent or the body is blank, so callers fall back
|
|
296
|
+
// to the template placeholder.
|
|
297
|
+
function extractUserSectionBody(content: string | null): string | null {
|
|
298
|
+
if (content === null) return null;
|
|
299
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
300
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
301
|
+
if (start === -1 || end === -1 || end < start) return null;
|
|
302
|
+
const body = content.slice(start + USER_SECTION_BEGIN.length, end).trim();
|
|
303
|
+
return body.length === 0 ? null : body;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function hasUserSectionMarkers(content: string): boolean {
|
|
307
|
+
const start = content.indexOf(USER_SECTION_BEGIN);
|
|
308
|
+
const end = content.indexOf(USER_SECTION_END);
|
|
309
|
+
return start !== -1 && end !== -1 && start < end;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Legacy (pre-WI-331) templates nested the user-section inside the managed
|
|
313
|
+
// section, so replacing the managed block wholesale would wipe user-authored
|
|
314
|
+
// instructions with the template placeholder. Re-inject the existing
|
|
315
|
+
// user-section body into the incoming block. Blocks without user-section
|
|
316
|
+
// markers (all current templates) are returned unchanged; the outside-of-block
|
|
317
|
+
// migration for those is handled in mergeManagedMarkdown.
|
|
318
|
+
function restoreUserSection(block: string, existing: string | null): string {
|
|
319
|
+
const start = block.indexOf(USER_SECTION_BEGIN);
|
|
320
|
+
const end = block.indexOf(USER_SECTION_END);
|
|
321
|
+
if (start === -1 || end === -1 || end < start) return block;
|
|
322
|
+
const preserved = extractUserSectionBody(existing);
|
|
323
|
+
if (preserved === null) return block;
|
|
324
|
+
return `${block.slice(0, start + USER_SECTION_BEGIN.length)}\n${preserved}\n${block.slice(end)}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
259
327
|
function mergeManagedMarkdown(existing: string | null, incoming: string): string {
|
|
260
|
-
const block = managedMarkdownBlock(incoming);
|
|
261
328
|
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
329
|
+
const incomingBlock = managedMarkdownBlock(incoming);
|
|
262
330
|
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
263
|
-
if (pattern.test(existing)) return existing.replace(
|
|
264
|
-
|
|
331
|
+
if (!pattern.test(existing)) return `${incomingBlock}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
332
|
+
|
|
333
|
+
const blockStart = existing.indexOf(MARKDOWN_BEGIN);
|
|
334
|
+
const blockEnd = existing.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
335
|
+
const existingBlock = existing.slice(blockStart, blockEnd);
|
|
336
|
+
const outsideBlock = existing.slice(0, blockStart) + existing.slice(blockEnd);
|
|
337
|
+
const block = restoreUserSection(incomingBlock, existing);
|
|
338
|
+
// Replacer function keeps user-authored text (possibly part of the block)
|
|
339
|
+
// from being interpreted as `$`-substitution patterns by String.replace.
|
|
340
|
+
let next = existing.replace(pattern, () => block);
|
|
341
|
+
|
|
342
|
+
// Structure migration (WI-331): the existing file nests the user-section
|
|
343
|
+
// inside the managed block (old template shape) while the incoming block
|
|
344
|
+
// keeps it outside. Replacing the block would drop the user-authored body,
|
|
345
|
+
// so relocate it to just after the managed block — the position the current
|
|
346
|
+
// template uses. When a user-section already exists outside the block, keep
|
|
347
|
+
// that one and do not duplicate.
|
|
348
|
+
if (
|
|
349
|
+
hasUserSectionMarkers(existingBlock) &&
|
|
350
|
+
!hasUserSectionMarkers(incomingBlock) &&
|
|
351
|
+
!hasUserSectionMarkers(outsideBlock)
|
|
352
|
+
) {
|
|
353
|
+
const body = extractUserSectionBody(existingBlock) ?? USER_SECTION_PLACEHOLDER;
|
|
354
|
+
const insertAt = next.indexOf(MARKDOWN_END) + MARKDOWN_END.length;
|
|
355
|
+
const relocated = `\n\n## User Section\n\n${USER_SECTION_BEGIN}\n${body}\n${USER_SECTION_END}`;
|
|
356
|
+
next = `${next.slice(0, insertAt)}${relocated}${next.slice(insertAt)}`;
|
|
357
|
+
}
|
|
358
|
+
return next.replace(/\s*$/, "\n");
|
|
265
359
|
}
|
|
266
360
|
|
|
267
361
|
function renderAgentContextTemplate(
|
|
@@ -290,7 +384,7 @@ function renderAgentContextTemplate(
|
|
|
290
384
|
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
291
385
|
.replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- `core skills`" : "- `all bundled skills`")
|
|
292
386
|
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
293
|
-
.replaceAll("{{PHASEGATE_USER_SECTION}}",
|
|
387
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", USER_SECTION_PLACEHOLDER);
|
|
294
388
|
}
|
|
295
389
|
|
|
296
390
|
function escapeRegExp(value: string): string {
|
|
@@ -321,7 +415,8 @@ function hasCustomJson(content: string | null): boolean {
|
|
|
321
415
|
const parsed = JSON.parse(content) as unknown;
|
|
322
416
|
if (!isRecord(parsed)) return true;
|
|
323
417
|
const withoutEmptyHooks = { ...parsed };
|
|
324
|
-
if (isRecord(withoutEmptyHooks.hooks) && Object.keys(withoutEmptyHooks.hooks).length === 0)
|
|
418
|
+
if (isRecord(withoutEmptyHooks.hooks) && Object.keys(withoutEmptyHooks.hooks).length === 0)
|
|
419
|
+
delete withoutEmptyHooks.hooks;
|
|
325
420
|
return Object.keys(withoutEmptyHooks).length > 0;
|
|
326
421
|
} catch {
|
|
327
422
|
return true;
|
|
@@ -343,10 +438,13 @@ function likelyCauseFor(code: string): string {
|
|
|
343
438
|
}
|
|
344
439
|
|
|
345
440
|
function recoveryFor(code: string, target: string): string {
|
|
346
|
-
if (code === "EPERM")
|
|
441
|
+
if (code === "EPERM")
|
|
442
|
+
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
443
|
if (code === "EACCES") return `Fix ownership or permissions for ${target}, then rerun phasegate install --apply.`;
|
|
348
|
-
if (code === "EROFS")
|
|
349
|
-
|
|
444
|
+
if (code === "EROFS")
|
|
445
|
+
return `Move the project to a writable filesystem or rerun in a writable workspace before applying ${target}.`;
|
|
446
|
+
if (code === "EEXIST" || code === "ENOTDIR")
|
|
447
|
+
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
448
|
return `Inspect ${target}, run phasegate install --dry-run --json, then rerun with --apply after resolving the filesystem issue.`;
|
|
351
449
|
}
|
|
352
450
|
|
|
@@ -380,12 +478,22 @@ export class RunInstallUseCase {
|
|
|
380
478
|
const skillSet = input.skillSet ?? "all";
|
|
381
479
|
const workflow = input.workflow ?? "standard";
|
|
382
480
|
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
383
|
-
const
|
|
481
|
+
const personal = input.personal ?? false;
|
|
482
|
+
const targets = personal
|
|
384
483
|
? this.createPersonalTargets({ includeClaude, includeCodex })
|
|
385
484
|
: this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
485
|
+
// Persist the effective opt-in state so a later reconcile can honor the
|
|
486
|
+
// original install options. Personal installs never deploy Husky/CI
|
|
487
|
+
// targets, so their effective state is recorded as false regardless of
|
|
488
|
+
// input.
|
|
489
|
+
const installationFlags: InstallationFlags = {
|
|
490
|
+
includeHusky: personal ? false : includeHusky,
|
|
491
|
+
includeCi: personal ? false : includeCi,
|
|
492
|
+
personal,
|
|
493
|
+
};
|
|
386
494
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
387
495
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
388
|
-
let manifest = baseManifest;
|
|
496
|
+
let manifest = baseManifest.withInstallationFlags(installationFlags);
|
|
389
497
|
const plan: InstallPlanItem[] = [];
|
|
390
498
|
const refused: InstallPlanItem[] = [];
|
|
391
499
|
const changed: InstallPlanItem[] = [];
|
|
@@ -396,16 +504,18 @@ export class RunInstallUseCase {
|
|
|
396
504
|
const absolutePath = join(input.projectRoot, target.path);
|
|
397
505
|
const before = await readTextOrNull(absolutePath);
|
|
398
506
|
const rawTemplate = await readFile(join(input.harnessRoot, target.templatePath), "utf8");
|
|
399
|
-
const template =
|
|
400
|
-
|
|
401
|
-
|
|
507
|
+
const template =
|
|
508
|
+
target.strategy === "markdown-managed"
|
|
509
|
+
? renderAgentContextTemplate(rawTemplate, { agent, skillSet, workflow, includeHusky, includeCi })
|
|
510
|
+
: rawTemplate;
|
|
402
511
|
const existingEntry = baseManifest.findEntry(target.path);
|
|
403
512
|
const beforeHash = before === null ? null : this.hashCalculator.compute(before);
|
|
404
|
-
const unmanagedPersonalRuntimeFile =
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
&&
|
|
408
|
-
|
|
513
|
+
const unmanagedPersonalRuntimeFile =
|
|
514
|
+
input.personal &&
|
|
515
|
+
(PERSONAL_AGENT_RUNTIME_FILES.has(target.path) || target.personalManualIfUnmanaged === true) &&
|
|
516
|
+
before !== null &&
|
|
517
|
+
!before.includes(MARKDOWN_BEGIN) &&
|
|
518
|
+
(existingEntry === null || beforeHash === null || !beforeHash.equals(existingEntry.hash));
|
|
409
519
|
const repairMode = unmanagedPersonalRuntimeFile ? "manual" : this.repairMode(target, before);
|
|
410
520
|
const next = unmanagedPersonalRuntimeFile ? before : this.merge(target, before, template, input.phasegateVersion);
|
|
411
521
|
const didChange = before !== next;
|
|
@@ -414,11 +524,11 @@ export class RunInstallUseCase {
|
|
|
414
524
|
// (merge returns `before`). For executable git hooks this means the
|
|
415
525
|
// phasegate hook is NOT wired in — warn instead of skipping silently.
|
|
416
526
|
const preservedExistingHook =
|
|
417
|
-
target.strategy === "copy"
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
527
|
+
target.strategy === "copy" &&
|
|
528
|
+
target.executable === true &&
|
|
529
|
+
before !== null &&
|
|
530
|
+
before.trim() !== template.trim() &&
|
|
531
|
+
!input.force;
|
|
422
532
|
const warning = preservedExistingHook
|
|
423
533
|
? `${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
534
|
: null;
|
|
@@ -432,7 +542,9 @@ export class RunInstallUseCase {
|
|
|
432
542
|
? `${target.path}: existing non-phasegate runtime path requires manual review`
|
|
433
543
|
: warning !== null
|
|
434
544
|
? warning
|
|
435
|
-
: didChange
|
|
545
|
+
: didChange
|
|
546
|
+
? `${target.path}: ${action}`
|
|
547
|
+
: `${target.path}: already up to date`,
|
|
436
548
|
diff: unmanagedPersonalRuntimeFile ? "manual review required" : this.diffSummary(before, next),
|
|
437
549
|
skillHint: repairMode === "ai-assisted" ? SKILL_HINT : null,
|
|
438
550
|
warning,
|
|
@@ -488,10 +600,7 @@ export class RunInstallUseCase {
|
|
|
488
600
|
}
|
|
489
601
|
|
|
490
602
|
const personalSkillTargets = input.personal
|
|
491
|
-
? [
|
|
492
|
-
...(includeClaude ? [".claude/skills"] : []),
|
|
493
|
-
...(includeCodex ? [".codex/skills"] : []),
|
|
494
|
-
]
|
|
603
|
+
? [...(includeClaude ? [".claude/skills"] : []), ...(includeCodex ? [".codex/skills"] : [])]
|
|
495
604
|
: [];
|
|
496
605
|
const selectedPersonalSkills = input.personal ? await listSelectedBundledSkills(input.harnessRoot, skillSet) : [];
|
|
497
606
|
for (const skillPath of personalSkillTargets) {
|
|
@@ -499,7 +608,13 @@ export class RunInstallUseCase {
|
|
|
499
608
|
plan.push(item);
|
|
500
609
|
if (input.apply && item.changed && item.repairMode === "mechanical") {
|
|
501
610
|
try {
|
|
502
|
-
await copySelectedSkillDirectories(
|
|
611
|
+
await copySelectedSkillDirectories(
|
|
612
|
+
this.modelDelegation,
|
|
613
|
+
input.harnessRoot,
|
|
614
|
+
input.projectRoot,
|
|
615
|
+
join(input.projectRoot, skillPath),
|
|
616
|
+
selectedPersonalSkills,
|
|
617
|
+
);
|
|
503
618
|
await writeFile(
|
|
504
619
|
join(input.projectRoot, skillPath, ".harness-version"),
|
|
505
620
|
`${JSON.stringify({ version: input.phasegateVersion, deployedAt: new Date().toISOString(), skillSet }, null, 2)}\n`,
|
|
@@ -512,7 +627,12 @@ export class RunInstallUseCase {
|
|
|
512
627
|
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
513
628
|
path: `${item.path}/.harness-version`,
|
|
514
629
|
mode: "created",
|
|
515
|
-
contentForHash: this.personalSkillsVersionHashInput(
|
|
630
|
+
contentForHash: this.personalSkillsVersionHashInput(
|
|
631
|
+
item.path,
|
|
632
|
+
input.phasegateVersion,
|
|
633
|
+
skillSet,
|
|
634
|
+
selectedPersonalSkills,
|
|
635
|
+
),
|
|
516
636
|
});
|
|
517
637
|
for (const skill of selectedPersonalSkills) {
|
|
518
638
|
manifest = this.addManifestEntry(baseManifest, manifest, {
|
|
@@ -567,7 +687,11 @@ export class RunInstallUseCase {
|
|
|
567
687
|
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "mkdir", error);
|
|
568
688
|
}
|
|
569
689
|
try {
|
|
570
|
-
await symlink(
|
|
690
|
+
await symlink(
|
|
691
|
+
linkSpec.target,
|
|
692
|
+
join(input.projectRoot, linkSpec.path),
|
|
693
|
+
process.platform === "win32" ? "junction" : "dir",
|
|
694
|
+
);
|
|
571
695
|
} catch (error) {
|
|
572
696
|
return this.withApplyError({ plan, refused, changed, backupDir }, linkSpec.path, "symlink", error);
|
|
573
697
|
}
|
|
@@ -586,24 +710,45 @@ export class RunInstallUseCase {
|
|
|
586
710
|
repairMode: "manual",
|
|
587
711
|
strategy: "json",
|
|
588
712
|
changed: false,
|
|
589
|
-
summary:
|
|
713
|
+
summary:
|
|
714
|
+
"~/.codex/config.toml: personal mode does not write user-level Codex feature flags; enable hooks manually when needed",
|
|
590
715
|
diff: "manual Codex hooks feature enablement may be required",
|
|
591
716
|
skillHint: null,
|
|
592
717
|
warning: null,
|
|
593
718
|
});
|
|
594
719
|
}
|
|
595
720
|
|
|
596
|
-
|
|
721
|
+
// Re-save on apply when the recorded install flags drift from a previous
|
|
722
|
+
// manifest (e.g. re-install with different --with-husky/--with-ci), even
|
|
723
|
+
// if no managed file content changed. A missing manifest with zero changes
|
|
724
|
+
// keeps the legacy behavior of not creating one.
|
|
725
|
+
const flagsDrifted =
|
|
726
|
+
existingManifest !== null && !this.installationFlagsEqual(existingManifest.installationFlags, installationFlags);
|
|
727
|
+
if (input.apply && (changed.length > 0 || flagsDrifted)) {
|
|
597
728
|
try {
|
|
598
729
|
await this.manifestRepository.save(input.projectRoot, manifest);
|
|
599
730
|
} catch (error) {
|
|
600
|
-
return this.withApplyError(
|
|
731
|
+
return this.withApplyError(
|
|
732
|
+
{ plan, refused, changed, backupDir },
|
|
733
|
+
".phasegate/manifest.json",
|
|
734
|
+
"manifest-save",
|
|
735
|
+
error,
|
|
736
|
+
);
|
|
601
737
|
}
|
|
602
738
|
}
|
|
603
739
|
|
|
604
740
|
return { plan, refused, changed, backupDir };
|
|
605
741
|
}
|
|
606
742
|
|
|
743
|
+
private installationFlagsEqual(existing: InstallationFlags | undefined, next: InstallationFlags): boolean {
|
|
744
|
+
return (
|
|
745
|
+
existing !== undefined &&
|
|
746
|
+
existing.includeHusky === next.includeHusky &&
|
|
747
|
+
existing.includeCi === next.includeCi &&
|
|
748
|
+
existing.personal === next.personal
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
607
752
|
private withApplyError(
|
|
608
753
|
result: RunInstallResult,
|
|
609
754
|
target: string,
|
|
@@ -638,7 +783,11 @@ export class RunInstallUseCase {
|
|
|
638
783
|
},
|
|
639
784
|
...(options.includeClaude
|
|
640
785
|
? [
|
|
641
|
-
{
|
|
786
|
+
{
|
|
787
|
+
path: ".claude/settings.json",
|
|
788
|
+
strategy: "json" as const,
|
|
789
|
+
templatePath: "templates/.claude/settings.json",
|
|
790
|
+
},
|
|
642
791
|
{
|
|
643
792
|
path: "CLAUDE.md",
|
|
644
793
|
strategy: "markdown-managed" as const,
|
|
@@ -696,7 +845,10 @@ export class RunInstallUseCase {
|
|
|
696
845
|
];
|
|
697
846
|
}
|
|
698
847
|
|
|
699
|
-
private createPersonalTargets(options: {
|
|
848
|
+
private createPersonalTargets(options: {
|
|
849
|
+
readonly includeClaude: boolean;
|
|
850
|
+
readonly includeCodex: boolean;
|
|
851
|
+
}): readonly InstallTarget[] {
|
|
700
852
|
return [
|
|
701
853
|
{
|
|
702
854
|
path: ".phasegate-local/phasegate.config.json",
|
|
@@ -709,7 +861,11 @@ export class RunInstallUseCase {
|
|
|
709
861
|
path: ".claude/CLAUDE.md",
|
|
710
862
|
strategy: "markdown-managed" as const,
|
|
711
863
|
templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
|
|
712
|
-
block: {
|
|
864
|
+
block: {
|
|
865
|
+
start: MARKDOWN_BEGIN,
|
|
866
|
+
end: MARKDOWN_END,
|
|
867
|
+
content: "phasegate personal .claude/CLAUDE.md managed section",
|
|
868
|
+
},
|
|
713
869
|
personalManualIfUnmanaged: true,
|
|
714
870
|
},
|
|
715
871
|
{
|
|
@@ -725,7 +881,11 @@ export class RunInstallUseCase {
|
|
|
725
881
|
path: "AGENTS.md",
|
|
726
882
|
strategy: "markdown-managed" as const,
|
|
727
883
|
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
728
|
-
block: {
|
|
884
|
+
block: {
|
|
885
|
+
start: MARKDOWN_BEGIN,
|
|
886
|
+
end: MARKDOWN_END,
|
|
887
|
+
content: "phasegate personal AGENTS.md managed section",
|
|
888
|
+
},
|
|
729
889
|
personalManualIfUnmanaged: true,
|
|
730
890
|
},
|
|
731
891
|
{
|
|
@@ -861,9 +1021,15 @@ export class RunInstallUseCase {
|
|
|
861
1021
|
break;
|
|
862
1022
|
}
|
|
863
1023
|
}
|
|
864
|
-
const missingManifest =
|
|
865
|
-
|
|
866
|
-
|
|
1024
|
+
const missingManifest =
|
|
1025
|
+
baseManifest.findEntry(`${relativePath}/.harness-version`) === null ||
|
|
1026
|
+
skills.some((skill) => baseManifest.findEntry(`${relativePath}/${skill}`) === null);
|
|
1027
|
+
const changed =
|
|
1028
|
+
current === null ||
|
|
1029
|
+
!current.includes(expectedVersion) ||
|
|
1030
|
+
!current.includes(expectedSkillSet) ||
|
|
1031
|
+
missingSkill ||
|
|
1032
|
+
missingManifest;
|
|
867
1033
|
return {
|
|
868
1034
|
path: relativePath,
|
|
869
1035
|
action: changed ? "missing" : "will-skip",
|
|
@@ -877,7 +1043,12 @@ export class RunInstallUseCase {
|
|
|
877
1043
|
};
|
|
878
1044
|
}
|
|
879
1045
|
|
|
880
|
-
private personalSkillsVersionHashInput(
|
|
1046
|
+
private personalSkillsVersionHashInput(
|
|
1047
|
+
path: string,
|
|
1048
|
+
version: string,
|
|
1049
|
+
skillSet: "core" | "all",
|
|
1050
|
+
skills: readonly string[],
|
|
1051
|
+
): string {
|
|
881
1052
|
return `personal-skills-version:${path}:${version}:${skillSet}:${skills.join(",")}`;
|
|
882
1053
|
}
|
|
883
1054
|
|
|
@@ -902,9 +1073,15 @@ export class RunInstallUseCase {
|
|
|
902
1073
|
break;
|
|
903
1074
|
}
|
|
904
1075
|
}
|
|
905
|
-
const missingManifest =
|
|
906
|
-
|
|
907
|
-
|
|
1076
|
+
const missingManifest =
|
|
1077
|
+
baseManifest.findEntry(SHARED_SKILLS_VERSION_PATH) === null ||
|
|
1078
|
+
skills.some((skill) => baseManifest.findEntry(`skills/${skill}`) === null);
|
|
1079
|
+
const changed =
|
|
1080
|
+
current === null ||
|
|
1081
|
+
!current.includes(expectedVersion) ||
|
|
1082
|
+
!current.includes(expectedSkillSet) ||
|
|
1083
|
+
missingSkill ||
|
|
1084
|
+
missingManifest;
|
|
908
1085
|
return {
|
|
909
1086
|
path: "skills",
|
|
910
1087
|
action: changed ? "missing" : "will-skip",
|
|
@@ -918,7 +1095,11 @@ export class RunInstallUseCase {
|
|
|
918
1095
|
};
|
|
919
1096
|
}
|
|
920
1097
|
|
|
921
|
-
private async deploySharedSkills(
|
|
1098
|
+
private async deploySharedSkills(
|
|
1099
|
+
input: RunInstallInput,
|
|
1100
|
+
skills: readonly string[],
|
|
1101
|
+
skillSet: SkillSet,
|
|
1102
|
+
): Promise<void> {
|
|
922
1103
|
const targetRoot = join(input.projectRoot, "skills");
|
|
923
1104
|
await copySelectedSkillDirectories(this.modelDelegation, input.harnessRoot, input.projectRoot, targetRoot, skills);
|
|
924
1105
|
await writeFile(
|