agentplane 0.2.3 → 0.2.5

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 (131) hide show
  1. package/assets/AGENTS.md +59 -33
  2. package/assets/framework.manifest.json +89 -0
  3. package/dist/adapters/clock/system-clock-adapter.d.ts +5 -0
  4. package/dist/adapters/clock/system-clock-adapter.d.ts.map +1 -0
  5. package/dist/adapters/clock/system-clock-adapter.js +5 -0
  6. package/dist/adapters/fs/node-fs-adapter.d.ts +15 -0
  7. package/dist/adapters/fs/node-fs-adapter.d.ts.map +1 -0
  8. package/dist/adapters/fs/node-fs-adapter.js +47 -0
  9. package/dist/adapters/git/git-context-adapter.d.ts +21 -0
  10. package/dist/adapters/git/git-context-adapter.d.ts.map +1 -0
  11. package/dist/adapters/git/git-context-adapter.js +27 -0
  12. package/dist/adapters/index.d.ts +13 -0
  13. package/dist/adapters/index.d.ts.map +1 -0
  14. package/dist/adapters/index.js +12 -0
  15. package/dist/adapters/task-backend/task-backend-adapter.d.ts +12 -0
  16. package/dist/adapters/task-backend/task-backend-adapter.d.ts.map +1 -0
  17. package/dist/adapters/task-backend/task-backend-adapter.js +22 -0
  18. package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
  19. package/dist/backends/task-backend/local-backend.js +39 -34
  20. package/dist/backends/task-index.d.ts +9 -3
  21. package/dist/backends/task-index.d.ts.map +1 -1
  22. package/dist/backends/task-index.js +64 -14
  23. package/dist/cli/cli-error.d.ts +9 -0
  24. package/dist/cli/cli-error.d.ts.map +1 -0
  25. package/dist/cli/cli-error.js +13 -0
  26. package/dist/cli/command-guide.js +2 -2
  27. package/dist/cli/http.d.ts.map +1 -1
  28. package/dist/cli/http.js +13 -2
  29. package/dist/cli/parse/lifecycle.d.ts.map +1 -1
  30. package/dist/cli/parse/lifecycle.js +6 -1
  31. package/dist/cli/run-cli/command-catalog.d.ts +1 -1
  32. package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
  33. package/dist/cli/run-cli/command-catalog.js +8 -0
  34. package/dist/cli/run-cli/commands/init/conflicts.d.ts.map +1 -1
  35. package/dist/cli/run-cli/commands/init/conflicts.js +2 -1
  36. package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -1
  37. package/dist/cli/run-cli/commands/init/write-agents.js +27 -4
  38. package/dist/cli/run-cli/commands/init/write-config.d.ts.map +1 -1
  39. package/dist/cli/run-cli/commands/init/write-config.js +0 -4
  40. package/dist/cli/run-cli.d.ts.map +1 -1
  41. package/dist/cli/run-cli.js +14 -5
  42. package/dist/cli/run-cli.test-helpers.d.ts.map +1 -1
  43. package/dist/cli/run-cli.test-helpers.js +35 -1
  44. package/dist/commands/branch/internal/work-validate.d.ts.map +1 -1
  45. package/dist/commands/branch/internal/work-validate.js +13 -4
  46. package/dist/commands/branch/status.d.ts.map +1 -1
  47. package/dist/commands/branch/status.js +9 -4
  48. package/dist/commands/branch/work-start.command.d.ts.map +1 -1
  49. package/dist/commands/branch/work-start.command.js +3 -2
  50. package/dist/commands/branch/work-start.d.ts.map +1 -1
  51. package/dist/commands/branch/work-start.js +95 -2
  52. package/dist/commands/doctor.command.d.ts +8 -0
  53. package/dist/commands/doctor.command.d.ts.map +1 -0
  54. package/dist/commands/doctor.command.js +137 -0
  55. package/dist/commands/guard/impl/allow.d.ts.map +1 -1
  56. package/dist/commands/guard/impl/allow.js +7 -2
  57. package/dist/commands/guard/impl/close-message.d.ts.map +1 -1
  58. package/dist/commands/guard/impl/close-message.js +7 -2
  59. package/dist/commands/pr/check.d.ts.map +1 -1
  60. package/dist/commands/pr/check.js +7 -2
  61. package/dist/commands/pr/integrate/artifacts.d.ts.map +1 -1
  62. package/dist/commands/pr/integrate/artifacts.js +6 -1
  63. package/dist/commands/pr/integrate/internal/merge.d.ts.map +1 -1
  64. package/dist/commands/pr/integrate/internal/merge.js +7 -6
  65. package/dist/commands/pr/integrate/internal/prepare.d.ts.map +1 -1
  66. package/dist/commands/pr/integrate/internal/prepare.js +9 -4
  67. package/dist/commands/pr/integrate/verify.d.ts.map +1 -1
  68. package/dist/commands/pr/integrate/verify.js +3 -1
  69. package/dist/commands/pr/note.d.ts.map +1 -1
  70. package/dist/commands/pr/note.js +13 -4
  71. package/dist/commands/pr/open.d.ts.map +1 -1
  72. package/dist/commands/pr/open.js +8 -3
  73. package/dist/commands/recipes/impl/apply.d.ts.map +1 -1
  74. package/dist/commands/recipes/impl/apply.js +2 -1
  75. package/dist/commands/recipes/impl/commands/explain.d.ts.map +1 -1
  76. package/dist/commands/recipes/impl/commands/explain.js +2 -1
  77. package/dist/commands/recipes/impl/commands/info.d.ts.map +1 -1
  78. package/dist/commands/recipes/impl/commands/info.js +2 -1
  79. package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -1
  80. package/dist/commands/recipes/impl/commands/install.js +5 -4
  81. package/dist/commands/recipes/impl/commands/remove.d.ts.map +1 -1
  82. package/dist/commands/recipes/impl/commands/remove.js +2 -1
  83. package/dist/commands/scenario/impl/commands.d.ts.map +1 -1
  84. package/dist/commands/scenario/impl/commands.js +8 -7
  85. package/dist/commands/shared/git-ops.d.ts.map +1 -1
  86. package/dist/commands/shared/git-ops.js +4 -3
  87. package/dist/commands/shared/task-store.d.ts.map +1 -1
  88. package/dist/commands/shared/task-store.js +7 -2
  89. package/dist/commands/task/finish.d.ts.map +1 -1
  90. package/dist/commands/task/finish.js +24 -0
  91. package/dist/commands/task/list.command.d.ts.map +1 -1
  92. package/dist/commands/task/list.command.js +4 -5
  93. package/dist/commands/task/migrate-doc.d.ts.map +1 -1
  94. package/dist/commands/task/migrate-doc.js +2 -1
  95. package/dist/commands/task/new.command.d.ts.map +1 -1
  96. package/dist/commands/task/new.command.js +2 -8
  97. package/dist/commands/task/rebuild-index.command.d.ts +6 -0
  98. package/dist/commands/task/rebuild-index.command.d.ts.map +1 -0
  99. package/dist/commands/task/rebuild-index.command.js +18 -0
  100. package/dist/commands/task/shared.d.ts.map +1 -1
  101. package/dist/commands/task/shared.js +15 -6
  102. package/dist/commands/upgrade.command.d.ts.map +1 -1
  103. package/dist/commands/upgrade.command.js +52 -4
  104. package/dist/commands/upgrade.d.ts +10 -0
  105. package/dist/commands/upgrade.d.ts.map +1 -1
  106. package/dist/commands/upgrade.js +332 -91
  107. package/dist/policy/engine.d.ts +21 -0
  108. package/dist/policy/engine.d.ts.map +1 -0
  109. package/dist/policy/engine.js +32 -0
  110. package/dist/ports/clock-port.d.ts +4 -0
  111. package/dist/ports/clock-port.d.ts.map +1 -0
  112. package/dist/ports/clock-port.js +1 -0
  113. package/dist/ports/fs-port.d.ts +18 -0
  114. package/dist/ports/fs-port.d.ts.map +1 -0
  115. package/dist/ports/fs-port.js +1 -0
  116. package/dist/ports/git-port.d.ts +18 -0
  117. package/dist/ports/git-port.d.ts.map +1 -0
  118. package/dist/ports/git-port.js +1 -0
  119. package/dist/ports/task-backend-port.d.ts +8 -0
  120. package/dist/ports/task-backend-port.d.ts.map +1 -0
  121. package/dist/ports/task-backend-port.js +1 -0
  122. package/dist/usecases/context/resolve-context.d.ts +14 -0
  123. package/dist/usecases/context/resolve-context.d.ts.map +1 -0
  124. package/dist/usecases/context/resolve-context.js +13 -0
  125. package/dist/usecases/task/task-list-usecase.d.ts +9 -0
  126. package/dist/usecases/task/task-list-usecase.d.ts.map +1 -0
  127. package/dist/usecases/task/task-list-usecase.js +17 -0
  128. package/dist/usecases/task/task-new-usecase.d.ts +9 -0
  129. package/dist/usecases/task/task-new-usecase.d.ts.map +1 -0
  130. package/dist/usecases/task/task-new-usecase.js +17 -0
  131. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
