phasegate 0.152.7 → 0.152.9
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 +12 -0
- package/README.md +17 -1
- package/docs/ADR/ADR-002-pre-commit-validators.md +5 -2
- package/docs/ADR/ADR-004-scheduled-validators.md +4 -2
- package/docs/ADR/ADR-013-story-reflection-gate.md +2 -2
- package/docs/guide/cli-reference.md +4 -2
- package/docs/guide/configuration.md +9 -0
- package/docs/guide/getting-started.md +64 -0
- package/docs/guide/installation.md +13 -4
- package/docs/guide/recipes.md +65 -0
- package/docs/guide/setup-artifacts.md +21 -1
- package/docs/guide/troubleshooting.md +60 -0
- package/docs/templates/agent-context/AGENTS.md.template.md +45 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +4 -0
- package/package.json +1 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/agents-md-file-adapter.ts +31 -5
- package/scripts/harness/installation/application/usecases/run-install.ts +78 -4
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +57 -4
- package/scripts/harness/installation/application/usecases/run-uninstall.ts +13 -1
- package/scripts/harness/installation/presentation/cli/install-handler.ts +7 -0
- package/scripts/harness/main.ts +293 -1
- package/skills/phasegate-config-doctor/SKILL.md +4 -0
- package/skills/phasegate-toolkit-guide/SKILL.md +5 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-146
|
|
4
|
+
// @work-item-id WI-174
|
|
4
5
|
|
|
5
6
|
import { mkdir, readFile, writeFile, copyFile, chmod, access, lstat, readlink, symlink } from "node:fs/promises";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
@@ -12,7 +13,7 @@ import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.j
|
|
|
12
13
|
import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
13
14
|
|
|
14
15
|
type InstallAction = "missing" | "will-merge" | "will-skip" | "will-overwrite";
|
|
15
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json";
|
|
16
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed";
|
|
16
17
|
|
|
17
18
|
export interface InstallPlanItem {
|
|
18
19
|
readonly path: string;
|
|
@@ -36,6 +37,9 @@ export interface RunInstallInput {
|
|
|
36
37
|
readonly includeCodex?: boolean;
|
|
37
38
|
readonly includeHusky?: boolean;
|
|
38
39
|
readonly includeCi?: boolean;
|
|
40
|
+
readonly skillSet?: "core" | "all";
|
|
41
|
+
readonly workflow?: "standard" | "strict";
|
|
42
|
+
readonly agent?: "claude" | "codex" | "both";
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface RunInstallResult {
|
|
@@ -58,6 +62,8 @@ const PHASEGATE_SCRIPT_VERSION = "^0.0.0";
|
|
|
58
62
|
|
|
59
63
|
const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
60
64
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
65
|
+
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
66
|
+
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
61
67
|
|
|
62
68
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
63
69
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -131,6 +137,50 @@ function mergeShell(existing: string | null, incoming: string): string {
|
|
|
131
137
|
return `${existing.replace(/\s*$/, "\n\n")}${block}\n`;
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
function managedMarkdownBlock(content: string): string {
|
|
141
|
+
const start = content.indexOf(MARKDOWN_BEGIN);
|
|
142
|
+
const end = content.indexOf(MARKDOWN_END);
|
|
143
|
+
if (start === -1 || end === -1 || end < start) return content.trim();
|
|
144
|
+
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mergeManagedMarkdown(existing: string | null, incoming: string): string {
|
|
148
|
+
const block = managedMarkdownBlock(incoming);
|
|
149
|
+
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
150
|
+
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
151
|
+
if (pattern.test(existing)) return existing.replace(pattern, block).replace(/\s*$/, "\n");
|
|
152
|
+
return `${block}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderAgentContextTemplate(
|
|
156
|
+
template: string,
|
|
157
|
+
options: {
|
|
158
|
+
readonly agent: "claude" | "codex" | "both";
|
|
159
|
+
readonly skillSet: "core" | "all";
|
|
160
|
+
readonly workflow: "standard" | "strict";
|
|
161
|
+
readonly includeHusky: boolean;
|
|
162
|
+
readonly includeCi: boolean;
|
|
163
|
+
},
|
|
164
|
+
): string {
|
|
165
|
+
const commands = [
|
|
166
|
+
"- `phasegate doctor`",
|
|
167
|
+
"- `phasegate phasegate:check-ready`",
|
|
168
|
+
"- `phasegate validate --layer L2 --format human`",
|
|
169
|
+
"- `phasegate setup:agent --dry-run`",
|
|
170
|
+
"- `phasegate config:plan --intent l4-strict --dry-run`",
|
|
171
|
+
].join("\n");
|
|
172
|
+
return template
|
|
173
|
+
.replaceAll("{{PHASEGATE_AGENT}}", options.agent)
|
|
174
|
+
.replaceAll("{{PHASEGATE_SKILLS_MODE}}", options.skillSet)
|
|
175
|
+
.replaceAll("{{PHASEGATE_WORKFLOW}}", options.workflow)
|
|
176
|
+
.replaceAll("{{PHASEGATE_HUSKY_STATE}}", options.includeHusky ? "managed" : "not managed by this setup run")
|
|
177
|
+
.replaceAll("{{PHASEGATE_CI_STATE}}", options.includeCi ? "managed" : "not managed by this setup run")
|
|
178
|
+
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
179
|
+
.replaceAll("{{PHASEGATE_SKILLS}}", options.skillSet === "core" ? "- core skills" : "- all bundled skills")
|
|
180
|
+
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
181
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", "Project-specific agent instructions go here.");
|
|
182
|
+
}
|
|
183
|
+
|
|
134
184
|
function escapeRegExp(value: string): string {
|
|
135
185
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
136
186
|
}
|
|
@@ -192,6 +242,9 @@ export class RunInstallUseCase {
|
|
|
192
242
|
const includeCodex = input.includeCodex ?? true;
|
|
193
243
|
const includeHusky = input.includeHusky ?? true;
|
|
194
244
|
const includeCi = input.includeCi ?? true;
|
|
245
|
+
const skillSet = input.skillSet ?? "all";
|
|
246
|
+
const workflow = input.workflow ?? "standard";
|
|
247
|
+
const agent = input.agent ?? (includeClaude && includeCodex ? "both" : includeCodex ? "codex" : "claude");
|
|
195
248
|
const targets = this.createTargets({ includeClaude, includeCodex, includeHusky, includeCi });
|
|
196
249
|
const existingManifest = await this.manifestRepository.load(input.projectRoot);
|
|
197
250
|
const baseManifest = existingManifest ?? DeploymentManifest.create(input.phasegateVersion);
|
|
@@ -205,7 +258,10 @@ export class RunInstallUseCase {
|
|
|
205
258
|
for (const target of targets) {
|
|
206
259
|
const absolutePath = join(input.projectRoot, target.path);
|
|
207
260
|
const before = await readTextOrNull(absolutePath);
|
|
208
|
-
const
|
|
261
|
+
const rawTemplate = await readFile(join(input.harnessRoot, target.templatePath), "utf8");
|
|
262
|
+
const template = target.strategy === "markdown-managed"
|
|
263
|
+
? renderAgentContextTemplate(rawTemplate, { agent, skillSet, workflow, includeHusky, includeCi })
|
|
264
|
+
: rawTemplate;
|
|
209
265
|
const repairMode = this.repairMode(target, before);
|
|
210
266
|
const next = this.merge(target, before, template, input.phasegateVersion);
|
|
211
267
|
const didChange = before !== next;
|
|
@@ -300,10 +356,26 @@ export class RunInstallUseCase {
|
|
|
300
356
|
}): readonly InstallTarget[] {
|
|
301
357
|
return [
|
|
302
358
|
...(options.includeClaude
|
|
303
|
-
? [
|
|
359
|
+
? [
|
|
360
|
+
{ path: ".claude/settings.json", strategy: "json" as const, templatePath: "templates/.claude/settings.json" },
|
|
361
|
+
{
|
|
362
|
+
path: "CLAUDE.md",
|
|
363
|
+
strategy: "markdown-managed" as const,
|
|
364
|
+
templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
|
|
365
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
|
|
366
|
+
},
|
|
367
|
+
]
|
|
304
368
|
: []),
|
|
305
369
|
...(options.includeCodex
|
|
306
|
-
? [
|
|
370
|
+
? [
|
|
371
|
+
{ path: ".codex/hooks.json", strategy: "json" as const, templatePath: "templates/.codex/hooks.json" },
|
|
372
|
+
{
|
|
373
|
+
path: "AGENTS.md",
|
|
374
|
+
strategy: "markdown-managed" as const,
|
|
375
|
+
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
376
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate AGENTS.md managed section" },
|
|
377
|
+
},
|
|
378
|
+
]
|
|
307
379
|
: []),
|
|
308
380
|
...(options.includeHusky
|
|
309
381
|
? [
|
|
@@ -346,6 +418,7 @@ export class RunInstallUseCase {
|
|
|
346
418
|
private repairMode(target: InstallTarget, before: string | null): RepairMode {
|
|
347
419
|
if (target.strategy === "shell") return shellRepairMode(before);
|
|
348
420
|
if (target.strategy === "json") return jsonRepairMode(before);
|
|
421
|
+
if (target.strategy === "markdown-managed") return "mechanical";
|
|
349
422
|
return "mechanical";
|
|
350
423
|
}
|
|
351
424
|
|
|
@@ -358,6 +431,7 @@ export class RunInstallUseCase {
|
|
|
358
431
|
private merge(target: InstallTarget, before: string | null, template: string, version: string): string {
|
|
359
432
|
if (target.strategy === "yaml-add") return before ?? template;
|
|
360
433
|
if (target.strategy === "shell") return mergeShell(before, template);
|
|
434
|
+
if (target.strategy === "markdown-managed") return mergeManagedMarkdown(before, template);
|
|
361
435
|
if (target.strategy === "package-json") {
|
|
362
436
|
const existing = before === null ? {} : (JSON.parse(before) as unknown);
|
|
363
437
|
const merged = mergePackageJson(isRecord(existing) ? existing : {}, version || PHASEGATE_SCRIPT_VERSION);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-148
|
|
4
|
+
// @work-item-id WI-174
|
|
4
5
|
|
|
5
6
|
import { access, chmod, copyFile, lstat, mkdir, readFile, readlink, symlink, writeFile } from "node:fs/promises";
|
|
6
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -12,7 +13,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
|
12
13
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
13
14
|
|
|
14
15
|
type ReconcileAction = "missing-manifest" | "update" | "add" | "link" | "skip" | "refuse";
|
|
15
|
-
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "symlink" | "unknown";
|
|
16
|
+
type StrategyType = "json" | "shell" | "yaml-add" | "package-json" | "markdown-managed" | "symlink" | "unknown";
|
|
16
17
|
|
|
17
18
|
export interface ReconcilePlanItem {
|
|
18
19
|
readonly path: string;
|
|
@@ -52,6 +53,8 @@ interface ReconcileTarget {
|
|
|
52
53
|
const SKILL_HINT = "invoke /phasegate-config-doctor";
|
|
53
54
|
const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
54
55
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
56
|
+
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
57
|
+
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
55
58
|
|
|
56
59
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
57
60
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -136,6 +139,41 @@ function reconcileShell(existing: string | null, incoming: string): string {
|
|
|
136
139
|
return `${existing.replace(/\s*$/, "\n\n")}${incomingBlock}\n`;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
function managedMarkdownBlock(content: string): string {
|
|
143
|
+
const start = content.indexOf(MARKDOWN_BEGIN);
|
|
144
|
+
const end = content.indexOf(MARKDOWN_END);
|
|
145
|
+
if (start === -1 || end === -1 || end < start) return content.trim();
|
|
146
|
+
return content.slice(start, end + MARKDOWN_END.length).trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function reconcileManagedMarkdown(existing: string | null, incoming: string): string {
|
|
150
|
+
const block = managedMarkdownBlock(incoming);
|
|
151
|
+
if (existing === null || existing.trim().length === 0) return `${incoming.trim()}\n`;
|
|
152
|
+
const pattern = new RegExp(`${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}`);
|
|
153
|
+
if (pattern.test(existing)) return existing.replace(pattern, block).replace(/\s*$/, "\n");
|
|
154
|
+
return `${block}\n\n${existing.replace(/\s*$/, "\n")}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderAgentContextTemplate(template: string): string {
|
|
158
|
+
const commands = [
|
|
159
|
+
"- `phasegate doctor`",
|
|
160
|
+
"- `phasegate phasegate:check-ready`",
|
|
161
|
+
"- `phasegate validate --layer L2 --format human`",
|
|
162
|
+
"- `phasegate setup:agent --dry-run`",
|
|
163
|
+
"- `phasegate config:plan --intent l4-strict --dry-run`",
|
|
164
|
+
].join("\n");
|
|
165
|
+
return template
|
|
166
|
+
.replaceAll("{{PHASEGATE_AGENT}}", "both")
|
|
167
|
+
.replaceAll("{{PHASEGATE_SKILLS_MODE}}", "all")
|
|
168
|
+
.replaceAll("{{PHASEGATE_WORKFLOW}}", "standard")
|
|
169
|
+
.replaceAll("{{PHASEGATE_HUSKY_STATE}}", "managed")
|
|
170
|
+
.replaceAll("{{PHASEGATE_CI_STATE}}", "managed")
|
|
171
|
+
.replaceAll("{{PHASEGATE_COMMANDS}}", commands)
|
|
172
|
+
.replaceAll("{{PHASEGATE_SKILLS}}", "- all bundled skills")
|
|
173
|
+
.replaceAll("{{PHASEGATE_PRESETS}}", "- `minimal`\n- `standard`\n- `full`\n- `custom`")
|
|
174
|
+
.replaceAll("{{PHASEGATE_USER_SECTION}}", "Project-specific agent instructions go here.");
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
function reconcilePackageJson(existing: Record<string, unknown>, version: string): Record<string, unknown> {
|
|
140
178
|
const devDependencies = isRecord(existing.devDependencies) ? existing.devDependencies : {};
|
|
141
179
|
const scripts = isRecord(existing.scripts) ? existing.scripts : {};
|
|
@@ -259,7 +297,8 @@ export class RunReconcileUseCase {
|
|
|
259
297
|
if (before === null) return this.planMissingTarget(input, target);
|
|
260
298
|
const currentHash = this.hashCalculator.compute(before);
|
|
261
299
|
const matchesManifest = currentHash.equals(entry.hash);
|
|
262
|
-
const
|
|
300
|
+
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
301
|
+
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
263
302
|
const next = entry.mode === "created" && target.strategy !== "package-json"
|
|
264
303
|
? template
|
|
265
304
|
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
@@ -290,7 +329,8 @@ export class RunReconcileUseCase {
|
|
|
290
329
|
if (target.strategy === "symlink") return this.planSymlink(input.projectRoot, target.path);
|
|
291
330
|
const absolutePath = this.resolveProjectPath(input.projectRoot, target.path);
|
|
292
331
|
const before = await readTextOrNull(absolutePath);
|
|
293
|
-
const
|
|
332
|
+
const rawTemplate = target.templatePath ? await readFile(join(input.harnessRoot, target.templatePath), "utf8") : "";
|
|
333
|
+
const template = target.strategy === "markdown-managed" ? renderAgentContextTemplate(rawTemplate) : rawTemplate;
|
|
294
334
|
const next = before === null && target.strategy !== "package-json"
|
|
295
335
|
? template
|
|
296
336
|
: this.reconcileContent(target, before, template, input.phasegateVersion);
|
|
@@ -350,6 +390,7 @@ export class RunReconcileUseCase {
|
|
|
350
390
|
private reconcileContent(target: ReconcileTarget, before: string | null, template: string, version: string): string {
|
|
351
391
|
if (target.strategy === "yaml-add") return template;
|
|
352
392
|
if (target.strategy === "shell") return reconcileShell(before, template);
|
|
393
|
+
if (target.strategy === "markdown-managed") return reconcileManagedMarkdown(before, template);
|
|
353
394
|
if (target.strategy === "package-json") {
|
|
354
395
|
const existing = before === null ? {} : (JSON.parse(before) as unknown);
|
|
355
396
|
return `${JSON.stringify(reconcilePackageJson(isRecord(existing) ? existing : {}, version), null, 2)}\n`;
|
|
@@ -362,7 +403,19 @@ export class RunReconcileUseCase {
|
|
|
362
403
|
private createTargets(): readonly ReconcileTarget[] {
|
|
363
404
|
return [
|
|
364
405
|
{ path: ".claude/settings.json", strategy: "json", templatePath: "templates/.claude/settings.json" },
|
|
406
|
+
{
|
|
407
|
+
path: "CLAUDE.md",
|
|
408
|
+
strategy: "markdown-managed",
|
|
409
|
+
templatePath: "docs/templates/agent-context/CLAUDE.md.template.md",
|
|
410
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate CLAUDE.md managed section" },
|
|
411
|
+
},
|
|
365
412
|
{ path: ".codex/hooks.json", strategy: "json", templatePath: "templates/.codex/hooks.json" },
|
|
413
|
+
{
|
|
414
|
+
path: "AGENTS.md",
|
|
415
|
+
strategy: "markdown-managed",
|
|
416
|
+
templatePath: "docs/templates/agent-context/AGENTS.md.template.md",
|
|
417
|
+
block: { start: MARKDOWN_BEGIN, end: MARKDOWN_END, content: "phasegate AGENTS.md managed section" },
|
|
418
|
+
},
|
|
366
419
|
{
|
|
367
420
|
path: ".husky/pre-commit",
|
|
368
421
|
strategy: "shell",
|
|
@@ -401,7 +454,7 @@ export class RunReconcileUseCase {
|
|
|
401
454
|
|
|
402
455
|
private managedBlockFor(path: string, strategy: StrategyType): ManagedBlockInput | null {
|
|
403
456
|
if (strategy === "shell") return { start: SHELL_BEGIN, end: SHELL_END, content: `phasegate ${path} managed block` };
|
|
404
|
-
if (strategy === "json" || strategy === "package-json") {
|
|
457
|
+
if (strategy === "json" || strategy === "package-json" || strategy === "markdown-managed") {
|
|
405
458
|
return { start: "phasegate structured merge", end: "phasegate structured merge", content: `${strategy}:${path}` };
|
|
406
459
|
}
|
|
407
460
|
return null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @unit installation
|
|
2
2
|
// @layer application
|
|
3
3
|
// @work-item-id WI-147
|
|
4
|
+
// @work-item-id WI-174
|
|
4
5
|
|
|
5
6
|
import { access, copyFile, lstat, mkdir, readFile, readlink, rm, rmdir, writeFile } from "node:fs/promises";
|
|
6
7
|
import { dirname, join, relative, resolve } from "node:path";
|
|
@@ -10,7 +11,7 @@ import type { HashCalculatorPort } from "../ports/hash-calculator-port.js";
|
|
|
10
11
|
import type { ManifestRepositoryPort } from "../ports/manifest-repository-port.js";
|
|
11
12
|
|
|
12
13
|
type UninstallAction = "missing-manifest" | "delete" | "unlink" | "reverse-merge" | "skip" | "refuse";
|
|
13
|
-
type StrategyType = "created" | "json" | "shell" | "package-json" | "symlink" | "yaml-add" | "unknown";
|
|
14
|
+
type StrategyType = "created" | "json" | "shell" | "package-json" | "markdown-managed" | "symlink" | "yaml-add" | "unknown";
|
|
14
15
|
|
|
15
16
|
export interface UninstallPlanItem {
|
|
16
17
|
readonly path: string;
|
|
@@ -42,6 +43,8 @@ export interface RunUninstallResult {
|
|
|
42
43
|
const SKILL_HINT = "invoke /phasegate-config-doctor";
|
|
43
44
|
const SHELL_BEGIN = "# === phasegate managed (BEGIN) ===";
|
|
44
45
|
const SHELL_END = "# === phasegate managed (END) ===";
|
|
46
|
+
const MARKDOWN_BEGIN = "<!-- phasegate:managed-section:start -->";
|
|
47
|
+
const MARKDOWN_END = "<!-- phasegate:managed-section:end -->";
|
|
45
48
|
const PHASEGATE_SCRIPT_PREFIX = "phasegate:";
|
|
46
49
|
|
|
47
50
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -108,6 +111,11 @@ export function reverseShellMerge(currentContent: string): string {
|
|
|
108
111
|
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
109
112
|
}
|
|
110
113
|
|
|
114
|
+
export function reverseManagedMarkdown(currentContent: string): string {
|
|
115
|
+
const pattern = new RegExp(`\\n?${escapeRegExp(MARKDOWN_BEGIN)}[\\s\\S]*?${escapeRegExp(MARKDOWN_END)}\\n?`);
|
|
116
|
+
return currentContent.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").replace(/\s*$/, "\n").replace(/^\n/, "");
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
export function reversePackageJsonMerge(currentContent: string): string {
|
|
112
120
|
const parsed = JSON.parse(currentContent) as unknown;
|
|
113
121
|
const result = isRecord(parsed) ? { ...parsed } : {};
|
|
@@ -300,6 +308,7 @@ export class RunUninstallUseCase {
|
|
|
300
308
|
|
|
301
309
|
private async reverseMerged(harnessRoot: string, path: string, currentContent: string, strategy: StrategyType): Promise<string> {
|
|
302
310
|
if (strategy === "shell") return currentContent.includes(SHELL_BEGIN) ? reverseShellMerge(currentContent) : currentContent;
|
|
311
|
+
if (strategy === "markdown-managed") return currentContent.includes(MARKDOWN_BEGIN) ? reverseManagedMarkdown(currentContent) : currentContent;
|
|
303
312
|
if (strategy === "package-json") return reversePackageJsonMerge(currentContent);
|
|
304
313
|
if (strategy === "json") return reverseJsonMerge(currentContent, await readFile(join(harnessRoot, this.templateFor(path)), "utf8"));
|
|
305
314
|
throw new Error(`Unsupported merged strategy: ${strategy}`);
|
|
@@ -309,6 +318,7 @@ export class RunUninstallUseCase {
|
|
|
309
318
|
if (mode === "symlink") return "symlink";
|
|
310
319
|
if (mode === "created") return path.endsWith(".yml") || path.endsWith(".yaml") ? "yaml-add" : "created";
|
|
311
320
|
if (path === "package.json") return "package-json";
|
|
321
|
+
if (path === "AGENTS.md" || path === "CLAUDE.md") return "markdown-managed";
|
|
312
322
|
if (path.endsWith(".json")) return "json";
|
|
313
323
|
if (path.startsWith(".husky/")) return "shell";
|
|
314
324
|
return "unknown";
|
|
@@ -317,6 +327,8 @@ export class RunUninstallUseCase {
|
|
|
317
327
|
private templateFor(path: string): string {
|
|
318
328
|
if (path === ".claude/settings.json") return "templates/.claude/settings.json";
|
|
319
329
|
if (path === ".codex/hooks.json") return "templates/.codex/hooks.json";
|
|
330
|
+
if (path === "CLAUDE.md") return "docs/templates/agent-context/CLAUDE.md.template.md";
|
|
331
|
+
if (path === "AGENTS.md") return "docs/templates/agent-context/AGENTS.md.template.md";
|
|
320
332
|
throw new Error(`No template for ${path}`);
|
|
321
333
|
}
|
|
322
334
|
|
|
@@ -12,6 +12,13 @@ export interface InstallHandlerInput {
|
|
|
12
12
|
readonly apply: boolean;
|
|
13
13
|
readonly force: boolean;
|
|
14
14
|
readonly json: boolean;
|
|
15
|
+
readonly includeClaude?: boolean;
|
|
16
|
+
readonly includeCodex?: boolean;
|
|
17
|
+
readonly includeHusky?: boolean;
|
|
18
|
+
readonly includeCi?: boolean;
|
|
19
|
+
readonly skillSet?: "core" | "all";
|
|
20
|
+
readonly workflow?: "standard" | "strict";
|
|
21
|
+
readonly agent?: "claude" | "codex" | "both";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export interface InstallHandlerResult {
|