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.
@@ -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 === undefined)
10070
- obj.shape = command;
10071
- if (obj.schemaVersion === undefined)
10072
- obj.schemaVersion = 1;
10073
- return obj;
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 contentProjection = projectMarkdownContent(parsed.content);
13761
- if (contentProjection)
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 executable = tokens[0];
16995
- if (SHELL_ASSIGNMENT_WORD.test(executable) || !shellStableV2Executable(executable)) {
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 identity3(filePath, kind) {
29464
- const stat = fs26.lstatSync(filePath, { bigint: true });
29465
- const valid = kind === "file" ? stat.isFile() : stat.isDirectory();
29466
- if (stat.isSymbolicLink() || !valid)
29467
- throw migrationError(`${filePath} must be a real ${kind}.`);
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
- const after = identity3(filePath, "file");
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 rootIdentity = identity3(root.root, "directory");
29500
- const bundleIdentity = identity3(root.bundleRoot ?? root.root, "directory");
29501
- const physicalRoot = rootIdentity.realPath;
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 identity4(filePath, kind) {
29651
- const stat = fs27.lstatSync(filePath, { bigint: true });
29652
- const valid = kind === "file" ? stat.isFile() : stat.isDirectory();
29653
- if (stat.isSymbolicLink() || !valid)
29654
- throw migrationError2(`${filePath} must be a real ${kind}.`);
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
- const after = identity4(filePath, "file");
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 rootIdentity = identity4(root.root, "directory");
29687
- const bundleIdentity = identity4(root.bundleRoot ?? root.root, "directory");
29688
- const physicalRoot = rootIdentity.realPath;
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.status !== "ready")
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.status !== "current") {
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.status !== "ready")
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.status !== "current") {
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 is missing proposedTarget.");
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
- const proposedTarget = currentProposalTarget(meta.proposedTarget);
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
- if (!Array.isArray(proposal.changes) || proposal.changes.length === 0) {
237
- throw new Error(`Proposal ${proposal.id} has no file changes.`);
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
- metaObj.proposedTarget = currentProposalTarget(proposal.proposedTarget);
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
- return { logicalId: nativeId, logicalKind: "workflow" };
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
- const bundleIndex = invocation.indexOf("--bundle", 3);
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
  }