@kylecheng3146/agent-ops 0.3.1 → 0.3.2
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/dist/packages/cli/src/args.js +19 -1
- package/dist/packages/cli/src/bin.js +1 -0
- package/dist/packages/cli/src/cli.js +3 -0
- package/dist/packages/cli/src/commands/init.js +27 -3
- package/dist/packages/cli/src/commands/update.js +47 -3
- package/dist/packages/cli/src/wizard.js +21 -2
- package/dist/runtime/src/install/parallel-setup.js +63 -0
- package/dist/runtime/src/install/plan.js +8 -7
- package/dist/runtime/src/install/update.js +4 -1
- package/package.json +1 -1
|
@@ -86,6 +86,7 @@ export function parseArgs(argv) {
|
|
|
86
86
|
let versionSeen = false;
|
|
87
87
|
let worktreeName;
|
|
88
88
|
let force = false;
|
|
89
|
+
let worktree;
|
|
89
90
|
for (let index = 0; index < argv.length; index += 1) {
|
|
90
91
|
const token = argv[index];
|
|
91
92
|
if (token === undefined) {
|
|
@@ -229,6 +230,18 @@ export function parseArgs(argv) {
|
|
|
229
230
|
}
|
|
230
231
|
completionGate = true;
|
|
231
232
|
break;
|
|
233
|
+
case "--worktree": {
|
|
234
|
+
if (worktree !== undefined) {
|
|
235
|
+
duplicate(token);
|
|
236
|
+
}
|
|
237
|
+
const value = readOptionValue(argv, index, token);
|
|
238
|
+
if (value !== "auto" && value !== "off") {
|
|
239
|
+
throw new CliArgumentError("CLI_INVALID_VALUE", `Worktree mode must be auto or off: ${value}`, token);
|
|
240
|
+
}
|
|
241
|
+
worktree = value;
|
|
242
|
+
index += 1;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
232
245
|
case "--dry-run":
|
|
233
246
|
if (dryRun) {
|
|
234
247
|
duplicate(token);
|
|
@@ -367,6 +380,7 @@ export function parseArgs(argv) {
|
|
|
367
380
|
dryRun ||
|
|
368
381
|
yes ||
|
|
369
382
|
rerun ||
|
|
383
|
+
worktree !== undefined ||
|
|
370
384
|
force) {
|
|
371
385
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "Only --json may be combined with global help or version.");
|
|
372
386
|
}
|
|
@@ -432,6 +446,9 @@ export function parseArgs(argv) {
|
|
|
432
446
|
if (completionGate !== undefined && command !== "init") {
|
|
433
447
|
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--completion-gate may be used only with init.");
|
|
434
448
|
}
|
|
449
|
+
if (worktree !== undefined && command !== "init" && command !== "update") {
|
|
450
|
+
throw new CliArgumentError("CLI_OPTION_NOT_ALLOWED", "--worktree may be used only with init or update.");
|
|
451
|
+
}
|
|
435
452
|
if (command === "task") {
|
|
436
453
|
if (harness !== undefined ||
|
|
437
454
|
profiles.length > 0 ||
|
|
@@ -551,6 +568,7 @@ export function parseArgs(argv) {
|
|
|
551
568
|
rerun,
|
|
552
569
|
dryRun,
|
|
553
570
|
json,
|
|
554
|
-
yes
|
|
571
|
+
yes,
|
|
572
|
+
...(worktree === undefined ? {} : { worktree })
|
|
555
573
|
};
|
|
556
574
|
}
|
|
@@ -348,6 +348,7 @@ else {
|
|
|
348
348
|
trustStore: store,
|
|
349
349
|
calculateTrustBinding: async (config) => await plannedTrustBinding(root, config),
|
|
350
350
|
confirm: async (plan, trust) => await confirmPlan(formatUpdatePlan(plan, trust)),
|
|
351
|
+
promptWorktree: async (message) => await selectYesNo(message, { input: process.stdin, output: process.stdout }, true),
|
|
351
352
|
...(updateArgs.targetVersion === undefined
|
|
352
353
|
? {}
|
|
353
354
|
: { targetVersion: updateArgs.targetVersion })
|
|
@@ -39,6 +39,7 @@ Options:
|
|
|
39
39
|
--profile <core|advisory|guardrails|loop> Repeatable
|
|
40
40
|
--review-target <codex|agy|claude> Repeatable init option; review pair order
|
|
41
41
|
--completion-gate Init only: enable the project-loop completion gate
|
|
42
|
+
--worktree <auto|off> Init/update: configure session worktree isolation
|
|
42
43
|
--check-auth Doctor only: probe selected review targets'
|
|
43
44
|
authentication with one real call
|
|
44
45
|
--check-auth-target <target> Repeatable doctor filter with --check-auth
|
|
@@ -75,6 +76,7 @@ Options:
|
|
|
75
76
|
--profile <core|advisory|guardrails|loop> Repeatable
|
|
76
77
|
--review-target <codex|agy|claude> Repeatable, in fallback-chain order
|
|
77
78
|
--completion-gate Enable the project-loop completion gate
|
|
79
|
+
--worktree <auto|off> Configure session worktree isolation
|
|
78
80
|
--dry-run Print the plan without writing
|
|
79
81
|
--json
|
|
80
82
|
--yes
|
|
@@ -114,6 +116,7 @@ Options:
|
|
|
114
116
|
--scope <project|user>
|
|
115
117
|
--harness <all|both|agy|claude|codex|opencode|comma-separated>
|
|
116
118
|
--target-version <version> Offline-capable update target
|
|
119
|
+
--worktree <auto|off> Configure session worktree isolation
|
|
117
120
|
--dry-run
|
|
118
121
|
--json
|
|
119
122
|
--yes
|
|
@@ -5,6 +5,7 @@ import { okEnvelope } from "../output.js";
|
|
|
5
5
|
import { formatOperationPlan } from "../plan-output.js";
|
|
6
6
|
import { toPublicInstallPlan } from "../public-plan.js";
|
|
7
7
|
import { formatTrustChange, planTrustGrant } from "./trust.js";
|
|
8
|
+
import { checkWorktreeEnvWarning, detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
|
|
8
9
|
export function formatInstallPlan(plan, trust, warnings = []) {
|
|
9
10
|
const hooks = plan.manifest.hooks ?? [];
|
|
10
11
|
const detectedVerification = plan.detectedVerification;
|
|
@@ -14,6 +15,13 @@ export function formatInstallPlan(plan, trust, warnings = []) {
|
|
|
14
15
|
`Scope: ${plan.scope}`,
|
|
15
16
|
`Harness: ${plan.harness}`,
|
|
16
17
|
`Profiles: ${plan.profiles.join(", ")}`,
|
|
18
|
+
...(plan.config.worktree === undefined
|
|
19
|
+
? []
|
|
20
|
+
: [
|
|
21
|
+
`Worktree: mode=${plan.config.worktree.mode}${plan.config.worktree.setup && plan.config.worktree.setup.length > 0
|
|
22
|
+
? `, setup=${plan.config.worktree.setup.map((s) => `${s.command} ${s.args.join(" ")}`.trim()).join("; ")}`
|
|
23
|
+
: ""}`
|
|
24
|
+
]),
|
|
17
25
|
...(plan.scope === "user" && plan.harness.includes("agy")
|
|
18
26
|
? ["Notice: .gemini/GEMINI.md is a shared Gemini rule surface."]
|
|
19
27
|
: []),
|
|
@@ -85,6 +93,11 @@ export async function runInitCommand(options) {
|
|
|
85
93
|
args.profiles.length === 0) {
|
|
86
94
|
throw new AgentOpsError("INIT_CHOICES_REQUIRED", "Init requires complete scope, harness, and profile choices.");
|
|
87
95
|
}
|
|
96
|
+
const worktreeConfig = args.worktree === "auto"
|
|
97
|
+
? { mode: "auto", setup: await detectWorktreeSetup(options.root) }
|
|
98
|
+
: args.worktree === "off"
|
|
99
|
+
? { mode: "off" }
|
|
100
|
+
: undefined;
|
|
88
101
|
const plan = await createInstallPlan({
|
|
89
102
|
root: options.root,
|
|
90
103
|
scope: args.scope,
|
|
@@ -105,7 +118,10 @@ export async function runInitCommand(options) {
|
|
|
105
118
|
: { reviewTargets: args.reviewTargets }),
|
|
106
119
|
...(args.completionGate === undefined
|
|
107
120
|
? {}
|
|
108
|
-
: { completionGateEnabled: args.completionGate })
|
|
121
|
+
: { completionGateEnabled: args.completionGate }),
|
|
122
|
+
...(worktreeConfig === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { worktree: worktreeConfig })
|
|
109
125
|
});
|
|
110
126
|
const trust = await trustChange(options, plan);
|
|
111
127
|
// An installation with no verifier looks finished and is not: every task
|
|
@@ -121,7 +137,14 @@ export async function runInitCommand(options) {
|
|
|
121
137
|
: ` Detection stopped because — ${plan.verificationBlockers.join("; ")}`)
|
|
122
138
|
]
|
|
123
139
|
: [];
|
|
124
|
-
const
|
|
140
|
+
const worktreeEnvWarning = plan.config.worktree?.mode === "auto"
|
|
141
|
+
? await checkWorktreeEnvWarning(options.root)
|
|
142
|
+
: undefined;
|
|
143
|
+
const worktreeWarnings = worktreeEnvWarning === undefined ? [] : [worktreeEnvWarning];
|
|
144
|
+
const warnings = [
|
|
145
|
+
...verificationWarnings,
|
|
146
|
+
...worktreeWarnings,
|
|
147
|
+
...(plan.harness.includes("agy") && options.agyWarning !== undefined
|
|
125
148
|
? (() => {
|
|
126
149
|
try {
|
|
127
150
|
const warning = options.agyWarning();
|
|
@@ -131,7 +154,8 @@ export async function runInitCommand(options) {
|
|
|
131
154
|
return ["agy could not be probed; run `agent-ops doctor` to verify it."];
|
|
132
155
|
}
|
|
133
156
|
})()
|
|
134
|
-
: [])
|
|
157
|
+
: [])
|
|
158
|
+
];
|
|
135
159
|
if (args.dryRun) {
|
|
136
160
|
return okEnvelope("INIT_PLAN_READY", {
|
|
137
161
|
applied: false,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { detectWorktreeSetup } from "../../../../runtime/src/install/parallel-setup.js";
|
|
1
4
|
import { applyUpdatePlan, createUpdatePlan } from "../../../../runtime/src/install/update.js";
|
|
2
5
|
import { okEnvelope } from "../output.js";
|
|
3
6
|
import { formatOperationPlan } from "../plan-output.js";
|
|
@@ -10,6 +13,13 @@ export function formatUpdatePlan(plan, trust) {
|
|
|
10
13
|
metadata: [
|
|
11
14
|
`Target version: ${plan.targetVersion}`,
|
|
12
15
|
`Harness: ${plan.installation.harness.join(", ")}`,
|
|
16
|
+
...(plan.installation.config.worktree === undefined
|
|
17
|
+
? []
|
|
18
|
+
: [
|
|
19
|
+
`Worktree: mode=${plan.installation.config.worktree.mode}${plan.installation.config.worktree.setup && plan.installation.config.worktree.setup.length > 0
|
|
20
|
+
? `, setup=${plan.installation.config.worktree.setup.map((s) => `${s.command} ${s.args.join(" ")}`.trim()).join("; ")}`
|
|
21
|
+
: ""}`
|
|
22
|
+
]),
|
|
13
23
|
...(trust === undefined ? [] : formatTrustChange(trust)),
|
|
14
24
|
`Schema migrations: ${plan.migrationSteps.length === 0
|
|
15
25
|
? "none"
|
|
@@ -47,7 +57,40 @@ async function trustChange(options, plan) {
|
|
|
47
57
|
? { action: "skipped", reason: "no-verification-commands" }
|
|
48
58
|
: await planTrustGrant(binding, options.trustStore);
|
|
49
59
|
}
|
|
60
|
+
async function existingConfigHasWorktree(root) {
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(join(root, ".agent-ops", "config.json"), "utf8");
|
|
63
|
+
const parsed = JSON.parse(content);
|
|
64
|
+
return parsed.worktree !== undefined;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
50
70
|
export async function runUpdateCommand(options) {
|
|
71
|
+
let worktree;
|
|
72
|
+
if (options.args.worktree === "auto") {
|
|
73
|
+
worktree = {
|
|
74
|
+
mode: "auto",
|
|
75
|
+
setup: await detectWorktreeSetup(options.root)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
else if (options.args.worktree === "off") {
|
|
79
|
+
worktree = { mode: "off" };
|
|
80
|
+
}
|
|
81
|
+
else if (options.args.worktree === undefined &&
|
|
82
|
+
options.promptWorktree !== undefined &&
|
|
83
|
+
options.isTTY &&
|
|
84
|
+
!options.args.yes &&
|
|
85
|
+
!(await existingConfigHasWorktree(options.root))) {
|
|
86
|
+
const shouldEnable = await options.promptWorktree("Enable Git worktree isolation for parallel agent sessions?");
|
|
87
|
+
if (shouldEnable) {
|
|
88
|
+
worktree = {
|
|
89
|
+
mode: "auto",
|
|
90
|
+
setup: await detectWorktreeSetup(options.root)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
51
94
|
const plan = await createUpdatePlan({
|
|
52
95
|
root: options.root,
|
|
53
96
|
adapters: options.adapters,
|
|
@@ -57,9 +100,9 @@ export async function runUpdateCommand(options) {
|
|
|
57
100
|
...(options.registry === undefined
|
|
58
101
|
? {}
|
|
59
102
|
: { registry: options.registry }),
|
|
60
|
-
...(options.targetVersion === undefined
|
|
103
|
+
...((options.targetVersion ?? options.args.targetVersion) === undefined
|
|
61
104
|
? {}
|
|
62
|
-
: { targetVersion: options.targetVersion }),
|
|
105
|
+
: { targetVersion: options.targetVersion ?? options.args.targetVersion }),
|
|
63
106
|
...(options.toolkitVersion === undefined
|
|
64
107
|
? {}
|
|
65
108
|
: { toolkitVersion: options.toolkitVersion }),
|
|
@@ -68,7 +111,8 @@ export async function runUpdateCommand(options) {
|
|
|
68
111
|
: { hookRuntimePath: options.hookRuntimePath }),
|
|
69
112
|
...((options.hookTargets ?? options.args.hookTargets) === undefined
|
|
70
113
|
? {}
|
|
71
|
-
: { hookTargets: options.hookTargets ?? options.args.hookTargets })
|
|
114
|
+
: { hookTargets: options.hookTargets ?? options.args.hookTargets }),
|
|
115
|
+
...(worktree === undefined ? {} : { worktree })
|
|
72
116
|
});
|
|
73
117
|
const trust = await trustChange(options, plan);
|
|
74
118
|
if (options.args.dryRun) {
|
|
@@ -193,6 +193,20 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
193
193
|
const reviewTargets = args.reviewTargets ?? (enabled && eligibleReviewChoices.length > 0
|
|
194
194
|
? await selectOptions("Review targets (multi-select: tried in listed order)", eligibleReviewChoices, selectorIo, REVIEW_TARGET_ORDER.filter((target) => harness.includes(target)), { startEmpty: true })
|
|
195
195
|
: []);
|
|
196
|
+
const worktree = args.worktree ?? (scope === "project"
|
|
197
|
+
? (await selectOption("Enable Git worktree isolation for parallel agent sessions?", [
|
|
198
|
+
{
|
|
199
|
+
label: "yes",
|
|
200
|
+
value: "auto",
|
|
201
|
+
description: "Recommended. Isolates each agent session in a dedicated worktree."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
label: "no",
|
|
205
|
+
value: "off",
|
|
206
|
+
description: "Keep a single shared checkout."
|
|
207
|
+
}
|
|
208
|
+
], selectorIo))
|
|
209
|
+
: undefined);
|
|
196
210
|
await probeReviewTargets(reviewTargets, setup);
|
|
197
211
|
return {
|
|
198
212
|
...args,
|
|
@@ -200,7 +214,8 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
200
214
|
harness,
|
|
201
215
|
profiles,
|
|
202
216
|
...(completionGate ? { completionGate: true } : {}),
|
|
203
|
-
...(reviewTargets.length === 0 ? {} : { reviewTargets })
|
|
217
|
+
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
218
|
+
...(worktree !== undefined ? { worktree } : {})
|
|
204
219
|
};
|
|
205
220
|
}
|
|
206
221
|
const session = await createPromptSession(io);
|
|
@@ -220,13 +235,17 @@ export async function completeInitChoices(args, io, setup = {}) {
|
|
|
220
235
|
? selectReviewTargets(await session.question(`Review targets (${defaultReviewTargets.join(",") || "none"}): `), defaultReviewTargets)
|
|
221
236
|
: []);
|
|
222
237
|
await probeReviewTargets(reviewTargets, setup);
|
|
238
|
+
const worktree = args.worktree ?? (scope === "project"
|
|
239
|
+
? (!/^(n|no)$/i.test((await session.question("Enable Git worktree isolation for parallel agent sessions? [Y/n]: ")).trim()) ? "auto" : "off")
|
|
240
|
+
: undefined);
|
|
223
241
|
return {
|
|
224
242
|
...args,
|
|
225
243
|
scope,
|
|
226
244
|
harness,
|
|
227
245
|
profiles,
|
|
228
246
|
...(completionGate ? { completionGate: true } : {}),
|
|
229
|
-
...(reviewTargets.length === 0 ? {} : { reviewTargets })
|
|
247
|
+
...(reviewTargets.length === 0 ? {} : { reviewTargets }),
|
|
248
|
+
...(worktree !== undefined ? { worktree } : {})
|
|
230
249
|
};
|
|
231
250
|
}
|
|
232
251
|
finally {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const LOCKFILES = [
|
|
4
|
+
{ path: "bun.lock", packageManager: "bun" },
|
|
5
|
+
{ path: "bun.lockb", packageManager: "bun" },
|
|
6
|
+
{ path: "npm-shrinkwrap.json", packageManager: "npm" },
|
|
7
|
+
{ path: "package-lock.json", packageManager: "npm" },
|
|
8
|
+
{ path: "pnpm-lock.yaml", packageManager: "pnpm" },
|
|
9
|
+
{ path: "yarn.lock", packageManager: "yarn" }
|
|
10
|
+
];
|
|
11
|
+
const SETUP_COMMANDS = {
|
|
12
|
+
npm: ["ci"],
|
|
13
|
+
pnpm: ["install", "--frozen-lockfile"],
|
|
14
|
+
yarn: ["install", "--immutable"],
|
|
15
|
+
bun: ["install", "--frozen-lockfile"]
|
|
16
|
+
};
|
|
17
|
+
function hasErrorCode(error, code) {
|
|
18
|
+
return error instanceof Error && "code" in error && error.code === code;
|
|
19
|
+
}
|
|
20
|
+
async function regularFileExists(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
const status = await lstat(filePath);
|
|
23
|
+
return status.isFile() && !status.isSymbolicLink();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Detects project lockfiles and proposes safe, deterministic dependency installation
|
|
34
|
+
* commands for freshly cloned worktrees. Returns an empty array if detection is
|
|
35
|
+
* ambiguous or unsupported.
|
|
36
|
+
*/
|
|
37
|
+
export async function detectWorktreeSetup(root) {
|
|
38
|
+
const detected = await Promise.all(LOCKFILES.map(async (def) => (await regularFileExists(join(root, def.path))) ? def.packageManager : null));
|
|
39
|
+
const managers = [...new Set(detected.filter((m) => m !== null))];
|
|
40
|
+
if (managers.length !== 1) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const manager = managers[0];
|
|
44
|
+
const args = SETUP_COMMANDS[manager];
|
|
45
|
+
return [{ command: manager, args: [...args] }];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a warning message if local environment files (.env) are detected
|
|
49
|
+
* without a .worktreeinclude file to carry them into new worktrees.
|
|
50
|
+
*/
|
|
51
|
+
export async function checkWorktreeEnvWarning(root) {
|
|
52
|
+
const hasWorktreeInclude = await regularFileExists(join(root, ".worktreeinclude"));
|
|
53
|
+
if (hasWorktreeInclude) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const envCandidates = [".env", ".env.local"];
|
|
57
|
+
for (const candidate of envCandidates) {
|
|
58
|
+
if (await regularFileExists(join(root, candidate))) {
|
|
59
|
+
return "Local environment files (.env) detected without .worktreeinclude; consider creating .worktreeinclude to copy them to new worktrees.";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
@@ -78,7 +78,7 @@ async function detectVerificationCommands(root) {
|
|
|
78
78
|
blockers: discovery.decisions.map((decision) => `${decision.adapter}: ${decision.message}`)
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
-
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = [], completionGateEnabled = false) {
|
|
81
|
+
function buildConfig(profiles, existing, reviewTargets = [], detectedCommands = [], completionGateEnabled = false, worktree) {
|
|
82
82
|
// Absent reviewRoles means external review is disabled; an empty selection
|
|
83
83
|
// must therefore omit the field rather than write an empty array.
|
|
84
84
|
const reviewRoles = reviewTargets.length > 0
|
|
@@ -88,6 +88,9 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
88
88
|
existing.verification.commands.length > 0
|
|
89
89
|
? existing.verification
|
|
90
90
|
: { commands: [...detectedCommands] };
|
|
91
|
+
const effectiveWorktree = worktree !== undefined
|
|
92
|
+
? (worktree === null ? undefined : structuredClone(worktree))
|
|
93
|
+
: (existing?.worktree === undefined ? undefined : structuredClone(existing.worktree));
|
|
91
94
|
return {
|
|
92
95
|
schemaVersion: CONFIG_SCHEMA_VERSION,
|
|
93
96
|
profiles: [...profiles],
|
|
@@ -103,12 +106,10 @@ function buildConfig(profiles, existing, reviewTargets = [], detectedCommands =
|
|
|
103
106
|
pathMappings: existing?.pathMappings ?? [],
|
|
104
107
|
securityExceptions: existing?.securityExceptions ?? [],
|
|
105
108
|
...(reviewRoles === undefined ? {} : { reviewRoles: [...reviewRoles] }),
|
|
106
|
-
...(
|
|
107
|
-
? {}
|
|
108
|
-
: { worktree: structuredClone(existing.worktree) })
|
|
109
|
+
...(effectiveWorktree === undefined ? {} : { worktree: effectiveWorktree })
|
|
109
110
|
};
|
|
110
111
|
}
|
|
111
|
-
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = [], completionGateEnabled = false) {
|
|
112
|
+
async function planConfig(root, profiles, existingManifest, suppliedConfig, reviewTargets = [], completionGateEnabled = false, worktree) {
|
|
112
113
|
const current = await readCurrentFile(root, CONFIG_PATH);
|
|
113
114
|
const owned = findOwnedArtifact(existingManifest, CONFIG_PATH);
|
|
114
115
|
if (current !== null && owned === undefined) {
|
|
@@ -144,7 +145,7 @@ async function planConfig(root, profiles, existingManifest, suppliedConfig, revi
|
|
|
144
145
|
existingConfig.verification.commands.length === 0
|
|
145
146
|
? await detectVerificationCommands(root)
|
|
146
147
|
: { commands: [], blockers: [] };
|
|
147
|
-
const config = buildConfig(profiles, existingConfig, reviewTargets, detected.commands, completionGateEnabled);
|
|
148
|
+
const config = buildConfig(profiles, existingConfig, reviewTargets, detected.commands, completionGateEnabled, worktree);
|
|
148
149
|
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
149
150
|
return {
|
|
150
151
|
operation: {
|
|
@@ -493,7 +494,7 @@ export async function createInstallPlan(options) {
|
|
|
493
494
|
: [];
|
|
494
495
|
const operations = [];
|
|
495
496
|
const artifacts = [];
|
|
496
|
-
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled);
|
|
497
|
+
const config = await planConfig(options.root, resolved.profiles, existing?.manifest ?? null, options.existingConfig, options.reviewTargets ?? [], completionGateEnabled, options.worktree);
|
|
497
498
|
operations.push(config.operation);
|
|
498
499
|
artifacts.push(config.record);
|
|
499
500
|
for (const artifact of contribution.artifacts) {
|
|
@@ -123,7 +123,10 @@ export async function createUpdatePlan(options) {
|
|
|
123
123
|
existingConfig: {
|
|
124
124
|
value: configPreview.migrated,
|
|
125
125
|
sourceHash: configPreview.sourceHash
|
|
126
|
-
}
|
|
126
|
+
},
|
|
127
|
+
...(options.worktree === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { worktree: options.worktree })
|
|
127
130
|
});
|
|
128
131
|
return {
|
|
129
132
|
targetVersion,
|