akm-cli 0.9.6 → 0.9.8-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +408 -0
  2. package/dist/assets/hints/cli-hints-full.md +3 -3
  3. package/dist/assets/improve-strategies/catchup.json +40 -11
  4. package/dist/assets/improve-strategies/thorough.json +45 -7
  5. package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
  6. package/dist/commands/agent/contribute-cli.js +11 -0
  7. package/dist/commands/env/env.js +2 -21
  8. package/dist/commands/health/checks.js +0 -25
  9. package/dist/commands/health/improve-metrics.js +8 -34
  10. package/dist/commands/health/windows.js +0 -4
  11. package/dist/commands/health.js +1 -35
  12. package/dist/commands/improve/consolidate/eligibility.js +11 -5
  13. package/dist/commands/improve/extract.js +36 -32
  14. package/dist/commands/improve/improve-cli.js +1 -1
  15. package/dist/commands/improve/improve-strategies.js +0 -4
  16. package/dist/commands/improve/memory/memory-belief.js +15 -5
  17. package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
  18. package/dist/commands/improve/memory/memory-improve.js +9 -20
  19. package/dist/commands/improve/preparation.js +53 -37
  20. package/dist/commands/improve/reflect.js +14 -18
  21. package/dist/commands/lint/base-linter.js +182 -19
  22. package/dist/commands/lint/index.js +21 -9
  23. package/dist/commands/migrate/config-extra-params.js +61 -0
  24. package/dist/commands/migrate/dead-residue.js +113 -0
  25. package/dist/commands/migrate/stale-txn.js +49 -0
  26. package/dist/commands/migrate-cli.js +42 -1
  27. package/dist/commands/proposal/proposal.js +1 -21
  28. package/dist/commands/proposal/repository.js +0 -4
  29. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
  30. package/dist/commands/read/curate.js +51 -18
  31. package/dist/commands/read/search-cli.js +24 -1
  32. package/dist/commands/read/show.js +2 -1
  33. package/dist/commands/sources/installed-stashes.js +5 -1
  34. package/dist/commands/sources/self-update.js +38 -1
  35. package/dist/commands/sources/sources-cli.js +17 -1
  36. package/dist/commands/tasks/tasks.js +0 -8
  37. package/dist/commands/url-checker.js +20 -28
  38. package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
  39. package/dist/core/adapter/adapters/akm-adapter.js +2 -3
  40. package/dist/core/adapter/adapters/akm-lint.js +2 -20
  41. package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
  42. package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
  43. package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
  44. package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
  45. package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
  46. package/dist/core/adapter/adapters/okf-adapter.js +7 -18
  47. package/dist/core/adapter/adapters/shared.js +2 -0
  48. package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
  49. package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
  50. package/dist/core/adapter/validate-context.js +1 -3
  51. package/dist/core/asset/asset-placement.js +14 -5
  52. package/dist/core/asset/frontmatter.js +212 -0
  53. package/dist/core/asset/memory-archive.js +97 -0
  54. package/dist/core/common.js +66 -2
  55. package/dist/core/config/config-walker.js +6 -10
  56. package/dist/core/config/config.js +11 -8
  57. package/dist/core/config/engine-semantics.js +0 -2
  58. package/dist/core/extra-params.js +17 -13
  59. package/dist/core/improve-result.js +1 -3
  60. package/dist/core/json-schema.js +9 -11
  61. package/dist/core/state/migrations.js +52 -2
  62. package/dist/core/state-db.js +2 -1
  63. package/dist/execution/executable-identity.js +1 -3
  64. package/dist/execution/guarded-source.js +1 -6
  65. package/dist/indexer/bundle-identity-guard.js +6 -1
  66. package/dist/indexer/db/graph-db.js +139 -154
  67. package/dist/indexer/ensure-index.js +11 -19
  68. package/dist/indexer/graph/graph-boost.js +23 -34
  69. package/dist/indexer/graph/graph-extraction.js +12 -2
  70. package/dist/indexer/indexer.js +1 -1
  71. package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
  72. package/dist/indexer/passes/memory-inference.js +7 -2
  73. package/dist/indexer/scan/drain-dir.js +2 -4
  74. package/dist/indexer/search/db-search.js +3 -3
  75. package/dist/indexer/search/fts-query.js +10 -15
  76. package/dist/indexer/search/search-source.js +0 -13
  77. package/dist/indexer/usage/usage-events.js +9 -1
  78. package/dist/indexer/walk/walker.js +11 -6
  79. package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
  80. package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
  81. package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
  82. package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
  83. package/dist/integrations/harnesses/index.js +0 -4
  84. package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
  85. package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
  86. package/dist/integrations/lockfile.js +0 -14
  87. package/dist/integrations/session-logs/index.js +0 -81
  88. package/dist/llm/client.js +0 -12
  89. package/dist/llm/memory-infer.js +1 -3
  90. package/dist/llm/usage-telemetry.js +1 -3
  91. package/dist/registry/create-provider-registry.js +4 -0
  92. package/dist/registry/factory.js +4 -0
  93. package/dist/registry/resolve.js +23 -8
  94. package/dist/runtime.js +0 -12
  95. package/dist/scripts/akm-migrate-node.js +155 -251
  96. package/dist/scripts/akm-migrate.js +155 -251
  97. package/dist/setup/setup.js +6 -12
  98. package/dist/sources/providers/git-install.js +7 -2
  99. package/dist/sources/providers/tar-utils.js +1 -7
  100. package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
  101. package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
  102. package/dist/storage/managed-db.js +20 -7
  103. package/dist/storage/repositories/events-repository.js +0 -81
  104. package/dist/storage/repositories/index-connection.js +45 -3
  105. package/dist/storage/repositories/index-entries-repository.js +0 -17
  106. package/dist/storage/repositories/index-fts-repository.js +24 -30
  107. package/dist/storage/repositories/index-utility-repository.js +0 -57
  108. package/dist/storage/repositories/index-vec-repository.js +25 -27
  109. package/dist/storage/repositories/task-history-repository.js +9 -3
  110. package/dist/tasks/backends/cron.js +49 -9
  111. package/dist/tasks/backends/launchd.js +8 -18
  112. package/dist/tasks/resolve-akm-bin.js +17 -2
  113. package/dist/tasks/run/task-history.js +21 -31
  114. package/dist/tasks/scheduler-invocation.js +8 -1
  115. package/dist/tasks/scheduler-sync.js +1 -6
  116. package/dist/tasks/source/bounded-document.js +1 -14
  117. package/dist/tasks/source/parse-task-source.js +23 -9
  118. package/dist/workflows/exec/child-workflow.js +1 -1
  119. package/dist/workflows/exec/native-executor.js +2 -2
  120. package/dist/workflows/exec/step-work.js +5 -17
  121. package/dist/workflows/exec/worktree.js +40 -6
  122. package/dist/workflows/freeze/task-bindings.js +2 -4
  123. package/dist/workflows/ir/compile.js +3 -14
  124. package/dist/workflows/ir/schema-v4.js +4 -6
  125. package/dist/workflows/ir/schema.js +2 -5
  126. package/dist/workflows/parser.js +23 -26
  127. package/dist/workflows/source-files.js +8 -13
  128. package/dist/workflows/source-ir/schema.js +1 -19
  129. package/docs/migration/v0.9.0-troubleshooting.md +12 -2
  130. package/docs/reference/cli.md +7 -1
  131. package/docs/reference/workflow-schema.md +24 -0
  132. package/package.json +2 -5
  133. package/dist/assets/improve-strategies/frequent.json +0 -15
  134. package/dist/assets/improve-strategies/memory-focus.json +0 -15
  135. package/dist/workflows/source-ir/compare.js +0 -17
