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.
Files changed (97) hide show
  1. package/README.md +3 -1
  2. package/assets/AGENTS.md +124 -526
  3. package/assets/agents/UPGRADER.json +10 -9
  4. package/assets/framework.manifest.json +112 -7
  5. package/assets/policy/check-routing.mjs +180 -0
  6. package/assets/policy/dod.code.md +25 -0
  7. package/assets/policy/dod.core.md +32 -0
  8. package/assets/policy/dod.docs.md +32 -0
  9. package/assets/policy/examples/migration-note.md +6 -0
  10. package/assets/policy/examples/pr-note.md +16 -0
  11. package/assets/policy/examples/unit-test-pattern.md +19 -0
  12. package/assets/policy/governance.md +37 -0
  13. package/assets/policy/incidents.md +36 -0
  14. package/assets/policy/security.must.md +7 -0
  15. package/assets/policy/workflow.branch_pr.md +34 -0
  16. package/assets/policy/workflow.direct.md +47 -0
  17. package/assets/policy/workflow.md +9 -0
  18. package/assets/policy/workflow.release.md +31 -0
  19. package/assets/policy/workflow.upgrade.md +20 -0
  20. package/bin/agentplane.js +88 -87
  21. package/bin/dist-guard.js +124 -0
  22. package/bin/runtime-context.d.ts +20 -0
  23. package/bin/runtime-context.js +81 -0
  24. package/dist/.build-manifest.json +5 -5
  25. package/dist/agents/agents-template.d.ts +7 -0
  26. package/dist/agents/agents-template.d.ts.map +1 -1
  27. package/dist/agents/agents-template.js +41 -2
  28. package/dist/cli/bootstrap-guide.d.ts +18 -0
  29. package/dist/cli/bootstrap-guide.d.ts.map +1 -0
  30. package/dist/cli/bootstrap-guide.js +132 -0
  31. package/dist/cli/command-guide.d.ts.map +1 -1
  32. package/dist/cli/command-guide.js +58 -183
  33. package/dist/cli/command-snippets.d.ts +3 -3
  34. package/dist/cli/command-snippets.js +3 -3
  35. package/dist/cli/run-cli/commands/core.js +3 -3
  36. package/dist/cli/run-cli/commands/ide.d.ts.map +1 -1
  37. package/dist/cli/run-cli/commands/ide.js +8 -3
  38. package/dist/cli/run-cli/commands/init/ui.d.ts.map +1 -1
  39. package/dist/cli/run-cli/commands/init/ui.js +1 -2
  40. package/dist/cli/run-cli/commands/init/write-agents.d.ts +2 -0
  41. package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -1
  42. package/dist/cli/run-cli/commands/init/write-agents.js +24 -5
  43. package/dist/cli/run-cli/commands/init/write-workflow.d.ts +5 -0
  44. package/dist/cli/run-cli/commands/init/write-workflow.d.ts.map +1 -1
  45. package/dist/cli/run-cli/commands/init/write-workflow.js +6 -0
  46. package/dist/cli/run-cli/commands/init.d.ts +2 -0
  47. package/dist/cli/run-cli/commands/init.d.ts.map +1 -1
  48. package/dist/cli/run-cli/commands/init.js +47 -19
  49. package/dist/cli/run-cli.d.ts.map +1 -1
  50. package/dist/cli/run-cli.js +125 -7
  51. package/dist/commands/doctor.run.d.ts.map +1 -1
  52. package/dist/commands/doctor.run.js +48 -6
  53. package/dist/commands/finish.run.d.ts.map +1 -1
  54. package/dist/commands/finish.run.js +1 -0
  55. package/dist/commands/finish.spec.d.ts +1 -0
  56. package/dist/commands/finish.spec.d.ts.map +1 -1
  57. package/dist/commands/finish.spec.js +23 -2
  58. package/dist/commands/release/apply.command.d.ts +1 -0
  59. package/dist/commands/release/apply.command.d.ts.map +1 -1
  60. package/dist/commands/release/apply.command.js +20 -9
  61. package/dist/commands/release/plan.command.d.ts.map +1 -1
  62. package/dist/commands/release/plan.command.js +9 -3
  63. package/dist/commands/task/add.d.ts.map +1 -1
  64. package/dist/commands/task/add.js +32 -0
  65. package/dist/commands/task/doc.command.d.ts.map +1 -1
  66. package/dist/commands/task/doc.command.js +1 -0
  67. package/dist/commands/task/finish.d.ts +1 -0
  68. package/dist/commands/task/finish.d.ts.map +1 -1
  69. package/dist/commands/task/finish.js +28 -7
  70. package/dist/commands/task/new.d.ts.map +1 -1
  71. package/dist/commands/task/new.js +41 -4
  72. package/dist/commands/task/plan.d.ts.map +1 -1
  73. package/dist/commands/task/plan.js +7 -1
  74. package/dist/commands/task/shared.d.ts +7 -0
  75. package/dist/commands/task/shared.d.ts.map +1 -1
  76. package/dist/commands/task/shared.js +37 -0
  77. package/dist/commands/task/start-ready.js +1 -1
  78. package/dist/commands/upgrade.command.d.ts.map +1 -1
  79. package/dist/commands/upgrade.command.js +11 -7
  80. package/dist/commands/upgrade.d.ts.map +1 -1
  81. package/dist/commands/upgrade.js +284 -296
  82. package/dist/commands/workflow-build.command.d.ts.map +1 -1
  83. package/dist/commands/workflow-build.command.js +7 -0
  84. package/dist/commands/workflow-playbook.command.d.ts.map +1 -1
  85. package/dist/commands/workflow-playbook.command.js +0 -1
  86. package/dist/shared/policy-gateway.d.ts +15 -0
  87. package/dist/shared/policy-gateway.d.ts.map +1 -0
  88. package/dist/shared/policy-gateway.js +49 -0
  89. package/dist/shared/protected-paths.d.ts.map +1 -1
  90. package/dist/shared/protected-paths.js +1 -0
  91. package/dist/shared/runtime-artifacts.d.ts +2 -2
  92. package/dist/shared/runtime-artifacts.d.ts.map +1 -1
  93. package/dist/shared/runtime-artifacts.js +4 -0
  94. package/dist/workflow-runtime/build.d.ts +1 -1
  95. package/dist/workflow-runtime/build.d.ts.map +1 -1
  96. package/dist/workflow-runtime/build.js +14 -2
  97. package/package.json +2 -2
