agentplane 0.2.25 → 0.3.1

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 (221) hide show
  1. package/README.md +3 -1
  2. package/assets/AGENTS.md +123 -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 +46 -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 +47 -57
  21. package/bin/dist-guard.js +124 -0
  22. package/dist/.build-manifest.json +11 -0
  23. package/dist/agents/agents-template.d.ts +7 -0
  24. package/dist/agents/agents-template.d.ts.map +1 -1
  25. package/dist/agents/agents-template.js +41 -2
  26. package/dist/backends/task-backend/local-backend.d.ts +2 -0
  27. package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
  28. package/dist/backends/task-backend/local-backend.js +12 -1
  29. package/dist/backends/task-backend/redmine/mapping.d.ts.map +1 -1
  30. package/dist/backends/task-backend/redmine/mapping.js +26 -1
  31. package/dist/backends/task-backend/redmine-backend.d.ts +4 -0
  32. package/dist/backends/task-backend/redmine-backend.d.ts.map +1 -1
  33. package/dist/backends/task-backend/redmine-backend.js +92 -9
  34. package/dist/backends/task-backend/shared/types.d.ts +1 -0
  35. package/dist/backends/task-backend/shared/types.d.ts.map +1 -1
  36. package/dist/backends/task-index.d.ts.map +1 -1
  37. package/dist/backends/task-index.js +8 -1
  38. package/dist/cli/command-guide.d.ts.map +1 -1
  39. package/dist/cli/command-guide.js +39 -17
  40. package/dist/cli/command-snippets.d.ts +24 -0
  41. package/dist/cli/command-snippets.d.ts.map +1 -0
  42. package/dist/cli/command-snippets.js +23 -0
  43. package/dist/cli/reason-codes.d.ts +9 -0
  44. package/dist/cli/reason-codes.d.ts.map +1 -0
  45. package/dist/cli/reason-codes.js +79 -0
  46. package/dist/cli/recipes-bundled.d.ts +1 -0
  47. package/dist/cli/recipes-bundled.d.ts.map +1 -1
  48. package/dist/cli/recipes-bundled.js +4 -1
  49. package/dist/cli/run-cli/command-catalog.d.ts +1 -1
  50. package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
  51. package/dist/cli/run-cli/command-catalog.js +40 -1
  52. package/dist/cli/run-cli/commands/config.d.ts +5 -0
  53. package/dist/cli/run-cli/commands/config.d.ts.map +1 -1
  54. package/dist/cli/run-cli/commands/config.js +86 -1
  55. package/dist/cli/run-cli/commands/core.d.ts.map +1 -1
  56. package/dist/cli/run-cli/commands/core.js +57 -2
  57. package/dist/cli/run-cli/commands/ide.d.ts.map +1 -1
  58. package/dist/cli/run-cli/commands/ide.js +8 -3
  59. package/dist/cli/run-cli/commands/init/recipes.d.ts +5 -1
  60. package/dist/cli/run-cli/commands/init/recipes.d.ts.map +1 -1
  61. package/dist/cli/run-cli/commands/init/recipes.js +24 -4
  62. package/dist/cli/run-cli/commands/init/ui.d.ts.map +1 -1
  63. package/dist/cli/run-cli/commands/init/ui.js +1 -2
  64. package/dist/cli/run-cli/commands/init/write-agents.d.ts +2 -0
  65. package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -1
  66. package/dist/cli/run-cli/commands/init/write-agents.js +24 -5
  67. package/dist/cli/run-cli/commands/init/write-workflow.d.ts +12 -0
  68. package/dist/cli/run-cli/commands/init/write-workflow.d.ts.map +1 -0
  69. package/dist/cli/run-cli/commands/init/write-workflow.js +58 -0
  70. package/dist/cli/run-cli/commands/init.d.ts +4 -1
  71. package/dist/cli/run-cli/commands/init.d.ts.map +1 -1
  72. package/dist/cli/run-cli/commands/init.js +126 -48
  73. package/dist/cli/run-cli.d.ts.map +1 -1
  74. package/dist/cli/run-cli.js +195 -8
  75. package/dist/commands/backend/sync.command.d.ts.map +1 -1
  76. package/dist/commands/backend/sync.command.js +7 -6
  77. package/dist/commands/backend.d.ts.map +1 -1
  78. package/dist/commands/backend.js +2 -0
  79. package/dist/commands/doctor.run.d.ts.map +1 -1
  80. package/dist/commands/doctor.run.js +107 -16
  81. package/dist/commands/guard/impl/commands.d.ts.map +1 -1
  82. package/dist/commands/guard/impl/commands.js +12 -6
  83. package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -1
  84. package/dist/commands/recipes/impl/commands/install.js +36 -13
  85. package/dist/commands/recipes/impl/scenario.d.ts.map +1 -1
  86. package/dist/commands/recipes/impl/scenario.js +25 -0
  87. package/dist/commands/recipes/impl/types.d.ts +4 -0
  88. package/dist/commands/recipes/impl/types.d.ts.map +1 -1
  89. package/dist/commands/release/apply.command.d.ts.map +1 -1
  90. package/dist/commands/release/apply.command.js +9 -4
  91. package/dist/commands/release/plan.command.d.ts.map +1 -1
  92. package/dist/commands/release/plan.command.js +9 -3
  93. package/dist/commands/scenario/impl/commands.d.ts.map +1 -1
  94. package/dist/commands/scenario/impl/commands.js +74 -3
  95. package/dist/commands/scenario/impl/report.d.ts +8 -0
  96. package/dist/commands/scenario/impl/report.d.ts.map +1 -1
  97. package/dist/commands/scenario/impl/report.js +1 -0
  98. package/dist/commands/shared/reconcile-check.d.ts +7 -0
  99. package/dist/commands/shared/reconcile-check.d.ts.map +1 -0
  100. package/dist/commands/shared/reconcile-check.js +60 -0
  101. package/dist/commands/sync.command.d.ts.map +1 -1
  102. package/dist/commands/sync.command.js +9 -2
  103. package/dist/commands/task/add.d.ts.map +1 -1
  104. package/dist/commands/task/add.js +32 -0
  105. package/dist/commands/task/doc.command.d.ts.map +1 -1
  106. package/dist/commands/task/doc.command.js +1 -0
  107. package/dist/commands/task/finish.d.ts.map +1 -1
  108. package/dist/commands/task/finish.js +11 -1
  109. package/dist/commands/task/list.d.ts.map +1 -1
  110. package/dist/commands/task/list.js +2 -1
  111. package/dist/commands/task/list.spec.d.ts.map +1 -1
  112. package/dist/commands/task/list.spec.js +7 -0
  113. package/dist/commands/task/new.d.ts.map +1 -1
  114. package/dist/commands/task/new.js +41 -4
  115. package/dist/commands/task/next.d.ts.map +1 -1
  116. package/dist/commands/task/next.js +2 -1
  117. package/dist/commands/task/next.spec.d.ts.map +1 -1
  118. package/dist/commands/task/next.spec.js +7 -0
  119. package/dist/commands/task/plan.d.ts.map +1 -1
  120. package/dist/commands/task/plan.js +7 -1
  121. package/dist/commands/task/search.d.ts.map +1 -1
  122. package/dist/commands/task/search.js +2 -1
  123. package/dist/commands/task/search.spec.d.ts.map +1 -1
  124. package/dist/commands/task/search.spec.js +7 -0
  125. package/dist/commands/task/shared.d.ts +14 -0
  126. package/dist/commands/task/shared.d.ts.map +1 -1
  127. package/dist/commands/task/shared.js +58 -1
  128. package/dist/commands/task/start-ready.js +1 -1
  129. package/dist/commands/task/verify-record.d.ts.map +1 -1
  130. package/dist/commands/task/verify-record.js +2 -0
  131. package/dist/commands/upgrade.command.d.ts.map +1 -1
  132. package/dist/commands/upgrade.command.js +2 -2
  133. package/dist/commands/upgrade.d.ts.map +1 -1
  134. package/dist/commands/upgrade.js +263 -294
  135. package/dist/commands/workflow-build.command.d.ts +8 -0
  136. package/dist/commands/workflow-build.command.d.ts.map +1 -0
  137. package/dist/commands/workflow-build.command.js +103 -0
  138. package/dist/commands/workflow-playbook.command.d.ts +10 -0
  139. package/dist/commands/workflow-playbook.command.d.ts.map +1 -0
  140. package/dist/commands/workflow-playbook.command.js +173 -0
  141. package/dist/commands/workflow-restore.command.d.ts +5 -0
  142. package/dist/commands/workflow-restore.command.d.ts.map +1 -0
  143. package/dist/commands/workflow-restore.command.js +30 -0
  144. package/dist/commands/workflow.command.d.ts +6 -0
  145. package/dist/commands/workflow.command.d.ts.map +1 -0
  146. package/dist/commands/workflow.command.js +36 -0
  147. package/dist/harness/dynamic-tool-contract.d.ts +29 -0
  148. package/dist/harness/dynamic-tool-contract.d.ts.map +1 -0
  149. package/dist/harness/dynamic-tool-contract.js +86 -0
  150. package/dist/harness/hooks-lifecycle.d.ts +27 -0
  151. package/dist/harness/hooks-lifecycle.d.ts.map +1 -0
  152. package/dist/harness/hooks-lifecycle.js +67 -0
  153. package/dist/harness/index.d.ts +9 -0
  154. package/dist/harness/index.d.ts.map +1 -0
  155. package/dist/harness/index.js +8 -0
  156. package/dist/harness/reconcile.d.ts +37 -0
  157. package/dist/harness/reconcile.d.ts.map +1 -0
  158. package/dist/harness/reconcile.js +42 -0
  159. package/dist/harness/retry-policy.d.ts +31 -0
  160. package/dist/harness/retry-policy.d.ts.map +1 -0
  161. package/dist/harness/retry-policy.js +33 -0
  162. package/dist/harness/scheduler.d.ts +18 -0
  163. package/dist/harness/scheduler.d.ts.map +1 -0
  164. package/dist/harness/scheduler.js +55 -0
  165. package/dist/harness/state-machine.d.ts +17 -0
  166. package/dist/harness/state-machine.d.ts.map +1 -0
  167. package/dist/harness/state-machine.js +70 -0
  168. package/dist/harness/token-accounting.d.ts +19 -0
  169. package/dist/harness/token-accounting.d.ts.map +1 -0
  170. package/dist/harness/token-accounting.js +77 -0
  171. package/dist/harness/workspace-safety.d.ts +14 -0
  172. package/dist/harness/workspace-safety.d.ts.map +1 -0
  173. package/dist/harness/workspace-safety.js +62 -0
  174. package/dist/recipes/bundled-recipes.d.ts +4 -0
  175. package/dist/recipes/bundled-recipes.d.ts.map +1 -1
  176. package/dist/recipes/bundled-recipes.js +11 -0
  177. package/dist/shared/errors.d.ts +6 -0
  178. package/dist/shared/errors.d.ts.map +1 -1
  179. package/dist/shared/errors.js +1 -0
  180. package/dist/shared/policy-gateway.d.ts +15 -0
  181. package/dist/shared/policy-gateway.d.ts.map +1 -0
  182. package/dist/shared/policy-gateway.js +49 -0
  183. package/dist/shared/protected-paths.d.ts.map +1 -1
  184. package/dist/shared/protected-paths.js +1 -0
  185. package/dist/shared/runtime-artifacts.d.ts +2 -2
  186. package/dist/shared/runtime-artifacts.d.ts.map +1 -1
  187. package/dist/shared/runtime-artifacts.js +4 -0
  188. package/dist/workflow-runtime/build.d.ts +4 -0
  189. package/dist/workflow-runtime/build.d.ts.map +1 -0
  190. package/dist/workflow-runtime/build.js +126 -0
  191. package/dist/workflow-runtime/enforcement.d.ts +3 -0
  192. package/dist/workflow-runtime/enforcement.d.ts.map +1 -0
  193. package/dist/workflow-runtime/enforcement.js +10 -0
  194. package/dist/workflow-runtime/file-ops.d.ts +11 -0
  195. package/dist/workflow-runtime/file-ops.d.ts.map +1 -0
  196. package/dist/workflow-runtime/file-ops.js +248 -0
  197. package/dist/workflow-runtime/fix.d.ts +9 -0
  198. package/dist/workflow-runtime/fix.d.ts.map +1 -0
  199. package/dist/workflow-runtime/fix.js +107 -0
  200. package/dist/workflow-runtime/index.d.ts +11 -0
  201. package/dist/workflow-runtime/index.d.ts.map +1 -0
  202. package/dist/workflow-runtime/index.js +10 -0
  203. package/dist/workflow-runtime/markdown.d.ts +10 -0
  204. package/dist/workflow-runtime/markdown.d.ts.map +1 -0
  205. package/dist/workflow-runtime/markdown.js +147 -0
  206. package/dist/workflow-runtime/observability.d.ts +12 -0
  207. package/dist/workflow-runtime/observability.d.ts.map +1 -0
  208. package/dist/workflow-runtime/observability.js +14 -0
  209. package/dist/workflow-runtime/paths.d.ts +3 -0
  210. package/dist/workflow-runtime/paths.d.ts.map +1 -0
  211. package/dist/workflow-runtime/paths.js +11 -0
  212. package/dist/workflow-runtime/template.d.ts +7 -0
  213. package/dist/workflow-runtime/template.d.ts.map +1 -0
  214. package/dist/workflow-runtime/template.js +94 -0
  215. package/dist/workflow-runtime/types.d.ts +68 -0
  216. package/dist/workflow-runtime/types.d.ts.map +1 -0
  217. package/dist/workflow-runtime/types.js +1 -0
  218. package/dist/workflow-runtime/validate.d.ts +8 -0
  219. package/dist/workflow-runtime/validate.d.ts.map +1 -0
  220. package/dist/workflow-runtime/validate.js +331 -0
  221. package/package.json +3 -3