@@ -229,9 +229,14 @@ export function searchVec(db, queryEmbedding, k) {
229
229
  .all(buf, k);
230
230
  }
231
231
  catch (err) {
232
- // Log the failure so it's visible in diagnostics
233
- warn("[db] searchVec (sqlite-vec path) failed:", err instanceof Error ? err.message : String(err));
234
- return [];
232
+ // A dimension mismatch (e.g. the embedding provider/model changed since
233
+ // the fast-path table was built) is a real, expected reason this query
234
+ // specifically cannot use the vec table — the complete BLOB table below
235
+ // is unaffected, so fall back to it rather than either silently
236
+ // returning [] (masking a genuinely corrupt index) or failing the whole
237
+ // search over one degraded index.
238
+ warn("[db] searchVec (sqlite-vec path) failed, falling back to JS-cosine scan:", err instanceof Error ? err.message : String(err));
239
+ return searchBlobVec(db, queryEmbedding, k);
235
240
  }
236
241
  }
237
242
  // Fallback: JS-based cosine similarity over BLOB table
@@ -281,32 +286,25 @@ function bufferToFloat32(buf, expectedDim) {
281
286
  return Array.from(f32);
282
287
  }
283
288
  function searchBlobVec(db, queryEmbedding, k) {
284
- try {
285
- const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
286
- if (rows.length === 0)
287
- return [];
288
- const expectedDim = queryEmbedding.length;
289
- const scored = [];
290
- for (const row of rows) {
291
- const embedding = bufferToFloat32(row.embedding, expectedDim);
292
- if (embedding === null)
293
- continue;
294
- const similarity = cosineSimilarity(queryEmbedding, embedding);
295
- scored.push({ id: row.id, similarity });
296
- }
297
- scored.sort((a, b) => b.similarity - a.similarity);
298
- // Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
299
- // For normalized vectors: L2² = 2(1 - cos_sim)
300
- return scored.slice(0, k).map(({ id, similarity }) => ({
301
- id,
302
- distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
303
- }));
304
- }
305
- catch (err) {
306
- // MD-5: Log the failure so it's visible in diagnostics
307
- warn("[db] searchBlobVec (JS fallback) failed:", err instanceof Error ? err.message : String(err));
289
+ const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
290
+ if (rows.length === 0)
308
291
  return [];
292
+ const expectedDim = queryEmbedding.length;
293
+ const scored = [];
294
+ for (const row of rows) {
295
+ const embedding = bufferToFloat32(row.embedding, expectedDim);
296
+ if (embedding === null)
297
+ continue;
298
+ const similarity = cosineSimilarity(queryEmbedding, embedding);
299
+ scored.push({ id: row.id, similarity });
309
300
  }
301
+ scored.sort((a, b) => b.similarity - a.similarity);
302
+ // Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
303
+ // For normalized vectors: L2² = 2(1 - cos_sim)
304
+ return scored.slice(0, k).map(({ id, similarity }) => ({
305
+ id,
306
+ distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
307
+ }));
310
308
  }
311
309
  /**
312
310
  * Return all entries that do not yet have an embedding row.
@@ -1,12 +1,18 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Repository for the state.db `task_history` table (per-task run ledger,
6
+ * replaces the old per-task JSONL files). Extracted verbatim from
7
+ * core/state-db.ts — queries unchanged, only relocated behind the repository
8
+ * boundary. Re-exported by core/state-db.ts so existing importers resolve.
9
+ *
10
+ * @module task-history-repository
11
+ */
12
+ import { isRecord } from "../../core/common.js";
4
13
  function metadataError(message) {
5
14
  throw new Error(`invalid task_history metadata_json: ${message}`);
6
15
  }
7
- function isRecord(value) {
8
- return typeof value === "object" && value !== null && !Array.isArray(value);
9
- }
10
16
  function validateDetail(value) {
11
17
  if (value === undefined || value === null)
12
18
  return;
@@ -31,7 +31,7 @@ import { getTaskLogDir } from "../../core/paths.js";
31
31
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
32
32
  import { parseSchedule, translateToCron } from "../schedule.js";
33
33
  import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
34
- import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
34
+ import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
35
35
  import { nodeFs, throwIfNotOk } from "./exec-utils.js";
36
36
  const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
37
37
  const END = (id) => `# akm:task ${assertCronValue(id)} END`;
@@ -105,7 +105,7 @@ export function CRON_BACKEND(options = {}) {
105
105
  replaceCrontab(exec, existing, next);
106
106
  },
107
107
  list() {
108
- return [...inspectCronState(readCrontab(exec)).installed];
108
+ return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
109
109
  },
110
110
  listForRebind() {
111
111
  const existing = readCrontab(exec);
@@ -123,14 +123,14 @@ export function CRON_BACKEND(options = {}) {
123
123
  });
124
124
  },
125
125
  listNativeArtifacts() {
126
- return [...inspectCronState(readCrontab(exec)).artifacts];
126
+ return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
127
127
  },
128
128
  inspectBindings() {
129
- return inspectCronState(readCrontab(exec));
129
+ return inspectCronState(readCrontab(exec), defaultContextPath);
130
130
  },
131
131
  snapshotBindings(ids) {
132
132
  const crontab = readCrontab(exec);
133
- const inspection = inspectCronState(crontab);
133
+ const inspection = inspectCronState(crontab, defaultContextPath);
134
134
  const keys = new Set(ids.map(schedulerNativeArtifactKey));
135
135
  return Object.freeze({
136
136
  kind: CRON_SNAPSHOT,
@@ -144,7 +144,7 @@ export function CRON_BACKEND(options = {}) {
144
144
  throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
145
145
  }
146
146
  const existing = readCrontab(exec);
147
- const current = inspectCronState(existing);
147
+ const current = inspectCronState(existing, defaultContextPath);
148
148
  const safeNativeIds = [];
149
149
  const errors = [];
150
150
  if (expectedCurrent) {
@@ -192,7 +192,7 @@ export function CRON_BACKEND(options = {}) {
192
192
  },
193
193
  };
194
194
  }
195
- function inspectCronState(crontab) {
195
+ function inspectCronState(crontab, fallbackContextPath) {
196
196
  const installed = [];
197
197
  const artifacts = [];
198
198
  for (const { id, body } of listBlocks(crontab)) {
@@ -207,7 +207,14 @@ function inspectCronState(crontab) {
207
207
  signature: fingerprint,
208
208
  ...(parsed.target !== undefined ? { target: parsed.target } : {}),
209
209
  binding: parsed.binding,
210
- contextPath: parsed.contextPath,
210
+ // A legacy (pre-`--scheduler-context`) row has no real descriptor
211
+ // path to report — `extractLegacyCronInvocation` leaves it "". Fall
212
+ // back to the current default so downstream consumers (context
213
+ // validation in `akm task prune`/`explain`, `sync`'s reuse of an
214
+ // existing binding's contextPath) see a real, resolvable descriptor
215
+ // rather than an empty path, since the row is about to be reconciled
216
+ // to a current one anyway (#881).
217
+ contextPath: parsed.contextPath || fallbackContextPath,
211
218
  };
212
219
  Object.defineProperty(ref, "nativeId", { value: id });
213
220
  Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
@@ -323,7 +330,40 @@ export function extractCronInvocation(body) {
323
330
  const redirectIndex = fields.indexOf(">>", commandStart);
324
331
  if (redirectIndex === -1)
325
332
  return undefined;
326
- return parseScheduledBindingArgv(fields.slice(commandStart, redirectIndex));
333
+ const tail = fields.slice(commandStart, redirectIndex);
334
+ const parsed = parseScheduledBindingArgv(tail);
335
+ if (parsed)
336
+ return parsed;
337
+ // Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
338
+ // no context argument at all — just the akm argv immediately followed by
339
+ // the public `task run …` / `workflow run …` tail. `extractCronInvocation`
340
+ // only ever runs on a body already isolated between this backend's own
341
+ // `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
342
+ // this older shape here doesn't extend trust to any unmarked crontab
343
+ // line — it only lets sync see and reconcile a row akm already owns
344
+ // instead of treating it as absent and colliding with the still-present
345
+ // artifact (#881). Guarded on the marker's absence so a row that DOES
346
+ // carry `--scheduler-context` but fails to parse for some other reason
347
+ // is never silently reinterpreted as legacy.
348
+ if (tail.includes(SCHEDULER_CONTEXT_ARG))
349
+ return undefined;
350
+ return extractLegacyCronInvocation(tail);
351
+ }
352
+ function extractLegacyCronInvocation(tail) {
353
+ for (let index = 0; index < tail.length - 1; index += 1) {
354
+ if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
355
+ const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
356
+ if (!publicInvocation)
357
+ return undefined;
358
+ return {
359
+ binding: tail.slice(0, index),
360
+ contextPath: "",
361
+ invocation: publicInvocation.invocation,
362
+ ...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
363
+ };
364
+ }
365
+ }
366
+ return undefined;
327
367
  }
328
368
  /** Reverse {@link quoteForCron} for a single whitespace-free token. */
329
369
  function splitCronShellWords(value) {
@@ -25,6 +25,7 @@ import fs from "node:fs";
25
25
  import os from "node:os";
26
26
  import path from "node:path";
27
27
  import launchdTemplate from "../../assets/backends/launchd-template.xml" with { type: "text" };
28
+ import { hasErrnoCode } from "../../core/common.js";
28
29
  import { ConfigError } from "../../core/errors.js";
29
30
  import { getTaskLogDir } from "../../core/paths.js";
30
31
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
@@ -851,22 +852,6 @@ export function parseLaunchdLoadedLabels(output) {
851
852
  }
852
853
  return labels;
853
854
  }
854
- function countCharacter(value, needle) {
855
- let count = 0;
856
- for (const character of value)
857
- if (character === needle)
858
- count += 1;
859
- return count;
860
- }
861
- function hasUnsafeLaunchdControlCharacter(output) {
862
- for (let index = 0; index < output.length; index += 1) {
863
- const code = output.charCodeAt(index);
864
- if (code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || (code >= 127 && code <= 159)) {
865
- return true;
866
- }
867
- }
868
- return false;
869
- }
870
855
  function readDisabledLabels(exec) {
871
856
  try {
872
857
  const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
@@ -933,8 +918,13 @@ function defaultLaunchdFs() {
933
918
  try {
934
919
  return fs.readdirSync(dir);
935
920
  }
936
- catch {
937
- return [];
921
+ catch (error) {
922
+ // Genuinely absent is an empty listing; anything else (e.g. EACCES) is
923
+ // not "no plists" — surfacing it as such could make inspectBindings
924
+ // think there is nothing to reconcile.
925
+ if (hasErrnoCode(error, "ENOENT"))
926
+ return [];
927
+ throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");
938
928
  }
939
929
  },
940
930
  exists(file) {
@@ -10,7 +10,11 @@
10
10
  * Resolution order:
11
11
  *
12
12
  * 1. `process.execPath` alone for a Bun standalone executable.
13
- * 2. Absolute Node plus the public `dist/akm` package launcher.
13
+ * 2. Absolute Node plus the public `dist/akm` package launcher — eligible
14
+ * when it is the active npm global install, or when this process
15
+ * cannot write to the directory containing it (a read-only mount, e.g.
16
+ * an image-baked install, gives the same "won't change out from under
17
+ * the scheduler" guarantee npm-global ownership does).
14
18
  * 3. Absolute runtime plus the source/build CLI entry, classified as a
15
19
  * checkout that requires explicit `--rebind` for scheduler writes.
16
20
  *
@@ -51,12 +55,13 @@ export function resolveAkmInvocation(options = {}) {
51
55
  }
52
56
  }
53
57
  const npmGlobal = !checkout && packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot);
58
+ const readOnlyInstall = !checkout && !npmGlobal && !(options.isPathWritable ?? isPathWritable)(path.dirname(launcherPath));
54
59
  const kind = checkout ? "checkout" : npmGlobal ? "npm" : "package-local";
55
60
  return {
56
61
  argv: [absoluteInvocationPath(nodePath), absoluteInvocationPath(launcherPath)],
57
62
  via: kind,
58
63
  kind,
59
- eligible: npmGlobal,
64
+ eligible: npmGlobal || readOnlyInstall,
60
65
  };
61
66
  }
62
67
  const checkoutEntry = resolveCheckoutEntry(options.cliEntryUrl ?? import.meta.url, runtime, mainPath);
@@ -117,6 +122,16 @@ function isCheckoutLauncher(file) {
117
122
  const packageRoot = path.dirname(path.dirname(launcher));
118
123
  return fs.existsSync(path.join(packageRoot, ".git"));
119
124
  }
125
+ /** Whether this process can write to `dir` — false also covers a read-only mount. */
126
+ function isPathWritable(dir) {
127
+ try {
128
+ fs.accessSync(dir, fs.constants.W_OK);
129
+ return true;
130
+ }
131
+ catch {
132
+ return false;
133
+ }
134
+ }
120
135
  function packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot) {
121
136
  if (!npmGlobalRoot)
122
137
  return false;
@@ -14,20 +14,15 @@
14
14
  *
15
15
  * D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
16
16
  * docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
17
- * The READ side's mapping rule is a PERMANENT invariant kept here, not moved
18
- * a maintainer touching `taskHistoryRowToResult` needs it right here, and
19
- * row B-51 (docs/plans/specs/p4-deletions-closeout.md) makes deleting it a
20
- * review-blocking violation: it reads rows written by every previous
21
- * release, forever.
22
- *
23
- * A legacy row (no `targetVocab` marker written before D8) maps:
24
- * `"prompt"` -> `{kind:"command", engine}`, `"command"` -> `{kind:"shell"}`,
25
- * `"workflow"` unchanged, anything else (including the new vocabulary's own
26
- * "shell"/"script"/"prompt" written WITHOUT a marker, which no production
27
- * writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
28
- * workflow `ref` falls back to `""`, the command/prompt arm's `engine`
29
- * falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
30
- * directly in the current vocabulary — no mapping needed.
17
+ * The READ side used to carry a permanent legacy-vocabulary mapping here
18
+ * (row B-51 of docs/plans/specs/p4-deletions-closeout.md called deleting it
19
+ * "review-blocking") — SUPERSEDED: `task_history` is DB-owned data, so the
20
+ * remap is now a one-time schema migration
21
+ * (`025-task-history-vocabulary-backfill` in src/core/state/migrations.ts)
22
+ * instead of a read-side shim run forever. Every row this function sees now
23
+ * carries the current vocabulary's `target_kind` strings; the P0-pinned null
24
+ * fallbacks still apply (workflow `ref` falls back to `""`, the command
25
+ * arm's `engine` falls back to `null`).
31
26
  *
32
27
  * A DAG leaf with respect to the rest of src/tasks/run/**: this module
33
28
  * imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
@@ -37,6 +32,7 @@
37
32
  */
38
33
  import { rethrowIfTestIsolationError } from "../../core/errors.js";
39
34
  import { withStateDb } from "../../core/state-db.js";
35
+ import { warn } from "../../core/warn.js";
40
36
  import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
41
37
  /** Append (or finalize a reserved attempt into) one task_history row. */
42
38
  export function appendHistory(result, historyReserved = false) {
@@ -67,7 +63,11 @@ export function appendHistory(result, historyReserved = false) {
67
63
  }
68
64
  catch (error) {
69
65
  rethrowIfTestIsolationError(error);
70
- // History recording is fully best-effort and must not alter CLI output.
66
+ // History recording must not alter the task's own result/exit status
67
+ // the task already ran and its outcome is independent of whether we can
68
+ // persist a history row. But silence would make a failed write
69
+ // indistinguishable from the task never having run, so warn.
70
+ warn(`task history: failed to record history for task ${result.id}: ${error instanceof Error ? error.message : String(error)}`);
71
71
  }
72
72
  }
73
73
  export function readTaskHistory(options = {}) {
@@ -111,32 +111,22 @@ function decodeTaskHistoryRows(rows) {
111
111
  * Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
112
112
  * that callers of `readTaskHistory()` expect.
113
113
  *
114
- * D8 read boundary (spec §5.3): branches on the decoded metadata's
115
- * `targetVocab` marker see the module header's table.
114
+ * Reads `target_kind` directly in the current (post-D8) vocabulary the
115
+ * `025-task-history-vocabulary-backfill` state migration rewrites every
116
+ * legacy-vocabulary row before this ever runs against it.
116
117
  */
117
118
  function taskHistoryRowToResult(row) {
118
119
  const meta = decodeTaskHistoryMetadata(row.metadata_json);
119
- const marked = meta.targetVocab === 2;
120
120
  const target = (() => {
121
121
  switch (row.target_kind) {
122
122
  case "workflow":
123
- // PRESERVED for both vintages (incl. the null-ref fallback).
124
123
  return { kind: "workflow", ref: row.target_ref ?? "" };
125
124
  case "command":
126
- // NEW vocabulary: a prepared command (agent/LLM) result.
127
- // LEGACY vocabulary: the native shell/script arm's shared string.
128
- return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
125
+ return { kind: "command", engine: meta.engine ?? null };
129
126
  case "shell":
130
- // Only the NEW vocabulary ever writes this string; an unmarked
131
- // "shell" row is unreachable from any production writer.
132
- return marked ? { kind: "shell" } : { kind: "unknown" };
127
+ return { kind: "shell" };
133
128
  case "script":
134
- // Only the NEW vocabulary ever writes this string; an unmarked
135
- // "script" row is unreachable from any production writer.
136
- return marked ? { kind: "script" } : { kind: "unknown" };
137
- case "prompt":
138
- // Only LEGACY rows (pre-P1b) ever wrote this string.
139
- return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
129
+ return { kind: "script" };
140
130
  default:
141
131
  return { kind: "unknown" };
142
132
  }
@@ -189,7 +189,14 @@ export function parseScheduledBindingArgv(argv) {
189
189
  ...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
190
190
  };
191
191
  }
192
- function parsePublicSchedulerInvocation(invocation) {
192
+ /**
193
+ * Parse just the public `task run …` / `workflow run …` tail, with no
194
+ * `--scheduler-context` wrapper. Exported so a backend can recognize a
195
+ * pre-`--scheduler-context` invocation still sitting inside akm's own
196
+ * ownership-marked block (see `extractCronInvocation` in
197
+ * `src/tasks/backends/cron.ts`) without re-implementing this grammar.
198
+ */
199
+ export function parsePublicSchedulerInvocation(invocation) {
193
200
  if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
194
201
  try {
195
202
  if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
6
6
  import path from "node:path";
7
7
  import { loadAdapterExecutionSource } from "../commands/command/execution-source-loader.js";
8
8
  import { makeBundleRef } from "../core/asset/asset-ref.js";
9
+ import { compareCodePoints, toPosix } from "../core/common.js";
9
10
  import { UsageError } from "../core/errors.js";
10
11
  import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
11
12
  import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
@@ -742,12 +743,6 @@ function freezeOperation(operation) {
742
743
  function sameInvocation(left, right) {
743
744
  return left.length === right.length && left.every((value, index) => value === right[index]);
744
745
  }
745
- function compareCodePoints(left, right) {
746
- return left < right ? -1 : left > right ? 1 : 0;
747
- }
748
- function toPosix(value) {
749
- return value.replaceAll("\\", "/");
750
- }
751
746
  // Retain the concrete error in this module's public dependency graph so callers
752
747
  // can continue to identify ownership failures without importing an adapter.
753
748
  export { WorkflowSourceRejectionError };
@@ -47,6 +47,7 @@ import fs from "node:fs";
47
47
  import path from "node:path";
48
48
  import { types as utilTypes } from "node:util";
49
49
  import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
50
+ import { wellFormedUnicode } from "../../core/common.js";
50
51
  import { UsageError } from "../../core/errors.js";
51
52
  import { DURATION_UNITS, parseDuration } from "../../core/time.js";
52
53
  import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
@@ -67,20 +68,6 @@ export function own(value, key) {
67
68
  export function utf8Bytes(value) {
68
69
  return new TextEncoder().encode(value).byteLength;
69
70
  }
70
- export function wellFormedUnicode(value) {
71
- for (let index = 0; index < value.length; index += 1) {
72
- const code = value.charCodeAt(index);
73
- if (code >= 0xd800 && code <= 0xdbff) {
74
- const next = value.charCodeAt(index + 1);
75
- if (!(next >= 0xdc00 && next <= 0xdfff))
76
- return false;
77
- index += 1;
78
- }
79
- else if (code >= 0xdc00 && code <= 0xdfff)
80
- return false;
81
- }
82
- return true;
83
- }
84
71
  /** The one per-field error funnel both grammars render through, distinguished only by `ctx.sourceLabel` (D2-N4). */
85
72
  export function sourceError(ctx, fieldPath, detail) {
86
73
  const dotted = fieldPath.length === 0
@@ -17,7 +17,7 @@
17
17
  * | root `version` | outcome |
18
18
  * |------------------------|-----------------------------------------------------------------|
19
19
  * | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
20
- * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) — the shim removes friction for the deterministic case, it never hides a real problem |
20
+ * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
21
21
  * | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
22
22
  * | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
23
23
  *
@@ -57,6 +57,18 @@ export function peekTaskSourceVersion(root) {
57
57
  return typeof value === "number" ? value : undefined;
58
58
  }
59
59
  const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
60
+ /**
61
+ * Thrown only when the deterministic conversion itself could not produce a
62
+ * task source v4 document — a case where a person must decide the intended
63
+ * behavior (e.g. an ambiguous shell command), not one the migrator can just
64
+ * be re-run to fix. `reason`/`detail` are the SAME blocked outcome
65
+ * `akm migrate status`/`apply` reports for this file, so the message names
66
+ * the actual decision instead of pointing at a command that will report the
67
+ * identical block.
68
+ */
69
+ function unmigratableVersionError(filePath, version, reason, detail) {
70
+ return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
71
+ }
60
72
  function unsupportedVersionError(filePath, version) {
61
73
  return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
62
74
  }
@@ -64,9 +76,10 @@ function unsupportedVersionError(filePath, version) {
64
76
  * Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
65
77
  * chained v2->v3->v4) migration planner(s) — never touches disk, never
66
78
  * writes the file, never re-reads it from disk. Returns the produced v4
67
- * YAML text, or `undefined` when the deterministic conversion cannot
68
- * proceed (an unmigratable v2/v3 shape) — the caller falls back to the same
69
- * hard error this gate threw before the shim existed.
79
+ * YAML text, or the blocked reason/detail when the deterministic conversion
80
+ * cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
81
+ * the same hard error this gate threw before the shim existed, now naming
82
+ * that reason.
70
83
  */
71
84
  function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
72
85
  const bytes = Buffer.from(yaml, "utf8");
@@ -89,12 +102,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
89
102
  else {
90
103
  const v3Outcome = planTaskToV3File(baseInput);
91
104
  if (v3Outcome.status !== "changed")
92
- return undefined;
105
+ return { reason: v3Outcome.reason, detail: v3Outcome.detail };
93
106
  v3Bytes = v3Outcome.after;
94
107
  }
95
108
  const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
96
109
  if (v4Outcome.status !== "changed")
97
- return undefined;
110
+ return { reason: v4Outcome.reason, detail: v4Outcome.detail };
98
111
  return v4Outcome.after.toString("utf8");
99
112
  }
100
113
  /** Parse task source YAML, routing per the terminal table above. */
@@ -103,16 +116,17 @@ export function parseTaskSource(input) {
103
116
  const version = peekTaskSourceVersion(root);
104
117
  if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
105
118
  if (version === 2 || version === 3) {
106
- const v4Yaml = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
107
- if (v4Yaml !== undefined) {
119
+ const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
120
+ if (typeof shimmed === "string") {
108
121
  const v4 = parseTaskSourceV4({
109
- yaml: v4Yaml,
122
+ yaml: shimmed,
110
123
  filePath: input.filePath,
111
124
  ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
112
125
  });
113
126
  warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
114
127
  return Object.freeze({ version: 4, v4 });
115
128
  }
129
+ throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
116
130
  }
117
131
  throw unsupportedVersionError(input.filePath, version);
118
132
  }
@@ -284,7 +284,7 @@ async function driveChildRun(input, childRow) {
284
284
  * before step 6 (publication) produces `child_workflow_publish_failed`.
285
285
  */
286
286
  export async function driveChildWorkflowUnit(input) {
287
- const { request, target, ctx } = input;
287
+ const { request, ctx } = input;
288
288
  const precheck = precheckAndDeriveInvocationKey(input);
289
289
  if (!precheck.ok) {
290
290
  return precheck.outcome;
@@ -708,7 +708,7 @@ async function prepareAttemptWorktree(input) {
708
708
  }
709
709
  return { ok: true, request: { ...input.request, cwd: created.path }, worktreePath: created.path };
710
710
  }
711
- async function reserveJournaledDispatch(input, request, worktreePath, startedAt) {
711
+ async function reserveJournaledDispatch(input, worktreePath, startedAt) {
712
712
  const { plan, workUnit, ctx, attemptId, inputHash } = input;
713
713
  let durableAttempt;
714
714
  await enqueueUnitWrite(async () => {
@@ -799,7 +799,7 @@ async function dispatchJournaledAttempt(input) {
799
799
  const startedAt = new Date().toISOString();
800
800
  let durableAttempt;
801
801
  try {
802
- durableAttempt = await reserveJournaledDispatch(input, request, worktreePath, startedAt);
802
+ durableAttempt = await reserveJournaledDispatch(input, worktreePath, startedAt);
803
803
  }
804
804
  catch (err) {
805
805
  // A failed dispatch-row insert means NOTHING dispatched (the row is the
@@ -20,7 +20,7 @@ import { UsageError } from "../../core/errors.js";
20
20
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
21
21
  import { canonicalInputJson, validateInputs } from "../../execution/input-contract.js";
22
22
  import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
23
- import { canonicalJson as canonicalJsonString } from "../ir/plan-hash.js";
23
+ import { canonicalJson } from "../ir/plan-hash.js";
24
24
  import { parseReference, resolveReferenceString, } from "../program/expressions.js";
25
25
  import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
26
26
  import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
@@ -463,7 +463,7 @@ function buildExecContextEnv(args) {
463
463
  function computeUnitInputHash(ctx, item) {
464
464
  return createHash("sha256")
465
465
  .update("akm.workflow.unit\0v7\0")
466
- .update(canonicalJsonString({
466
+ .update(canonicalJson({
467
467
  hashVersion: 7,
468
468
  role: "unit",
469
469
  stepId: ctx.plan.stepId,
@@ -855,20 +855,8 @@ export function unitOutcomeFromRow(unitId, row, hasSchema) {
855
855
  ...(row.tokens !== null ? { tokens: row.tokens } : {}),
856
856
  };
857
857
  }
858
- /** Stable stringify (sorted object keys, recursively) so equal values vote together. */
859
- export function canonicalJson(value) {
860
- return JSON.stringify(sortKeys(value));
861
- }
862
- function sortKeys(value) {
863
- if (Array.isArray(value))
864
- return value.map(sortKeys);
865
- if (value && typeof value === "object") {
866
- return Object.fromEntries(Object.entries(value)
867
- .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
868
- .map(([k, v]) => [k, sortKeys(v)]));
869
- }
870
- return value;
871
- }
858
+ /** Re-exported so existing importers (`tests/workflows/fuzz/*`) keep resolving; canonical impl lives in `../ir/plan-hash`. */
859
+ export { canonicalJson };
872
860
  // ── Gate-feedback recovery (PURE) ────────────────────────────────────────────
873
861
  //
874
862
  // A gate rejection is journaled as `<stepId>.gate:l<loop>` with result_json
@@ -1431,7 +1419,7 @@ export async function finalizeExecutedStep(input) {
1431
1419
  // fields are unchanged).
1432
1420
  inputHash: createHash("sha256")
1433
1421
  .update("akm.workflow.gate\0v7\0")
1434
- .update(canonicalJsonString({
1422
+ .update(canonicalJson({
1435
1423
  hashVersion: 7,
1436
1424
  dispatch: gateTarget,
1437
1425
  invocation: null,