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
@@ -71,7 +71,22 @@ import { isProcessAlive, isWithinAsync, safeRealpathAsync } from "../../core/com
71
71
  import { serializeByKey } from "../../core/concurrent.js";
72
72
  import { runManagedSubprocess } from "../../core/subprocess.js";
73
73
  import { warn } from "../../core/warn.js";
74
- const GIT_TIMEOUT_MS = 30_000;
74
+ /**
75
+ * Timeout for every `git worktree add|prune|remove|status` call this module
76
+ * makes. Was 30s; raised to 120s after #891 confirmed a real product gap:
77
+ * these calls run under {@link withRepoWorktreeLock}, a per-process,
78
+ * per-repository queue, so a machine also busy with OTHER git-heavy work
79
+ * (other akm runs, other agents' worktrees, a loaded CI runner) can genuinely
80
+ * push one `git worktree remove` past 30s without anything being stuck —
81
+ * plain CPU/fork/IO contention. At 30s that showed up as `{ removed: false,
82
+ * error: "... timed out after 30000ms" }` on a perfectly healthy op: a clean
83
+ * worktree left retained, or a create failing a unit outright, purely because
84
+ * the box was busy. 120s matches the same trade already made for
85
+ * {@link GIT_PUSH_TIMEOUT_MS} (`core/write-source.ts`,
86
+ * `sources/providers/git-stash.ts`) for the same class of administrative git
87
+ * call under load; a call that is truly hung (not just slow) is still caught.
88
+ */
89
+ const GIT_TIMEOUT_MS = 120_000;
75
90
  /** Directory under `os.tmpdir()` that owns every run's worktree roots. */
76
91
  export const WORKTREES_DIR_NAME = "akm-worktrees";
77
92
  /**
@@ -89,12 +104,10 @@ function gitExitError(args, code, stderr, stdout) {
89
104
  return `git ${args.join(" ")} exited ${code}${detail ? `: ${detail}` : ""}`;
90
105
  }
91
106
  /**
92
- * Run one git command asynchronously; `ok` = exit 0. Never throws (spawn
93
- * errors and the 30 s timeout → ok: false). Async so a git lock wait parks a
94
- * promise instead of blocking the event loop; repo-mutating callers must hold
95
- * {@link withRepoWorktreeLock}.
107
+ * Run one git command asynchronously via the real `git` binary; `ok` = exit
108
+ * 0. Never throws (spawn errors and the timeout → ok: false).
96
109
  */