@@ -11,6 +11,8 @@ 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;
@@ -148,83 +150,22 @@ async function resolveUpgradeRoot(extractedDir) {
148
150
  function isAllowedUpgradePath(relPath) {
149
151
  if (relPath === "AGENTS.md")
150
152
  return true;
153
+ if (relPath === "CLAUDE.md")
154
+ return true;
151
155
  if (relPath.startsWith(".agentplane/agents/") && relPath.endsWith(".json"))
152
156
  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
- }
157
+ if (relPath.startsWith(".agentplane/policy/") &&
158
+ (relPath.endsWith(".md") ||
159
+ relPath.endsWith(".ts") ||
160
+ relPath.endsWith(".js") ||
161
+ relPath.endsWith(".mjs"))) {
162
+ return true;
223
163
  }
224
- if (overrides.length === 0)
225
- return incoming;
226
- return withLocalOverridesBlock(incoming, overrides.join("\n"));
164
+ return false;
227
165
  }
166
+ const INCIDENTS_POLICY_PATH = ".agentplane/policy/incidents.md";
167
+ const INCIDENTS_APPEND_MARKER = "<!-- AGENTPLANE:UPGRADE-APPEND incidents.md -->";
168
+ const CONFIG_REL_PATH = ".agentplane/config.json";
228
169
  function isJsonRecord(value) {
229
170
  return !!value && typeof value === "object" && !Array.isArray(value);
230
171
  }
