akm-cli 0.9.4 → 0.9.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/CHANGELOG.md +160 -0
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +89 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/scripts/akm-migrate-node.js +120 -112
- package/dist/scripts/akm-migrate.js +120 -112
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/task-to-v3.js +24 -12
- package/dist/tasks/source/task-to-v4.js +0 -10
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
|
@@ -10066,11 +10066,13 @@ function makeStampHandler(command) {
|
|
|
10066
10066
|
if (typeof result !== "object" || Array.isArray(result))
|
|
10067
10067
|
return result;
|
|
10068
10068
|
const obj = result;
|
|
10069
|
-
if (obj.shape
|
|
10070
|
-
obj
|
|
10071
|
-
|
|
10072
|
-
obj
|
|
10073
|
-
|
|
10069
|
+
if (obj.shape !== undefined && obj.schemaVersion !== undefined)
|
|
10070
|
+
return obj;
|
|
10071
|
+
return {
|
|
10072
|
+
...obj,
|
|
10073
|
+
shape: obj.shape ?? command,
|
|
10074
|
+
schemaVersion: obj.schemaVersion ?? 1
|
|
10075
|
+
};
|
|
10074
10076
|
};
|
|
10075
10077
|
}
|
|
10076
10078
|
var PASSTHROUGH_COMMANDS = [
|
|
@@ -10110,6 +10112,7 @@ var PASSTHROUGH_COMMANDS = [
|
|
|
10110
10112
|
"task-doctor",
|
|
10111
10113
|
"task-explain",
|
|
10112
10114
|
"task-history",
|
|
10115
|
+
"task-prune",
|
|
10113
10116
|
"task-run",
|
|
10114
10117
|
"task-sync",
|
|
10115
10118
|
"task-sync-dry-run",
|
|
@@ -12946,8 +12949,17 @@ import path8 from "path";
|
|
|
12946
12949
|
// src/core/warn.ts
|
|
12947
12950
|
import fs4 from "fs";
|
|
12948
12951
|
var quiet = false;
|
|
12952
|
+
var verbose = false;
|
|
12949
12953
|
var logFilePath;
|
|
12950
12954
|
var sinkOverride;
|
|
12955
|
+
function isVerbose() {
|
|
12956
|
+
const env = process.env.AKM_VERBOSE?.trim().toLowerCase();
|
|
12957
|
+
if (env === "1" || env === "true" || env === "yes" || env === "on")
|
|
12958
|
+
return true;
|
|
12959
|
+
if (env === "0" || env === "false" || env === "no" || env === "off")
|
|
12960
|
+
return false;
|
|
12961
|
+
return verbose;
|
|
12962
|
+
}
|
|
12951
12963
|
function appendToLogFile(level, args) {
|
|
12952
12964
|
if (!logFilePath)
|
|
12953
12965
|
return;
|
|
@@ -12973,6 +12985,15 @@ function warn(...args) {
|
|
|
12973
12985
|
console.warn(...args);
|
|
12974
12986
|
}
|
|
12975
12987
|
}
|
|
12988
|
+
function warnVerbose(...args) {
|
|
12989
|
+
if (sinkOverride) {
|
|
12990
|
+
sinkOverride("warnVerbose", args);
|
|
12991
|
+
return;
|
|
12992
|
+
}
|
|
12993
|
+
if (isVerbose()) {
|
|
12994
|
+
warn(...args);
|
|
12995
|
+
}
|
|
12996
|
+
}
|
|
12976
12997
|
|
|
12977
12998
|
// src/indexer/passes/metadata.ts
|
|
12978
12999
|
var SCOPE_KEYS = ["user", "agent", "run", "channel"];
|
|
@@ -13688,7 +13709,7 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
13688
13709
|
}
|
|
13689
13710
|
return visible;
|
|
13690
13711
|
}
|
|
13691
|
-
function projectMarkdownContent(body) {
|
|
13712
|
+
function projectMarkdownContent(body, truncationInfo) {
|
|
13692
13713
|
const lines = body.split(/\r?\n/);
|
|
13693
13714
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
13694
13715
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
@@ -13734,6 +13755,8 @@ function projectMarkdownContent(body) {
|
|
|
13734
13755
|
const text = projected.join(" ").replace(/\s+/g, " ").trim();
|
|
13735
13756
|
if (!text)
|
|
13736
13757
|
return;
|
|
13758
|
+
if (truncationInfo)
|
|
13759
|
+
truncationInfo.truncated = text.length > MARKDOWN_CONTENT_MAX_CHARS;
|
|
13737
13760
|
return truncateUnicodeSafe(text, MARKDOWN_CONTENT_MAX_CHARS);
|
|
13738
13761
|
}
|
|
13739
13762
|
function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
@@ -13757,9 +13780,15 @@ function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
13757
13780
|
applyWikiFrontmatter(entry, parsed.data);
|
|
13758
13781
|
applyProvenanceFrontmatter(entry, parsed.data);
|
|
13759
13782
|
if (entry.type !== "env" && entry.type !== "session" && !hasSessionMemoryMarker(parsed.data, parsed.content)) {
|
|
13760
|
-
const
|
|
13761
|
-
|
|
13783
|
+
const truncationInfo = { truncated: false };
|
|
13784
|
+
const contentProjection = projectMarkdownContent(parsed.content, truncationInfo);
|
|
13785
|
+
if (contentProjection) {
|
|
13762
13786
|
entry.content = contentProjection;
|
|
13787
|
+
if (truncationInfo.truncated) {
|
|
13788
|
+
entry.contentTruncated = true;
|
|
13789
|
+
warnVerbose(`${file}: indexed content truncated to ${MARKDOWN_CONTENT_MAX_CHARS} chars`);
|
|
13790
|
+
}
|
|
13791
|
+
}
|
|
13763
13792
|
}
|
|
13764
13793
|
if (entry.type === "command") {
|
|
13765
13794
|
const cmdParams = extractCommandParameters(parsed.content);
|
|
@@ -16722,6 +16751,16 @@ var SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
|
16722
16751
|
function shellStableV2Executable(executable) {
|
|
16723
16752
|
return executable === "akm" || executable.includes("/");
|
|
16724
16753
|
}
|
|
16754
|
+
function skipEnvAssignmentPrefix(tokens) {
|
|
16755
|
+
if (tokens[0] !== "env")
|
|
16756
|
+
return { tokens, envWrapped: false };
|
|
16757
|
+
let index = 1;
|
|
16758
|
+
while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
|
|
16759
|
+
index += 1;
|
|
16760
|
+
if (index >= tokens.length)
|
|
16761
|
+
return { tokens, envWrapped: false };
|
|
16762
|
+
return { tokens: tokens.slice(index), envWrapped: true };
|
|
16763
|
+
}
|
|
16725
16764
|
var KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
16726
16765
|
"agents",
|
|
16727
16766
|
"commands",
|
|
@@ -16742,11 +16781,6 @@ function hash(bytes) {
|
|
|
16742
16781
|
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
16743
16782
|
}
|
|
16744
16783
|
function base(input) {
|
|
16745
|
-
const inspectionIdentity = input.inspectionIdentity ? Object.freeze({
|
|
16746
|
-
file: Object.freeze({ ...input.inspectionIdentity.file }),
|
|
16747
|
-
root: Object.freeze({ ...input.inspectionIdentity.root }),
|
|
16748
|
-
...input.inspectionIdentity.bundleRoot ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) } : {}
|
|
16749
|
-
}) : undefined;
|
|
16750
16784
|
return {
|
|
16751
16785
|
filePath: input.filePath,
|
|
16752
16786
|
before: Buffer.from(input.bytes),
|
|
@@ -16754,8 +16788,7 @@ function base(input) {
|
|
|
16754
16788
|
mode: input.mode,
|
|
16755
16789
|
writable: input.writable,
|
|
16756
16790
|
...input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {},
|
|
16757
|
-
...input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}
|
|
16758
|
-
...inspectionIdentity ? { inspectionIdentity } : {}
|
|
16791
|
+
...input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}
|
|
16759
16792
|
};
|
|
16760
16793
|
}
|
|
16761
16794
|
function blocked(input, reason, detail) {
|
|
@@ -16991,8 +17024,9 @@ function migratedObject(data) {
|
|
|
16991
17024
|
if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
16992
17025
|
return "shell-operators-change-v2-literal-argv-semantics";
|
|
16993
17026
|
}
|
|
16994
|
-
const
|
|
16995
|
-
|
|
17027
|
+
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
|
|
17028
|
+
const executable = targetTokens[0];
|
|
17029
|
+
if (SHELL_ASSIGNMENT_WORD.test(executable) || !envWrapped && !shellStableV2Executable(executable)) {
|
|
16996
17030
|
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
16997
17031
|
}
|
|
16998
17032
|
output.run = tokens.join(" ");
|
|
@@ -17186,11 +17220,6 @@ function causeMessage(cause) {
|
|
|
17186
17220
|
return cause instanceof Error ? cause.message : String(cause);
|
|
17187
17221
|
}
|
|
17188
17222
|
function base2(input) {
|
|
17189
|
-
const inspectionIdentity = input.inspectionIdentity ? Object.freeze({
|
|
17190
|
-
file: Object.freeze({ ...input.inspectionIdentity.file }),
|
|
17191
|
-
root: Object.freeze({ ...input.inspectionIdentity.root }),
|
|
17192
|
-
...input.inspectionIdentity.bundleRoot ? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) } : {}
|
|
17193
|
-
}) : undefined;
|
|
17194
17223
|
return {
|
|
17195
17224
|
filePath: input.filePath,
|
|
17196
17225
|
before: Buffer.from(input.bytes),
|
|
@@ -17198,8 +17227,7 @@ function base2(input) {
|
|
|
17198
17227
|
mode: input.mode,
|
|
17199
17228
|
writable: input.writable,
|
|
17200
17229
|
...input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {},
|
|
17201
|
-
...input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}
|
|
17202
|
-
...inspectionIdentity ? { inspectionIdentity } : {}
|
|
17230
|
+
...input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}
|
|
17203
17231
|
};
|
|
17204
17232
|
}
|
|
17205
17233
|
function blocked2(input, reason, detail) {
|
|
@@ -21764,6 +21792,8 @@ function indexDocumentFromEntry(entry, base3, rendererName) {
|
|
|
21764
21792
|
doc.tags = entry.tags;
|
|
21765
21793
|
if (entry.content !== undefined)
|
|
21766
21794
|
doc.content = entry.content;
|
|
21795
|
+
if (entry.contentTruncated !== undefined)
|
|
21796
|
+
doc.contentTruncated = entry.contentTruncated;
|
|
21767
21797
|
if (entry.aliases !== undefined)
|
|
21768
21798
|
doc.aliases = entry.aliases;
|
|
21769
21799
|
if (entry.searchHints !== undefined)
|
|
@@ -28814,6 +28844,43 @@ var AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) => {
|
|
|
28814
28844
|
}
|
|
28815
28845
|
});
|
|
28816
28846
|
|
|
28847
|
+
// src/core/config/config-version-shim.ts
|
|
28848
|
+
var KNOWN_OLD_CONFIG_VERSIONS = ["0.0.1"];
|
|
28849
|
+
function isKnownOldConfigVersion(value) {
|
|
28850
|
+
return typeof value === "string" && KNOWN_OLD_CONFIG_VERSIONS.includes(value);
|
|
28851
|
+
}
|
|
28852
|
+
function upgradeFrom080(raw) {
|
|
28853
|
+
const { defaultEngine, defaults, ...rest } = raw;
|
|
28854
|
+
if (typeof defaultEngine !== "string" || defaultEngine.length === 0) {
|
|
28855
|
+
return { ...rest, ...defaults !== undefined ? { defaults } : {}, configVersion: CURRENT_CONFIG_VERSION };
|
|
28856
|
+
}
|
|
28857
|
+
const existingDefaults = defaults !== null && typeof defaults === "object" ? defaults : {};
|
|
28858
|
+
return {
|
|
28859
|
+
...rest,
|
|
28860
|
+
configVersion: CURRENT_CONFIG_VERSION,
|
|
28861
|
+
defaults: { llmEngine: defaultEngine, ...existingDefaults }
|
|
28862
|
+
};
|
|
28863
|
+
}
|
|
28864
|
+
var CONFIG_VERSION_UPGRADES = {
|
|
28865
|
+
"0.0.1": upgradeFrom080
|
|
28866
|
+
};
|
|
28867
|
+
function unsupportedConfigVersionError(rawVersion, sourcePath) {
|
|
28868
|
+
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
28869
|
+
const supported = [CURRENT_CONFIG_VERSION, ...KNOWN_OLD_CONFIG_VERSIONS].map((v) => `"${v}"`).join(", ");
|
|
28870
|
+
return new ConfigError(`Unsupported configVersion${where}: got ${JSON.stringify(rawVersion)}, expected one of ${supported}.`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
28871
|
+
}
|
|
28872
|
+
function upgradeConfigVersion(raw, sourcePath) {
|
|
28873
|
+
const version = raw.configVersion;
|
|
28874
|
+
if (version === CURRENT_CONFIG_VERSION)
|
|
28875
|
+
return raw;
|
|
28876
|
+
if (isKnownOldConfigVersion(version)) {
|
|
28877
|
+
const upgraded = CONFIG_VERSION_UPGRADES[version](raw);
|
|
28878
|
+
warn(`Config${sourcePath ? ` at ${sourcePath}` : ""} uses configVersion "${version}" \u2014 auto-upgraded to ${CURRENT_CONFIG_VERSION} in memory; the next config write (e.g. \`akm config set\`) persists this and silences the warning.`);
|
|
28879
|
+
return upgraded;
|
|
28880
|
+
}
|
|
28881
|
+
throw unsupportedConfigVersionError(version, sourcePath);
|
|
28882
|
+
}
|
|
28883
|
+
|
|
28817
28884
|
// src/core/config/deep-merge.ts
|
|
28818
28885
|
var UNSAFE_KEYS3 = new Set(["__proto__", "constructor", "prototype"]);
|
|
28819
28886
|
function isPlainObject4(value) {
|
|
@@ -28904,10 +28971,7 @@ function loadUserConfig() {
|
|
|
28904
28971
|
return finalConfig;
|
|
28905
28972
|
}
|
|
28906
28973
|
function parseAndValidateConfigText(text, sourcePath) {
|
|
28907
|
-
const parsedRaw = parseConfigText(text, sourcePath);
|
|
28908
|
-
if (parsedRaw.configVersion !== CURRENT_CONFIG_VERSION) {
|
|
28909
|
-
throw new ConfigError(`Unsupported configVersion${sourcePath ? ` at ${sourcePath}` : ""}: expected "${CURRENT_CONFIG_VERSION}".`, "UNSUPPORTED_CONFIG_VERSION", "Recreate engines and improve.strategies manually for AKM 0.9.0; profile-based configuration is not translated automatically.");
|
|
28910
|
-
}
|
|
28974
|
+
const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
|
|
28911
28975
|
const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
|
|
28912
28976
|
const where = sourcePath ? ` at ${sourcePath}` : "";
|
|
28913
28977
|
if (conflicts.length > 0) {
|
|
@@ -29460,32 +29524,18 @@ function contained2(root, candidate) {
|
|
|
29460
29524
|
const relative = path31.relative(root, candidate);
|
|
29461
29525
|
return relative === "" || !relative.startsWith("..") && !path31.isAbsolute(relative);
|
|
29462
29526
|
}
|
|
29463
|
-
function
|
|
29464
|
-
const stat = fs26.lstatSync(filePath
|
|
29465
|
-
|
|
29466
|
-
|
|
29467
|
-
|
|
29468
|
-
if (kind === "file" && stat.nlink !== 1n)
|
|
29469
|
-
throw migrationError(`${filePath} must not be hard-linked.`);
|
|
29470
|
-
return Object.freeze({
|
|
29471
|
-
realPath: fs26.realpathSync(filePath),
|
|
29472
|
-
device: stat.dev.toString(),
|
|
29473
|
-
inode: stat.ino.toString(),
|
|
29474
|
-
linkCount: stat.nlink.toString(),
|
|
29475
|
-
changeTimeNs: stat.ctimeNs.toString()
|
|
29476
|
-
});
|
|
29477
|
-
}
|
|
29478
|
-
function sameIdentity2(left, right) {
|
|
29479
|
-
return left.realPath === right.realPath && left.device === right.device && left.inode === right.inode && left.linkCount === right.linkCount && left.changeTimeNs === right.changeTimeNs;
|
|
29527
|
+
function realDirectory(filePath) {
|
|
29528
|
+
const stat = fs26.lstatSync(filePath);
|
|
29529
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
29530
|
+
throw migrationError(`${filePath} must be a real directory.`);
|
|
29531
|
+
return fs26.realpathSync(filePath);
|
|
29480
29532
|
}
|
|
29481
29533
|
function snapshot(filePath) {
|
|
29482
|
-
const before = identity3(filePath, "file");
|
|
29483
29534
|
const stat = fs26.lstatSync(filePath);
|
|
29535
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
29536
|
+
throw migrationError(`${filePath} must be a real file.`);
|
|
29484
29537
|
const bytes = fs26.readFileSync(filePath);
|
|
29485
|
-
|
|
29486
|
-
if (!sameIdentity2(before, after))
|
|
29487
|
-
throw migrationError(`${filePath} changed while it was read.`);
|
|
29488
|
-
return Object.freeze({ bytes, mode: stat.mode & 511, identity: after });
|
|
29538
|
+
return Object.freeze({ bytes, mode: stat.mode & 511 });
|
|
29489
29539
|
}
|
|
29490
29540
|
function writable(filePath) {
|
|
29491
29541
|
try {
|
|
@@ -29496,10 +29546,9 @@ function writable(filePath) {
|
|
|
29496
29546
|
}
|
|
29497
29547
|
}
|
|
29498
29548
|
function walkTasks(root, tasksDir, out) {
|
|
29499
|
-
const
|
|
29500
|
-
const
|
|
29501
|
-
|
|
29502
|
-
if (!contained2(bundleIdentity.realPath, physicalRoot)) {
|
|
29549
|
+
const physicalRoot = realDirectory(root.root);
|
|
29550
|
+
const physicalBundleRoot = realDirectory(root.bundleRoot ?? root.root);
|
|
29551
|
+
if (!contained2(physicalBundleRoot, physicalRoot)) {
|
|
29503
29552
|
throw migrationError(`${root.root} resolves outside bundle ${root.bundleId}.`);
|
|
29504
29553
|
}
|
|
29505
29554
|
const visit2 = (directory) => {
|
|
@@ -29525,12 +29574,7 @@ function walkTasks(root, tasksDir, out) {
|
|
|
29525
29574
|
mode: current.mode,
|
|
29526
29575
|
writable: root.writable,
|
|
29527
29576
|
onDiskWritable: writable(candidate) && writable(parent) && (current.mode & 146) !== 0,
|
|
29528
|
-
containmentRoot: physicalRoot
|
|
29529
|
-
inspectionIdentity: Object.freeze({
|
|
29530
|
-
file: current.identity,
|
|
29531
|
-
root: rootIdentity,
|
|
29532
|
-
bundleRoot: bundleIdentity
|
|
29533
|
-
})
|
|
29577
|
+
containmentRoot: physicalRoot
|
|
29534
29578
|
});
|
|
29535
29579
|
}
|
|
29536
29580
|
};
|
|
@@ -29594,17 +29638,9 @@ function assertUnchanged(change) {
|
|
|
29594
29638
|
if (!current.bytes.equals(change.before) || current.mode !== change.mode) {
|
|
29595
29639
|
throw migrationError(`${change.filePath} changed after preview; no task files were replaced.`);
|
|
29596
29640
|
}
|
|
29597
|
-
const planned = change.inspectionIdentity?.file;
|
|
29598
|
-
if (planned && !sameIdentity2(planned, current.identity)) {
|
|
29599
|
-
throw migrationError(`${change.filePath} identity changed after preview; no task files were replaced.`);
|
|
29600
|
-
}
|
|
29601
29641
|
return current;
|
|
29602
29642
|
}
|
|
29603
29643
|
function applyTaskToV3MigrationPlan(plan, options) {
|
|
29604
|
-
const blocked3 = plan.files.filter((file) => file.status === "blocked");
|
|
29605
|
-
if (blocked3.length > 0) {
|
|
29606
|
-
throw migrationError(`plan is blocked: ${blocked3.map((file) => `${file.filePath} (${file.reason})`).join(", ")}. No files were written.`);
|
|
29607
|
-
}
|
|
29608
29644
|
const changes = plan.files.filter((file) => file.status === "changed");
|
|
29609
29645
|
for (const change of changes) {
|
|
29610
29646
|
assertUnchanged(change);
|
|
@@ -29647,32 +29683,18 @@ function contained3(root, candidate) {
|
|
|
29647
29683
|
const relative = path32.relative(root, candidate);
|
|
29648
29684
|
return relative === "" || !relative.startsWith("..") && !path32.isAbsolute(relative);
|
|
29649
29685
|
}
|
|
29650
|
-
function
|
|
29651
|
-
const stat = fs27.lstatSync(filePath
|
|
29652
|
-
|
|
29653
|
-
|
|
29654
|
-
|
|
29655
|
-
if (kind === "file" && stat.nlink !== 1n)
|
|
29656
|
-
throw migrationError2(`${filePath} must not be hard-linked.`);
|
|
29657
|
-
return Object.freeze({
|
|
29658
|
-
realPath: fs27.realpathSync(filePath),
|
|
29659
|
-
device: stat.dev.toString(),
|
|
29660
|
-
inode: stat.ino.toString(),
|
|
29661
|
-
linkCount: stat.nlink.toString(),
|
|
29662
|
-
changeTimeNs: stat.ctimeNs.toString()
|
|
29663
|
-
});
|
|
29664
|
-
}
|
|
29665
|
-
function sameIdentity3(left, right) {
|
|
29666
|
-
return left.realPath === right.realPath && left.device === right.device && left.inode === right.inode && left.linkCount === right.linkCount && left.changeTimeNs === right.changeTimeNs;
|
|
29686
|
+
function realDirectory2(filePath) {
|
|
29687
|
+
const stat = fs27.lstatSync(filePath);
|
|
29688
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
29689
|
+
throw migrationError2(`${filePath} must be a real directory.`);
|
|
29690
|
+
return fs27.realpathSync(filePath);
|
|
29667
29691
|
}
|
|
29668
29692
|
function snapshot2(filePath) {
|
|
29669
|
-
const before = identity4(filePath, "file");
|
|
29670
29693
|
const stat = fs27.lstatSync(filePath);
|
|
29694
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
29695
|
+
throw migrationError2(`${filePath} must be a real file.`);
|
|
29671
29696
|
const bytes = fs27.readFileSync(filePath);
|
|
29672
|
-
|
|
29673
|
-
if (!sameIdentity3(before, after))
|
|
29674
|
-
throw migrationError2(`${filePath} changed while it was read.`);
|
|
29675
|
-
return Object.freeze({ bytes, mode: stat.mode & 511, identity: after });
|
|
29697
|
+
return Object.freeze({ bytes, mode: stat.mode & 511 });
|
|
29676
29698
|
}
|
|
29677
29699
|
function writable2(filePath) {
|
|
29678
29700
|
try {
|
|
@@ -29683,10 +29705,9 @@ function writable2(filePath) {
|
|
|
29683
29705
|
}
|
|
29684
29706
|
}
|
|
29685
29707
|
function walkTasks2(root, tasksDir, out) {
|
|
29686
|
-
const
|
|
29687
|
-
const
|
|
29688
|
-
|
|
29689
|
-
if (!contained3(bundleIdentity.realPath, physicalRoot)) {
|
|
29708
|
+
const physicalRoot = realDirectory2(root.root);
|
|
29709
|
+
const physicalBundleRoot = realDirectory2(root.bundleRoot ?? root.root);
|
|
29710
|
+
if (!contained3(physicalBundleRoot, physicalRoot)) {
|
|
29690
29711
|
throw migrationError2(`${root.root} resolves outside bundle ${root.bundleId}.`);
|
|
29691
29712
|
}
|
|
29692
29713
|
const visit2 = (directory) => {
|
|
@@ -29712,12 +29733,7 @@ function walkTasks2(root, tasksDir, out) {
|
|
|
29712
29733
|
mode: current.mode,
|
|
29713
29734
|
writable: root.writable,
|
|
29714
29735
|
onDiskWritable: writable2(candidate) && writable2(parent) && (current.mode & 146) !== 0,
|
|
29715
|
-
containmentRoot: physicalRoot
|
|
29716
|
-
inspectionIdentity: Object.freeze({
|
|
29717
|
-
file: current.identity,
|
|
29718
|
-
root: rootIdentity,
|
|
29719
|
-
bundleRoot: bundleIdentity
|
|
29720
|
-
})
|
|
29736
|
+
containmentRoot: physicalRoot
|
|
29721
29737
|
});
|
|
29722
29738
|
}
|
|
29723
29739
|
};
|
|
@@ -29781,17 +29797,9 @@ function assertUnchanged2(change) {
|
|
|
29781
29797
|
if (!current.bytes.equals(change.before) || current.mode !== change.mode) {
|
|
29782
29798
|
throw migrationError2(`${change.filePath} changed after preview; no task files were replaced.`);
|
|
29783
29799
|
}
|
|
29784
|
-
const planned = change.inspectionIdentity?.file;
|
|
29785
|
-
if (planned && !sameIdentity3(planned, current.identity)) {
|
|
29786
|
-
throw migrationError2(`${change.filePath} identity changed after preview; no task files were replaced.`);
|
|
29787
|
-
}
|
|
29788
29800
|
return current;
|
|
29789
29801
|
}
|
|
29790
29802
|
function applyTaskToV4MigrationPlan(plan, options) {
|
|
29791
|
-
const blocked3 = plan.files.filter((file) => file.status === "blocked");
|
|
29792
|
-
if (blocked3.length > 0) {
|
|
29793
|
-
throw migrationError2(`plan is blocked: ${blocked3.map((file) => `${file.filePath} (${file.reason})`).join(", ")}. No files were written.`);
|
|
29794
|
-
}
|
|
29795
29803
|
const changes = plan.files.filter((file) => file.status === "changed");
|
|
29796
29804
|
for (const change of changes) {
|
|
29797
29805
|
assertUnchanged2(change);
|
|
@@ -29935,12 +29943,12 @@ async function runMigrationApply(options = {}) {
|
|
|
29935
29943
|
}
|
|
29936
29944
|
const result = withConfigLock(() => withMaintenanceStartBarrier(() => {
|
|
29937
29945
|
const before = inspectCurrentTaskPlan();
|
|
29938
|
-
if (before.result.
|
|
29946
|
+
if (before.result.taskV3Migration.changed === 0)
|
|
29939
29947
|
return before.result;
|
|
29940
29948
|
const backupPath = path33.join(getDataDir(), "backups", "task-v3", `${Date.now()}-${randomUUID2()}`);
|
|
29941
29949
|
const applied = applyTaskToV3MigrationPlan(before.plan, { backupRoot: backupPath });
|
|
29942
29950
|
const after = inspectCurrentTaskPlan().result;
|
|
29943
|
-
if (after.
|
|
29951
|
+
if (after.taskV3Migration.changed > 0) {
|
|
29944
29952
|
throw new ConfigError("Task migration did not converge to task v3.", "INVALID_CONFIG_FILE");
|
|
29945
29953
|
}
|
|
29946
29954
|
return { ...after, backupPath, applied: applied.changed.length };
|
|
@@ -29998,12 +30006,12 @@ async function runTaskV4MigrationApply(options = {}) {
|
|
|
29998
30006
|
}
|
|
29999
30007
|
const result = withConfigLock(() => withMaintenanceStartBarrier(() => {
|
|
30000
30008
|
const before = inspectCurrentTaskV4Plan();
|
|
30001
|
-
if (before.result.
|
|
30009
|
+
if (before.result.taskV4Migration.changed === 0)
|
|
30002
30010
|
return before.result;
|
|
30003
30011
|
const backupPath = path33.join(getDataDir(), "backups", "task-v4", `${Date.now()}-${randomUUID2()}`);
|
|
30004
30012
|
const applied = applyTaskToV4MigrationPlan(before.plan, { backupRoot: backupPath });
|
|
30005
30013
|
const after = inspectCurrentTaskV4Plan().result;
|
|
30006
|
-
if (after.
|
|
30014
|
+
if (after.taskV4Migration.changed > 0) {
|
|
30007
30015
|
throw new ConfigError("Task migration did not converge to task source v4.", "INVALID_CONFIG_FILE");
|
|
30008
30016
|
}
|
|
30009
30017
|
return { ...after, backupPath, applied: applied.changed.length };
|
|
@@ -85,7 +85,7 @@ function currentProposalRef(ref, requireQualified = false) {
|
|
|
85
85
|
}
|
|
86
86
|
function currentProposalTarget(value) {
|
|
87
87
|
if (typeof value !== "object" || value === null)
|
|
88
|
-
throw new Error("Proposal metadata
|
|
88
|
+
throw new Error("Proposal metadata has an invalid proposedTarget.");
|
|
89
89
|
const target = value;
|
|
90
90
|
if (typeof target.source !== "string" ||
|
|
91
91
|
!isBundleSlug(target.source) ||
|
|
@@ -200,7 +200,13 @@ export function proposalRowToProposal(row) {
|
|
|
200
200
|
}
|
|
201
201
|
validatePresentMetadata(meta);
|
|
202
202
|
const changes = storedToChanges(meta.changes, row.content);
|
|
203
|
-
|
|
203
|
+
// #859: absent proposedTarget (~93% of the real archive's accepted/rejected
|
|
204
|
+
// rows) is envelope metadata that predates this field, not corruption — an
|
|
205
|
+
// already-decided proposal is counted/displayed, never re-applied, so
|
|
206
|
+
// decode tolerates its absence the same way it already tolerates absent
|
|
207
|
+
// `changes`. A *present but malformed* value is still corruption and
|
|
208
|
+
// throws via currentProposalTarget, same as before.
|
|
209
|
+
const proposedTarget = meta.proposedTarget !== undefined ? currentProposalTarget(meta.proposedTarget) : undefined;
|
|
204
210
|
return {
|
|
205
211
|
id: row.id,
|
|
206
212
|
ref: currentProposalRef(row.ref),
|
|
@@ -214,7 +220,7 @@ export function proposalRowToProposal(row) {
|
|
|
214
220
|
...(frontmatter !== undefined ? { frontmatter } : {}),
|
|
215
221
|
},
|
|
216
222
|
changes,
|
|
217
|
-
proposedTarget,
|
|
223
|
+
...(proposedTarget !== undefined ? { proposedTarget } : {}),
|
|
218
224
|
...(typeof meta.beforeHash === "string" ? { beforeHash: meta.beforeHash } : {}),
|
|
219
225
|
...(meta.review !== undefined ? { review: meta.review } : {}),
|
|
220
226
|
...(typeof meta.confidence === "number" ? { confidence: meta.confidence } : {}),
|
|
@@ -233,8 +239,27 @@ export function proposalRowToProposal(row) {
|
|
|
233
239
|
export function proposalToRowValues(proposal, stashDir) {
|
|
234
240
|
// Fields that have no dedicated column live in metadata_json.
|
|
235
241
|
const metaObj = {};
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
// #859: `changes` and `proposedTarget` are only enforced as REQUIRED when
|
|
243
|
+
// minting/updating a `pending` proposal — the one status createProposal
|
|
244
|
+
// ever writes, and the one status every real accept/reject transition
|
|
245
|
+
// writes FROM (a pending row always carries the full envelope; see
|
|
246
|
+
// storedToChanges / currentProposalTarget doc comments). Writing a
|
|
247
|
+
// non-pending status (accepted/rejected/reverted) is always a status
|
|
248
|
+
// TRANSITION of an already-persisted proposal, spread-copied from the row
|
|
249
|
+
// this same repository just decoded — for a legacy archived row that
|
|
250
|
+
// never had `changes`/`proposedTarget` to begin with (the pre-envelope
|
|
251
|
+
// shape), re-persisting it unchanged (e.g. `proposal revert` on an old
|
|
252
|
+
// accepted row) must not be blocked by a requirement the row never met.
|
|
253
|
+
// This does NOT weaken validation of a genuinely new proposal: every
|
|
254
|
+
// pending-status write still requires the full envelope, exactly as
|
|
255
|
+
// before.
|
|
256
|
+
if (proposal.status === "pending") {
|
|
257
|
+
if (!Array.isArray(proposal.changes) || proposal.changes.length === 0) {
|
|
258
|
+
throw new Error(`Proposal ${proposal.id} has no file changes.`);
|
|
259
|
+
}
|
|
260
|
+
if (proposal.proposedTarget === undefined) {
|
|
261
|
+
throw new Error(`Proposal ${proposal.id} is missing proposedTarget.`);
|
|
262
|
+
}
|
|
238
263
|
}
|
|
239
264
|
if (proposal.changes.some((change) => typeof change.path !== "string" ||
|
|
240
265
|
change.path.length === 0 ||
|
|
@@ -243,7 +268,8 @@ export function proposalToRowValues(proposal, stashDir) {
|
|
|
243
268
|
throw new Error(`Proposal ${proposal.id} has invalid file changes.`);
|
|
244
269
|
}
|
|
245
270
|
metaObj.changes = changesToStored(proposal.changes);
|
|
246
|
-
|
|
271
|
+
if (proposal.proposedTarget !== undefined)
|
|
272
|
+
metaObj.proposedTarget = currentProposalTarget(proposal.proposedTarget);
|
|
247
273
|
if (proposal.beforeHash !== undefined)
|
|
248
274
|
metaObj.beforeHash = proposal.beforeHash;
|
|
249
275
|
if (proposal.sourceRun !== undefined)
|
|
@@ -159,12 +159,15 @@ export function schedulerLogicalBindingOwner(nativeId, invocation) {
|
|
|
159
159
|
return schedulerNativeArtifactOwner(nativeId, invocation)?.logicalId;
|
|
160
160
|
}
|
|
161
161
|
export function schedulerNativeArtifactOwner(nativeId, invocation) {
|
|
162
|
-
if (invocation[0] === "workflow" && invocation[1] === "run") {
|
|
163
|
-
|
|
162
|
+
if (invocation[0] === "workflow" && invocation[1] === "run" && invocation.length === 3) {
|
|
163
|
+
const ref = invocation[2];
|
|
164
|
+
if (ref && workflowRefDigestsToNativeId(ref, nativeId)) {
|
|
165
|
+
return { logicalId: nativeId, logicalKind: "workflow" };
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
164
168
|
}
|
|
165
169
|
const taskId = invocation[0] === "task" && invocation[1] === "run" ? invocation[2] : undefined;
|
|
166
|
-
|
|
167
|
-
if (!taskId || bundleIndex === -1 || !invocation[bundleIndex + 1])
|
|
170
|
+
if (!taskId)
|
|
168
171
|
return undefined;
|
|
169
172
|
return {
|
|
170
173
|
logicalId: schedulerNativeBindingId(taskId) === nativeId ? taskId : nativeId,
|
|
@@ -225,7 +228,6 @@ export function assertSchedulerMutationArtifact(artifact, expected) {
|
|
|
225
228
|
artifact.nativeId === expected.nativeId &&
|
|
226
229
|
artifact.bindingId === expected.bindingId &&
|
|
227
230
|
artifact.invocation !== undefined &&
|
|
228
|
-
sameInvocation(artifact.invocation, expected.invocation) &&
|
|
229
231
|
expected.fingerprint !== undefined &&
|
|
230
232
|
artifact.fingerprint === expected.fingerprint) {
|
|
231
233
|
return;
|
|
@@ -306,6 +308,14 @@ export function schedulerBindingOrdinal(bindingId, logicalSource, invocation) {
|
|
|
306
308
|
}
|
|
307
309
|
return undefined;
|
|
308
310
|
}
|
|
311
|
+
/** Whether some schedule ordinal on `ref` would digest to `nativeId` (mirrors schedulerBindingOrdinal). */
|
|
312
|
+
function workflowRefDigestsToNativeId(ref, nativeId) {
|
|
313
|
+
for (let ordinal = 0; ordinal < 4096; ordinal += 1) {
|
|
314
|
+
if (digestBindingId("workflow", ref, ordinal) === nativeId)
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
309
319
|
function digestBindingId(kind, ref, ordinal) {
|
|
310
320
|
const prefix = kind === "workflow" ? "wf" : "task";
|
|
311
321
|
const digest = createHash("sha256")
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `SchedulerSyncPlan`) so a future `task prune` can build a preview from
|
|
8
8
|
* its own remove-only operation set without needing a full sync plan.
|
|
9
9
|
*/
|
|
10
|
-
export function renderSchedulerPlanPreview(backend, operations, unchanged = []) {
|
|
10
|
+
export function renderSchedulerPlanPreview(backend, operations, unchanged = [], failures = []) {
|
|
11
11
|
const adds = [];
|
|
12
12
|
const updates = [];
|
|
13
13
|
const removes = [];
|
|
@@ -18,6 +18,7 @@ export function renderSchedulerPlanPreview(backend, operations, unchanged = [])
|
|
|
18
18
|
kind: "remove",
|
|
19
19
|
nativeId: operation.nativeId,
|
|
20
20
|
...(operation.ownerBundlePath !== undefined ? { ownerBundlePath: operation.ownerBundlePath } : {}),
|
|
21
|
+
...(operation.reason !== undefined ? { reason: operation.reason } : {}),
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
else if (operation.kind === "install") {
|
|
@@ -35,9 +36,10 @@ export function renderSchedulerPlanPreview(backend, operations, unchanged = [])
|
|
|
35
36
|
removes: Object.freeze(removes),
|
|
36
37
|
unchanged: Object.freeze([...unchanged]),
|
|
37
38
|
hasRemovals: removes.length > 0,
|
|
39
|
+
failures: Object.freeze([...failures]),
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
/** Convenience wrapper for the common case: previewing a whole {@link SchedulerSyncPlan}. */
|
|
41
43
|
export function renderSchedulerSyncPlanPreview(backend, plan) {
|
|
42
|
-
return renderSchedulerPlanPreview(backend, plan.operations, plan.unchanged);
|
|
44
|
+
return renderSchedulerPlanPreview(backend, plan.operations, plan.unchanged, plan.failures);
|
|
43
45
|
}
|