@@ -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
- return false;
154
- }
155
- const LOCAL_OVERRIDES_START = "<!-- AGENTPLANE:LOCAL-START -->";
156
- const LOCAL_OVERRIDES_END = "<!-- AGENTPLANE:LOCAL-END -->";
157
- function extractLocalOverridesBlock(text) {
158
- const start = text.indexOf(LOCAL_OVERRIDES_START);
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
- if (overrides.length === 0)
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
- // Used as a fallback for 3-way merges when no baseline is available. Incoming (upstream) values
266
- // win for scalar/object conflicts, while user-added keys and array items are preserved.
267
- function mergeAgentJsonIncomingWins(incomingText, currentText) {
268
- let incoming;
269
- let current;
270
- try {
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
- catch {
275
- return null;
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
- if (!isJsonRecord(incoming) || !isJsonRecord(current))
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
- const out = { ...incoming };
280
- for (const [k, curVal] of Object.entries(current)) {
281
- const incVal = incoming[k];
282
- if (incVal === undefined) {
283
- out[k] = curVal;
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 JSON.stringify(out, null, 2) + "\n";
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 mergeAgentJson3Way(opts) {
311
- let incoming;
312
- let current;
313
- let base;
314
- try {
315
- incoming = JSON.parse(opts.incomingText);
316
- current = JSON.parse(opts.currentText);
317
- base = JSON.parse(opts.baseText);
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
- catch {
320
- return null;
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
- if (!isJsonRecord(incoming) || !isJsonRecord(current) || !isJsonRecord(base))
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
- const keys = new Set([...Object.keys(incoming), ...Object.keys(current), ...Object.keys(base)]);
325
- const out = {};
326
- for (const key of keys) {
327
- const incVal = incoming[key];
328
- const curVal = current[key];
329
- const baseVal = base[key];
330
- // Arrays: always take incoming as base; if user changed vs base, append user-only items.
331
- if (Array.isArray(incVal) && Array.isArray(curVal) && Array.isArray(baseVal)) {
332
- const merged = [...incVal];
333
- const userChanged = !jsonEqual(curVal, baseVal);
334
- if (userChanged) {
335
- const seen = new Set();
336
- for (const x of merged)
337
- seen.add(JSON.stringify(canonicalizeJson(x)));
338
- for (const item of curVal) {
339
- const k = JSON.stringify(canonicalizeJson(item));
340
- if (!seen.has(k)) {
341
- merged.push(item);
342
- seen.add(k);
343
- }
344
- }
345
- }
346
- out[key] = merged;
347
- continue;
348
- }
349
- // Objects: shallow merge; for each subkey, prefer incoming unless user changed vs base.
350
- if (isJsonRecord(incVal) && isJsonRecord(curVal) && isJsonRecord(baseVal)) {
351
- const merged = { ...incVal };
352
- const subKeys = new Set([
353
- ...Object.keys(incVal),
354
- ...Object.keys(curVal),
355
- ...Object.keys(baseVal),
356
- ]);
357
- for (const sk of subKeys) {
358
- const incSub = incVal[sk];
359
- const curSub = curVal[sk];
360
- const baseSub = baseVal[sk];
361
- const userChanged = !jsonEqual(curSub, baseSub);
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
- return JSON.stringify(out, null, 2) + "\n";
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
- const reviewSnapshots = new Map();
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 rel = entry.path.replaceAll("\\", "/").trim();
580
- if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
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(rel)) {
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: ${rel}`,
613
+ message: `Manifest includes a denied path: ${relRaw}`,
592
614
  });
593
615
  }
594
- if (!isAllowedUpgradePath(rel)) {
616
+ if (!isAllowedUpgradePath(relRaw)) {
595
617
  throw new CliError({
596
618
  exitCode: 3,
597
619
  code: "E_VALIDATION",
598
- message: `Manifest path not allowed: ${rel}`,
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
- try {
614
- data = await readFile(sourcePath);
615
- }
616
- catch {
617
- if (entry.required)
618
- missingRequired.push(rel);
619
- continue;
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: if incoming already equals local, semantic merge/snapshots are unnecessary.
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 without semantic merge.
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
- // Merge logic only needs text for a small subset of managed files.
688
- if (existingBuf) {
689
- if (entry.merge_strategy === "agents_policy_markdown" && rel === "AGENTS.md") {
690
- existingText = existingBuf.toString("utf8");
691
- const mergedText = mergeAgentsPolicyMarkdown(data.toString("utf8"), existingText);
692
- data = Buffer.from(mergedText, "utf8");
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 = "markdownOverrides";
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 baselineConflict = baselineText === null
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 with \`agentplane upgrade --auto\` to apply managed files.\n` +
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
- `- AGENTS.md is the canonical policy file at the workspace root.\n`;
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(`Prompt merge required: ${needsReview.length} files\n`);
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 AGENTS.md is a symlink, avoid overwriting an arbitrary external target.
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 AGENTS.md target outside repo: ${linkTarget}. ` +
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 raw = { ...loaded.raw };
937
- if (normalizedSourceToPersist) {
938
- setByDottedKey(raw, "framework.source", normalizedSourceToPersist);
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 {