- import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
1
+ import { lstat, mkdir, mkdtemp, readdir, readFile, readlink, rm, symlink, writeFile, } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { loadConfig, resolveProject, saveConfig, setByDottedKey } from "@agentplaneorg/core";
5
6
  import { backupPath, fileExists, getPathKind } from "../cli/fs-utils.js";
6
7
  import { downloadToFile, fetchJson } from "../cli/http.js";
@@ -13,6 +14,39 @@ import { ensureNetworkApproved } from "./shared/network-approval.js";
13
14
  const DEFAULT_UPGRADE_ASSET = "agentplane-upgrade.tar.gz";
14
15
  const DEFAULT_UPGRADE_CHECKSUM_ASSET = "agentplane-upgrade.tar.gz.sha256";
15
16
  const UPGRADE_DOWNLOAD_TIMEOUT_MS = 60_000;
17
+ const UPGRADE_RELEASE_METADATA_TIMEOUT_MS = 15_000;
18
+ const ASSETS_DIR_URL = new URL("../../assets/", import.meta.url);
19
+ async function loadFrameworkManifestFromPath(manifestPath) {
20
+ const text = await readFile(manifestPath, "utf8");
21
+ const parsed = JSON.parse(text);
22
+ if (parsed?.schema_version !== 1 || !Array.isArray(parsed?.files)) {
23
+ throw new CliError({
24
+ exitCode: 3,
25
+ code: "E_VALIDATION",
26
+ message: "Invalid framework.manifest.json (expected schema_version=1 and files array).",
27
+ });
28
+ }
29
+ return parsed;
30
+ }
31
+ function isDeniedUpgradePath(relPath) {
32
+ if (relPath === ".agentplane/config.json")
33
+ return true;
34
+ if (relPath === ".agentplane/tasks.json")
35
+ return true;
36
+ if (relPath.startsWith(".agentplane/backends/"))
37
+ return true;
38
+ if (relPath.startsWith(".agentplane/worktrees/"))
39
+ return true;
40
+ if (relPath.startsWith(".agentplane/recipes/"))
41
+ return true;
42
+ if (relPath.startsWith(".agentplane/tasks/"))
43
+ return true;
44
+ if (relPath.startsWith(".agentplane/.upgrade/"))
45
+ return true;
46
+ if (relPath === ".git" || relPath.startsWith(".git/"))
47
+ return true;
48
+ return false;
49
+ }
16
50
  function parseGitHubRepo(source) {
17
51
  const trimmed = source.trim();
18
52
  if (!trimmed)
@@ -64,6 +98,29 @@ export function resolveUpgradeDownloadFromRelease(opts) {
64
98
  }
65
99
  return { kind: "tarball", tarballUrl };
66
100
  }
