agentplane 0.2.26 → 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/README.md +3 -1
- package/assets/AGENTS.md +124 -526
- package/assets/agents/UPGRADER.json +10 -9
- package/assets/framework.manifest.json +112 -7
- package/assets/policy/check-routing.mjs +180 -0
- package/assets/policy/dod.code.md +25 -0
- package/assets/policy/dod.core.md +32 -0
- package/assets/policy/dod.docs.md +32 -0
- package/assets/policy/examples/migration-note.md +6 -0
- package/assets/policy/examples/pr-note.md +16 -0
- package/assets/policy/examples/unit-test-pattern.md +19 -0
- package/assets/policy/governance.md +37 -0
- package/assets/policy/incidents.md +36 -0
- package/assets/policy/security.must.md +7 -0
- package/assets/policy/workflow.branch_pr.md +34 -0
- package/assets/policy/workflow.direct.md +47 -0
- package/assets/policy/workflow.md +9 -0
- package/assets/policy/workflow.release.md +31 -0
- package/assets/policy/workflow.upgrade.md +20 -0
- package/bin/agentplane.js +88 -87
- package/bin/dist-guard.js +124 -0
- package/bin/runtime-context.d.ts +20 -0
- package/bin/runtime-context.js +81 -0
- package/dist/.build-manifest.json +5 -5
- package/dist/agents/agents-template.d.ts +7 -0
- package/dist/agents/agents-template.d.ts.map +1 -1
- package/dist/agents/agents-template.js +41 -2
- package/dist/cli/bootstrap-guide.d.ts +18 -0
- package/dist/cli/bootstrap-guide.d.ts.map +1 -0
- package/dist/cli/bootstrap-guide.js +132 -0
- package/dist/cli/command-guide.d.ts.map +1 -1
- package/dist/cli/command-guide.js +58 -183
- package/dist/cli/command-snippets.d.ts +3 -3
- package/dist/cli/command-snippets.js +3 -3
- package/dist/cli/run-cli/commands/core.js +3 -3
- package/dist/cli/run-cli/commands/ide.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/ide.js +8 -3
- package/dist/cli/run-cli/commands/init/ui.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/ui.js +1 -2
- package/dist/cli/run-cli/commands/init/write-agents.d.ts +2 -0
- package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-agents.js +24 -5
- package/dist/cli/run-cli/commands/init/write-workflow.d.ts +5 -0
- package/dist/cli/run-cli/commands/init/write-workflow.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-workflow.js +6 -0
- package/dist/cli/run-cli/commands/init.d.ts +2 -0
- package/dist/cli/run-cli/commands/init.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init.js +47 -19
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +125 -7
- package/dist/commands/doctor.run.d.ts.map +1 -1
- package/dist/commands/doctor.run.js +48 -6
- package/dist/commands/finish.run.d.ts.map +1 -1
- package/dist/commands/finish.run.js +1 -0
- package/dist/commands/finish.spec.d.ts +1 -0
- package/dist/commands/finish.spec.d.ts.map +1 -1
- package/dist/commands/finish.spec.js +23 -2
- package/dist/commands/release/apply.command.d.ts +1 -0
- package/dist/commands/release/apply.command.d.ts.map +1 -1
- package/dist/commands/release/apply.command.js +20 -9
- package/dist/commands/release/plan.command.d.ts.map +1 -1
- package/dist/commands/release/plan.command.js +9 -3
- package/dist/commands/task/add.d.ts.map +1 -1
- package/dist/commands/task/add.js +32 -0
- package/dist/commands/task/doc.command.d.ts.map +1 -1
- package/dist/commands/task/doc.command.js +1 -0
- package/dist/commands/task/finish.d.ts +1 -0
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +28 -7
- package/dist/commands/task/new.d.ts.map +1 -1
- package/dist/commands/task/new.js +41 -4
- package/dist/commands/task/plan.d.ts.map +1 -1
- package/dist/commands/task/plan.js +7 -1
- package/dist/commands/task/shared.d.ts +7 -0
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +37 -0
- package/dist/commands/task/start-ready.js +1 -1
- package/dist/commands/upgrade.command.d.ts.map +1 -1
- package/dist/commands/upgrade.command.js +11 -7
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +284 -296
- package/dist/commands/workflow-build.command.d.ts.map +1 -1
- package/dist/commands/workflow-build.command.js +7 -0
- package/dist/commands/workflow-playbook.command.d.ts.map +1 -1
- package/dist/commands/workflow-playbook.command.js +0 -1
- package/dist/shared/policy-gateway.d.ts +15 -0
- package/dist/shared/policy-gateway.d.ts.map +1 -0
- package/dist/shared/policy-gateway.js +49 -0
- package/dist/shared/protected-paths.d.ts.map +1 -1
- package/dist/shared/protected-paths.js +1 -0
- package/dist/shared/runtime-artifacts.d.ts +2 -2
- package/dist/shared/runtime-artifacts.d.ts.map +1 -1
- package/dist/shared/runtime-artifacts.js +4 -0
- package/dist/workflow-runtime/build.d.ts +1 -1
- package/dist/workflow-runtime/build.d.ts.map +1 -1
- package/dist/workflow-runtime/build.js +14 -2
- package/package.json +2 -2
package/dist/commands/upgrade.js
CHANGED
|
@@ -11,10 +11,23 @@ import { invalidFieldMessage, invalidValueMessage, requiredFieldMessage, warnMes
|
|
|
11
11
|
import { exitCodeForError } from "../cli/exit-codes.js";
|
|
12
12
|
import { CliError } from "../shared/errors.js";
|
|
13
13
|
import { ensureNetworkApproved } from "./shared/network-approval.js";
|
|
14
|
+
import { execFileAsync, gitEnv } from "./shared/git.js";
|
|
15
|
+
import { getVersion } from "../meta/version.js";
|
|
14
16
|
const DEFAULT_UPGRADE_ASSET = "agentplane-upgrade.tar.gz";
|
|
15
17
|
const DEFAULT_UPGRADE_CHECKSUM_ASSET = "agentplane-upgrade.tar.gz.sha256";
|
|
16
18
|
const UPGRADE_DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
17
19
|
const UPGRADE_RELEASE_METADATA_TIMEOUT_MS = 15_000;
|
|
20
|
+
function describeUpgradeSource(opts) {
|
|
21
|
+
if (opts.bundleLayout === "local_assets")
|
|
22
|
+
return "local installed agentplane CLI assets";
|
|
23
|
+
if (opts.bundleLayout === "repo_tarball")
|
|
24
|
+
return "GitHub repo tarball fallback";
|
|
25
|
+
if (opts.hasExplicitBundle)
|
|
26
|
+
return "explicit upgrade bundle";
|
|
27
|
+
if (opts.useRemote)
|
|
28
|
+
return "GitHub release bundle";
|
|
29
|
+
return "upgrade bundle";
|
|
30
|
+
}
|
|
18
31
|
async function safeRemovePath(targetPath) {
|
|
19
32
|
try {
|
|
20
33
|
await rm(targetPath, { recursive: true, force: true });
|
|
@@ -148,83 +161,22 @@ async function resolveUpgradeRoot(extractedDir) {
|
|
|
148
161
|
function isAllowedUpgradePath(relPath) {
|
|
149
162
|
if (relPath === "AGENTS.md")
|
|
150
163
|
return true;
|
|
164
|
+
if (relPath === "CLAUDE.md")
|
|
165
|
+
return true;
|
|
151
166
|
if (relPath.startsWith(".agentplane/agents/") && relPath.endsWith(".json"))
|
|
152
167
|
return true;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const end = text.indexOf(LOCAL_OVERRIDES_END);
|
|
160
|
-
if (start === -1 || end === -1 || end < start)
|
|
161
|
-
return null;
|
|
162
|
-
return text.slice(start + LOCAL_OVERRIDES_START.length, end).trim();
|
|
163
|
-
}
|
|
164
|
-
function withLocalOverridesBlock(base, localOverrides) {
|
|
165
|
-
const start = base.indexOf(LOCAL_OVERRIDES_START);
|
|
166
|
-
const end = base.indexOf(LOCAL_OVERRIDES_END);
|
|
167
|
-
if (start === -1 || end === -1 || end < start) {
|
|
168
|
-
const suffix = "\n\n## Local Overrides (preserved across upgrades)\n\n" +
|
|
169
|
-
`${LOCAL_OVERRIDES_START}\n` +
|
|
170
|
-
(localOverrides.trim() ? `${localOverrides.trim()}\n` : "") +
|
|
171
|
-
`${LOCAL_OVERRIDES_END}\n`;
|
|
172
|
-
return `${base.trimEnd()}${suffix}`;
|
|
173
|
-
}
|
|
174
|
-
const before = base.slice(0, start + LOCAL_OVERRIDES_START.length);
|
|
175
|
-
const after = base.slice(end);
|
|
176
|
-
return `${before}\n${localOverrides.trim() ? `${localOverrides.trim()}\n` : ""}${after}`;
|
|
177
|
-
}
|
|
178
|
-
function parseH2Sections(text) {
|
|
179
|
-
const lines = text.replaceAll("\r\n", "\n").split("\n");
|
|
180
|
-
const sections = new Map();
|
|
181
|
-
let current = null;
|
|
182
|
-
let buf = [];
|
|
183
|
-
const flush = () => {
|
|
184
|
-
if (!current)
|
|
185
|
-
return;
|
|
186
|
-
if (!sections.has(current)) {
|
|
187
|
-
sections.set(current, buf.join("\n").trimEnd());
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
for (const line of lines) {
|
|
191
|
-
const m = /^##\s+(.+?)\s*$/.exec(line);
|
|
192
|
-
if (m) {
|
|
193
|
-
flush();
|
|
194
|
-
current = (m[1] ?? "").trim();
|
|
195
|
-
buf = [];
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (current)
|
|
199
|
-
buf.push(line);
|
|
200
|
-
}
|
|
201
|
-
flush();
|
|
202
|
-
return sections;
|
|
203
|
-
}
|
|
204
|
-
function mergeAgentsPolicyMarkdown(incoming, current) {
|
|
205
|
-
const local = extractLocalOverridesBlock(current);
|
|
206
|
-
if (local !== null) {
|
|
207
|
-
return withLocalOverridesBlock(incoming, local);
|
|
208
|
-
}
|
|
209
|
-
// Fallback: if the user edited AGENTS.md without the local markers, preserve their changes by
|
|
210
|
-
// appending differing/extra sections into a dedicated local overrides block.
|
|
211
|
-
const incomingSections = parseH2Sections(incoming);
|
|
212
|
-
const currentSections = parseH2Sections(current);
|
|
213
|
-
const overrides = [];
|
|
214
|
-
for (const [title, body] of currentSections.entries()) {
|
|
215
|
-
const incomingBody = incomingSections.get(title);
|
|
216
|
-
if (incomingBody === undefined) {
|
|
217
|
-
overrides.push(`### Added section: ${title}\n\n${body.trim()}\n`);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (incomingBody.trim() !== body.trim()) {
|
|
221
|
-
overrides.push(`### Local edits for: ${title}\n\n${body.trim()}\n`);
|
|
222
|
-
}
|
|
168
|
+
if (relPath.startsWith(".agentplane/policy/") &&
|
|
169
|
+
(relPath.endsWith(".md") ||
|
|
170
|
+
relPath.endsWith(".ts") ||
|
|
171
|
+
relPath.endsWith(".js") ||
|
|
172
|
+
relPath.endsWith(".mjs"))) {
|
|
173
|
+
return true;
|
|
223
174
|
}
|
|
224
|
-
|
|
225
|
-
return incoming;
|
|
226
|
-
return withLocalOverridesBlock(incoming, overrides.join("\n"));
|
|
175
|
+
return false;
|
|
227
176
|
}
|
|
177
|
+
const INCIDENTS_POLICY_PATH = ".agentplane/policy/incidents.md";
|
|
178
|
+
const INCIDENTS_APPEND_MARKER = "<!-- AGENTPLANE:UPGRADE-APPEND incidents.md -->";
|
|
179
|
+
const CONFIG_REL_PATH = ".agentplane/config.json";
|
|
228
180
|
function isJsonRecord(value) {
|
|
229
181
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
230
182
|
}
|
|
@@ -262,127 +214,167 @@ function textChangedForType(opts) {
|
|
|
262
214
|
}
|
|
263
215
|
return opts.aText.trimEnd() !== opts.bText.trimEnd();
|
|
264
216
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
incoming = JSON.parse(incomingText);
|
|
272
|
-
current = JSON.parse(currentText);
|
|
217
|
+
function parseIncidentEntryBlocks(entriesBody) {
|
|
218
|
+
const lines = entriesBody.replaceAll("\r\n", "\n").split("\n");
|
|
219
|
+
const starts = [];
|
|
220
|
+
for (const [index, line] of lines.entries()) {
|
|
221
|
+
if (/^\s*-\s*id:\s+/i.test(line ?? ""))
|
|
222
|
+
starts.push(index);
|
|
273
223
|
}
|
|
274
|
-
|
|
275
|
-
|
|
224
|
+
const blocks = [];
|
|
225
|
+
for (const [idx, start] of starts.entries()) {
|
|
226
|
+
const end = starts.at(idx + 1) ?? lines.length;
|
|
227
|
+
const slice = lines.slice(start, end);
|
|
228
|
+
while (slice.length > 0 && !(slice[0] ?? "").trim())
|
|
229
|
+
slice.shift();
|
|
230
|
+
while (slice.length > 0 && !(slice.at(-1) ?? "").trim())
|
|
231
|
+
slice.pop();
|
|
232
|
+
const block = slice.join("\n").trim();
|
|
233
|
+
if (block)
|
|
234
|
+
blocks.push(block);
|
|
276
235
|
}
|
|
277
|
-
|
|
236
|
+
return blocks;
|
|
237
|
+
}
|
|
238
|
+
function normalizeEntryBlock(block) {
|
|
239
|
+
return block
|
|
240
|
+
.replaceAll("\r\n", "\n")
|
|
241
|
+
.split("\n")
|
|
242
|
+
.map((line) => line.trimEnd())
|
|
243
|
+
.join("\n")
|
|
244
|
+
.trim();
|
|
245
|
+
}
|
|
246
|
+
function splitEntriesSection(text) {
|
|
247
|
+
const lines = text.replaceAll("\r\n", "\n").split("\n");
|
|
248
|
+
const headingIndex = lines.findIndex((line) => /^\s*##\s+Entries\s*$/i.test(line));
|
|
249
|
+
if (headingIndex === -1)
|
|
278
250
|
return null;
|
|
279
|
-
|
|
280
|
-
for (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if (Array.isArray(incVal) && Array.isArray(curVal)) {
|
|
287
|
-
const merged = [...incVal];
|
|
288
|
-
const seen = new Set();
|
|
289
|
-
for (const x of merged)
|
|
290
|
-
seen.add(JSON.stringify(canonicalizeJson(x)));
|
|
291
|
-
for (const item of curVal) {
|
|
292
|
-
const key = JSON.stringify(canonicalizeJson(item));
|
|
293
|
-
if (!seen.has(key)) {
|
|
294
|
-
merged.push(item);
|
|
295
|
-
seen.add(key);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
out[k] = merged;
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
if (isJsonRecord(incVal) && isJsonRecord(curVal)) {
|
|
302
|
-
// Preserve user-only subkeys but let upstream win for conflicts.
|
|
303
|
-
out[k] = { ...curVal, ...incVal };
|
|
304
|
-
continue;
|
|
251
|
+
let nextHeadingIndex = lines.length;
|
|
252
|
+
for (let i = headingIndex + 1; i < lines.length; i++) {
|
|
253
|
+
if (/^\s*##\s+/.test(lines[i] ?? "")) {
|
|
254
|
+
nextHeadingIndex = i;
|
|
255
|
+
break;
|
|
305
256
|
}
|
|
306
|
-
out[k] = incVal;
|
|
307
257
|
}
|
|
308
|
-
return
|
|
258
|
+
return {
|
|
259
|
+
before: lines.slice(0, headingIndex + 1).join("\n"),
|
|
260
|
+
entriesBody: lines.slice(headingIndex + 1, nextHeadingIndex).join("\n"),
|
|
261
|
+
after: lines.slice(nextHeadingIndex).join("\n"),
|
|
262
|
+
};
|
|
309
263
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
264
|
+
function mergeIncidentsPolicy(opts) {
|
|
265
|
+
const incomingTrimmed = opts.incomingText.trim();
|
|
266
|
+
if (!incomingTrimmed)
|
|
267
|
+
return { nextText: opts.currentText, appended: false, appendedCount: 0 };
|
|
268
|
+
const incomingSection = splitEntriesSection(opts.incomingText);
|
|
269
|
+
const currentSection = splitEntriesSection(opts.currentText);
|
|
270
|
+
if (!incomingSection || !currentSection) {
|
|
271
|
+
return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
|
|
318
272
|
}
|
|
319
|
-
|
|
320
|
-
|
|
273
|
+
const incomingBlocks = parseIncidentEntryBlocks(incomingSection.entriesBody).map((block) => normalizeEntryBlock(block));
|
|
274
|
+
const currentBlocks = parseIncidentEntryBlocks(currentSection.entriesBody).map((block) => normalizeEntryBlock(block));
|
|
275
|
+
if (currentBlocks.length === 0) {
|
|
276
|
+
return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
|
|
277
|
+
}
|
|
278
|
+
const baselineSection = opts.baselineText ? splitEntriesSection(opts.baselineText) : null;
|
|
279
|
+
const baselineBlocks = baselineSection
|
|
280
|
+
? parseIncidentEntryBlocks(baselineSection.entriesBody).map((block) => normalizeEntryBlock(block))
|
|
281
|
+
: [];
|
|
282
|
+
const baselineSet = new Set(baselineBlocks);
|
|
283
|
+
const incomingSet = new Set(incomingBlocks);
|
|
284
|
+
const userAdded = currentBlocks.filter((block) => {
|
|
285
|
+
if (baselineSet.size > 0 && baselineSet.has(block))
|
|
286
|
+
return false;
|
|
287
|
+
return true;
|
|
288
|
+
});
|
|
289
|
+
const toAppend = userAdded.filter((block) => !incomingSet.has(block));
|
|
290
|
+
if (toAppend.length === 0) {
|
|
291
|
+
return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
|
|
321
292
|
}
|
|
322
|
-
|
|
293
|
+
const mergedBlocks = [...incomingBlocks, ...toAppend];
|
|
294
|
+
const renderedEntries = mergedBlocks.length > 0 ? `\n\n${mergedBlocks.join("\n\n")}\n` : "\n\n- None yet.\n";
|
|
295
|
+
const afterSuffix = incomingSection.after ? `\n${incomingSection.after.trimStart()}` : "";
|
|
296
|
+
const nextText = `${incomingSection.before.trimEnd()}` +
|
|
297
|
+
`${renderedEntries}` +
|
|
298
|
+
`${INCIDENTS_APPEND_MARKER}\n` +
|
|
299
|
+
`${afterSuffix}` +
|
|
300
|
+
`\n`;
|
|
301
|
+
return { nextText, appended: true, appendedCount: toAppend.length };
|
|
302
|
+
}
|
|
303
|
+
function normalizeUpgradeVersionLabel(input) {
|
|
304
|
+
const trimmed = input.trim();
|
|
305
|
+
if (!trimmed)
|
|
306
|
+
return "unknown";
|
|
307
|
+
if (/^v\d/i.test(trimmed))
|
|
308
|
+
return trimmed;
|
|
309
|
+
return `v${trimmed}`;
|
|
310
|
+
}
|
|
311
|
+
async function ensureCleanTrackedTreeForUpgrade(gitRoot) {
|
|
312
|
+
const { stdout } = await execFileAsync("git", ["status", "--short", "--untracked-files=no"], {
|
|
313
|
+
cwd: gitRoot,
|
|
314
|
+
env: gitEnv(),
|
|
315
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
316
|
+
});
|
|
317
|
+
const dirty = String(stdout ?? "")
|
|
318
|
+
.split(/\r?\n/u)
|
|
319
|
+
.map((line) => line.trimEnd())
|
|
320
|
+
.filter((line) => line.length > 0);
|
|
321
|
+
if (dirty.length === 0)
|
|
322
|
+
return;
|
|
323
|
+
throw new CliError({
|
|
324
|
+
exitCode: exitCodeForError("E_GIT"),
|
|
325
|
+
code: "E_GIT",
|
|
326
|
+
message: "Upgrade --auto requires a clean tracked working tree.\n" +
|
|
327
|
+
`Found tracked changes:\n${dirty.map((line) => ` ${line}`).join("\n")}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
async function createUpgradeCommit(opts) {
|
|
331
|
+
const uniquePaths = [...new Set(opts.paths.filter(Boolean))];
|
|
332
|
+
if (uniquePaths.length === 0)
|
|
323
333
|
return null;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (userChanged)
|
|
363
|
-
merged[sk] = curSub;
|
|
364
|
-
else if (incSub !== undefined)
|
|
365
|
-
merged[sk] = incSub;
|
|
366
|
-
else if (curSub !== undefined)
|
|
367
|
-
merged[sk] = curSub;
|
|
368
|
-
}
|
|
369
|
-
out[key] = merged;
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
// Scalars: prefer incoming unless the user changed vs base.
|
|
373
|
-
if (!jsonEqual(curVal, baseVal)) {
|
|
374
|
-
if (curVal !== undefined)
|
|
375
|
-
out[key] = curVal;
|
|
376
|
-
else if (incVal !== undefined)
|
|
377
|
-
out[key] = incVal;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (incVal !== undefined)
|
|
381
|
-
out[key] = incVal;
|
|
382
|
-
else if (curVal !== undefined)
|
|
383
|
-
out[key] = curVal;
|
|
334
|
+
await execFileAsync("git", ["add", "--", ...uniquePaths], {
|
|
335
|
+
cwd: opts.gitRoot,
|
|
336
|
+
env: gitEnv(),
|
|
337
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
338
|
+
});
|
|
339
|
+
const { stdout: stagedOut } = await execFileAsync("git", ["diff", "--cached", "--name-only", "-z"], {
|
|
340
|
+
cwd: opts.gitRoot,
|
|
341
|
+
env: gitEnv(),
|
|
342
|
+
encoding: "buffer",
|
|
343
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
344
|
+
});
|
|
345
|
+
const staged = (Buffer.isBuffer(stagedOut) ? stagedOut.toString("utf8") : String(stagedOut ?? ""))
|
|
346
|
+
.split("\0")
|
|
347
|
+
.map((entry) => entry.trim())
|
|
348
|
+
.some(Boolean);
|
|
349
|
+
if (!staged)
|
|
350
|
+
return null;
|
|
351
|
+
const subject = `⬆️ upgrade: apply framework ${opts.versionLabel}`;
|
|
352
|
+
const body = `Upgrade-Version: ${opts.versionLabel}\n` +
|
|
353
|
+
`Source: ${opts.source}\n` +
|
|
354
|
+
`Managed-Changes: add=${opts.additions}, update=${opts.updates}, unchanged=${opts.unchanged}\n` +
|
|
355
|
+
`Incidents-Appended: ${opts.incidentsAppendedCount}\n`;
|
|
356
|
+
try {
|
|
357
|
+
await execFileAsync("git", ["commit", "-m", subject, "-m", body], {
|
|
358
|
+
cwd: opts.gitRoot,
|
|
359
|
+
env: gitEnv(),
|
|
360
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
const details = err?.stderr ?? "";
|
|
365
|
+
throw new CliError({
|
|
366
|
+
exitCode: exitCodeForError("E_GIT"),
|
|
367
|
+
code: "E_GIT",
|
|
368
|
+
message: "Upgrade applied but failed to create the upgrade commit.\n" +
|
|
369
|
+
"Fix commit policy/hook issues and commit the staged upgrade files as a dedicated upgrade commit.\n" +
|
|
370
|
+
(String(details).trim() ? `Details:\n${String(details).trim()}` : ""),
|
|
371
|
+
});
|
|
384
372
|
}
|
|
385
|
-
|
|
373
|
+
const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
374
|
+
cwd: opts.gitRoot,
|
|
375
|
+
env: gitEnv(),
|
|
376
|
+
});
|
|
377
|
+
return { hash: String(hashOut ?? "").trim(), subject };
|
|
386
378
|
}
|
|
387
379
|
export async function cmdUpgradeParsed(opts) {
|
|
388
380
|
const flags = opts.flags;
|
|
@@ -400,6 +392,9 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
400
392
|
rootOverride: opts.rootOverride ?? null,
|
|
401
393
|
});
|
|
402
394
|
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
395
|
+
if (flags.mode === "auto" && !flags.dryRun) {
|
|
396
|
+
await ensureCleanTrackedTreeForUpgrade(resolved.gitRoot);
|
|
397
|
+
}
|
|
403
398
|
const upgradeStateDir = path.join(resolved.agentplaneDir, ".upgrade");
|
|
404
399
|
const lockPath = path.join(upgradeStateDir, "lock.json");
|
|
405
400
|
const statePath = path.join(upgradeStateDir, "state.json");
|
|
@@ -438,6 +433,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
438
433
|
let bundleLayout = "upgrade_bundle";
|
|
439
434
|
let bundleRoot = "";
|
|
440
435
|
let normalizedSourceToPersist = null;
|
|
436
|
+
let upgradeVersionLabel = normalizeUpgradeVersionLabel(getVersion());
|
|
441
437
|
if (!hasBundle && !useRemote) {
|
|
442
438
|
bundleLayout = "local_assets";
|
|
443
439
|
bundleRoot = fileURLToPath(ASSETS_DIR_URL);
|
|
@@ -477,6 +473,12 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
477
473
|
const assetName = flags.asset ?? DEFAULT_UPGRADE_ASSET;
|
|
478
474
|
const checksumName = flags.checksumAsset ?? DEFAULT_UPGRADE_CHECKSUM_ASSET;
|
|
479
475
|
const release = (await fetchJson(releaseUrl, UPGRADE_RELEASE_METADATA_TIMEOUT_MS));
|
|
476
|
+
const releaseTag = (typeof release.tag_name === "string" && release.tag_name.trim()) ||
|
|
477
|
+
(typeof flags.tag === "string" && flags.tag.trim()) ||
|
|
478
|
+
"";
|
|
479
|
+
if (releaseTag) {
|
|
480
|
+
upgradeVersionLabel = normalizeUpgradeVersionLabel(releaseTag);
|
|
481
|
+
}
|
|
480
482
|
const download = resolveUpgradeDownloadFromRelease({
|
|
481
483
|
release,
|
|
482
484
|
owner,
|
|
@@ -546,6 +548,14 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
546
548
|
? fileURLToPath(new URL("../../assets/framework.manifest.json", import.meta.url))
|
|
547
549
|
: path.join(bundleRoot, "framework.manifest.json");
|
|
548
550
|
const manifest = await loadFrameworkManifestFromPath(manifestPath);
|
|
551
|
+
const modeLabel = flags.dryRun ? "dry-run" : flags.mode === "agent" ? "review" : "apply";
|
|
552
|
+
process.stdout.write(`Upgrade source: ${describeUpgradeSource({
|
|
553
|
+
bundleLayout,
|
|
554
|
+
hasExplicitBundle: hasBundle,
|
|
555
|
+
useRemote,
|
|
556
|
+
})}\n` +
|
|
557
|
+
`Upgrade version: ${upgradeVersionLabel}\n` +
|
|
558
|
+
`Upgrade mode: ${modeLabel}\n`);
|
|
549
559
|
const additions = [];
|
|
550
560
|
const updates = [];
|
|
551
561
|
const skipped = [];
|
|
@@ -553,7 +563,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
553
563
|
const merged = [];
|
|
554
564
|
const missingRequired = [];
|
|
555
565
|
const reviewRecords = [];
|
|
556
|
-
|
|
566
|
+
let incidentsAppendedCount = 0;
|
|
557
567
|
const readBaselineText = async (baselineKey) => {
|
|
558
568
|
try {
|
|
559
569
|
return await readFile(path.join(baselineDirNew, baselineKey), "utf8");
|
|
@@ -571,33 +581,46 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
571
581
|
const toBaselineKey = (rel) => {
|
|
572
582
|
if (rel === "AGENTS.md")
|
|
573
583
|
return "AGENTS.md";
|
|
584
|
+
if (rel === "CLAUDE.md")
|
|
585
|
+
return "CLAUDE.md";
|
|
574
586
|
if (rel.startsWith(".agentplane/"))
|
|
575
587
|
return rel.slice(".agentplane/".length);
|
|
576
588
|
return null;
|
|
577
589
|
};
|
|
590
|
+
const policyGatewayRel = (await fileExists(path.join(resolved.gitRoot, "AGENTS.md")))
|
|
591
|
+
? "AGENTS.md"
|
|
592
|
+
: (await fileExists(path.join(resolved.gitRoot, "CLAUDE.md")))
|
|
593
|
+
? "CLAUDE.md"
|
|
594
|
+
: "AGENTS.md";
|
|
595
|
+
const remapManagedGatewayRel = (rel) => {
|
|
596
|
+
if (rel === "AGENTS.md" && policyGatewayRel === "CLAUDE.md")
|
|
597
|
+
return "CLAUDE.md";
|
|
598
|
+
return rel;
|
|
599
|
+
};
|
|
578
600
|
for (const entry of manifest.files) {
|
|
579
|
-
const
|
|
580
|
-
if (!
|
|
601
|
+
const relRaw = entry.path.replaceAll("\\", "/").trim();
|
|
602
|
+
if (!relRaw || relRaw.startsWith("..") || path.isAbsolute(relRaw)) {
|
|
581
603
|
throw new CliError({
|
|
582
604
|
exitCode: 3,
|
|
583
605
|
code: "E_VALIDATION",
|
|
584
606
|
message: `Invalid manifest path: ${entry.path}`,
|
|
585
607
|
});
|
|
586
608
|
}
|
|
587
|
-
if (isDeniedUpgradePath(
|
|
609
|
+
if (isDeniedUpgradePath(relRaw)) {
|
|
588
610
|
throw new CliError({
|
|
589
611
|
exitCode: 3,
|
|
590
612
|
code: "E_VALIDATION",
|
|
591
|
-
message: `Manifest includes a denied path: ${
|
|
613
|
+
message: `Manifest includes a denied path: ${relRaw}`,
|
|
592
614
|
});
|
|
593
615
|
}
|
|
594
|
-
if (!isAllowedUpgradePath(
|
|
616
|
+
if (!isAllowedUpgradePath(relRaw)) {
|
|
595
617
|
throw new CliError({
|
|
596
618
|
exitCode: 3,
|
|
597
619
|
code: "E_VALIDATION",
|
|
598
|
-
message: `Manifest path not allowed: ${
|
|
620
|
+
message: `Manifest path not allowed: ${relRaw}`,
|
|
599
621
|
});
|
|
600
622
|
}
|
|
623
|
+
const rel = remapManagedGatewayRel(relRaw);
|
|
601
624
|
const destPath = path.join(resolved.gitRoot, rel);
|
|
602
625
|
const kind = await getPathKind(destPath);
|
|
603
626
|
if (kind === "dir") {
|
|
@@ -607,16 +630,27 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
607
630
|
message: `Upgrade target is a directory: ${rel}`,
|
|
608
631
|
});
|
|
609
632
|
}
|
|
610
|
-
const sourceRel = (entry.source_path ?? entry.path).replaceAll("\\", "/").trim();
|
|
611
|
-
const sourcePath = path.join(bundleRoot, sourceRel);
|
|
612
633
|
let data;
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
634
|
+
{
|
|
635
|
+
const sourceRelRaw = (entry.source_path ?? entry.path).replaceAll("\\", "/").trim();
|
|
636
|
+
const mappedSourceRel = rel === "CLAUDE.md" && sourceRelRaw === "AGENTS.md" ? "CLAUDE.md" : sourceRelRaw;
|
|
637
|
+
const sourceCandidates = [...new Set([mappedSourceRel, sourceRelRaw])];
|
|
638
|
+
let loaded = null;
|
|
639
|
+
for (const candidate of sourceCandidates) {
|
|
640
|
+
try {
|
|
641
|
+
loaded = await readFile(path.join(bundleRoot, candidate));
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
// try next candidate
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!loaded) {
|
|
649
|
+
if (entry.required)
|
|
650
|
+
missingRequired.push(rel);
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
data = loaded;
|
|
620
654
|
}
|
|
621
655
|
let existingBuf = null;
|
|
622
656
|
let existingText = null;
|
|
@@ -649,7 +683,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
649
683
|
aText: currentTextForReview,
|
|
650
684
|
bText: incomingTextOriginal,
|
|
651
685
|
}) === false;
|
|
652
|
-
// Fast-path:
|
|
686
|
+
// Fast-path: incoming already equals local.
|
|
653
687
|
if (currentTextForReview !== null && currentAndIncomingEqual) {
|
|
654
688
|
skipped.push(rel);
|
|
655
689
|
reviewRecords.push({
|
|
@@ -665,7 +699,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
665
699
|
});
|
|
666
700
|
continue;
|
|
667
701
|
}
|
|
668
|
-
// No local edits vs baseline: file can be safely replaced with incoming
|
|
702
|
+
// No local edits vs baseline: file can be safely replaced with incoming.
|
|
669
703
|
if (currentTextForReview !== null && changedCurrentVsBaseline === false) {
|
|
670
704
|
updates.push(rel);
|
|
671
705
|
fileContents.set(rel, data);
|
|
@@ -684,53 +718,24 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
684
718
|
}
|
|
685
719
|
let mergeApplied = false;
|
|
686
720
|
let mergePath = "none";
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
721
|
+
// Simplified policy for upgrade:
|
|
722
|
+
// - All managed files are replaced with incoming bundle content.
|
|
723
|
+
// - incidents.md is append-only when local file already has content.
|
|
724
|
+
if (existingBuf && rel === INCIDENTS_POLICY_PATH) {
|
|
725
|
+
existingText = existingBuf.toString("utf8");
|
|
726
|
+
const mergedIncidents = mergeIncidentsPolicy({
|
|
727
|
+
incomingText: data.toString("utf8"),
|
|
728
|
+
currentText: existingText,
|
|
729
|
+
baselineText,
|
|
730
|
+
});
|
|
731
|
+
data = Buffer.from(mergedIncidents.nextText, "utf8");
|
|
732
|
+
if (mergedIncidents.appended) {
|
|
693
733
|
merged.push(rel);
|
|
694
734
|
mergeApplied = true;
|
|
695
|
-
mergePath = "
|
|
696
|
-
|
|
697
|
-
else if (entry.merge_strategy === "agent_json_3way" &&
|
|
698
|
-
rel.startsWith(".agentplane/agents/") &&
|
|
699
|
-
rel.endsWith(".json")) {
|
|
700
|
-
existingText = existingBuf.toString("utf8");
|
|
701
|
-
let mergedText = null;
|
|
702
|
-
if (baselineText !== null) {
|
|
703
|
-
try {
|
|
704
|
-
mergedText = mergeAgentJson3Way({
|
|
705
|
-
incomingText: data.toString("utf8"),
|
|
706
|
-
currentText: existingText,
|
|
707
|
-
baseText: baselineText,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
catch {
|
|
711
|
-
mergedText = null;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (mergedText) {
|
|
715
|
-
mergePath = "3way";
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
mergedText = mergeAgentJsonIncomingWins(data.toString("utf8"), existingText);
|
|
719
|
-
if (mergedText) {
|
|
720
|
-
mergePath = baselineText === null ? "incomingWins" : "incomingWinsFallback";
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
if (mergedText) {
|
|
724
|
-
data = Buffer.from(mergedText, "utf8");
|
|
725
|
-
merged.push(rel);
|
|
726
|
-
mergeApplied = true;
|
|
727
|
-
}
|
|
728
|
-
else {
|
|
729
|
-
mergePath = "parseFailed";
|
|
730
|
-
}
|
|
735
|
+
mergePath = "incidentsAppend";
|
|
736
|
+
incidentsAppendedCount += mergedIncidents.appendedCount;
|
|
731
737
|
}
|
|
732
738
|
}
|
|
733
|
-
const proposedText = data.toString("utf8");
|
|
734
739
|
const currentDiffersFromIncoming = currentTextForReview === null
|
|
735
740
|
? false
|
|
736
741
|
: textChangedForType({
|
|
@@ -738,16 +743,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
738
743
|
aText: currentTextForReview,
|
|
739
744
|
bText: incomingTextOriginal,
|
|
740
745
|
});
|
|
741
|
-
const
|
|
742
|
-
? false
|
|
743
|
-
: currentDiffersFromIncoming &&
|
|
744
|
-
Boolean(changedCurrentVsBaseline) &&
|
|
745
|
-
Boolean(changedIncomingVsBaseline);
|
|
746
|
-
const unresolvedLocalEditsConflict = baselineText === null
|
|
747
|
-
? false
|
|
748
|
-
: currentDiffersFromIncoming && Boolean(changedCurrentVsBaseline) && !mergeApplied;
|
|
749
|
-
const parseFailedConflict = mergePath === "parseFailed";
|
|
750
|
-
const needsSemanticReview = baselineConflict || unresolvedLocalEditsConflict || parseFailedConflict;
|
|
746
|
+
const needsSemanticReview = false;
|
|
751
747
|
reviewRecords.push({
|
|
752
748
|
relPath: rel,
|
|
753
749
|
mergeStrategy: entry.merge_strategy,
|
|
@@ -759,14 +755,6 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
759
755
|
mergeApplied,
|
|
760
756
|
mergePath,
|
|
761
757
|
});
|
|
762
|
-
if (flags.mode === "agent" && needsSemanticReview) {
|
|
763
|
-
reviewSnapshots.set(rel, {
|
|
764
|
-
incomingText: incomingTextOriginal,
|
|
765
|
-
currentText: currentTextForReview,
|
|
766
|
-
baselineText,
|
|
767
|
-
proposedText,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
758
|
fileContents.set(rel, data);
|
|
771
759
|
if (kind === null)
|
|
772
760
|
additions.push(rel);
|
|
@@ -808,7 +796,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
808
796
|
await mkdir(runDir, { recursive: true });
|
|
809
797
|
const managedFiles = manifest.files.map((f) => f.path.replaceAll("\\", "/").trim());
|
|
810
798
|
const planMd = `# agentplane upgrade plan (${runId})\n\n` +
|
|
811
|
-
`Mode: agent-assisted (no files modified)\n\n` +
|
|
799
|
+
`Mode: agent-assisted review (no files modified)\n\n` +
|
|
812
800
|
`## Summary\n\n` +
|
|
813
801
|
`- additions: ${additions.length}\n` +
|
|
814
802
|
`- updates: ${updates.length}\n` +
|
|
@@ -829,7 +817,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
829
817
|
`\n` +
|
|
830
818
|
`## Next steps\n\n` +
|
|
831
819
|
`1. Review the proposed changes list.\n` +
|
|
832
|
-
`2. Apply changes manually or re-run
|
|
820
|
+
`2. Apply changes manually or re-run without \`--agent\` to apply managed files.\n` +
|
|
833
821
|
`3. Run \`agentplane doctor\` (or \`agentplane doctor --fix\`) and ensure checks pass.\n`;
|
|
834
822
|
const constraintsMd = `# Upgrade constraints\n\n` +
|
|
835
823
|
`This upgrade is restricted to framework-managed files only.\n\n` +
|
|
@@ -841,7 +829,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
841
829
|
`- .git/**\n\n` +
|
|
842
830
|
`## Notes\n\n` +
|
|
843
831
|
`- The upgrade bundle is validated against framework.manifest.json.\n` +
|
|
844
|
-
`-
|
|
832
|
+
`- The policy gateway file at workspace root is AGENTS.md or CLAUDE.md.\n`;
|
|
845
833
|
const reportMd = `# Upgrade report (${runId})\n\n` +
|
|
846
834
|
`## Actions taken\n\n` +
|
|
847
835
|
`- [ ] Reviewed plan.md\n` +
|
|
@@ -862,30 +850,9 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
862
850
|
},
|
|
863
851
|
files: reviewRecords,
|
|
864
852
|
}, null, 2) + "\n", "utf8");
|
|
865
|
-
if (needsReview.length > 0) {
|
|
866
|
-
const snapshotsRoot = path.join(runDir, "snapshots");
|
|
867
|
-
for (const [rel, snap] of reviewSnapshots.entries()) {
|
|
868
|
-
const variants = [
|
|
869
|
-
["current", snap.currentText],
|
|
870
|
-
["incoming", snap.incomingText],
|
|
871
|
-
["baseline", snap.baselineText],
|
|
872
|
-
["proposed", snap.proposedText],
|
|
873
|
-
];
|
|
874
|
-
for (const [variant, text] of variants) {
|
|
875
|
-
if (text === null)
|
|
876
|
-
continue;
|
|
877
|
-
const outPath = path.join(snapshotsRoot, variant, rel);
|
|
878
|
-
await mkdir(path.dirname(outPath), { recursive: true });
|
|
879
|
-
await writeFile(outPath, text, "utf8");
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
853
|
const relRunDir = path.relative(resolved.gitRoot, runDir);
|
|
884
854
|
process.stdout.write(`Upgrade plan written: ${relRunDir}\n`);
|
|
885
|
-
process.stdout.write(`
|
|
886
|
-
if (needsReview.length > 0) {
|
|
887
|
-
process.stdout.write(`Hint: Create an UPGRADER task and attach ${relRunDir}\n`);
|
|
888
|
-
}
|
|
855
|
+
process.stdout.write(`Review-required files: ${needsReview.length}\n`);
|
|
889
856
|
return 0;
|
|
890
857
|
}
|
|
891
858
|
for (const rel of [...additions, ...updates]) {
|
|
@@ -897,8 +864,8 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
897
864
|
await mkdir(path.dirname(destPath), { recursive: true });
|
|
898
865
|
const data = fileContents.get(rel);
|
|
899
866
|
if (data) {
|
|
900
|
-
if (rel === "AGENTS.md") {
|
|
901
|
-
// If
|
|
867
|
+
if (rel === "AGENTS.md" || rel === "CLAUDE.md") {
|
|
868
|
+
// If policy gateway file is a symlink, avoid overwriting an arbitrary external target.
|
|
902
869
|
// This permits repo-internal symlinks (e.g. the agentplane repo itself) while
|
|
903
870
|
// keeping user workspaces safe.
|
|
904
871
|
try {
|
|
@@ -911,7 +878,7 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
911
878
|
throw new CliError({
|
|
912
879
|
exitCode: exitCodeForError("E_VALIDATION"),
|
|
913
880
|
code: "E_VALIDATION",
|
|
914
|
-
message: `Refusing to overwrite symlinked
|
|
881
|
+
message: `Refusing to overwrite symlinked ${rel} target outside repo: ${linkTarget}. ` +
|
|
915
882
|
"Replace the symlink with a regular file and retry.",
|
|
916
883
|
});
|
|
917
884
|
}
|
|
@@ -933,12 +900,17 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
933
900
|
await writeFile(baselinePath, data);
|
|
934
901
|
}
|
|
935
902
|
}
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
903
|
+
const hasManagedMutations = additions.length > 0 || updates.length > 0;
|
|
904
|
+
const hasSourceMigration = normalizedSourceToPersist !== null;
|
|
905
|
+
const shouldMutateConfig = hasManagedMutations || hasSourceMigration;
|
|
906
|
+
if (shouldMutateConfig) {
|
|
907
|
+
const raw = { ...loaded.raw };
|
|
908
|
+
if (normalizedSourceToPersist) {
|
|
909
|
+
setByDottedKey(raw, "framework.source", normalizedSourceToPersist);
|
|
910
|
+
}
|
|
911
|
+
setByDottedKey(raw, "framework.last_update", new Date().toISOString());
|
|
912
|
+
await saveConfig(resolved.agentplaneDir, raw);
|
|
939
913
|
}
|
|
940
|
-
setByDottedKey(raw, "framework.last_update", new Date().toISOString());
|
|
941
|
-
await saveConfig(resolved.agentplaneDir, raw);
|
|
942
914
|
await writeFile(statePath, JSON.stringify({
|
|
943
915
|
applied_at: new Date().toISOString(),
|
|
944
916
|
source: bundleLayout,
|
|
@@ -952,8 +924,24 @@ export async function cmdUpgradeParsed(opts) {
|
|
|
952
924
|
},
|
|
953
925
|
files: reviewRecords,
|
|
954
926
|
}, null, 2) + "\n", "utf8");
|
|
927
|
+
const commitPaths = [
|
|
928
|
+
...new Set([...additions, ...updates, ...(shouldMutateConfig ? [CONFIG_REL_PATH] : [])]),
|
|
929
|
+
];
|
|
930
|
+
const commit = await createUpgradeCommit({
|
|
931
|
+
gitRoot: resolved.gitRoot,
|
|
932
|
+
paths: commitPaths,
|
|
933
|
+
versionLabel: upgradeVersionLabel,
|
|
934
|
+
source: bundleLayout,
|
|
935
|
+
additions: additions.length,
|
|
936
|
+
updates: updates.length,
|
|
937
|
+
unchanged: skipped.length,
|
|
938
|
+
incidentsAppendedCount,
|
|
939
|
+
});
|
|
955
940
|
await cleanupAutoUpgradeArtifacts({ upgradeStateDir, createdBackups });
|
|
956
941
|
process.stdout.write(`Upgrade applied: ${additions.length} add, ${updates.length} update, ${skipped.length} unchanged\n`);
|
|
942
|
+
if (commit) {
|
|
943
|
+
process.stdout.write(`Upgrade commit: ${commit.hash.slice(0, 12)} ${commit.subject}\n`);
|
|
944
|
+
}
|
|
957
945
|
return 0;
|
|
958
946
|
}
|
|
959
947
|
finally {
|