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
@@ -21,6 +21,7 @@
21
21
  * - z.record(...) → JSON-parse value, validate
22
22
  */
23
23
  import { z } from "zod";
24
+ import { isRecord } from "../common.js";
24
25
  import { UsageError } from "../errors.js";
25
26
  import { AkmConfigBaseSchema, EngineConfigSchema, listTopLevelConfigKeys } from "./config-schema.js";
26
27
  import { deepMergeConfig } from "./deep-merge.js";
@@ -110,7 +111,7 @@ function resolveSchemaAt(path, config, raw) {
110
111
  // Cannot descend into a non-object schema.
111
112
  return undefined;
112
113
  }
113
- existing = isPlainObject(existing) ? existing[segment] : undefined;
114
+ existing = isRecord(existing) ? existing[segment] : undefined;
114
115
  }
115
116
  // Preserve leaf refinements/transforms for validation. Traversal unwraps at
116
117
  // the start of each loop iteration, while coercion unwraps independently.
@@ -130,7 +131,7 @@ function selectUnionObjectOption(candidates, segment, existing, raw) {
130
131
  if (requested)
131
132
  return requested;
132
133
  }
133
- if (isPlainObject(existing)) {
134
+ if (isRecord(existing)) {
134
135
  const selected = options.find((option) => Object.entries(option.shape).some(([key, field]) => key in existing && unwrap(field) instanceof z.ZodLiteral && field.safeParse(existing[key]).success));
135
136
  if (selected)
136
137
  return segment in selected.shape ? selected : undefined;
@@ -197,9 +198,7 @@ export function configSet(config, dotted, raw) {
197
198
  // Merge a JSON object patch with the existing value before that current
198
199
  // schema transform,
199
200
  // otherwise the synthesized default can overwrite an explicit false.
200
- const candidate = judgmentObjectPath && isPlainObject(value)
201
- ? deepMergeConfig(isPlainObject(existing) ? existing : {}, value)
202
- : value;
201
+ const candidate = judgmentObjectPath && isRecord(value) ? deepMergeConfig(isRecord(existing) ? existing : {}, value) : value;
203
202
  // Validate the coerced value against the leaf schema. This catches enum
204
203
  // mismatches, out-of-range numbers, schema-level shape errors (writable
205
204
  // npm/website sources via .superRefine, strict-mode unknown keys in nested
@@ -220,7 +219,7 @@ export function configSet(config, dotted, raw) {
220
219
  .join("\n");
221
220
  throw new UsageError(`Invalid value for ${dotted}:\n${lines}`, "INVALID_FLAG_VALUE");
222
221
  }
223
- const next = setPath(config, path, isPlainObject(existing) && isPlainObject(parsed.data) ? deepMergeConfig(existing, parsed.data) : parsed.data);
222
+ const next = setPath(config, path, isRecord(existing) && isRecord(parsed.data) ? deepMergeConfig(existing, parsed.data) : parsed.data);
224
223
  // Targeted invariant: defaultWriteTarget must point at a configured bundle
225
224
  // (#464.a). Whole-config validation happens at save time; this check fires
226
225
  // at set time so the user sees the typo immediately.
@@ -277,7 +276,7 @@ function rejectApiKeyPath(path, dotted) {
277
276
  function parseObjectPatch(raw, key) {
278
277
  try {
279
278
  const value = JSON.parse(raw);
280
- if (!isPlainObject(value))
279
+ if (!isRecord(value))
281
280
  throw new Error("expected an object");
282
281
  return value;
283
282
  }
@@ -285,9 +284,6 @@ function parseObjectPatch(raw, key) {
285
284
  throw new UsageError(`Invalid JSON object for ${key}: ${err instanceof Error ? err.message : String(err)}`, "INVALID_JSON_CONFIG_VALUE");
286
285
  }
287
286
  }
288
- function isPlainObject(value) {
289
- return value !== null && typeof value === "object" && !Array.isArray(value);
290
- }
291
287
  function recipeForApiKey(path, _dotted) {
292
288
  if (path[0] === "embedding")
293
289
  return "AKM_EMBED_API_KEY";
@@ -146,13 +146,16 @@ export function acquireConfigReadFence() {
146
146
  */
147
147
  export function parseAndValidateConfigText(text, sourcePath) {
148
148
  const parsedRaw = upgradeConfigVersion(parseConfigText(text, sourcePath), sourcePath);
149
- // #852 (following #815): lift legacy `extraParams` keys — e.g.
150
- // `reasoning_effort`, a documented 0.9.1 workaround — onto the first-class
151
- // engine field they now shadow, before the protected-key check in
152
- // `ExtraParamsSchema` gets a chance to hard-reject them. In-memory only;
153
- // never written back to the file.
154
- const { config: raw, lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
149
+ // #852 (following #815): a config still using legacy `extraParams` keys —
150
+ // e.g. `reasoning_effort`, a documented 0.9.1 workaround — needs to be
151
+ // rewritten onto the first-class engine field they now shadow. This used
152
+ // to happen silently, in memory, on every load; that ran forever and never
153
+ // converged. The lift itself is now `akm migrate apply`'s job (see
154
+ // src/commands/migrate/config-extra-params.ts) and persists to disk, so a
155
+ // config that has not been migrated yet fails closed here instead of
156
+ // silently drifting from what's on disk.
155
157
  const where = sourcePath ? ` at ${sourcePath}` : "";
158
+ const { lifted, conflicts } = liftLegacyEngineExtraParams(parsedRaw);
156
159
  if (conflicts.length > 0) {
157
160
  const lines = conflicts
158
161
  .map((c) => ` - engines.${c.engine}.extraParams.${c.key} (${JSON.stringify(c.extraParamsValue)}) conflicts with engines.${c.engine}.${c.field} (${JSON.stringify(c.fieldValue)})`)
@@ -160,9 +163,9 @@ export function parseAndValidateConfigText(text, sourcePath) {
160
163
  throw new ConfigError(`Invalid config${where}: extraParams and the first-class field disagree:\n${lines}\n\nEach extraParams key above has a first-class equivalent and akm will not guess which value you meant — remove the extraParams entry once the field carries the value you want.`, "INVALID_CONFIG_FILE");
161
164
  }
162
165
  if (lifted.length > 0) {
163
- warn(`Config${where} uses deprecated extraParams keys with first-class equivalents; treating them as the first-class fields for this run (not written back to the file):\n - ${lifted.join("\n - ")}`);
166
+ throw new ConfigError(`Config${where} uses deprecated extraParams keys with first-class equivalents:\n - ${lifted.join("\n - ")}\n\nRun \`akm migrate apply\` to rewrite the config file, or move the values onto the first-class fields yourself.`, "INVALID_CONFIG_FILE");
164
167
  }
165
- const parsed = AkmConfigSchema.safeParse(raw);
168
+ const parsed = AkmConfigSchema.safeParse(parsedRaw);
166
169
  if (!parsed.success) {
167
170
  const lines = parsed.error.issues.map((i) => ` - ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n");
168
171
  throw new ConfigError(`Invalid config${where}:\n${lines}`, "INVALID_CONFIG_FILE");
@@ -6,9 +6,7 @@ export const BUILTIN_IMPROVE_STRATEGY_NAMES = [
6
6
  "default",
7
7
  "quick",
8
8
  "thorough",
9
- "memory-focus",
10
9
  "graph-refresh",
11
- "frequent",
12
10
  "consolidate",
13
11
  "catchup",
14
12
  "reflect-distill",
@@ -1,6 +1,7 @@
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
+ import { isRecord } from "./common.js";
4
5
  export const EXTRA_PARAMS_PROTECTED_TOP_LEVEL_KEYS = [
5
6
  "model",
6
7
  "messages",
@@ -116,31 +117,34 @@ export function formatExtraParamsIssue(label, issue) {
116
117
  const suffix = issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("");
117
118
  return `${label}${suffix} ${issue.message}`;
118
119
  }
119
- function isPlainObject(value) {
120
- return typeof value === "object" && value !== null && !Array.isArray(value);
121
- }
122
120
  /**
123
- * Lift legacy `extraParams` keys onto their first-class engine field before
124
- * schema validation runs, so a 0.9.1-shaped config using (e.g.)
125
- * `extraParams.reasoning_effort` keeps loading now that `reasoningEffort` is
126
- * a first-class — and therefore protected — field (#852, following #815).
121
+ * Compute the legacy `extraParams` -> first-class-field lift for a raw
122
+ * parsed config object: a 0.9.1-shaped config using (e.g.)
123
+ * `extraParams.reasoning_effort` now needs `reasoningEffort` set instead,
124
+ * since that field became first-class — and therefore protected — in 0.9.2
125
+ * (#852, following #815).
127
126
  *
128
- * In-memory only: this never rewrites the config file. Callers should warn
129
- * using the returned `lifted` descriptions so the user knows to update the
130
- * file by hand, and reject using `conflicts` rather than silently preferring
131
- * either value.
127
+ * Pure: never touches the filesystem. Two callers use this differently:
128
+ * `akm migrate apply` (src/commands/migrate/config-extra-params.ts) uses the
129
+ * returned `config` to persist the rewrite to disk, once; `parseAndValidateConfigText`
130
+ * (src/core/config/config.ts) calls this only to detect whether a lift is
131
+ * needed and discards `config` — an unmigrated config fails closed there
132
+ * with a pointer to `akm migrate apply` rather than silently drifting from
133
+ * what's on disk. `conflicts` (an extraParams key and its first-class field
134
+ * set to different values) is a genuine authoring error in both callers and
135
+ * is never auto-resolved.
132
136
  */
133
137
  export function liftLegacyEngineExtraParams(raw) {
134
138
  const lifted = [];
135
139
  const conflicts = [];
136
140
  const rawEngines = raw.engines;
137
- if (!isPlainObject(rawEngines)) {
141
+ if (!isRecord(rawEngines)) {
138
142
  return { config: raw, lifted, conflicts };
139
143
  }
140
144
  const engines = {};
141
145
  let anyEngineChanged = false;
142
146
  for (const [name, engineValue] of Object.entries(rawEngines)) {
143
- if (!isPlainObject(engineValue) || !isPlainObject(engineValue.extraParams)) {
147
+ if (!isRecord(engineValue) || !isRecord(engineValue.extraParams)) {
144
148
  engines[name] = engineValue;
145
149
  continue;
146
150
  }
@@ -2,6 +2,7 @@
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
4
  import { cloneExecutionJsonObject } from "../execution/json.js";
5
+ import { isRecord } from "./common.js";
5
6
  const COMMON_FIELDS = [
6
7
  "schemaVersion",
7
8
  "ok",
@@ -50,9 +51,6 @@ const V2_FIELDS = new Set([...COMMON_FIELDS, "strategy", "strategyFilteredRefs"]
50
51
  function fail(message) {
51
52
  throw new Error(`invalid improve-result envelope: ${message}`);
52
53
  }
53
- function isRecord(value) {
54
- return typeof value === "object" && value !== null && !Array.isArray(value);
55
- }
56
54
  function requireExactFields(value, allowed) {
57
55
  const unknown = Object.keys(value).filter((key) => !allowed.has(key));
58
56
  if (unknown.length > 0)
@@ -53,6 +53,7 @@
53
53
  * exactly that for `output:` and `params` schemas). It deliberately does NOT
54
54
  * change {@link validateJsonSchemaSubset}'s permissive evaluation semantics.
55
55
  */
56
+ import { isRecord } from "./common.js";
56
57
  /**
57
58
  * Deepest schema nesting either walk descends into — {@link
58
59
  * validateJsonSchemaSubset} evaluating a value, and {@link
@@ -168,9 +169,6 @@ function pointerFor(path) {
168
169
  function pushIssue(issues, path, keyword, kind, message) {
169
170
  issues.push({ path: [...path], pointer: pointerFor(path), keyword, kind, message });
170
171
  }
171
- function isPlainObject(value) {
172
- return typeof value === "object" && value !== null && !Array.isArray(value);
173
- }
174
172
  /** Values the runtime's reference-equality `enum` check can enforce correctly. */
175
173
  function isSupportedEnumValue(value) {
176
174
  return (value === null ||
@@ -228,7 +226,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
228
226
  continue;
229
227
  }
230
228
  branches.forEach((branch, index) => {
231
- if (isPlainObject(branch)) {
229
+ if (isRecord(branch)) {
232
230
  checkDefinitionNode(branch, [...path, keyword, index], issues, depth + 1);
233
231
  }
234
232
  else {
@@ -237,7 +235,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
237
235
  });
238
236
  }
239
237
  if (schema.not !== undefined) {
240
- if (isPlainObject(schema.not)) {
238
+ if (isRecord(schema.not)) {
241
239
  checkDefinitionNode(schema.not, [...path, "not"], issues, depth + 1);
242
240
  }
243
241
  else {
@@ -250,12 +248,12 @@ function checkDefinitionNode(schema, path, issues, depth) {
250
248
  }
251
249
  }
252
250
  if (schema.properties !== undefined) {
253
- if (!isPlainObject(schema.properties)) {
251
+ if (!isRecord(schema.properties)) {
254
252
  pushIssue(issues, [...path, "properties"], "properties", "malformed", `"properties" must be an object mapping property names to schemas`);
255
253
  }
256
254
  else {
257
255
  for (const [key, propSchema] of Object.entries(schema.properties)) {
258
- if (isPlainObject(propSchema)) {
256
+ if (isRecord(propSchema)) {
259
257
  checkDefinitionNode(propSchema, [...path, "properties", key], issues, depth + 1);
260
258
  }
261
259
  else {
@@ -265,7 +263,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
265
263
  }
266
264
  }
267
265
  if (schema.items !== undefined) {
268
- if (isPlainObject(schema.items)) {
266
+ if (isRecord(schema.items)) {
269
267
  checkDefinitionNode(schema.items, [...path, "items"], issues, depth + 1);
270
268
  }
271
269
  else if (Array.isArray(schema.items)) {
@@ -276,7 +274,7 @@ function checkDefinitionNode(schema, path, issues, depth) {
276
274
  }
277
275
  }
278
276
  if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== "boolean") {
279
- if (isPlainObject(schema.additionalProperties)) {
277
+ if (isRecord(schema.additionalProperties)) {
280
278
  pushIssue(issues, [...path, "additionalProperties"], "additionalProperties", "unsupported", `schema-form "additionalProperties" is not enforced by the workflow schema subset — only "additionalProperties: false" is`);
281
279
  }
282
280
  else {
@@ -329,7 +327,7 @@ function combinatorBranches(schema, keyword) {
329
327
  const raw = schema[keyword];
330
328
  if (!Array.isArray(raw))
331
329
  return [];
332
- return raw.filter(isPlainObject);
330
+ return raw.filter(isRecord);
333
331
  }
334
332
  /** First error of each failing branch, truncated — enough to act on without dumping every branch. */
335
333
  function summarizeBranchFailures(failures) {
@@ -364,7 +362,7 @@ function validateCombinators(value, schema, path, ctx) {
364
362
  }
365
363
  }
366
364
  const not = schema.not;
367
- if (isPlainObject(not) && branchErrors(value, not, path, ctx).length === 0) {
365
+ if (isRecord(not) && branchErrors(value, not, path, ctx).length === 0) {
368
366
  ctx.errors.push(`${path}: value must not match the "not" schema`);
369
367
  }
370
368
  }
@@ -35,6 +35,7 @@ export const STATE_MIGRATION_SAFETY_BY_ID = Object.freeze({
35
35
  "022-workflow-unit-attempts": "additive",
36
36
  "023-child-workflow-runs": "additive",
37
37
  "024-workflow-run-outputs": "additive",
38
+ "025-task-history-vocabulary-backfill": "data-preserving-rebuild",
38
39
  });
39
40
  export const STATE_MIGRATIONS = [
40
41
  // ── Migration 001 — initial schema ──────────────────────────────────────────
@@ -1079,6 +1080,53 @@ export const STATE_MIGRATIONS = [
1079
1080
  ALTER TABLE workflow_runs ADD COLUMN outputs_json TEXT;
1080
1081
  `,
1081
1082
  },
1083
+ // ── Migration 025 — task_history D8 result-vocabulary backfill ─────────────
1084
+ //
1085
+ // The D8 re-code (docs/architecture/decisions/0005-task-result-vocabulary-
1086
+ // and-legacy-read-mapping.md) renamed the written `target_kind` vocabulary
1087
+ // and marked every new row with `targetVocab: 2` in `metadata_json`, but
1088
+ // left every row written before it in the OLD vocabulary forever, pushing
1089
+ // the "which generation is this row" decision onto three separate read
1090
+ // sites (src/tasks/run/task-history.ts, src/commands/health/
1091
+ // improve-metrics.ts, src/commands/health/windows.ts). That is exactly the
1092
+ // shape `docs/plans/specs/p4-deletions-closeout.md` row B-51 ratified
1093
+ // keeping "forever" — SUPERSEDED here: `task_history` is DB-owned data, the
1094
+ // remap is deterministic and total, so it belongs in a one-time migration,
1095
+ // not three permanently-recurring read-side shims.
1096
+ //
1097
+ // The remap, applied only to rows with no `targetVocab` marker yet (a
1098
+ // NULL/absent `$.targetVocab` in the JSON column — already-marked or
1099
+ // malformed metadata_json is left untouched; malformed rows are a
1100
+ // pre-existing, unrelated skip-and-warn concern at read time):
1101
+ // - target_kind 'prompt' -> 'command' (the prepared command/agent-LLM
1102
+ // arm; D8 renamed its written label, this backfills the old one)
1103
+ // - target_kind 'command' -> 'shell' (the legacy native shell/script
1104
+ // arm's shared old label; D8 split it into 'shell'/'script', and only
1105
+ // 'shell' is reconstructable — the old vocabulary never distinguished
1106
+ // script from shell, matching the read mapping this migration retires)
1107
+ // Only 'prompt' and legacy 'command' rows actually change meaning between
1108
+ // vocabularies (see the read mapping this migration retires); every other
1109
+ // target_kind ('workflow', NULL, or anything else) already reads
1110
+ // identically in both, so it is left alone — including its metadata_json,
1111
+ // since no reader consults `targetVocab` once the shim is deleted.
1112
+ {
1113
+ id: "025-task-history-vocabulary-backfill",
1114
+ up: `
1115
+ UPDATE task_history
1116
+ SET target_kind = 'command',
1117
+ metadata_json = json_set(metadata_json, '$.targetVocab', 2)
1118
+ WHERE target_kind = 'prompt'
1119
+ AND json_valid(metadata_json)
1120
+ AND json_extract(metadata_json, '$.targetVocab') IS NULL;
1121
+
1122
+ UPDATE task_history
1123
+ SET target_kind = 'shell',
1124
+ metadata_json = json_set(metadata_json, '$.targetVocab', 2)
1125
+ WHERE target_kind = 'command'
1126
+ AND json_valid(metadata_json)
1127
+ AND json_extract(metadata_json, '$.targetVocab') IS NULL;
1128
+ `,
1129
+ },
1082
1130
  ];
1083
1131
  assertMigrationRegistry(STATE_MIGRATIONS);
1084
1132
  function assertStateMigrationSafetyRegistry() {
@@ -1117,7 +1165,8 @@ export function runMigrations(db, options) {
1117
1165
  }
1118
1166
  if (!options.allowHistoricalDestructiveStateUpgrade) {
1119
1167
  throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
1120
- "Run `akm upgrade --force` to snapshot it before migration 001.");
1168
+ "Run `akm upgrade --force` to snapshot it before migration 001, " +
1169
+ "or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
1121
1170
  }
1122
1171
  const ledger = assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
1123
1172
  if (ledger.migrationIds.length !== 0) {
@@ -1147,7 +1196,8 @@ export function runMigrations(db, options) {
1147
1196
  assertMigrationLedger(lockedDb, STATE_MIGRATIONS);
1148
1197
  if (!options?.allowHistoricalDestructiveStateUpgrade) {
1149
1198
  throw new Error(`Refusing to apply historical destructive state migration ${migration.id} during an ordinary managed open. ` +
1150
- "Run `akm upgrade --force` to create a sibling state.db safety copy and apply it deliberately.");
1199
+ "Run `akm upgrade --force` to create a sibling state.db safety copy and apply it deliberately, " +
1200
+ "or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
1151
1201
  }
1152
1202
  if (!options.beforeHistoricalDestructiveMigration) {
1153
1203
  throw new Error(`Historical destructive state migration ${migration.id} requires a verified safety-copy hook.`);
@@ -407,7 +407,8 @@ export function openStateDatabase(dbPath, options) {
407
407
  existingUnversionedDatabase = ledger.migrationIds.length === 0;
408
408
  if (existingUnversionedDatabase && !options?.allowHistoricalDestructiveStateUpgrade) {
409
409
  throw new Error("Refusing to migrate an existing unversioned state.db during an ordinary managed open. " +
410
- "Run `akm upgrade --force` to create a verified snapshot before migration 001.");
410
+ "Run `akm upgrade --force` to create a verified snapshot before migration 001, " +
411
+ "or `akm upgrade --state-only` where akm cannot reinstall itself (container/global install).");
411
412
  }
412
413
  }
413
414
  finally {
@@ -4,6 +4,7 @@
4
4
  import { createHash } from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import { isRecord } from "../core/common.js";
7
8
  import { UsageError } from "../core/errors.js";
8
9
  export function freezeExecutableIdentity(requested, options = {}) {
9
10
  const absolutePath = resolveExecutable(requested, options.cwd ?? process.cwd(), options.path ?? process.env.PATH ?? "");
@@ -96,9 +97,6 @@ function resolveExecutable(requested, cwd, pathValue) {
96
97
  }
97
98
  throw new UsageError(`Executable ${JSON.stringify(requested)} was not found.`, "INVALID_FLAG_VALUE");
98
99
  }
99
- function isRecord(value) {
100
- return typeof value === "object" && value !== null && !Array.isArray(value);
101
- }
102
100
  function invalid(label) {
103
101
  return new UsageError(`Frozen ${label} identity is invalid.`, "INVALID_FLAG_VALUE");
104
102
  }
@@ -5,17 +5,12 @@ import { createHash } from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
7
  import { parseFrontmatter } from "../core/asset/frontmatter.js";
8
+ import { compareCodePoints, toPosix } from "../core/common.js";
8
9
  import { UsageError } from "../core/errors.js";
9
10
  import { createExecutionSourceIdentity } from "./source.js";
10
11
  function errorMessage(cause) {
11
12
  return cause instanceof Error ? cause.message : String(cause);
12
13
  }
13
- function toPosix(value) {
14
- return value.replaceAll("\\", "/");
15
- }
16
- function compareCodePoints(left, right) {
17
- return left < right ? -1 : left > right ? 1 : 0;
18
- }
19
14
  function physicalIdentity(realPath, stat) {
20
15
  return stat.ino === 0n ? `path:${realPath}` : `inode:${stat.dev}:${stat.ino}`;
21
16
  }
@@ -29,6 +29,7 @@
29
29
  */
30
30
  import fs from "node:fs";
31
31
  import { loadConfig } from "../core/config/config.js";
32
+ import { rethrowIfTestIsolationError } from "../core/errors.js";
32
33
  import { getDbPath } from "../core/paths.js";
33
34
  import { warn } from "../core/warn.js";
34
35
  import { closeDatabase, openReadonlyExistingDatabase } from "../storage/repositories/index-connection.js";
@@ -50,7 +51,11 @@ function indexBundlePrefixes(dbPath) {
50
51
  .prepare("SELECT DISTINCT bundle_id AS b FROM entries WHERE bundle_id IS NOT NULL AND bundle_id != ''")
51
52
  .all().map((row) => row.b);
52
53
  }
53
- catch {
54
+ catch (error) {
55
+ rethrowIfTestIsolationError(error);
56
+ // Best-effort startup diagnostic: a locked/corrupted index just means this
57
+ // pass skips the drift check (retried on the next command) rather than
58
+ // blocking every command on a heuristic warning.
54
59
  return undefined;
55
60
  }
56
61
  finally {