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.
- package/assets/AGENTS.md +59 -33
- package/assets/framework.manifest.json +89 -0
- package/dist/adapters/clock/system-clock-adapter.d.ts +5 -0
- package/dist/adapters/clock/system-clock-adapter.d.ts.map +1 -0
- package/dist/adapters/clock/system-clock-adapter.js +5 -0
- package/dist/adapters/fs/node-fs-adapter.d.ts +15 -0
- package/dist/adapters/fs/node-fs-adapter.d.ts.map +1 -0
- package/dist/adapters/fs/node-fs-adapter.js +47 -0
- package/dist/adapters/git/git-context-adapter.d.ts +21 -0
- package/dist/adapters/git/git-context-adapter.d.ts.map +1 -0
- package/dist/adapters/git/git-context-adapter.js +27 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/task-backend/task-backend-adapter.d.ts +12 -0
- package/dist/adapters/task-backend/task-backend-adapter.d.ts.map +1 -0
- package/dist/adapters/task-backend/task-backend-adapter.js +22 -0
- package/dist/backends/task-backend/local-backend.d.ts.map +1 -1
- package/dist/backends/task-backend/local-backend.js +39 -34
- package/dist/backends/task-index.d.ts +9 -3
- package/dist/backends/task-index.d.ts.map +1 -1
- package/dist/backends/task-index.js +64 -14
- package/dist/cli/cli-error.d.ts +9 -0
- package/dist/cli/cli-error.d.ts.map +1 -0
- package/dist/cli/cli-error.js +13 -0
- package/dist/cli/command-guide.js +2 -2
- package/dist/cli/http.d.ts.map +1 -1
- package/dist/cli/http.js +13 -2
- package/dist/cli/parse/lifecycle.d.ts.map +1 -1
- package/dist/cli/parse/lifecycle.js +6 -1
- package/dist/cli/run-cli/command-catalog.d.ts +1 -1
- package/dist/cli/run-cli/command-catalog.d.ts.map +1 -1
- package/dist/cli/run-cli/command-catalog.js +8 -0
- package/dist/cli/run-cli/commands/init/conflicts.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/conflicts.js +2 -1
- package/dist/cli/run-cli/commands/init/write-agents.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-agents.js +27 -4
- package/dist/cli/run-cli/commands/init/write-config.d.ts.map +1 -1
- package/dist/cli/run-cli/commands/init/write-config.js +0 -4
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +14 -5
- package/dist/cli/run-cli.test-helpers.d.ts.map +1 -1
- package/dist/cli/run-cli.test-helpers.js +35 -1
- package/dist/commands/branch/internal/work-validate.d.ts.map +1 -1
- package/dist/commands/branch/internal/work-validate.js +13 -4
- package/dist/commands/branch/status.d.ts.map +1 -1
- package/dist/commands/branch/status.js +9 -4
- package/dist/commands/branch/work-start.command.d.ts.map +1 -1
- package/dist/commands/branch/work-start.command.js +3 -2
- package/dist/commands/branch/work-start.d.ts.map +1 -1
- package/dist/commands/branch/work-start.js +95 -2
- package/dist/commands/doctor.command.d.ts +8 -0
- package/dist/commands/doctor.command.d.ts.map +1 -0
- package/dist/commands/doctor.command.js +137 -0
- package/dist/commands/guard/impl/allow.d.ts.map +1 -1
- package/dist/commands/guard/impl/allow.js +7 -2
- package/dist/commands/guard/impl/close-message.d.ts.map +1 -1
- package/dist/commands/guard/impl/close-message.js +7 -2
- package/dist/commands/pr/check.d.ts.map +1 -1
- package/dist/commands/pr/check.js +7 -2
- package/dist/commands/pr/integrate/artifacts.d.ts.map +1 -1
- package/dist/commands/pr/integrate/artifacts.js +6 -1
- package/dist/commands/pr/integrate/internal/merge.d.ts.map +1 -1
- package/dist/commands/pr/integrate/internal/merge.js +7 -6
- package/dist/commands/pr/integrate/internal/prepare.d.ts.map +1 -1
- package/dist/commands/pr/integrate/internal/prepare.js +9 -4
- package/dist/commands/pr/integrate/verify.d.ts.map +1 -1
- package/dist/commands/pr/integrate/verify.js +3 -1
- package/dist/commands/pr/note.d.ts.map +1 -1
- package/dist/commands/pr/note.js +13 -4
- package/dist/commands/pr/open.d.ts.map +1 -1
- package/dist/commands/pr/open.js +8 -3
- package/dist/commands/recipes/impl/apply.d.ts.map +1 -1
- package/dist/commands/recipes/impl/apply.js +2 -1
- package/dist/commands/recipes/impl/commands/explain.d.ts.map +1 -1
- package/dist/commands/recipes/impl/commands/explain.js +2 -1
- package/dist/commands/recipes/impl/commands/info.d.ts.map +1 -1
- package/dist/commands/recipes/impl/commands/info.js +2 -1
- package/dist/commands/recipes/impl/commands/install.d.ts.map +1 -1
- package/dist/commands/recipes/impl/commands/install.js +5 -4
- package/dist/commands/recipes/impl/commands/remove.d.ts.map +1 -1
- package/dist/commands/recipes/impl/commands/remove.js +2 -1
- package/dist/commands/scenario/impl/commands.d.ts.map +1 -1
- package/dist/commands/scenario/impl/commands.js +8 -7
- package/dist/commands/shared/git-ops.d.ts.map +1 -1
- package/dist/commands/shared/git-ops.js +4 -3
- package/dist/commands/shared/task-store.d.ts.map +1 -1
- package/dist/commands/shared/task-store.js +7 -2
- package/dist/commands/task/finish.d.ts.map +1 -1
- package/dist/commands/task/finish.js +24 -0
- package/dist/commands/task/list.command.d.ts.map +1 -1
- package/dist/commands/task/list.command.js +4 -5
- package/dist/commands/task/migrate-doc.d.ts.map +1 -1
- package/dist/commands/task/migrate-doc.js +2 -1
- package/dist/commands/task/new.command.d.ts.map +1 -1
- package/dist/commands/task/new.command.js +2 -8
- package/dist/commands/task/rebuild-index.command.d.ts +6 -0
- package/dist/commands/task/rebuild-index.command.d.ts.map +1 -0
- package/dist/commands/task/rebuild-index.command.js +18 -0
- package/dist/commands/task/shared.d.ts.map +1 -1
- package/dist/commands/task/shared.js +15 -6
- package/dist/commands/upgrade.command.d.ts.map +1 -1
- package/dist/commands/upgrade.command.js +52 -4
- package/dist/commands/upgrade.d.ts +10 -0
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +332 -91
- package/dist/policy/engine.d.ts +21 -0
- package/dist/policy/engine.d.ts.map +1 -0
- package/dist/policy/engine.js +32 -0
- package/dist/ports/clock-port.d.ts +4 -0
- package/dist/ports/clock-port.d.ts.map +1 -0
- package/dist/ports/clock-port.js +1 -0
- package/dist/ports/fs-port.d.ts +18 -0
- package/dist/ports/fs-port.d.ts.map +1 -0
- package/dist/ports/fs-port.js +1 -0
- package/dist/ports/git-port.d.ts +18 -0
- package/dist/ports/git-port.d.ts.map +1 -0
- package/dist/ports/git-port.js +1 -0
- package/dist/ports/task-backend-port.d.ts +8 -0
- package/dist/ports/task-backend-port.d.ts.map +1 -0
- package/dist/ports/task-backend-port.js +1 -0
- package/dist/usecases/context/resolve-context.d.ts +14 -0
- package/dist/usecases/context/resolve-context.d.ts.map +1 -0
- package/dist/usecases/context/resolve-context.js +13 -0
- package/dist/usecases/task/task-list-usecase.d.ts +9 -0
- package/dist/usecases/task/task-list-usecase.d.ts.map +1 -0
- package/dist/usecases/task/task-list-usecase.js +17 -0
- package/dist/usecases/task/task-new-usecase.d.ts +9 -0
- package/dist/usecases/task/task-new-usecase.d.ts.map +1 -0
- package/dist/usecases/task/task-new-usecase.js +17 -0
- package/package.json +2 -2
package/dist/commands/upgrade.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 (
|
|
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
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
let
|
|
321
|
-
if (
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
|
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
|
|
407
|
-
|
|
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
|
|
549
|
+
message: `Invalid manifest path: ${entry.path}`,
|
|
413
550
|
});
|
|
414
551
|
}
|
|
415
|
-
if (rel
|
|
552
|
+
if (isDeniedUpgradePath(rel)) {
|
|
416
553
|
throw new CliError({
|
|
417
554
|
exitCode: 3,
|
|
418
555
|
code: "E_VALIDATION",
|
|
419
|
-
message: `
|
|
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: `
|
|
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:
|
|
570
|
+
exitCode: exitCodeForError("E_IO"),
|
|
437
571
|
code: "E_IO",
|
|
438
572
|
message: `Upgrade target is a directory: ${rel}`,
|
|
439
573
|
});
|
|
440
574
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
|
586
|
+
let existingBuf = null;
|
|
587
|
+
let existingText = null;
|
|
447
588
|
if (kind !== null) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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 (
|
|
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
|
|
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:
|
|
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 ??=
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 (
|
|
635
|
+
if (missingRequired.length > 0) {
|
|
492
636
|
throw new CliError({
|
|
493
637
|
exitCode: 3,
|
|
494
638
|
code: "E_VALIDATION",
|
|
495
|
-
message:
|
|
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 (
|
|
511
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
532
|
-
setByDottedKey(raw, "framework.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
|