97
- async function git(cwd, args) {
110
+ async function realGitExecutor(cwd, args) {
98
111
  const result = await runManagedSubprocess(["git", "-C", cwd, ...args], {
99
112
  capture: true,
100
113
  timeoutMs: GIT_TIMEOUT_MS,
@@ -114,6 +127,27 @@ async function git(cwd, args) {
114
127
  }
115
128
  return { ok: true, stdout: result.stdout };
116
129
  }
130
+ let gitExecutor = realGitExecutor;
131
+ /**
132
+ * TEST-ONLY seam (#891): swap the executor every repo-mutating git call in
133
+ * this module goes through. Lets a test prove those calls genuinely
134
+ * interleave with other event-loop work — the property the old "count
135
+ * setInterval ticks during real git calls" test asserted before it was
136
+ * deleted for timing on the real scheduler instead of on behavior — using a
137
+ * fake, deterministically-timed async git in place of the real subprocess.
138
+ * Call with `undefined` to restore the real spawn-based executor.
139
+ */
140
+ export function setGitExecutorForTesting(executor) {
141
+ gitExecutor = executor ?? realGitExecutor;
142
+ }
143
+ /**
144
+ * Run one git command asynchronously; `ok` = exit 0. Never throws. Async so a
145
+ * git lock wait parks a promise instead of blocking the event loop;
146
+ * repo-mutating callers must hold {@link withRepoWorktreeLock}.
147
+ */
148
+ async function git(cwd, args) {
149
+ return gitExecutor(cwd, args);
150
+ }
117
151
  /**
118
152
  * Synchronous git for the two read-only probes that run before any unit is in
119
153
  * flight ({@link isGitAvailable}, {@link assertGitWorkTree}). They take no
@@ -20,15 +20,13 @@
20
20
  * full design history, including why re-binding cannot reuse the
21
21
  * shape-driven normalizer (a code-review finding).
22
22
  */
23
+ import { isRecord } from "../../core/common.js";
23
24
  import { UsageError } from "../../core/errors.js";
24
25
  import { validateInputs } from "../../execution/input-contract.js";
25
26
  import { parseReference } from "../program/expressions.js";
26
27
  function inputBindingInvalid(message) {
27
28
  return new UsageError(message, "INPUT_BINDING_INVALID");
28
29
  }
29
- function isPlainObject(value) {
30
- return typeof value === "object" && value !== null && !Array.isArray(value);
31
- }
32
30
  function unknownBindingNameError(stepId, targetRef, name, declaredNames) {
33
31
  return inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name}, which is not a declared input. ` +
34
32
  `Declared inputs: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}.`);
@@ -152,7 +150,7 @@ function finalizeBindings(stepId, targetRef, contract, byName) {
152
150
  * reinterpreted as a literal.
153
151
  */
154
152
  function normalizeOneEntry(stepId, targetRef, name, value, schema, earlierStepIds, declaredParamNames) {
155
- if (!isPlainObject(value) || !Object.hasOwn(value, "from")) {
153
+ if (!isRecord(value) || !Object.hasOwn(value, "from")) {
156
154
  return Object.freeze({ kind: "literal", name, value });
157
155
  }
158
156
  const keys = Object.keys(value);
@@ -25,6 +25,7 @@
25
25
  * Returns accumulated `WorkflowError`s rather than throwing. Pure and
26
26
  * deterministic: the same document always compiles to the same plan.
27
27
  */
28
+ import { compareCodePoints } from "../../core/common.js";
28
29
  import { formatReference, parseReference } from "../program/expressions.js";
29
30
  import { projectExecCore } from "../program/schema.js";
30
31
  import { sourceStepInstructions, sourceStepProgramUnit, sourceStepRef } from "../source-ir/program.js";
@@ -114,9 +115,6 @@ function sortedOutputs(outputs) {
114
115
  }
115
116
  return sorted;
116
117
  }
117
- function compareCodePoints(left, right) {
118
- return left < right ? -1 : left > right ? 1 : 0;
119
- }
120
118
  function compileStep(step, sequenceIndex, defaults, resolved) {
121
119
  const gate = {
122
120
  kind: "gate",
@@ -269,14 +267,12 @@ function checkOutputReference(name, text, check) {
269
267
  * (human + JSON output, via `core/adapter/adapters/akm-lint.ts#
270
268
  * workflowCompileWarnings`) and as `warn()` lines at `workflow run`.
271
269
  *
272
- * A. A unit/map step with NO step-level `output:` schema carries its units'
273
- * raw results as an untyped artifact — permitted, but worth flagging.
274
- * B. A `params.<name>` reference (in `map.over`/`route.input`) to an
270
+ * A. A `params.<name>` reference (in `map.over`/`route.input`) to an
275
271
  * UNDECLARED param, but ONLY when the document declares a `params:`
276
272
  * block — a likely typo. Prose can no longer carry param references at
277
273
  * all (it is never scanned), so this warning's surface shrinks to the
278
274
  * two whole-value fields that can legally contain one.
279
- * C. `gate.max_loops` above 1 on an `exec` step. The engine judges such a
275
+ * B. `gate.max_loops` above 1 on an `exec` step. The engine judges such a
280
276
  * step but never loops it (`exec/step-work.ts#effectiveGateMaxLoops`):
281
277
  * a frozen argv cannot read the judge's feedback, so a second loop would
282
278
  * only re-run the identical command — and its side effects. The declared
@@ -299,13 +295,6 @@ export function collectWorkflowWarnings(input) {
299
295
  `can still fail the step.`,
300
296
  });
301
297
  }