101
+ function buildCodeloadTarGzUrl(opts) {
102
+ // Prefer codeload over api.github.com tarball_url. It is less brittle and does not require
103
+ // GitHub API-specific behavior/rate limits.
104
+ const tag = opts.tag.trim();
105
+ if (!tag)
106
+ throw new Error("tag is required");
107
+ return `https://codeload.github.com/${opts.owner}/${opts.repo}/tar.gz/${encodeURIComponent(tag)}`;
108
+ }
109
+ export function resolveRepoTarballUrl(opts) {
110
+ const tag = (typeof opts.explicitTag === "string" && opts.explicitTag.trim()) ||
111
+ (typeof opts.release.tag_name === "string" && opts.release.tag_name.trim()) ||
112
+ "";
113
+ if (tag)
114
+ return buildCodeloadTarGzUrl({ owner: opts.owner, repo: opts.repo, tag });
115
+ const tarballUrl = typeof opts.release.tarball_url === "string" ? opts.release.tarball_url : "";
116
+ if (tarballUrl)
117
+ return tarballUrl;
118
+ throw new CliError({
119
+ exitCode: exitCodeForError("E_NETWORK"),
120
+ code: "E_NETWORK",
121
+ message: "GitHub release did not provide tag_name or tarball_url; cannot fall back to repo tarball.",
122
+ });
123
+ }
67
124
  async function resolveUpgradeRoot(extractedDir) {
68
125
  const entries = await readdir(extractedDir, { withFileTypes: true });
69
126
  const dirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -73,24 +130,12 @@ async function resolveUpgradeRoot(extractedDir) {
73
130
  }
74
131
  return extractedDir;
75
132
  }
76
- async function listFilesRecursive(rootDir) {
77
- const out = [];
78
- const entries = await readdir(rootDir, { withFileTypes: true });
79
- for (const entry of entries) {
80
- const fullPath = path.join(rootDir, entry.name);
81
- if (entry.isDirectory()) {
82
- out.push(...(await listFilesRecursive(fullPath)));
83
- }
84
- else if (entry.isFile()) {
85
- out.push(fullPath);
86
- }
87
- }
88
- return out;
89
- }
90
133
  function isAllowedUpgradePath(relPath) {
91
134
  if (relPath === "AGENTS.md")
92
135
  return true;
93
- return relPath.startsWith(".agentplane/");
136
+ if (relPath.startsWith(".agentplane/agents/") && relPath.endsWith(".json"))
137
+ return true;
138
+ return false;
94
139
  }
95
140
  const LOCAL_OVERRIDES_START = "<!-- AGENTPLANE:LOCAL-START -->";
96
141
  const LOCAL_OVERRIDES_END = "<!-- AGENTPLANE:LOCAL-END -->";