@@ -262,127 +203,167 @@ function textChangedForType(opts) {
262
203
  }
263
204
  return opts.aText.trimEnd() !== opts.bText.trimEnd();
264
205
  }
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);
206
+ function parseIncidentEntryBlocks(entriesBody) {
207
+ const lines = entriesBody.replaceAll("\r\n", "\n").split("\n");
208
+ const starts = [];
209
+ for (const [index, line] of lines.entries()) {
210
+ if (/^\s*-\s*id:\s+/i.test(line ?? ""))
211
+ starts.push(index);
273
212
  }
274
- catch {
275
- return null;
213
+ const blocks = [];
214
+ for (const [idx, start] of starts.entries()) {
215
+ const end = starts.at(idx + 1) ?? lines.length;
216
+ const slice = lines.slice(start, end);
217
+ while (slice.length > 0 && !(slice[0] ?? "").trim())
218
+ slice.shift();
219
+ while (slice.length > 0 && !(slice.at(-1) ?? "").trim())
220
+ slice.pop();
221
+ const block = slice.join("\n").trim();
222
+ if (block)
223
+ blocks.push(block);
276
224
  }
277
- if (!isJsonRecord(incoming) || !isJsonRecord(current))
225
+ return blocks;
226
+ }
227
+ function normalizeEntryBlock(block) {
228
+ return block
229
+ .replaceAll("\r\n", "\n")
230
+ .split("\n")
231
+ .map((line) => line.trimEnd())
232
+ .join("\n")
233
+ .trim();
234
+ }
235
+ function splitEntriesSection(text) {
236
+ const lines = text.replaceAll("\r\n", "\n").split("\n");
237
+ const headingIndex = lines.findIndex((line) => /^\s*##\s+Entries\s*$/i.test(line));
238
+ if (headingIndex === -1)
278
239
  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;
240
+ let nextHeadingIndex = lines.length;
241
+ for (let i = headingIndex + 1; i < lines.length; i++) {
242
+ if (/^\s*##\s+/.test(lines[i] ?? "")) {
243
+ nextHeadingIndex = i;
244
+ break;
305
245
  }
306
- out[k] = incVal;
307
246
  }
308
- return JSON.stringify(out, null, 2) + "\n";
247
+ return {
248
+ before: lines.slice(0, headingIndex + 1).join("\n"),
249
+ entriesBody: lines.slice(headingIndex + 1, nextHeadingIndex).join("\n"),
250
+ after: lines.slice(nextHeadingIndex).join("\n"),
251
+ };
309
252
  }
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);
253
+ function mergeIncidentsPolicy(opts) {
254
+ const incomingTrimmed = opts.incomingText.trim();
255
+ if (!incomingTrimmed)
256
+ return { nextText: opts.currentText, appended: false, appendedCount: 0 };
257
+ const incomingSection = splitEntriesSection(opts.incomingText);
258
+ const currentSection = splitEntriesSection(opts.currentText);
259
+ if (!incomingSection || !currentSection) {
260
+ return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
318
261
  }
319
- catch {
320
- return null;
262
+ const incomingBlocks = parseIncidentEntryBlocks(incomingSection.entriesBody).map((block) => normalizeEntryBlock(block));
263
+ const currentBlocks = parseIncidentEntryBlocks(currentSection.entriesBody).map((block) => normalizeEntryBlock(block));
264
+ if (currentBlocks.length === 0) {
265
+ return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
266
+ }
267
+ const baselineSection = opts.baselineText ? splitEntriesSection(opts.baselineText) : null;
268
+ const baselineBlocks = baselineSection
269
+ ? parseIncidentEntryBlocks(baselineSection.entriesBody).map((block) => normalizeEntryBlock(block))
270
+ : [];
271
+ const baselineSet = new Set(baselineBlocks);
272
+ const incomingSet = new Set(incomingBlocks);
273
+ const userAdded = currentBlocks.filter((block) => {
274
+ if (baselineSet.size > 0 && baselineSet.has(block))
275
+ return false;
276
+ return true;
277
+ });
278
+ const toAppend = userAdded.filter((block) => !incomingSet.has(block));
279
+ if (toAppend.length === 0) {
280
+ return { nextText: opts.incomingText, appended: false, appendedCount: 0 };
321
281
  }
322
- if (!isJsonRecord(incoming) || !isJsonRecord(current) || !isJsonRecord(base))
282
+ const mergedBlocks = [...incomingBlocks, ...toAppend];
283
+ const renderedEntries = mergedBlocks.length > 0 ? `\n\n${mergedBlocks.join("\n\n")}\n` : "\n\n- None yet.\n";
284
+ const afterSuffix = incomingSection.after ? `\n${incomingSection.after.trimStart()}` : "";
285
+ const nextText = `${incomingSection.before.trimEnd()}` +
286
+ `${renderedEntries}` +
287
+ `${INCIDENTS_APPEND_MARKER}\n` +
288
+ `${afterSuffix}` +
289
+ `\n`;
290
+ return { nextText, appended: true, appendedCount: toAppend.length };
291
+ }
292
+ function normalizeUpgradeVersionLabel(input) {
293
+ const trimmed = input.trim();
294
+ if (!trimmed)
295
+ return "unknown";
296
+ if (/^v\d/i.test(trimmed))
297
+ return trimmed;
298
+ return `v${trimmed}`;
299
+ }
300
+ async function ensureCleanTrackedTreeForUpgrade(gitRoot) {
301
+ const { stdout } = await execFileAsync("git", ["status", "--short", "--untracked-files=no"], {
302
+ cwd: gitRoot,
303
+ env: gitEnv(),
304
+ maxBuffer: 10 * 1024 * 1024,
305
+ });
306
+ const dirty = String(stdout ?? "")
307
+ .split(/\r?\n/u)
308
+ .map((line) => line.trimEnd())
309
+ .filter((line) => line.length > 0);
310
+ if (dirty.length === 0)
311
+ return;
312
+ throw new CliError({
313
+ exitCode: exitCodeForError("E_GIT"),
314
+ code: "E_GIT",
315
+ message: "Upgrade --auto requires a clean tracked working tree.\n" +
316
+ `Found tracked changes:\n${dirty.map((line) => ` ${line}`).join("\n")}`,
317
+ });
318
+ }
319
+ async function createUpgradeCommit(opts) {
320
+ const uniquePaths = [...new Set(opts.paths.filter(Boolean))];
321
+ if (uniquePaths.length === 0)
323
322
  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;
323
+ await execFileAsync("git", ["add", "--", ...uniquePaths], {
324
+ cwd: opts.gitRoot,
325
+ env: gitEnv(),
326
+ maxBuffer: 10 * 1024 * 1024,
327
+ });
328
+ const { stdout: stagedOut } = await execFileAsync("git", ["diff", "--cached", "--name-only", "-z"], {
329
+ cwd: opts.gitRoot,
330
+ env: gitEnv(),
331
+ encoding: "buffer",
332
+ maxBuffer: 10 * 1024 * 1024,
333
+ });
334
+ const staged = (Buffer.isBuffer(stagedOut) ? stagedOut.toString("utf8") : String(stagedOut ?? ""))
335
+ .split("\0")
336
+ .map((entry) => entry.trim())
337
+ .some(Boolean);
338
+ if (!staged)
339
+ return null;
340
+ const subject = `⬆️ upgrade: apply framework ${opts.versionLabel}`;
341
+ const body = `Upgrade-Version: ${opts.versionLabel}\n` +
342
+ `Source: ${opts.source}\n` +
343
+ `Managed-Changes: add=${opts.additions}, update=${opts.updates}, unchanged=${opts.unchanged}\n` +
344
+ `Incidents-Appended: ${opts.incidentsAppendedCount}\n`;
345
+ try {
346
+ await execFileAsync("git", ["commit", "-m", subject, "-m", body], {
347
+ cwd: opts.gitRoot,
348
+ env: gitEnv(),
349
+ maxBuffer: 10 * 1024 * 1024,
350
+ });
351
+ }
352
+ catch (err) {
353
+ const details = err?.stderr ?? "";
354
+ throw new CliError({
355
+ exitCode: exitCodeForError("E_GIT"),
356
+ code: "E_GIT",
357
+ message: "Upgrade applied but failed to create the upgrade commit.\n" +
358
+ "Fix commit policy/hook issues and commit the staged upgrade files as a dedicated upgrade commit.\n" +
359
+ (String(details).trim() ? `Details:\n${String(details).trim()}` : ""),
360
+ });
384
361
  }
385
- return JSON.stringify(out, null, 2) + "\n";
362
+ const { stdout: hashOut } = await execFileAsync("git", ["rev-parse", "HEAD"], {
363
+ cwd: opts.gitRoot,
364
+ env: gitEnv(),
365
+ });
366
+ return { hash: String(hashOut ?? "").trim(), subject };
386
367
  }
387
368
  export async function cmdUpgradeParsed(opts) {
388
369
  const flags = opts.flags;
@@ -400,6 +381,9 @@ export async function cmdUpgradeParsed(opts) {
400
381
  rootOverride: opts.rootOverride ?? null,
401
382
  });
402
383
  const loaded = await loadConfig(resolved.agentplaneDir);
384
+ if (flags.mode === "auto" && !flags.dryRun) {
385
+ await ensureCleanTrackedTreeForUpgrade(resolved.gitRoot);
386
+ }
403
387
  const upgradeStateDir = path.join(resolved.agentplaneDir, ".upgrade");
404
388
  const lockPath = path.join(upgradeStateDir, "lock.json");
405
389
  const statePath = path.join(upgradeStateDir, "state.json");
@@ -438,6 +422,7 @@ export async function cmdUpgradeParsed(opts) {
438
422
  let bundleLayout = "upgrade_bundle";
439
423
  let bundleRoot = "";
440
424
  let normalizedSourceToPersist = null;
425
+ let upgradeVersionLabel = normalizeUpgradeVersionLabel(getVersion());
441
426
  if (!hasBundle && !useRemote) {
442
427
  bundleLayout = "local_assets";
443
428
  bundleRoot = fileURLToPath(ASSETS_DIR_URL);
@@ -477,6 +462,12 @@ export async function cmdUpgradeParsed(opts) {
477
462
  const assetName = flags.asset ?? DEFAULT_UPGRADE_ASSET;
478
463
  const checksumName = flags.checksumAsset ?? DEFAULT_UPGRADE_CHECKSUM_ASSET;
479
464
  const release = (await fetchJson(releaseUrl, UPGRADE_RELEASE_METADATA_TIMEOUT_MS));
465
+ const releaseTag = (typeof release.tag_name === "string" && release.tag_name.trim()) ||
466
+ (typeof flags.tag === "string" && flags.tag.trim()) ||
467
+ "";
468
+ if (releaseTag) {
469
+ upgradeVersionLabel = normalizeUpgradeVersionLabel(releaseTag);
470
+ }
480
471
  const download = resolveUpgradeDownloadFromRelease({
481
472
  release,
482
473
  owner,
@@ -553,7 +544,7 @@ export async function cmdUpgradeParsed(opts) {
553
544
  const merged = [];
554
545
  const missingRequired = [];
555
546
  const reviewRecords = [];
556
- const reviewSnapshots = new Map();
547
+ let incidentsAppendedCount = 0;
557
548
  const readBaselineText = async (baselineKey) => {
558
549
  try {
559
550
  return await readFile(path.join(baselineDirNew, baselineKey), "utf8");
@@ -571,33 +562,46 @@ export async function cmdUpgradeParsed(opts) {
571
562
  const toBaselineKey = (rel) => {
572
563
  if (rel === "AGENTS.md")
573
564
  return "AGENTS.md";
565
+ if (rel === "CLAUDE.md")
566
+ return "CLAUDE.md";
574
567
  if (rel.startsWith(".agentplane/"))
575
568
  return rel.slice(".agentplane/".length);
576
569
  return null;
577
570
  };
571
+ const policyGatewayRel = (await fileExists(path.join(resolved.gitRoot, "AGENTS.md")))
572
+ ? "AGENTS.md"
573
+ : (await fileExists(path.join(resolved.gitRoot, "CLAUDE.md")))
574
+ ? "CLAUDE.md"
575
+ : "AGENTS.md";
576
+ const remapManagedGatewayRel = (rel) => {
577
+ if (rel === "AGENTS.md" && policyGatewayRel === "CLAUDE.md")
578
+ return "CLAUDE.md";
579
+ return rel;
580
+ };
578
581
  for (const entry of manifest.files) {
579
- const rel = entry.path.replaceAll("\\", "/").trim();
580
- if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
582
+ const relRaw = entry.path.replaceAll("\\", "/").trim();
583
+ if (!relRaw || relRaw.startsWith("..") || path.isAbsolute(relRaw)) {
581
584
  throw new CliError({
582
585
  exitCode: 3,
583
586
  code: "E_VALIDATION",
584
587
  message: `Invalid manifest path: ${entry.path}`,
585
588
  });
586
589
  }
587
- if (isDeniedUpgradePath(rel)) {
590
+ if (isDeniedUpgradePath(relRaw)) {
588
591
  throw new CliError({
589
592
  exitCode: 3,
590
593
  code: "E_VALIDATION",
591
- message: `Manifest includes a denied path: ${rel}`,
594
+ message: `Manifest includes a denied path: ${relRaw}`,
592
595
  });
593
596
  }
594
- if (!isAllowedUpgradePath(rel)) {
597
+ if (!isAllowedUpgradePath(relRaw)) {
595
598
  throw new CliError({
596
599
  exitCode: 3,
597
600
  code: "E_VALIDATION",
598
- message: `Manifest path not allowed: ${rel}`,
601
+ message: `Manifest path not allowed: ${relRaw}`,
599
602
  });
600
603
  }
604
+ const rel = remapManagedGatewayRel(relRaw);
601
605
  const destPath = path.join(resolved.gitRoot, rel);
602
606
  const kind = await getPathKind(destPath);
603
607
  if (kind === "dir") {
@@ -607,16 +611,27 @@ export async function cmdUpgradeParsed(opts) {
607
611
  message: `Upgrade target is a directory: ${rel}`,
608
612
  });
609
613
  }
610
- const sourceRel = (entry.source_path ?? entry.path).replaceAll("\\", "/").trim();
611
- const sourcePath = path.join(bundleRoot, sourceRel);
612
614
  let data;
613
- try {
614
- data = await readFile(sourcePath);
615
- }
616
- catch {
617
- if (entry.required)
618
- missingRequired.push(rel);
619
- continue;
615
+ {
616
+ const sourceRelRaw = (entry.source_path ?? entry.path).replaceAll("\\", "/").trim();
617
+ const mappedSourceRel = rel === "CLAUDE.md" && sourceRelRaw === "AGENTS.md" ? "CLAUDE.md" : sourceRelRaw;
618
+ const sourceCandidates = [...new Set([mappedSourceRel, sourceRelRaw])];
619
+ let loaded = null;
620
+ for (const candidate of sourceCandidates) {
621
+ try {
622
+ loaded = await readFile(path.join(bundleRoot, candidate));
623
+ break;
624
+ }
625
+ catch {
626
+ // try next candidate
627
+ }
628
+ }
629
+ if (!loaded) {
630
+ if (entry.required)
631
+ missingRequired.push(rel);
632
+ continue;
633
+ }
634
+ data = loaded;
620
635
  }
621
636
  let existingBuf = null;
622
637
  let existingText = null;
@@ -649,7 +664,7 @@ export async function cmdUpgradeParsed(opts) {
649
664
  aText: currentTextForReview,
650
665
  bText: incomingTextOriginal,
651
666
  }) === false;
652
- // Fast-path: if incoming already equals local, semantic merge/snapshots are unnecessary.
667
+ // Fast-path: incoming already equals local.
653
668
  if (currentTextForReview !== null && currentAndIncomingEqual) {
654
669
  skipped.push(rel);
655
670
  reviewRecords.push({
@@ -665,7 +680,7 @@ export async function cmdUpgradeParsed(opts) {
665
680
  });
666
681
  continue;
667
682
  }
668
- // No local edits vs baseline: file can be safely replaced with incoming without semantic merge.
683
+ // No local edits vs baseline: file can be safely replaced with incoming.
669
684
  if (currentTextForReview !== null && changedCurrentVsBaseline === false) {
670
685
  updates.push(rel);
671
686
  fileContents.set(rel, data);
@@ -684,53 +699,24 @@ export async function cmdUpgradeParsed(opts) {
684
699
  }
685
700
  let mergeApplied = false;
686
701
  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");
702
+ // Simplified policy for upgrade:
703
+ // - All managed files are replaced with incoming bundle content.
704
+ // - incidents.md is append-only when local file already has content.
705
+ if (existingBuf && rel === INCIDENTS_POLICY_PATH) {
706
+ existingText = existingBuf.toString("utf8");
707
+ const mergedIncidents = mergeIncidentsPolicy({
708
+ incomingText: data.toString("utf8"),
709
+ currentText: existingText,
710
+ baselineText,
711
+ });
712
+ data = Buffer.from(mergedIncidents.nextText, "utf8");
713
+ if (mergedIncidents.appended) {
693
714
  merged.push(rel);
694
715
  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
- }
716
+ mergePath = "incidentsAppend";
717
+ incidentsAppendedCount += mergedIncidents.appendedCount;
731
718
  }
732
719
  }
733
- const proposedText = data.toString("utf8");
734
720
  const currentDiffersFromIncoming = currentTextForReview === null
735
721
  ? false
736
722
  : textChangedForType({
@@ -738,16 +724,7 @@ export async function cmdUpgradeParsed(opts) {
738
724
  aText: currentTextForReview,
739
725
  bText: incomingTextOriginal,
740
726
  });
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;
727
+ const needsSemanticReview = false;
751
728
  reviewRecords.push({
752
729
  relPath: rel,
753
730
  mergeStrategy: entry.merge_strategy,
@@ -759,14 +736,6 @@ export async function cmdUpgradeParsed(opts) {
759
736
  mergeApplied,
760
737
  mergePath,
761
738
  });
762
- if (flags.mode === "agent" && needsSemanticReview) {
763
- reviewSnapshots.set(rel, {
764
- incomingText: incomingTextOriginal,
765
- currentText: currentTextForReview,
766
- baselineText,
767
- proposedText,
768
- });
769
- }
770
739
  fileContents.set(rel, data);
771
740
  if (kind === null)
772
741
  additions.push(rel);
@@ -841,7 +810,7 @@ export async function cmdUpgradeParsed(opts) {
841
810
  `- .git/**\n\n` +
842
811
  `## Notes\n\n` +
843
812
  `- The upgrade bundle is validated against framework.manifest.json.\n` +
844
- `- AGENTS.md is the canonical policy file at the workspace root.\n`;
813
+ `- The policy gateway file at workspace root is AGENTS.md or CLAUDE.md.\n`;
845
814
  const reportMd = `# Upgrade report (${runId})\n\n` +
846
815
  `## Actions taken\n\n` +
847
816
  `- [ ] Reviewed plan.md\n` +
@@ -862,30 +831,9 @@ export async function cmdUpgradeParsed(opts) {
862
831
  },
863
832
  files: reviewRecords,
864
833
  }, 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
834
  const relRunDir = path.relative(resolved.gitRoot, runDir);
884
835
  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
- }
836
+ process.stdout.write(`Review-required files: ${needsReview.length}\n`);
889
837
  return 0;
890
838
  }
891
839
  for (const rel of [...additions, ...updates]) {
@@ -897,8 +845,8 @@ export async function cmdUpgradeParsed(opts) {
897
845
  await mkdir(path.dirname(destPath), { recursive: true });
898
846
  const data = fileContents.get(rel);
899
847
  if (data) {
900
- if (rel === "AGENTS.md") {
901
- // If AGENTS.md is a symlink, avoid overwriting an arbitrary external target.
848
+ if (rel === "AGENTS.md" || rel === "CLAUDE.md") {
849
+ // If policy gateway file is a symlink, avoid overwriting an arbitrary external target.
902
850
  // This permits repo-internal symlinks (e.g. the agentplane repo itself) while
903
851
  // keeping user workspaces safe.
904
852
  try {
@@ -911,7 +859,7 @@ export async function cmdUpgradeParsed(opts) {
911
859
  throw new CliError({
912
860
  exitCode: exitCodeForError("E_VALIDATION"),
913
861
  code: "E_VALIDATION",
914
- message: `Refusing to overwrite symlinked AGENTS.md target outside repo: ${linkTarget}. ` +
862
+ message: `Refusing to overwrite symlinked ${rel} target outside repo: ${linkTarget}. ` +
915
863
  "Replace the symlink with a regular file and retry.",
916
864
  });
917
865
  }
@@ -933,12 +881,17 @@ export async function cmdUpgradeParsed(opts) {
933
881
  await writeFile(baselinePath, data);
934
882
  }
935
883
  }
936
- const raw = { ...loaded.raw };
937
- if (normalizedSourceToPersist) {
938
- setByDottedKey(raw, "framework.source", normalizedSourceToPersist);
884
+ const hasManagedMutations = additions.length > 0 || updates.length > 0;
885
+ const hasSourceMigration = normalizedSourceToPersist !== null;
886
+ const shouldMutateConfig = hasManagedMutations || hasSourceMigration;
887
+ if (shouldMutateConfig) {
888
+ const raw = { ...loaded.raw };
889
+ if (normalizedSourceToPersist) {
890
+ setByDottedKey(raw, "framework.source", normalizedSourceToPersist);
891
+ }
892
+ setByDottedKey(raw, "framework.last_update", new Date().toISOString());
893
+ await saveConfig(resolved.agentplaneDir, raw);
939
894
  }
940
- setByDottedKey(raw, "framework.last_update", new Date().toISOString());
941
- await saveConfig(resolved.agentplaneDir, raw);
942
895
  await writeFile(statePath, JSON.stringify({
943
896
  applied_at: new Date().toISOString(),
944
897
  source: bundleLayout,
@@ -952,8 +905,24 @@ export async function cmdUpgradeParsed(opts) {
952
905
  },
953
906
  files: reviewRecords,
954
907
  }, null, 2) + "\n", "utf8");
908
+ const commitPaths = [
909
+ ...new Set([...additions, ...updates, ...(shouldMutateConfig ? [CONFIG_REL_PATH] : [])]),
910
+ ];
911
+ const commit = await createUpgradeCommit({
912
+ gitRoot: resolved.gitRoot,
913
+ paths: commitPaths,
914
+ versionLabel: upgradeVersionLabel,
915
+ source: bundleLayout,
916
+ additions: additions.length,
917
+ updates: updates.length,
918
+ unchanged: skipped.length,
919
+ incidentsAppendedCount,
920
+ });
955
921
  await cleanupAutoUpgradeArtifacts({ upgradeStateDir, createdBackups });
956
922
  process.stdout.write(`Upgrade applied: ${additions.length} add, ${updates.length} update, ${skipped.length} unchanged\n`);
923
+ if (commit) {
924
+ process.stdout.write(`Upgrade commit: ${commit.hash.slice(0, 12)} ${commit.subject}\n`);
925
+ }
957
926
  return 0;
958
927
  }
959
928
  finally {