302
- if ((step.map || step.route === undefined) && step.output === undefined) {
303
- warnings.push({
304
- line: step.source.start,
305
- message: `Step "${step.id}" declares no \`output:\` schema — its unit results are carried as an untyped ` +
306
- `artifact (permitted). Add an \`output:\` JSON Schema to type and validate the step artifact.`,
307
- });
308
- }
309
298
  if (declaredParams) {
310
299
  const declaredList = [...declaredParams].join(", ");
311
300
  const scan = (text, label) => {
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { createHash } from "node:crypto";
12
12
  import path from "node:path";
13
+ import { compareCodePoints } from "../../core/common.js";
13
14
  import { UsageError } from "../../core/errors.js";
14
15
  import { decodeFrozenExecutableIdentity } from "../../execution/executable-identity.js";
15
16
  import { INPUT_NAME_PATTERN } from "../../execution/input-contract.js";
@@ -38,7 +39,7 @@ export const WORKFLOW_IR_V5_VERSION = 5;
38
39
  * freeze-time `COMPOSITION_INVALID` gates live in
39
40
  * `src/workflows/freeze/targets/child-workflow.ts` (Lane B).
40
41
  */
41
- export function decodeWorkflowPlanV4(input, hooks = {}, depth = 0, budget = { embeddedBytes: 0 }) {
42
+ export function decodeWorkflowPlanV4(input, depth = 0, budget = { embeddedBytes: 0 }) {
42
43
  const raw = record(input, "plan");
43
44
  if (raw.irVersion !== WORKFLOW_IR_V5_VERSION)
44
45
  fail("irVersion must be 5");
@@ -51,7 +52,7 @@ export function decodeWorkflowPlanV4(input, hooks = {}, depth = 0, budget = { em
51
52
  planExtraKeys: ["sourceReadSet", "outputs"],
52
53
  unitExtraKeys: ["frozenTarget", "environment"],
53
54
  gateExtraKeys: ["frozenJudge"],
54
- }, hooks);
55
+ });
55
56
  const rawSteps = raw.steps;
56
57
  const stepIds = new Set(rawSteps.map((rawStep) => rawStep.stepId));
57
58
  const requiredSources = [];
@@ -232,7 +233,7 @@ function decodeChildWorkflowTarget(target, unit, depth, budget) {
232
233
  fail(`unit ${unit.id} child workflow contentHash does not match its frozen dispatch`);
233
234
  }
234
235
  const childDepth = depth + 1;
235
- const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, {}, childDepth, budget);
236
+ const frozenPlan = decodeWorkflowPlanV4(target.frozenPlan, childDepth, budget);
236
237
  const embeddedPlanJson = canonicalJsonLocal(frozenPlan);
237
238
  const actualPlanHash = sha256(embeddedPlanJson);
238
239
  if (actualPlanHash !== planHash) {
@@ -711,9 +712,6 @@ function digest(value, label) {
711
712
  function sha256(value) {
712
713
  return createHash("sha256").update(value).digest("hex");
713
714
  }
714
- function compareCodePoints(left, right) {
715
- return left < right ? -1 : left > right ? 1 : 0;
716
- }
717
715
  function canonicalJsonLocal(value) {
718
716
  const sort = (entry) => {
719
717
  if (Array.isArray(entry))
@@ -1,7 +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 { isContainedRelativePath } from "../../core/common.js";
4
+ import { isContainedRelativePath, isRecord } from "../../core/common.js";
5
5
  import { UsageError } from "../../core/errors.js";
6
6
  import { parseReference } from "../program/expressions.js";
7
7
  import { PROGRAM_PARAM_NAME_PATTERN, PROGRAM_RETRY_REASONS, PROGRAM_STEP_ID_PATTERN } from "../program/schema.js";
@@ -13,7 +13,7 @@ export { WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_TIMEOUT_MS };
13
13
  const MAX_LIST_ITEMS = 1024;
14
14
  const MAX_STRING_LENGTH = 1_000_000;
15
15
  /** Validate the current durable execution graph. */
16
- export function validateWorkflowPlanStructure(input, options, hooks = {}) {
16
+ export function validateWorkflowPlanStructure(input, options) {
17
17
  if (!isRecord(input) || input.irVersion !== options.expectedVersion) {
18
18
  fail(`irVersion must be ${options.expectedVersion}`);
19
19
  }
@@ -351,9 +351,6 @@ function assertString(value, label) {
351
351
  if (typeof value !== "string" || value.length === 0 || value.length > MAX_STRING_LENGTH)
352
352
  fail(`${label} must be a non-empty bounded string`);
353
353
  }
354
- function isRecord(value) {
355
- return typeof value === "object" && value !== null && !Array.isArray(value);
356
- }
357
354
  function fail(message) {
358
355
  throw new UsageError(`Invalid frozen workflow plan: ${message}.`);
359
356
  }
@@ -33,7 +33,7 @@
33
33
  import { LineCounter, parseDocument } from "yaml";
34
34
  import { parseFrontmatterBlock } from "../core/asset/frontmatter.js";
35
35
  import { parseMarkdownToc } from "../core/asset/markdown.js";
36
- import { isContainedRelativePath } from "../core/common.js";
36
+ import { isContainedRelativePath, isRecord } from "../core/common.js";
37
37
  import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
38
38
  import { checkJsonSchemaDefinition, JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS } from "../core/json-schema.js";
39
39
  import { parseReference } from "./program/expressions.js";
@@ -165,7 +165,7 @@ export function parseWorkflow(markdown, source) {
165
165
  };
166
166
  if (root === null || root === undefined)
167
167
  root = {};
168
- if (!isPlainRecord(root)) {
168
+ if (!isRecord(root)) {
169
169
  return {
170
170
  ok: false,
171
171
  errors: [{ line: 2, message: `Workflow frontmatter must be a YAML mapping (key: value pairs).` }],
@@ -355,7 +355,7 @@ function checkEnvelopeFields(ctx, root, fmEndLine) {
355
355
  checkActorStamp(ctx, root.verified, ["verified"], `"verified"`);
356
356
  }
357
357
  }
358
- if (root.provenance !== undefined && !isPlainRecord(root.provenance)) {
358
+ if (root.provenance !== undefined && !isRecord(root.provenance)) {
359
359
  ctx.err(["provenance"], `Workflow frontmatter "provenance" must be a mapping.`);
360
360
  }
361
361
  if (root.status !== undefined && (typeof root.status !== "string" || !LIFECYCLE_STATUSES.has(root.status))) {
@@ -368,7 +368,7 @@ function checkEnvelopeFields(ctx, root, fmEndLine) {
368
368
  function checkActorStamp(ctx, value, path, label) {
369
369
  if (value === undefined)
370
370
  return;
371
- if (!isPlainRecord(value)) {
371
+ if (!isRecord(value)) {
372
372
  ctx.err(path, `Workflow frontmatter ${label} must be a mapping with a non-empty "by".`);
373
373
  return;
374
374
  }
@@ -402,7 +402,7 @@ function checkXrefs(ctx, value, fmEndLine) {
402
402
  function parseParams(ctx, raw) {
403
403
  if (raw === undefined)
404
404
  return undefined;
405
- if (!isPlainRecord(raw)) {
405
+ if (!isRecord(raw)) {
406
406
  ctx.err(["params"], `"params" must be a mapping of param name to a JSON Schema object (e.g. changed_files: { type: array }).`);
407
407
  return undefined;
408
408
  }
@@ -412,7 +412,7 @@ function parseParams(ctx, raw) {
412
412
  ctx.err(["params", paramName], `Param name "${paramName}" is invalid. Use letters, digits, and underscores, starting with a letter or underscore, so "params.${paramName}" can address it.`);
413
413
  continue;
414
414
  }
415
- if (!isPlainRecord(value)) {
415
+ if (!isRecord(value)) {
416
416
  ctx.err(["params", paramName], `Param "${paramName}" must be a JSON Schema object (e.g. { type: string }).`);
417
417
  continue;
418
418
  }
@@ -441,7 +441,7 @@ function parseParams(ctx, raw) {
441
441
  function parseOutputs(ctx, raw) {
442
442
  if (raw === undefined)
443
443
  return undefined;
444
- if (!isPlainRecord(raw)) {
444
+ if (!isRecord(raw)) {
445
445
  ctx.err(["outputs"], `"outputs" must be a mapping of output name to { from, schema? } (e.g. report: { from: steps.summarize.output }).`);
446
446
  return undefined;
447
447
  }
@@ -453,7 +453,7 @@ function parseOutputs(ctx, raw) {
453
453
  `underscore, so "steps.<child>.output.${outputName}" can address it.`);
454
454
  continue;
455
455
  }
456
- if (!isPlainRecord(value)) {
456
+ if (!isRecord(value)) {
457
457
  ctx.err(path, `Output "${outputName}" must be a mapping with "from" (and optional "schema").`);
458
458
  continue;
459
459
  }
@@ -474,7 +474,7 @@ function parseOutputs(ctx, raw) {
474
474
  }
475
475
  const entry = { from: value.from };
476
476
  if (value.schema !== undefined) {
477
- if (!isPlainRecord(value.schema)) {
477
+ if (!isRecord(value.schema)) {
478
478
  ctx.err([...path, "schema"], `Output "${outputName}" "schema" must be a JSON Schema object.`);
479
479
  }
480
480
  else {
@@ -493,7 +493,7 @@ function parseDefaults(ctx, raw) {
493
493
  if (raw === undefined)
494
494
  return undefined;
495
495
  const path = ["defaults"];
496
- if (!isPlainRecord(raw)) {
496
+ if (!isRecord(raw)) {
497
497
  ctx.err(path, `"defaults" must be a mapping with any of: ${DEFAULTS_KEYS.join(", ")}.`);
498
498
  return undefined;
499
499
  }
@@ -525,7 +525,7 @@ function parseBudget(ctx, raw) {
525
525
  if (raw === undefined)
526
526
  return undefined;
527
527
  const path = ["budget"];
528
- if (!isPlainRecord(raw)) {
528
+ if (!isRecord(raw)) {
529
529
  ctx.err(path, `"budget" must be a mapping with any of: ${BUDGET_KEYS.join(", ")}.`);
530
530
  return undefined;
531
531
  }
@@ -558,7 +558,7 @@ function parseSteps(ctx, raw) {
558
558
  // (including ones that fail their own validation).
559
559
  const idIndex = new Map();
560
560
  raw.forEach((rawStep, index) => {
561
- if (isPlainRecord(rawStep) && typeof rawStep.id === "string" && !idIndex.has(rawStep.id)) {
561
+ if (isRecord(rawStep) && typeof rawStep.id === "string" && !idIndex.has(rawStep.id)) {
562
562
  idIndex.set(rawStep.id, index);
563
563
  }
564
564
  });
@@ -567,7 +567,7 @@ function parseSteps(ctx, raw) {
567
567
  const routeChecks = [];
568
568
  raw.forEach((rawStep, index) => {
569
569
  const path = ["steps", index];
570
- if (!isPlainRecord(rawStep)) {
570
+ if (!isRecord(rawStep)) {
571
571
  ctx.err(path, `Step ${index + 1} must be a mapping with an "id".`);
572
572
  return;
573
573
  }
@@ -655,7 +655,7 @@ function parseSteps(ctx, raw) {
655
655
  // Step blocks
656
656
  // ---------------------------------------------------------------------------
657
657
  function parseUnit(ctx, raw, path, stepLabel) {
658
- if (!isPlainRecord(raw)) {
658
+ if (!isRecord(raw)) {
659
659
  ctx.err(path, `${stepLabel} "unit" must be a mapping (a dispatch-override bag).`);
660
660
  return undefined;
661
661
  }
@@ -733,7 +733,7 @@ function parseUnit(ctx, raw, path, stepLabel) {
733
733
  * keeps that decision visible in the frontmatter diff.
734
734
  */
735
735
  function parseExec(ctx, raw, path, stepLabel) {
736
- if (!isPlainRecord(raw)) {
736
+ if (!isRecord(raw)) {
737
737
  ctx.err(path, `${stepLabel} "exec" must be a mapping with a "command" argv list.`);
738
738
  return undefined;
739
739
  }
@@ -832,7 +832,7 @@ function parseExecCwd(ctx, raw, path, stepLabel) {
832
832
  return value;
833
833
  }
834
834
  function parseMap(ctx, raw, path, stepLabel) {
835
- if (!isPlainRecord(raw)) {
835
+ if (!isRecord(raw)) {
836
836
  ctx.err(path, `${stepLabel} "map" must be a mapping with an "over" key.`);
837
837
  return undefined;
838
838
  }
@@ -866,7 +866,7 @@ function parseMap(ctx, raw, path, stepLabel) {
866
866
  return map;
867
867
  }
868
868
  function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
869
- if (!isPlainRecord(raw)) {
869
+ if (!isRecord(raw)) {
870
870
  ctx.err(path, `${stepLabel} "route" must be a mapping with "input" and "when" keys.`);
871
871
  return undefined;
872
872
  }
@@ -888,7 +888,7 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
888
888
  const seenMatches = new Map();
889
889
  raw.when.forEach((branch, i) => {
890
890
  const branchPath = [...whenPath, i];
891
- if (!isPlainRecord(branch)) {
891
+ if (!isRecord(branch)) {
892
892
  ctx.err(branchPath, `${stepLabel} "when[${i}]" must be a mapping: { match, step }.`);
893
893
  return;
894
894
  }
@@ -967,7 +967,7 @@ function parseInputs(ctx, raw, path, stepLabel) {
967
967
  return out.length > 0 ? out : undefined;
968
968
  }
969
969
  function parseGate(ctx, raw, path, stepLabel) {
970
- if (!isPlainRecord(raw)) {
970
+ if (!isRecord(raw)) {
971
971
  ctx.err(path, `${stepLabel} "gate" must be a mapping with any of: ${GATE_KEYS.join(", ")}.`);
972
972
  return undefined;
973
973
  }
@@ -1009,7 +1009,7 @@ function parseEngineName(ctx, raw, path, label) {
1009
1009
  function parseRetry(ctx, raw, path, stepLabel) {
1010
1010
  if (raw === undefined)
1011
1011
  return undefined;
1012
- if (!isPlainRecord(raw)) {
1012
+ if (!isRecord(raw)) {
1013
1013
  ctx.err(path, `${stepLabel} "retry" must be a mapping: { max: <n>, on: [<failure_reason>, …] }.`);
1014
1014
  return undefined;
1015
1015
  }
@@ -1092,7 +1092,7 @@ function parseEnumField(ctx, raw, path, label, allowed) {
1092
1092
  function parseLlmOverrides(ctx, raw, path, label) {
1093
1093
  if (raw === undefined)
1094
1094
  return undefined;
1095
- if (!isPlainRecord(raw)) {
1095
+ if (!isRecord(raw)) {
1096
1096
  ctx.err(path, `${label} must be a mapping of LLM invocation overrides.`);
1097
1097
  return undefined;
1098
1098
  }
@@ -1127,7 +1127,7 @@ function parseLlmOverrides(ctx, raw, path, label) {
1127
1127
  ctx.err([...path, "supports_json_schema"], `${label}.supports_json_schema must be a boolean.`);
1128
1128
  }
1129
1129
  if (raw.extra_params !== undefined) {
1130
- if (!isPlainRecord(raw.extra_params)) {
1130
+ if (!isRecord(raw.extra_params)) {
1131
1131
  ctx.err([...path, "extra_params"], `${label}.extra_params must be a JSON object.`);
1132
1132
  }
1133
1133
  else {
@@ -1168,7 +1168,7 @@ function parseLlmOverrides(ctx, raw, path, label) {
1168
1168
  function parseSchemaObject(ctx, raw, path, label) {
1169
1169
  if (raw === undefined)
1170
1170
  return undefined;
1171
- if (!isPlainRecord(raw)) {
1171
+ if (!isRecord(raw)) {
1172
1172
  ctx.err(path, `${label} must be a JSON Schema object (e.g. { type: object, properties: { … } }).`);
1173
1173
  return undefined;
1174
1174
  }
@@ -1212,9 +1212,6 @@ function checkUnknownKeys(ctx, obj, path, allowed, label) {
1212
1212
  // ---------------------------------------------------------------------------
1213
1213
  // Utilities
1214
1214
  // ---------------------------------------------------------------------------
1215
- function isPlainRecord(value) {
1216
- return typeof value === "object" && value !== null && !Array.isArray(value);
1217
- }
1218
1215
  function describeError(cause) {
1219
1216
  return cause instanceof Error ? cause.message : String(cause);
1220
1217
  }
@@ -12,20 +12,21 @@
12
12
  */
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
+ import { compareCodePoints, toPosix } from "../core/common.js";
15
16
  import { UsageError } from "../core/errors.js";
16
17
  import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
17
18
  export class WorkflowSourceRejectionError extends UsageError {
18
19
  sourcePaths;
19
20
  constructor(message, code, sourcePaths) {
20
21
  super(message, code);
21
- this.sourcePaths = [...sourcePaths].sort(comparePaths);
22
+ this.sourcePaths = [...sourcePaths].sort(compareCodePoints);
22
23
  Object.setPrototypeOf(this, new.target.prototype);
23
24
  }
24
25
  }
25
26
  export class WorkflowSourceCollisionError extends WorkflowSourceRejectionError {
26
27
  canonicalName;
27
28
  constructor(canonicalName, sourcePaths) {
28
- const sorted = [...sourcePaths].sort(comparePaths);
29
+ const sorted = [...sourcePaths].sort(compareCodePoints);
29
30
  super(`Workflow "${canonicalName}" resolves to multiple workflow source files: ${sorted.join(", ")}. ` +
30
31
  "A canonical workflow ref must be owned by exactly one recognized .md or .yml source; remove or rename the duplicate.", "RESOURCE_ALREADY_EXISTS", sorted);
31
32
  this.name = "WorkflowSourceCollisionError";
@@ -36,8 +37,8 @@ export class WorkflowSourceCollisionError extends WorkflowSourceRejectionError {
36
37
  export class WorkflowSourceDomainError extends WorkflowSourceRejectionError {
37
38
  canonicalName;
38
39
  constructor(canonicalName, sourcePaths, issues, collidingSourcePaths) {
39
- const sortedPaths = [...sourcePaths].sort(comparePaths);
40
- const sortedCollisions = [...collidingSourcePaths].sort(comparePaths);
40
+ const sortedPaths = [...sourcePaths].sort(compareCodePoints);
41
+ const sortedCollisions = [...collidingSourcePaths].sort(compareCodePoints);
41
42
  const code = issues.some((issue) => issue.code === "PATH_ESCAPE_VIOLATION")
42
43
  ? "PATH_ESCAPE_VIOLATION"
43
44
  : "WORKFLOW_SOURCE_INVALID";
@@ -169,7 +170,7 @@ export function listWorkflowSourceFiles(sourceRoot, adapterId, name) {
169
170
  extensionlessStem: entry.name.slice(0, -extension.length),
170
171
  });
171
172
  }
172
- candidates.sort((left, right) => comparePaths(left.relativePath, right.relativePath));
173
+ candidates.sort((left, right) => compareCodePoints(left.relativePath, right.relativePath));
173
174
  const { sources, issues } = inspectWorkflowSourceDomain(candidates, canonicalName, realRoot);
174
175
  if (issues.length > 0) {
175
176
  throw workflowSourceDomainError(adapterId, canonicalName, candidates, sources, issues);
@@ -229,9 +230,9 @@ export function resolveWorkflowSourceDomains(sourceRoot, adapterId, sourcePaths)
229
230
  candidatesByName.set(canonicalName, domain);
230
231
  }
231
232
  const resolutions = [];
232
- for (const canonicalName of [...candidatesByName.keys()].sort(comparePaths)) {
233
+ for (const canonicalName of [...candidatesByName.keys()].sort(compareCodePoints)) {
233
234
  const candidates = candidatesByName.get(canonicalName) ?? [];
234
- candidates.sort((left, right) => comparePaths(left.relativePath, right.relativePath));
235
+ candidates.sort((left, right) => compareCodePoints(left.relativePath, right.relativePath));
235
236
  const { sources, issues } = inspectWorkflowSourceDomain(candidates, canonicalName, realRoot);
236
237
  const sourcePaths = candidates.map((candidate) => candidate.relativePath);
237
238
  if (issues.length > 0) {
@@ -365,9 +366,3 @@ function isWithinResolved(candidate, root) {
365
366
  const relative = path.relative(root, path.resolve(candidate));
366
367
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
367
368
  }
368
- function toPosix(value) {
369
- return value.replaceAll("\\", "/");
370
- }
371
- function comparePaths(left, right) {
372
- return left < right ? -1 : left > right ? 1 : 0;
373
- }
@@ -10,12 +10,12 @@
10
10
  */
11
11
  import { types as utilTypes } from "node:util";
12
12
  import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
13
+ import { compareCodePoints, wellFormedUnicode } from "../../core/common.js";
13
14
  import { validateExtraParams } from "../../core/extra-params.js";
14
15
  import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
15
16
  import { parseReference } from "../program/expressions.js";
16
17
  import { PROGRAM_RETRY_REASONS } from "../program/schema.js";
17
18
  import { jsonBytes, WORKFLOW_ENGINE_NAME_PATTERN, WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_CONCURRENCY, WORKFLOW_MAX_ENGINE_NAME_LENGTH, WORKFLOW_MAX_EXTRA_PARAMS_BYTES, WORKFLOW_MAX_SCHEMA_BYTES, WORKFLOW_MAX_TIMEOUT_MS, } from "../resource-limits.js";
18
- import { compareWorkflowSourceCodePoints } from "./compare.js";
19
19
  import { canonicalizeWorkflowCron, canonicalizeWorkflowRun, canonicalizeWorkflowWorkingDirectory, classifyWorkflowStepUses, rejectNulInArgv, validateWorkflowBuiltinCommand, WorkflowSourceSemanticError, } from "./semantics.js";
20
20
  export const WORKFLOW_SOURCE_IR_VERSION = 1;
21
21
  export const WORKFLOW_SOURCE_IR_MAX_BYTES = 2 * 1024 * 1024;
@@ -269,9 +269,6 @@ function validateStep(value, jobId, index, stepIds, options) {
269
269
  extensions(step.extensions, `step ${id} extensions`);
270
270
  span(step.source, `step ${id} source`);
271
271
  }
272
- function compareCodePoints(left, right) {
273
- return compareWorkflowSourceCodePoints(left, right);
274
- }
275
272
  function validateExec(value, location, options) {
276
273
  if (value === undefined)
277
274
  return;
@@ -692,21 +689,6 @@ function countJsonBytes(state, token) {
692
689
  if (state.bytes > WORKFLOW_SOURCE_IR_MAX_BYTES)
693
690
  fail("source IR exceeds the byte limit");
694
691
  }
695
- function wellFormedUnicode(value) {
696
- for (let index = 0; index < value.length; index++) {
697
- const code = value.charCodeAt(index);
698
- if (code >= 0xd800 && code <= 0xdbff) {
699
- const next = value.charCodeAt(index + 1);
700
- if (!(next >= 0xdc00 && next <= 0xdfff))
701
- return false;
702
- index++;
703
- }
704
- else if (code >= 0xdc00 && code <= 0xdfff) {
705
- return false;
706
- }
707
- }
708
- return true;
709
- }
710
692
  function record(value, location) {
711
693
  if (value === null || typeof value !== "object" || Array.isArray(value))
712
694
  fail(`${location} must be an object`);
@@ -24,7 +24,17 @@ before that migration and tells you to run:
24
24
  akm upgrade --force
25
25
  ```
26
26
 
27
- Only a successful executable upgrade admits migration 018. Immediately before
27
+ Where akm cannot reinstall itself a container that ships the CLI globally,
28
+ an unprivileged runtime user — that command fails at its install step
29
+ (`EACCES` on the global module directory) before it ever reaches the
30
+ migration. Use the install-free route instead:
31
+
32
+ ```sh
33
+ akm upgrade --state-only
34
+ ```
35
+
36
+ Both admit migration 018 and both take the same verified safety copy described
37
+ below; `--state-only` simply skips the executable replacement. Immediately before
28
38
  the migration, AKM takes a SQLite writer-exclusion lock, rechecks the exact
29
39
  ledger, and writes a consistent SQLite snapshot beside the database as
30
40
  `state.db.pre-018-drop-dead-lane-schema.<UTC-digits>.<UUID>.bak`. The randomized
@@ -41,7 +51,7 @@ general storage migrator.
41
51
 
42
52
  An existing database with no applied migration IDs is never treated as a fresh
43
53
  install, whether its `schema_migrations` table is absent or empty. Ordinary
44
- commands reject it without writing. The explicit `akm upgrade --force` path
54
+ commands reject it without writing. The explicit `akm upgrade --force` (or `--state-only`) path
45
55
  takes and verifies a descriptor-bound snapshot named
46
56
  `state.db.pre-001-initial-schema.<UTC-digits>.<UUID>.bak` before it creates the
47
57
  ledger or applies any migration from 001 through 022. A truly new database is
@@ -1022,12 +1022,14 @@ computed, with a 256 MiB binary limit. Release/checksum metadata is capped at
1022
1022
  akm upgrade # Download and replace the running binary
1023
1023
  akm upgrade --check # Check for updates without installing
1024
1024
  akm upgrade --force # Force upgrade even if already on latest
1025
+ akm upgrade --state-only # Apply pending state.db migrations; install nothing
1025
1026
  ```
1026
1027
 
1027
1028
  | Flag | Description |
1028
1029
  | --- | --- |
1029
1030
  | `--check` | Check for updates without installing |
1030
1031
  | `--force` | Force upgrade even if on latest version |
1032
+ | `--state-only` | Apply pending `state.db` migrations without installing a new akm. For installs that cannot rewrite their own binary — a container shipping akm globally, an unprivileged runtime user — where the install step would fail `EACCES` before the migration could run. Takes the same verified safety copy as `--force`; it changes who may request the migration, not what it does. |
1031
1033
  | `--skip-post-upgrade` | Skip the post-upgrade index rebuild |
1032
1034
 
1033
1035
  Checksum verification is not optional and has no flag. If a release's
@@ -2032,7 +2034,9 @@ when one exists.
2032
2034
  Scan bundle markdown files for structural issues: unquoted colons, missing
2033
2035
  `updated` field, orphaned stubs, placeholder stubs, missing `name`/`type`,
2034
2036
  stale paths, and broken refs — in body text and in
2035
- `refs`/`xrefs`/`supersededBy`/`contradictedBy` frontmatter. Also reports
2037
+ `refs`/`xrefs`/`supersededBy`/`contradictedBy` frontmatter. A belief edge
2038
+ pointing at a memory that `akm improve` pruned resolves through the archive
2039
+ tombstone under `.akm/memory-cleanup/archive/` and is not reported (#884). Also reports
2036
2040
  `dangerous-env-key` findings for env files (the same key set `akm bundle add`
2037
2041
  enforces — see [Dangerous env key audit](#dangerous-env-key-audit) — but
2038
2042
  non-blocking here; `lint` only warns). `--type workflows` structurally parses
@@ -2046,6 +2050,7 @@ akm lint --fix # Auto-fix Tier-1 issues in place
2046
2050
  akm lint --type workflows # Only lint one asset type
2047
2051
  akm lint --dir ~/other-bundle # Override the bundle root (default: from config)
2048
2052
  akm lint --fail-on-flagged # CI-friendly: exit non-zero when summary.flagged > 0
2053
+ akm lint --prune-dangling-edges # Opt-in: drop belief edges whose target is gone
2049
2054
  ```
2050
2055
 
2051
2056
  | Flag | Description |
@@ -2054,6 +2059,7 @@ akm lint --fail-on-flagged # CI-friendly: exit non-zero when summary.flagge
2054
2059
  | `--dir` | Override the bundle root directory (default: from config) |
2055
2060
  | `--type` | Only lint assets of this type (e.g. `workflows`, `tasks`, `memories`). **akm bundles only** — every other adapter validates the whole bundle and warns on stderr that the flag had no effect. |
2056
2061
  | `--fail-on-flagged` | Exit non-zero when `summary.flagged > 0`. Default: exit 0 regardless of findings. |
2062
+ | `--prune-dangling-edges` | Opt-in repair (#884): drop `supersededBy`/`contradictedBy` entries whose target has neither a file nor a prune tombstone. **Not** implied by `--fix` — it edits well-formed files to delete a belief-graph claim, so review a plain `akm lint` report first. Clears the same `writable: false` gate `--fix` does. |
2057
2063
 
2058
2064
  Returns `fixed[]` and `flagged[]` arrays plus a `summary: { fixed, flagged }`
2059
2065
  count. Each entry carries `file`, `issue`, `detail`, and whether it was
@@ -572,6 +572,30 @@ that declares an `output` schema is unaffected — an empty response is not
572
572
  valid JSON, so it fails as a parse error and can never satisfy a schema as a
573
573
  silent `null`.
574
574
 
575
+ ### When a step declares no `output:` schema
576
+
577
+ A step-level `output:` schema is **optional**, and omitting it is a supported
578
+ mode, not a deficiency. The step artifact is then exactly the value described
579
+ above — the unit's result for a solo step, the collected array for a `map`
580
+ step — carried untyped and never validated.
581
+
582
+ The shape is stable either way: it does not depend on whether a schema is
583
+ present, and it does not vary between a live run and a resume. `steps.<id>.output`
584
+ resolves the same in both cases, so downstream references work with or without
585
+ a schema.
586
+
587
+ What a schema adds is **enforcement**, not shape. With one, the promoted
588
+ artifact is checked against it and the step fails on mismatch; without one, a
589
+ downstream reference to a field the artifact happens not to carry surfaces at
590
+ resolution time instead (`steps.X.output is not an object — cannot resolve
591
+ property "y"`). Declare `output:` when you want that mismatch caught at the
592
+ producing step rather than at the consuming one.
593
+
594
+ akm does not warn about a missing step `output:` schema. It briefly did, and
595
+ the advisory was removed: it fired on essentially every step, could not be
596
+ enforced (advisories never reach `--fail-on-flagged`), and flagged a state
597
+ that is legitimate.
598
+
575
599
  ## Workflow outputs
576
600
 
577
601
  A workflow can declare a run-level export: the values a **completed run**