@@ -168,7 +213,26 @@ function mergeAgentsPolicyMarkdown(incoming, current) {
168
213
  function isJsonRecord(value) {
169
214
  return !!value && typeof value === "object" && !Array.isArray(value);
170
215
  }
171
- function mergeAgentJson(incomingText, currentText) {
216
+ function canonicalizeJson(value) {
217
+ if (Array.isArray(value))
218
+ return value.map((v) => canonicalizeJson(v));
219
+ if (isJsonRecord(value)) {
220
+ const out = {};
221
+ for (const k of Object.keys(value).toSorted()) {
222
+ out[k] = canonicalizeJson(value[k]);
223
+ }
224
+ return out;
225
+ }
226
+ return value;
227
+ }
228
+ function jsonEqual(a, b) {
229
+ const ca = JSON.stringify(canonicalizeJson(a)) ?? "__undefined__";
230
+ const cb = JSON.stringify(canonicalizeJson(b)) ?? "__undefined__";
231
+ return ca === cb;
232
+ }
233
+ // Used as a fallback for 3-way merges when no baseline is available. Incoming (upstream) values
234
+ // win for scalar/object conflicts, while user-added keys and array items are preserved.
235
+ function mergeAgentJsonIncomingWins(incomingText, currentText) {
172
236
  let incoming;
173
237
  let current;
174
238
  try {
@@ -189,19 +253,22 @@ function mergeAgentJson(incomingText, currentText) {
189
253
  }
190
254
  if (Array.isArray(incVal) && Array.isArray(curVal)) {
191
255
  const merged = [...incVal];
256
+ const seen = new Set();
257
+ for (const x of merged)
258
+ seen.add(JSON.stringify(canonicalizeJson(x)));
192
259
  for (const item of curVal) {
193
- if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
260
+ const key = JSON.stringify(canonicalizeJson(item));
261
+ if (!seen.has(key)) {
194
262
  merged.push(item);
263
+ seen.add(key);
264
+ }
195
265
  }
196
266
  out[k] = merged;
197
267
  continue;
198
268
  }
199
269
  if (isJsonRecord(incVal) && isJsonRecord(curVal)) {
200
- out[k] = { ...incVal, ...curVal };
201
- continue;
202
- }
203
- if (curVal !== incVal && curVal !== null && curVal !== "") {
204
- out[k] = curVal;
270
+ // Preserve user-only subkeys but let upstream win for conflicts.
271
+ out[k] = { ...curVal, ...incVal };
205
272
  continue;
206
273
  }
207
274
  out[k] = incVal;
@@ -231,11 +298,17 @@ function mergeAgentJson3Way(opts) {
231
298
  // Arrays: always take incoming as base; if user changed vs base, append user-only items.
232
299
  if (Array.isArray(incVal) && Array.isArray(curVal) && Array.isArray(baseVal)) {
233
300
  const merged = [...incVal];
234
- const userChanged = JSON.stringify(curVal) !== JSON.stringify(baseVal);
301
+ const userChanged = !jsonEqual(curVal, baseVal);
235
302
  if (userChanged) {
303
+ const seen = new Set();
304
+ for (const x of merged)
305
+ seen.add(JSON.stringify(canonicalizeJson(x)));
236
306
  for (const item of curVal) {
237
- if (!merged.some((x) => JSON.stringify(x) === JSON.stringify(item)))
307
+ const k = JSON.stringify(canonicalizeJson(item));
308
+ if (!seen.has(k)) {
238
309
  merged.push(item);
310
+ seen.add(k);
311
+ }
239
312
  }
240
313
  }
241
314
  out[key] = merged;
@@ -253,7 +326,7 @@ function mergeAgentJson3Way(opts) {
253
326
  const incSub = incVal[sk];
254
327
  const curSub = curVal[sk];
255
328
  const baseSub = baseVal[sk];
256
- const userChanged = JSON.stringify(curSub) !== JSON.stringify(baseSub);
329
+ const userChanged = !jsonEqual(curSub, baseSub);
257
330
  if (userChanged)
258
331
  merged[sk] = curSub;
259
332
  else if (incSub !== undefined)
@@ -265,7 +338,7 @@ function mergeAgentJson3Way(opts) {
265
338
  continue;
266
339
  }
267
340
  // Scalars: prefer incoming unless the user changed vs base.
268
- if (JSON.stringify(curVal) !== JSON.stringify(baseVal)) {
341
+ if (!jsonEqual(curVal, baseVal)) {
269
342
  if (curVal !== undefined)
270
343
  out[key] = curVal;
271
344
  else if (incVal !== undefined)
@@ -295,13 +368,21 @@ export async function cmdUpgradeParsed(opts) {
295
368
  rootOverride: opts.rootOverride ?? null,
296
369
  });
297
370
  const loaded = await loadConfig(resolved.agentplaneDir);
298
- const sourceFromFlags = typeof flags.source === "string" && flags.source.trim().length > 0;
299
- const originalSource = flags.source ?? loaded.config.framework.source;
300
- const normalized = normalizeFrameworkSourceForUpgrade(originalSource);
301
- const source = normalized.source;
302
- if (normalized.migrated) {
303
- process.stderr.write(`${warnMessage(`config.framework.source uses deprecated repo basilisk-labs/agent-plane; using ${source}`)}\n`);
371
+ const upgradeStateDir = path.join(resolved.agentplaneDir, ".upgrade");
372
+ const lockPath = path.join(upgradeStateDir, "lock.json");
373
+ const statePath = path.join(upgradeStateDir, "state.json");
374
+ const baselineDirNew = path.join(upgradeStateDir, "baseline");
375
+ const baselineDirLegacy = path.join(resolved.agentplaneDir, "upgrade", "baseline");
376
+ await mkdir(upgradeStateDir, { recursive: true });
377
+ if (await fileExists(lockPath)) {
378
+ throw new CliError({
379
+ exitCode: 2,
380
+ code: "E_USAGE",
381
+ message: `Upgrade is locked (found ${path.relative(resolved.gitRoot, lockPath)})`,
382
+ });
304
383
  }
384
+ await writeFile(lockPath, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }, null, 2) + "\n", "utf8");
385
+ let lockAcquired = true;
305
386
  let networkApproved = false;
306
387
  const ensureApproved = async (reason) => {
307
388
  if (networkApproved)
@@ -309,16 +390,26 @@ export async function cmdUpgradeParsed(opts) {
309
390
  await ensureNetworkApproved({ config: loaded.config, yes: flags.yes, reason });
310
391
  networkApproved = true;
311
392
  };
393
+ const hasBundle = Boolean(flags.bundle);
394
+ const hasRemoteHints = Boolean(flags.source) ||
395
+ Boolean(flags.tag) ||
396
+ Boolean(flags.asset) ||
397
+ Boolean(flags.checksumAsset);
398
+ const useRemote = flags.remote === true || hasRemoteHints;
312
399
  let tempRoot = null;
313
400
  let extractRoot = null;
314
401
  try {
315
402
  tempRoot = await mkdtemp(path.join(os.tmpdir(), "agentplane-upgrade-"));
316
403
  let bundlePath = "";
317
404
  let checksumPath = "";
318
- // GitHub release tarballs contain the full repository. When we fall back to tarball_url,
319
- // we must ignore non-upgrade paths instead of failing validation.
320
- let allowNonUpgradePaths = false;
321
- if (flags.bundle) {
405
+ let bundleLayout = "upgrade_bundle";
406
+ let bundleRoot = "";
407
+ let normalizedSourceToPersist = null;
408
+ if (!hasBundle && !useRemote) {
409
+ bundleLayout = "local_assets";
410
+ bundleRoot = fileURLToPath(ASSETS_DIR_URL);
411
+ }
412
+ else if (flags.bundle) {
322
413
  const isUrl = flags.bundle.startsWith("http://") || flags.bundle.startsWith("https://");
323
414
  bundlePath = isUrl ? path.join(tempRoot, "bundle.tar.gz") : path.resolve(flags.bundle);
324
415
  if (isUrl) {
@@ -336,6 +427,15 @@ export async function cmdUpgradeParsed(opts) {
336
427
  }
337
428
  }
338
429
  else {
430
+ const sourceFromFlags = typeof flags.source === "string" && flags.source.trim().length > 0;
431
+ const originalSource = flags.source ?? loaded.config.framework.source;
432
+ const normalized = normalizeFrameworkSourceForUpgrade(originalSource);
433
+ if (!sourceFromFlags && normalized.migrated) {
434
+ normalizedSourceToPersist = normalized.source;
435
+ }
436
+ if (normalized.migrated) {
437
+ process.stderr.write(`${warnMessage(`config.framework.source uses deprecated repo basilisk-labs/agent-plane; using ${normalized.source}`)}\n`);
438
+ }
339
439
  const { owner, repo } = normalized;
340
440
  const releaseUrl = flags.tag
341
441
  ? `https://api.github.com/repos/${owner}/${repo}/releases/tags/${flags.tag}`
@@ -343,7 +443,7 @@ export async function cmdUpgradeParsed(opts) {
343
443
  await ensureApproved("upgrade fetches release metadata and downloads assets from the network");
344
444
  const assetName = flags.asset ?? DEFAULT_UPGRADE_ASSET;
345
445
  const checksumName = flags.checksumAsset ?? DEFAULT_UPGRADE_CHECKSUM_ASSET;
346
- const release = (await fetchJson(releaseUrl));
446
+ const release = (await fetchJson(releaseUrl, UPGRADE_RELEASE_METADATA_TIMEOUT_MS));
347
447
  const download = resolveUpgradeDownloadFromRelease({
348
448
  release,
349
449
  owner,
@@ -358,14 +458,28 @@ export async function cmdUpgradeParsed(opts) {
358
458
  await downloadToFile(download.checksumUrl, checksumPath, UPGRADE_DOWNLOAD_TIMEOUT_MS);
359
459
  }
360
460
  else {
361
- process.stderr.write(`${warnMessage(`upgrade release does not include ${assetName}/${checksumName}; falling back to tarball_url without checksum verification`)}\n`);
362
- allowNonUpgradePaths = true;
461
+ if (!flags.allowTarball) {
462
+ throw new CliError({
463
+ exitCode: exitCodeForError("E_NETWORK"),
464
+ code: "E_NETWORK",
465
+ message: `Upgrade assets ${assetName}/${checksumName} not found in ${owner}/${repo} release. ` +
466
+ "Publish the upgrade bundle assets, or re-run with --allow-tarball to download a repo tarball (no checksum verification).",
467
+ });
468
+ }
469
+ process.stderr.write(`${warnMessage(`upgrade release does not include ${assetName}/${checksumName}; falling back to repo tarball without checksum verification`)}\n`);
470
+ bundleLayout = "repo_tarball";
363
471
  bundlePath = path.join(tempRoot, "source.tar.gz");
364
- await downloadToFile(download.tarballUrl, bundlePath, UPGRADE_DOWNLOAD_TIMEOUT_MS);
472
+ const tarballUrl = resolveRepoTarballUrl({
473
+ release,
474
+ owner,
475
+ repo,
476
+ explicitTag: flags.tag,
477
+ });
478
+ await downloadToFile(tarballUrl, bundlePath, UPGRADE_DOWNLOAD_TIMEOUT_MS);
365
479
  checksumPath = "";
366
480
  }
367
481
  }
368
- if (checksumPath) {
482
+ if (bundleLayout !== "local_assets" && checksumPath) {
369
483
  const expected = parseSha256Text(await readFile(checksumPath, "utf8"));
370
484
  if (!expected) {
371
485
  throw new CliError({
@@ -383,19 +497,42 @@ export async function cmdUpgradeParsed(opts) {
383
497
  });
384
498
  }
385
499
  }
386
- extractRoot = await mkdtemp(path.join(os.tmpdir(), "agentplane-upgrade-extract-"));
387
- await extractArchive({
388
- archivePath: bundlePath,
389
- destDir: extractRoot,
390
- });
391
- const bundleRoot = await resolveUpgradeRoot(extractRoot);
392
- const files = await listFilesRecursive(bundleRoot);
500
+ if (bundleLayout !== "local_assets") {
501
+ extractRoot = await mkdtemp(path.join(os.tmpdir(), "agentplane-upgrade-extract-"));
502
+ await extractArchive({
503
+ archivePath: bundlePath,
504
+ destDir: extractRoot,
505
+ });
506
+ const extractedRoot = await resolveUpgradeRoot(extractRoot);
507
+ bundleRoot =
508
+ bundleLayout === "repo_tarball"
509
+ ? path.join(extractedRoot, "packages", "agentplane", "assets")
510
+ : extractedRoot;
511
+ }
512
+ const manifestPath = bundleLayout === "local_assets"
513
+ ? fileURLToPath(new URL("../../assets/framework.manifest.json", import.meta.url))
514
+ : path.join(bundleRoot, "framework.manifest.json");
515
+ const manifest = await loadFrameworkManifestFromPath(manifestPath);
393
516
  const additions = [];
394
517
  const updates = [];
395
518
  const skipped = [];
396
519
  const fileContents = new Map();
397
520
  const merged = [];
398
- const baselineDir = path.join(resolved.agentplaneDir, "upgrade", "baseline");
521
+ const missingRequired = [];
522
+ const readBaselineText = async (baselineKey) => {
523
+ try {
524
+ return await readFile(path.join(baselineDirNew, baselineKey), "utf8");
525
+ }
526
+ catch {
527
+ // Back-compat: older upgrades wrote baselines under .agentplane/upgrade/baseline.
528
+ try {
529
+ return await readFile(path.join(baselineDirLegacy, baselineKey), "utf8");
530
+ }
531
+ catch {
532
+ return null;
533
+ }
534
+ }
535
+ };
399
536
  const toBaselineKey = (rel) => {
400
537
  if (rel === "AGENTS.md")
401
538
  return "AGENTS.md";
@@ -403,63 +540,76 @@ export async function cmdUpgradeParsed(opts) {
403
540
  return rel.slice(".agentplane/".length);
404
541
  return null;
405
542
  };
406
- for (const filePath of files) {
407
- let rel = path.relative(bundleRoot, filePath).replaceAll("\\", "/");
543
+ for (const entry of manifest.files) {
544
+ const rel = entry.path.replaceAll("\\", "/").trim();
408
545
  if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
409
546
  throw new CliError({
410
547
  exitCode: 3,
411
548
  code: "E_VALIDATION",
412
- message: `Invalid bundle path: ${filePath}`,
549
+ message: `Invalid manifest path: ${entry.path}`,
413
550
  });
414
551
  }
415
- if (rel === ".git" || rel.startsWith(".git/")) {
552
+ if (isDeniedUpgradePath(rel)) {
416
553
  throw new CliError({
417
554
  exitCode: 3,
418
555
  code: "E_VALIDATION",
419
- message: `Upgrade bundle cannot write to .git (${rel})`,
556
+ message: `Manifest includes a denied path: ${rel}`,
420
557
  });
421
558
  }
422
559
  if (!isAllowedUpgradePath(rel)) {
423
- if (allowNonUpgradePaths) {
424
- continue;
425
- }
426
560
  throw new CliError({
427
561
  exitCode: 3,
428
562
  code: "E_VALIDATION",
429
- message: `Upgrade bundle path not allowed: ${rel}`,
563
+ message: `Manifest path not allowed: ${rel}`,
430
564
  });
431
565
  }
432
566
  const destPath = path.join(resolved.gitRoot, rel);
433
567
  const kind = await getPathKind(destPath);
434
568
  if (kind === "dir") {
435
569
  throw new CliError({
436
- exitCode: 5,
570
+ exitCode: exitCodeForError("E_IO"),
437
571
  code: "E_IO",
438
572
  message: `Upgrade target is a directory: ${rel}`,
439
573
  });
440
574
  }
441
- if (rel === ".agentplane/config.json") {
442
- // Never overwrite local config during upgrade.
443
- skipped.push(rel);
575
+ const sourceRel = (entry.source_path ?? entry.path).replaceAll("\\", "/").trim();
576
+ const sourcePath = path.join(bundleRoot, sourceRel);
577
+ let data;
578
+ try {
579
+ data = await readFile(sourcePath);
580
+ }
581
+ catch {
582
+ if (entry.required)
583
+ missingRequired.push(rel);
444
584
  continue;
445
585
  }
446
- let data = await readFile(filePath);
586
+ let existingBuf = null;
587
+ let existingText = null;
447
588
  if (kind !== null) {
448
- const existing = await readFile(destPath, "utf8");
449
- if (rel === "AGENTS.md") {
450
- const mergedText = mergeAgentsPolicyMarkdown(data.toString("utf8"), existing);
589
+ existingBuf = await readFile(destPath);
590
+ }
591
+ // Merge logic only needs text for a small subset of managed files.
592
+ if (existingBuf) {
593
+ if (entry.merge_strategy === "agents_policy_markdown" && rel === "AGENTS.md") {
594
+ existingText = existingBuf.toString("utf8");
595
+ const mergedText = mergeAgentsPolicyMarkdown(data.toString("utf8"), existingText);
451
596
  data = Buffer.from(mergedText, "utf8");
452
597
  merged.push(rel);
453
598
  }
454
- else if (rel.startsWith(".agentplane/agents/") && rel.endsWith(".json")) {
599
+ else if (entry.merge_strategy === "agent_json_3way" &&
600
+ rel.startsWith(".agentplane/agents/") &&
601
+ rel.endsWith(".json")) {
602
+ existingText = existingBuf.toString("utf8");
455
603
  const baselineKey = toBaselineKey(rel);
456
604
  let mergedText = null;
457
605
  if (baselineKey) {
458
606
  try {
459
- const baselineText = await readFile(path.join(baselineDir, baselineKey), "utf8");
607
+ const baselineText = await readBaselineText(baselineKey);
608
+ if (!baselineText)
609
+ throw new Error("missing baseline");
460
610
  mergedText = mergeAgentJson3Way({
461
611
  incomingText: data.toString("utf8"),
462
- currentText: existing,
612
+ currentText: existingText,
463
613
  baseText: baselineText,
464
614
  });
465
615
  }
@@ -467,7 +617,7 @@ export async function cmdUpgradeParsed(opts) {
467
617
  mergedText = null;
468
618
  }
469
619
  }
470
- mergedText ??= mergeAgentJson(data.toString("utf8"), existing);
620
+ mergedText ??= mergeAgentJsonIncomingWins(data.toString("utf8"), existingText);
471
621
  if (mergedText) {
472
622
  data = Buffer.from(mergedText, "utf8");
473
623
  merged.push(rel);
@@ -475,24 +625,18 @@ export async function cmdUpgradeParsed(opts) {
475
625
  }
476
626
  }
477
627
  fileContents.set(rel, data);
478
- if (kind === null) {
628
+ if (kind === null)
479
629
  additions.push(rel);
480
- }
481
- else {
482
- const existingBuf = await readFile(destPath);
483
- if (Buffer.compare(existingBuf, data) === 0) {
484
- skipped.push(rel);
485
- }
486
- else {
487
- updates.push(rel);
488
- }
489
- }
630
+ else if (existingBuf && Buffer.compare(existingBuf, data) === 0)
631
+ skipped.push(rel);
632
+ else
633
+ updates.push(rel);
490
634
  }
491
- if (fileContents.size === 0) {
635
+ if (missingRequired.length > 0) {
492
636
  throw new CliError({
493
637
  exitCode: 3,
494
638
  code: "E_VALIDATION",
495
- message: "Upgrade bundle contains no applicable files (expected AGENTS.md and/or .agentplane/).",
639
+ message: `Upgrade bundle is missing required managed files: ${missingRequired.join(", ")}`,
496
640
  });
497
641
  }
498
642
  if (flags.dryRun) {
@@ -507,8 +651,61 @@ export async function cmdUpgradeParsed(opts) {
507
651
  process.stdout.write(`MERGE ${rel}\n`);
508
652
  return 0;
509
653
  }
510
- if (skipped.includes(".agentplane/config.json")) {
511
- process.stderr.write(`${warnMessage("upgrade bundle includes .agentplane/config.json; skipping to preserve local configuration")}\n`);
654
+ if (flags.mode === "agent") {
655
+ const agentDir = path.join(upgradeStateDir, "agent");
656
+ const runId = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
657
+ const runDir = path.join(agentDir, runId);
658
+ await mkdir(runDir, { recursive: true });
659
+ const managedFiles = manifest.files.map((f) => f.path.replaceAll("\\", "/").trim());
660
+ const planMd = `# agentplane upgrade plan (${runId})\n\n` +
661
+ `Mode: agent-assisted (no files modified)\n\n` +
662
+ `## Summary\n\n` +
663
+ `- additions: ${additions.length}\n` +
664
+ `- updates: ${updates.length}\n` +
665
+ `- unchanged: ${skipped.length}\n` +
666
+ `- merged (auto-safe transforms already applied to incoming): ${merged.length}\n\n` +
667
+ `## Managed files (manifest)\n\n` +
668
+ managedFiles.map((p) => `- ${p}`).join("\n") +
669
+ `\n\n` +
670
+ `## Proposed changes\n\n` +
671
+ additions.map((p) => `- ADD ${p}`).join("\n") +
672
+ (additions.length > 0 ? "\n" : "") +
673
+ updates.map((p) => `- UPDATE ${p}`).join("\n") +
674
+ (updates.length > 0 ? "\n" : "") +
675
+ merged.map((p) => `- MERGE ${p}`).join("\n") +
676
+ (merged.length > 0 ? "\n" : "") +
677
+ skipped.map((p) => `- SKIP ${p}`).join("\n") +
678
+ (skipped.length > 0 ? "\n" : "") +
679
+ `\n` +
680
+ `## Next steps\n\n` +
681
+ `1. Review the proposed changes list.\n` +
682
+ `2. Apply changes manually or re-run with \`agentplane upgrade --auto\` to apply managed files.\n` +
683
+ `3. Run \`agentplane doctor\` (or \`agentplane doctor --fix\`) and ensure checks pass.\n`;
684
+ const constraintsMd = `# Upgrade constraints\n\n` +
685
+ `This upgrade is restricted to framework-managed files only.\n\n` +
686
+ `## Must not touch\n\n` +
687
+ `- .agentplane/tasks/** (task data)\n` +
688
+ `- .agentplane/tasks.json (export snapshot)\n` +
689
+ `- .agentplane/backends/** (backend configuration)\n` +
690
+ `- .agentplane/config.json (project config)\n` +
691
+ `- .git/**\n\n` +
692
+ `## Notes\n\n` +
693
+ `- The upgrade bundle is validated against framework.manifest.json.\n` +
694
+ `- AGENTS.md is managed under .agentplane/AGENTS.md and workspace-root AGENTS.md is a symlink.\n`;
695
+ const reportMd = `# Upgrade report (${runId})\n\n` +
696
+ `## Actions taken\n\n` +
697
+ `- [ ] Reviewed plan.md\n` +
698
+ `- [ ] Applied changes (manual or --auto)\n` +
699
+ `- [ ] Ran doctor\n` +
700
+ `- [ ] Ran tests / lint\n\n` +
701
+ `## Notes\n\n` +
702
+ `- \n`;
703
+ await writeFile(path.join(runDir, "plan.md"), planMd, "utf8");
704
+ await writeFile(path.join(runDir, "constraints.md"), constraintsMd, "utf8");
705
+ await writeFile(path.join(runDir, "report.md"), reportMd, "utf8");
706
+ await writeFile(path.join(runDir, "files.json"), JSON.stringify({ additions, updates, skipped, merged }, null, 2) + "\n", "utf8");
707
+ process.stdout.write(`Upgrade plan written: ${path.relative(resolved.gitRoot, runDir)}\n`);
708
+ return 0;
512
709
  }
513
710
  for (const rel of [...additions, ...updates]) {
514
711
  const destPath = path.join(resolved.gitRoot, rel);
@@ -517,22 +714,58 @@ export async function cmdUpgradeParsed(opts) {
517
714
  }
518
715
  await mkdir(path.dirname(destPath), { recursive: true });
519
716
  const data = fileContents.get(rel);
520
- if (data)
521
- await writeFile(destPath, data);
717
+ if (data) {
718
+ if (rel === "AGENTS.md") {
719
+ // Write the managed copy under .agentplane/ and keep the workspace-root policy path
720
+ // as a symlink to it.
721
+ const managedPath = path.join(resolved.agentplaneDir, "AGENTS.md");
722
+ await mkdir(path.dirname(managedPath), { recursive: true });
723
+ await writeFile(managedPath, data);
724
+ // Replace AGENTS.md with a symlink if needed.
725
+ const relTarget = path.relative(resolved.gitRoot, managedPath);
726
+ try {
727
+ const st = await lstat(destPath);
728
+ if (st.isSymbolicLink()) {
729
+ const currentTarget = await readlink(destPath);
730
+ if (currentTarget !== relTarget) {
731
+ await rm(destPath, { force: true });
732
+ }
733
+ }
734
+ else {
735
+ // If it's a regular file, remove it (backup already happened above when enabled).
736
+ await rm(destPath, { force: true });
737
+ }
738
+ }
739
+ catch {
740
+ // destPath doesn't exist
741
+ }
742
+ if (!(await fileExists(destPath))) {
743
+ await symlink(relTarget, destPath);
744
+ }
745
+ }
746
+ else {
747
+ await writeFile(destPath, data);
748
+ }
749
+ }
522
750
  // Record a baseline copy for future three-way merges.
523
751
  const baselineKey = toBaselineKey(rel);
524
752
  if (baselineKey && data) {
525
- const baselinePath = path.join(baselineDir, baselineKey);
753
+ const baselinePath = path.join(baselineDirNew, baselineKey);
526
754
  await mkdir(path.dirname(baselinePath), { recursive: true });
527
755
  await writeFile(baselinePath, data);
528
756
  }
529
757
  }
530
758
  const raw = { ...loaded.raw };
531
- if (!sourceFromFlags && normalized.migrated) {
532
- setByDottedKey(raw, "framework.source", source);
759
+ if (normalizedSourceToPersist) {
760
+ setByDottedKey(raw, "framework.source", normalizedSourceToPersist);
533
761
  }
534
762
  setByDottedKey(raw, "framework.last_update", new Date().toISOString());
535
763
  await saveConfig(resolved.agentplaneDir, raw);
764
+ await writeFile(statePath, JSON.stringify({
765
+ applied_at: new Date().toISOString(),
766
+ source: bundleLayout,
767
+ updated: { add: additions.length, update: updates.length, unchanged: skipped.length },
768
+ }, null, 2) + "\n", "utf8");
536
769
  process.stdout.write(`Upgrade applied: ${additions.length} add, ${updates.length} update, ${skipped.length} unchanged\n`);
537
770
  return 0;
538
771
  }
@@ -541,5 +774,13 @@ export async function cmdUpgradeParsed(opts) {
541
774
  await rm(extractRoot, { recursive: true, force: true });
542
775
  if (tempRoot)
543
776
  await rm(tempRoot, { recursive: true, force: true });
777
+ if (lockAcquired) {
778
+ try {
779
+ await rm(lockPath, { force: true });
780
+ }
781
+ catch {
782
+ // best-effort cleanup
783
+ }
784
+ }
544
785
  }
545
786
  }
@@ -0,0 +1,21 @@
1
+ import type { AgentplaneConfig } from "@agentplaneorg/core";
2
+ import type { PolicyAction, PolicyContext, PolicyProblem } from "./types.js";
3
+ export type PolicyDecision = {
4
+ ok: boolean;
5
+ violations: PolicyViolation[];
6
+ };
7
+ export type PolicyViolation = {
8
+ level: "error" | "warning";
9
+ code: PolicyProblem["code"];
10
+ exitCode: number;
11
+ message: string;
12
+ };
13
+ export type ActionId = PolicyAction | "task_list" | "task_new" | "upgrade_apply" | "doctor_fix" | (string & {});
14
+ export type PolicyEngineContext = Omit<PolicyContext, "action"> & {
15
+ action: ActionId;
16
+ config: AgentplaneConfig;
17
+ };
18
+ export declare class PolicyEngine {
19
+ evaluate(ctx: PolicyEngineContext): PolicyDecision;
20
+ }
21
+ //# sourceMappingURL=engine.d.ts.map