akm-cli 0.10.261012-beta → 0.10.261013-beta

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 (146) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/SECURITY.md +2 -2
  3. package/STABILITY.md +2 -2
  4. package/dist/assets/hints/cli-hints-full.md +6 -7
  5. package/dist/assets/hints/cli-hints-short.md +3 -4
  6. package/dist/commands/command/execution-source-loader.js +10 -14
  7. package/dist/commands/env/env-binding.js +47 -58
  8. package/dist/commands/env/env-cli.js +20 -22
  9. package/dist/commands/env/env.js +10 -58
  10. package/dist/commands/env/secret-cli.js +9 -27
  11. package/dist/commands/env/secret.js +3 -10
  12. package/dist/commands/health/scheduler-binary.js +1 -1
  13. package/dist/commands/improve/distill.js +0 -2
  14. package/dist/commands/improve/eligibility.js +0 -2
  15. package/dist/commands/lint/env-key-rules.js +7 -6
  16. package/dist/commands/lint/index.js +1 -1
  17. package/dist/commands/proposal/validators/proposal-validators.js +1 -4
  18. package/dist/commands/read/show.js +4 -25
  19. package/dist/commands/sources/add-cli.js +8 -63
  20. package/dist/commands/sources/bundle-cli.js +8 -9
  21. package/dist/commands/sources/installed-stashes.js +117 -61
  22. package/dist/commands/sources/schema-repair.js +0 -2
  23. package/dist/commands/sources/source-clone.js +31 -7
  24. package/dist/commands/tasks/explain.js +1 -1
  25. package/dist/commands/tasks/tasks.js +38 -56
  26. package/dist/commands/ui/redact.js +4 -4
  27. package/dist/commands/ui/workspace-actions.js +1 -1
  28. package/dist/commands/workflow/plan.js +47 -8
  29. package/dist/commands/workflow-cli.js +37 -41
  30. package/dist/core/adapter/adapters/akm-adapter.js +3 -3
  31. package/dist/core/adapter/adapters/akm-lint.js +3 -4
  32. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
  34. package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
  35. package/dist/core/asset/resolve-ref.js +5 -0
  36. package/dist/core/bundle-rename.js +12 -4
  37. package/dist/core/common.js +4 -52
  38. package/dist/core/config/config-walker.js +1 -1
  39. package/dist/core/config/config.js +55 -18
  40. package/dist/core/config/schema/primitives.js +12 -6
  41. package/dist/core/config/schema/sources-bundles.js +2 -2
  42. package/dist/core/env-secret-ref.js +84 -73
  43. package/dist/core/errors.js +1 -1
  44. package/dist/core/paths.js +1 -6
  45. package/dist/core/redaction.js +16 -3
  46. package/dist/core/ref-token.js +82 -0
  47. package/dist/core/secret-value.js +14 -0
  48. package/dist/core/write-source.js +20 -4
  49. package/dist/indexer/indexer.js +27 -36
  50. package/dist/indexer/installations.js +1 -8
  51. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  52. package/dist/indexer/materialize-embeddings.js +1 -1
  53. package/dist/indexer/scan/drain-dir.js +3 -3
  54. package/dist/indexer/search/search-source.js +4 -2
  55. package/dist/indexer/walk/path-resolver.js +13 -68
  56. package/dist/indexer/walk/walker.js +18 -22
  57. package/dist/integrations/agent/engine-resolution.js +9 -7
  58. package/dist/integrations/agent/runner-dispatch.js +4 -5
  59. package/dist/integrations/agent/runner.js +2 -0
  60. package/dist/llm/client.js +1 -1
  61. package/dist/llm/embedders/remote.js +4 -3
  62. package/dist/output/renderers.js +3 -4
  63. package/dist/output/shapes/helpers.js +0 -2
  64. package/dist/output/text/command-format.js +12 -2
  65. package/dist/output/text/show-directives.js +16 -28
  66. package/dist/output/text/workflow-format.js +37 -7
  67. package/dist/scripts/akm-migrate-node.js +45865 -44692
  68. package/dist/scripts/akm-migrate.js +45804 -44630
  69. package/dist/setup/steps/tasks.js +1 -2
  70. package/dist/sources/providers/provider-utils.js +12 -0
  71. package/dist/sources/resolve.js +12 -16
  72. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  73. package/dist/sources/snapshot-fetchers/x.js +1 -1
  74. package/dist/storage/repositories/proposals-repository.js +2 -2
  75. package/dist/storage/repositories/task-history-repository.js +2 -2
  76. package/dist/storage/repositories/workflow-runs-repository.js +38 -116
  77. package/dist/tasks/embedded.js +1 -1
  78. package/dist/tasks/prepare/prepare-support.js +3 -2
  79. package/dist/tasks/prepare/prepare.js +3 -4
  80. package/dist/tasks/prepare/script-capture.js +8 -22
  81. package/dist/tasks/run/load-task.js +46 -14
  82. package/dist/tasks/run/run-native-task.js +0 -8
  83. package/dist/tasks/run/run-workflow-task.js +2 -2
  84. package/dist/tasks/scheduler-sync.js +0 -1
  85. package/dist/tasks/source/bounded-document.js +0 -41
  86. package/dist/tasks/source/parse-task-source.js +0 -1
  87. package/dist/tasks/source/task-source-v4.js +1 -4
  88. package/dist/workflows/compile.js +1 -15
  89. package/dist/workflows/exec/child-workflow.js +1 -2
  90. package/dist/workflows/exec/dispatch-redaction.js +24 -6
  91. package/dist/workflows/exec/environment.js +11 -74
  92. package/dist/workflows/exec/exec-unit.js +31 -23
  93. package/dist/workflows/exec/frozen-judge.js +17 -3
  94. package/dist/workflows/exec/native-executor.js +70 -26
  95. package/dist/workflows/exec/run-workflow.js +31 -18
  96. package/dist/workflows/exec/step-work.js +4 -3
  97. package/dist/workflows/exec/unit-dispatch.js +1 -8
  98. package/dist/workflows/freeze/environment.js +14 -119
  99. package/dist/workflows/freeze/freeze.js +0 -1
  100. package/dist/workflows/freeze/step-values.js +1 -2
  101. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  102. package/dist/workflows/freeze/targets/command.js +6 -9
  103. package/dist/workflows/freeze/targets/shell.js +0 -3
  104. package/dist/workflows/parser.js +6 -25
  105. package/dist/workflows/plan.js +6 -2
  106. package/dist/workflows/renderer.js +77 -4
  107. package/dist/workflows/runtime/run-plan.js +37 -23
  108. package/dist/workflows/runtime/runs.js +41 -84
  109. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  110. package/dist/workflows/source-files.js +7 -19
  111. package/docs/architecture/0.10-process-rules.md +6 -6
  112. package/docs/architecture/architecture.md +6 -6
  113. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
  114. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
  115. package/docs/architecture/internals/storage-locations.md +4 -4
  116. package/docs/architecture/persisted-data-compat.md +3 -2
  117. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
  118. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
  119. package/docs/architecture/testing/manual-testing-checklist.md +46 -78
  120. package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
  121. package/docs/architecture/testing/testing-workflow.md +9 -11
  122. package/docs/architecture/workflow-engine.md +41 -27
  123. package/docs/guides/author-bundles.md +14 -17
  124. package/docs/guides/author-workflows.md +43 -9
  125. package/docs/guides/bundles.md +16 -11
  126. package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
  127. package/docs/guides/run-workflows.md +25 -13
  128. package/docs/guides/scheduling.md +1 -1
  129. package/docs/maintainers/README.md +2 -3
  130. package/docs/maintainers/release-checklist.md +73 -126
  131. package/docs/migration/v0.9-to-v0.10.md +5 -1
  132. package/docs/reference/cli.md +124 -112
  133. package/docs/reference/configuration.md +16 -9
  134. package/docs/reference/data-and-telemetry.md +1 -1
  135. package/docs/reference/env-and-secrets.md +66 -15
  136. package/docs/reference/registry.md +6 -11
  137. package/docs/reference/tasks.md +11 -3
  138. package/docs/reference/ui-api.md +3 -3
  139. package/docs/reference/workflow-schema.md +109 -36
  140. package/package.json +1 -1
  141. package/schemas/akm-task.json +2 -4
  142. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  143. package/dist/core/activation-policy.js +0 -67
  144. package/dist/execution/directory-identity.js +0 -78
  145. package/dist/workflows/authoring/scope-key.js +0 -79
  146. package/dist/workflows/source-semantics.js +0 -110
@@ -3,16 +3,13 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { createHash } from "node:crypto";
5
5
  import fs from "node:fs";
6
- import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
7
6
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
8
7
  import { loadConfig } from "../../core/config/config.js";
9
8
  import { COMPOSITION_INVALID_MULTI_JOB_HINT, NotFoundError, UsageError } from "../../core/errors.js";
10
- import { getDbPath } from "../../core/paths.js";
11
9
  import { canonicalizeWorkflowName } from "../../core/recognition-util.js";
12
- import { deriveInstallations } from "../../indexer/installations.js";
13
- import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
10
+ import { resolveConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
14
11
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
15
- import { withIndexDb } from "../../storage/repositories/index-db.js";
12
+ import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
16
13
  import { compileWorkflowSource } from "../compile.js";
17
14
  /**
18
15
  * Build the canonical `workflow_runs.workflow_ref` for an AKM workflow asset.
@@ -31,71 +28,70 @@ export function parseWorkflowRefInput(ref) {
31
28
  }
32
29
  return parsed;
33
30
  }
34
- /** Resolve input sugar to the workflow's canonical run identity. */
31
+ /** Resolve input sugar to the workflow's canonical run identity, without compiling it. */
35
32
  export async function canonicalizeWorkflowRefInput(ref) {
36
- return (await loadWorkflowAsset(ref)).ref;
33
+ return resolveWorkflowAsset(ref).fullRef;
37
34
  }
38
- /**
39
- * Resolve a workflow ref and compile its authored bytes into a plan.
40
- */
41
- export async function loadWorkflowAsset(ref) {
35
+ /** Resolve a workflow ref to its authored file and canonical run ref. */
36
+ function resolveWorkflowAsset(ref) {
42
37
  const bundleRef = parseWorkflowRefInput(ref);
43
- const config = loadConfig();
44
- const allSources = resolveSourceEntries(undefined, config);
45
- const installations = deriveInstallations(allSources);
46
- const searchSources = allSources.flatMap((source, index) => {
47
- const bundleId = installations[index]?.id;
48
- if (!bundleId || (bundleRef.bundle !== undefined && bundleRef.bundle !== bundleId))
49
- return [];
50
- return [{ source, bundleId }];
51
- });
52
- if (bundleRef.bundle && searchSources.length === 0) {
53
- throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "WORKFLOW_SOURCE_INVALID");
54
- }
55
- let assetPath;
56
- let sourcePath;
57
- let sourceBundleId;
58
- let workflowName;
59
- let workflowAdapterId;
60
- let rejectedSource;
61
- for (const candidateSource of searchSources) {
62
- const { source, bundleId } = candidateSource;
63
- const adapterId = source.adapterId ?? detectAdapterId(source.path);
64
- const ownedSource = source.adapterId ? source : { ...source, adapterId };
65
- const owner = resolveAdapterConceptOwner(source.path, adapterId, bundleRef.conceptId);
66
- if (!owner)
67
- continue;
68
- if (!ownsNativeWorkflowRuntime(ownedSource) || !owner.workflowSource) {
69
- rejectedSource ??= { source: ownedSource, bundleId };
70
- break;
71
- }
72
- assetPath = owner.path;
73
- sourcePath = source.path;
74
- sourceBundleId = bundleId;
75
- workflowName = owner.workflowSource.canonicalName;
76
- workflowAdapterId = adapterId;
77
- break;
78
- }
79
- if (!assetPath) {
80
- if (rejectedSource) {
81
- const sourceName = rejectedSource.bundleId;
82
- const adapterId = rejectedSource.source.adapterId ?? "unassigned";
83
- throw new UsageError(`Bundle "${sourceName}" uses adapter "${adapterId}", which does not support native workflow execution.`, "WORKFLOW_SOURCE_INVALID");
38
+ const sources = resolveSourceEntries(undefined, loadConfig());
39
+ const match = resolveConceptOwner(bundleRef, sources);
40
+ if (!match) {
41
+ if (bundleRef.bundle && resolveSourcesForOrigin(bundleRef.bundle, sources).length === 0) {
42
+ throw new UsageError(`Bundle "${bundleRef.bundle}" was not found among configured sources.`, "WORKFLOW_SOURCE_INVALID");
84
43
  }
85
44
  throw new NotFoundError(`Workflow not found for ref: ${ref}`);
86
45
  }
87
- const resolvedSourcePath = sourcePath;
46
+ const { owner, source, bundleId, adapterId } = match;
47
+ if ((adapterId !== "akm" && adapterId !== "akm-workflow") || !owner.workflowSource) {
48
+ throw new UsageError(`Bundle "${bundleId}" uses adapter "${adapterId}", which does not support native workflow execution.`, "WORKFLOW_SOURCE_INVALID");
49
+ }
88
50
  // Canonicalize the stored ref: `workflows/foo.md` and `workflows/foo` resolve
89
51
  // to the same file, so they MUST share one run identity.
90
- const canonicalName = canonicalizeWorkflowName(workflowName);
91
- const fullRef = workflowAdapterId === "akm-workflow"
92
- ? makeBundleRef(sourceBundleId, canonicalName)
93
- : canonicalWorkflowRunRef(sourceBundleId, canonicalName);
52
+ const canonicalName = canonicalizeWorkflowName(owner.workflowSource.canonicalName);
53
+ const fullRef = adapterId === "akm-workflow"
54
+ ? makeBundleRef(bundleId, canonicalName)
55
+ : canonicalWorkflowRunRef(bundleId, canonicalName);
56
+ return { assetPath: owner.path, sourcePath: source.path, adapterId, canonicalName, fullRef };
57
+ }
58
+ /**
59
+ * The root of the bundle that owns a workflow ref right now: where the run's exec units and agents work.
60
+ * A bundle-qualified ref names its bundle, so the workflow file need not exist (a bundle update may have
61
+ * renamed or removed it; the frozen plan is the run's authority). An unqualified legacy ref finds its
62
+ * owner by the existing walk. When the bundle does not resolve, the error keeps its type (and exit code)
63
+ * and names the ref in its message.
64
+ */
65
+ export function resolveRunBundleRoot(ref) {
66
+ const { bundle } = parseWorkflowRefInput(ref);
67
+ if (bundle === undefined) {
68
+ try {
69
+ return resolveWorkflowAsset(ref).sourcePath;
70
+ }
71
+ catch (err) {
72
+ // Only a miss means the bundle does not resolve; any other error (an
73
+ // unsupported adapter, a collision) is reported as itself.
74
+ if (err instanceof NotFoundError) {
75
+ throw new NotFoundError(`workflow ${ref} does not resolve to a bundle: ${err.message}`, err.code, err.hint());
76
+ }
77
+ throw err;
78
+ }
79
+ }
80
+ const source = resolveSourcesForOrigin(bundle, resolveSourceEntries(undefined, loadConfig()))[0];
81
+ if (!source) {
82
+ throw new UsageError(`workflow ${ref} does not resolve to a bundle: bundle "${bundle}" is not configured`, "WORKFLOW_SOURCE_INVALID");
83
+ }
84
+ return source.path;
85
+ }
86
+ /**
87
+ * Resolve a workflow ref and compile its authored bytes into a plan.
88
+ */
89
+ export async function loadWorkflowAsset(ref) {
90
+ const { assetPath, adapterId, canonicalName, fullRef } = resolveWorkflowAsset(ref);
94
91
  const title = canonicalName.split("/").pop() || canonicalName;
95
92
  const bytes = fs.readFileSync(assetPath);
96
93
  const result = compileWorkflowSource(bytes.toString("utf8"), {
97
94
  path: assetPath,
98
- workspaceRoot: resolvedSourcePath,
99
95
  title,
100
96
  });
101
97
  if (!result.ok) {
@@ -106,36 +102,9 @@ export async function loadWorkflowAsset(ref) {
106
102
  return {
107
103
  ref: fullRef,
108
104
  path: assetPath,
109
- sourcePath: resolvedSourcePath,
110
- adapterId: workflowAdapterId,
105
+ adapterId,
111
106
  title,
112
107
  plan: result.plan,
113
108
  sourceHash: createHash("sha256").update(bytes).digest("hex"),
114
109
  };
115
110
  }
116
- function ownsNativeWorkflowRuntime(source) {
117
- return source.adapterId === "akm" || source.adapterId === "akm-workflow";
118
- }
119
- /**
120
- * Resolve the `entries.id` for an indexed workflow, or null when the index
121
- * database does not yet exist or has no matching entry.
122
- */
123
- export function resolveWorkflowEntryId(_sourcePath, ref, adapterId) {
124
- if (!fs.existsSync(getDbPath()))
125
- return null;
126
- const parsed = parseBundleRef(ref);
127
- if (!parsed.bundle)
128
- throw new UsageError(`Expected a bundle-qualified workflow ref, got "${ref}".`);
129
- const itemRef = makeBundleRef(parsed.bundle, parsed.conceptId);
130
- return withIndexDb((db) => {
131
- const row = db
132
- .prepare(`SELECT id
133
- FROM entries
134
- WHERE type = 'workflow'
135
- AND item_ref = ?
136
- ${adapterId ? "AND adapter_id = ?" : ""}
137
- LIMIT 1`)
138
- .get(itemRef, ...(adapterId ? [adapterId] : []));
139
- return row?.id ?? null;
140
- });
141
- }
@@ -66,15 +66,6 @@ export class WorkflowSourceLinkResolutionError extends WorkflowSourceRejectionEr
66
66
  Object.setPrototypeOf(this, new.target.prototype);
67
67
  }
68
68
  }
69
- export class WorkflowSourcePathIdentityError extends WorkflowSourceRejectionError {
70
- constructor(sourcePath, targetPath) {
71
- super(`Workflow source ${sourcePath} resolves outside the bundle root to ${targetPath}.`, "PATH_ESCAPE_VIOLATION", [
72
- sourcePath,
73
- ]);
74
- this.name = "WorkflowSourcePathIdentityError";
75
- Object.setPrototypeOf(this, new.target.prototype);
76
- }
77
- }
78
69
  /** Convert an adapter-owned concept id into its extensionless workflow name. */
79
70
  export function workflowNameForConceptId(adapterId, conceptId) {
80
71
  if (adapterId === "akm-workflow")
@@ -270,21 +261,18 @@ function inspectWorkflowSourceCandidate(candidate, canonicalName, realRoot) {
270
261
  issues.push(new WorkflowSourceLinkResolutionError(candidate.relativePath));
271
262
  return { issues };
272
263
  }
264
+ // A symlink may point anywhere (installing a bundle is the trust decision),
265
+ // but it may not change the source's format.
273
266
  const targetPath = toPosix(path.relative(realRoot, realPath));
274
- const contained = isWithinResolved(realPath, realRoot);
275
- if (!contained)
276
- issues.push(new WorkflowSourcePathIdentityError(candidate.relativePath, targetPath));
277
267
  if (isLink && path.extname(realPath).toLowerCase() !== candidate.lowerExtension) {
278
268
  issues.push(new WorkflowSourceLinkIdentityError(candidate.relativePath, targetPath));
279
269
  }
280
- if (contained) {
281
- try {
282
- if (!fs.statSync(realPath).isFile())
283
- issues.push(new WorkflowSourceLinkResolutionError(candidate.relativePath));
284
- }
285
- catch {
270
+ try {
271
+ if (!fs.statSync(realPath).isFile())
286
272
  issues.push(new WorkflowSourceLinkResolutionError(candidate.relativePath));
287
- }
273
+ }
274
+ catch {
275
+ issues.push(new WorkflowSourceLinkResolutionError(candidate.relativePath));
288
276
  }
289
277
  if (issues.length > 0)
290
278
  return { issues };
@@ -770,13 +770,13 @@ Paths are relative to the rules-010 checkout. `repository.ts` is `src/commands/p
770
770
  - **Engine fields** — An `llm` engine requires `endpoint` (an `http(s)` URL; a path not ending in `/chat/completions` only warns) and `model`. Optional fields are `temperature`, `maxTokens`, `timeoutMs` (a positive integer up to 2^31−1, or `null`), `concurrency`, `contextLength`, `supportsJsonSchema`, `enableThinking`, `reasoningEffort`, `extraParams`. An unset temperature sends 0.3 at call time. `enableThinking` sends both `chat_template_kwargs.enable_thinking` and top-level `enable_thinking`. `reasoningEffort` sends `reasoning_effort`. *Why:* which wire form a backend honors varies (#949). (`src/core/config/schema/engines.ts:41-110`, `src/llm/client.ts:548-575`)
771
771
  - **Engine request fallbacks (per endpoint|model, per process)** — A 4xx naming `chat_template_kwargs`/`enable_thinking` triggers one retry without them, remembered. A 4xx naming `temperature` drops it for the connection. A "use `max_completion_tokens`" 4xx switches the token field. A schema 4xx, or a 5xx naming the response format, retries once without the schema, and the retry is recorded as one successful call. *Why:* #1045 (OpenAI rejects `chat_template_kwargs`) and #1150 (a gateway answered 502 `format_ignored`). (`src/llm/client.ts:343-480`, #1045, #1150)
772
772
  - **Agent-engine rules** — Agent engines need a `platform` that supports agent dispatch. LLM-only fields (`provider`, `endpoint`, `apiKey`, `temperature`, `maxTokens`, `extraParams`, `enableThinking`, `reasoningEffort` and others) are rejected on them. `llmEngine` is valid only on `opencode-sdk` and must name an `llm` engine. `args` is not valid on `opencode-sdk`. `agent` needs a harness with a native agent selector. Engine names are lowercase kebab, at most 63 chars, and must not start with `akm-`. (`src/core/config/schema/engines.ts:115-175`, `src/core/config/schema/primitives.ts:42`, `src/core/config/config-schema.ts:223-231`)
773
- - **`apiKey` must be a reference (enforced at write, only warned at load)** — `engines.<n>.apiKey`, `embedding.apiKey` and `search.curateRerank.apiKey` should be `$VAR`, `${VAR}` or `secret://<name>`. `akm config set` of a literal key is refused: for `engines.<n>.apiKey` and `embedding.apiKey` with `apiKey must be $VAR, ${VAR}, or secret://<name>`; the "apiKey cannot be persisted in config; export AKM_ENGINE_<NAME>_API_KEY instead" text belongs to `rejectApiKeyPath`, which skips those two paths. A literal already in `config.json` loads with a one-time warning and is used as configured. Git bundle `credential` is strictly `$VAR` or `secret://`, and a violation is a load error. `apiKey` and `apiKeyFile` cannot both be set. An unset `$VAR` warns once and sends an empty credential, and an unresolved `secret://` throws `SECRET_REFERENCE_UNRESOLVED`. *Why:* a literal key leaks through backups, logs and version control. (`src/core/config/schema/primitives.ts:48-60`, `src/core/config/config-walker.ts:242-248,319-345`, `src/core/config/schema/sources-bundles.ts:73,191`, `src/core/config/config.ts:1063-1098`, #454, #905, #953)
773
+ - **`apiKey` must be a reference (enforced at write; a literal only warned at load, a non-`secrets/` ref refused at load)** — `engines.<n>.apiKey`, `embedding.apiKey` and `search.curateRerank.apiKey` should be `$VAR`, `${VAR}` or `${secrets/<name>}` (for example `${secrets/openai}`). `akm config set` of a literal key is refused: for `engines.<n>.apiKey` and `embedding.apiKey` with `apiKey must be $VAR, ${VAR}, or ${secrets/<name>}`; the "apiKey cannot be persisted in config; export AKM_ENGINE_<NAME>_API_KEY instead" text belongs to `rejectApiKeyPath`, which skips those two paths. A literal already in `config.json` loads with a one-time warning and is used as configured, but a `${...}` value must name a `secrets/` asset: any other asset ref (for example `${knowledge/x}`) is a load error. Git bundle `credential` is strictly `$VAR`, `${VAR}` or `${secrets/<name>}`, and a violation is a load error. `apiKey` and `apiKeyFile` cannot both be set. An unset `$VAR` warns once and sends an empty credential, and an unresolved `${ref}` throws `SECRET_REFERENCE_UNRESOLVED`. A `secret://<name>` value from an older release is still read, as `${secrets/<name>}`, and named once. *Why:* a literal key leaks through backups, logs and version control. (`src/core/config/schema/primitives.ts:48-60`, `src/core/config/config-walker.ts:242-248,319-345`, `src/core/config/schema/sources-bundles.ts:73,191`, `src/core/config/config.ts:1063-1098`, #454, #905, #953)
774
774
  - **`defaults.llmEngine` and model-work engine rule** — Every engine used for unattended model work must be able to confine the model-work tool policy: an `llm` engine, or an agent engine on a harness in `HARNESS_MODEL_WORK_IDS`. This is enforced at config load for `defaults.llmEngine`, `index.<pass>.engine`, improve strategy, process, judgment and `qualityGate` engines. The execution cascade is `defaults.llmEngine → strategy → index.<pass> → process → current invocation`. `defaults.engine` and `workflow.judgeEngine` must name a configured engine, and `defaults.improveStrategy` must be `default`, `consolidate` or a configured strategy, except that load exempts `graph-refresh` and the removed names (the error is raised at resolution; `graph-refresh` is refused there even with a user block and dropped only by `akm migrate apply`). The removed strategy names `quick, thorough, catchup, reflect-distill, proactive-maintenance` pass load but fail at resolution naming `default`. *Why:* unattended model work runs over generated content with no one watching; #1130. (`src/core/config/config-schema.ts:243-325`, `src/core/config/engine-semantics.ts`, `src/commands/improve/execution.ts:58-70`, #1130)
775
775
  - **`extends` inheritance strips host-local settings** — An inherited layer loses `bundles, defaultBundle, defaultWriteTarget, embedding, execution, experimental, registries, scheduler, setup`. It also loses engine `apiKey/apiKeyFile/bin/args/workspace`, `search.curateRerank`, and improve strategy `sync`. Each strip warns once. *Why:* shared config contributes portable behavior only; credentials, executables, endpoints and publication policy stay host-local. (`src/core/config/config.ts:399-485`, #945)
776
776
  - **Bundle entry shape** — A bundle needs exactly one of `path|git|website|npm`. `writable: true` is allowed only on `path` and `git`. `credential` is allowed only on `git`. `components` holds exactly one component. `defaultBundle` and `defaultWriteTarget` must name a configured, enabled bundle. Bundle keys must be legal slugs (no `/ : . #` or whitespace), and two bundles may not share one physical content root. (`src/core/config/schema/sources-bundles.ts:155-240`, `src/core/config/config-schema.ts:190-225`, `src/core/config/config.ts:290-297`)
777
777
  - **Autonomy opt-in is explicit** — `experimental.improveAutonomy` is off whether absent or `false`, and is never inferred. It gates memory cleanup, memory-inference writes, and triage promote when judgment is on. `sync.push` is deliberately not gated by it. (`src/core/config/schema/experimental.ts:19-32`, `src/commands/improve/autonomy-gate.ts:10-20`)
778
778
  - **Other defaults** — `feedback.requireReason` is true, output is `json`/`brief`, and the default registries are `akm-registry` (enabled) and `skills.sh` (disabled). `workflow.maxConcurrency`, `workflow.defaultMapConcurrency` and `workflow.judgeEngine` are described under Tasks, workflows and sync. (`src/core/config/config.ts:76-87`, `src/commands/feedback-cli.ts:644-648`)
779
- - **Persisted-data compat by format** — `index.db` is layout 26: older layouts migrate in place and never drop `embeddings`, `utility_scores*` or `llm_enrichment_cache`, and a newer layout is refused with "Upgrade akm", leaving the file untouched. Only `SQLITE_CORRUPT` deletes and rebuilds it. `state.db` migrations are append-only; an older binary on a newer ledger continues with a warning, and a `.pre-<id>.bak` copy is taken before schema-dropping migrations. Frozen workflow plans are irVersion 6, and the irVersion 5 shape is still decoded; a plan that cannot be decoded is abandoned (status `failed`), never thrown. The legacy `{kind, ref, sourceId}` `scheduler.enabled` form is still read. *Why:* the `docs/architecture/persisted-data-compat.md` contract; #865. (`docs/architecture/persisted-data-compat.md:33-44`, `src/storage/repositories/index-entry-schema.ts:32`, `src/storage/repositories/index-schema.ts:61,245`, `src/core/state-db.ts:105`, `src/workflows/runtime/run-plan.ts:46,113`)
779
+ - **Persisted-data compat by format** — `index.db` is layout 26: older layouts migrate in place and never drop `embeddings`, `utility_scores*` or `llm_enrichment_cache`, and a newer layout is refused with "Upgrade akm", leaving the file untouched. Only `SQLITE_CORRUPT` deletes and rebuilds it. `state.db` migrations are append-only; an older binary on a newer ledger continues with a warning, and a `.pre-<id>.bak` copy is taken before schema-dropping migrations. Frozen workflow plans are irVersion 7; irVersion 5 and 6 plans still decode, with their location fields ignored; a plan that cannot be decoded is abandoned (status `failed`), never thrown. The legacy `{kind, ref, sourceId}` `scheduler.enabled` form is still read. *Why:* the `docs/architecture/persisted-data-compat.md` contract; #865. (`docs/architecture/persisted-data-compat.md:33-44`, `src/storage/repositories/index-entry-schema.ts:32`, `src/storage/repositories/index-schema.ts:61,245`, `src/core/state-db.ts:105`, `src/workflows/runtime/run-plan.ts:46,113`)
780
780
 
781
781
  ## Tasks, workflows and sync
782
782
 
@@ -801,13 +801,13 @@ Paths are relative to the rules-010 checkout. `repository.ts` is `src/commands/p
801
801
  **Workflows (the whole surface is Experimental; STABILITY.md)**
802
802
 
803
803
  - **Workflow source is Markdown only** — Step ids match `[A-Za-z_][A-Za-z0-9_-]*`. Every `##` heading must be a declared step id. An optional `### gate` section holds the rubric. A canonical ref must be owned by exactly one recognized `.md`. (`src/workflows/parser.ts:110`, `src/workflows/compile.ts:41-47`, `src/workflows/source-files.ts:69`, `docs/reference/workflow-schema.md:Body structure`)
804
- - **A run executes a frozen plan** — `akm workflow run <ref>` freezes an irVersion 6 plan (`plan_json`, `plan_hash`, `sourceHash`) at start. Resume reads only the frozen plan and never re-reads config, the index, or the source, other than to warn once if the source changed. A stored plan that does not decode is abandoned (status `failed`) with a message naming `akm workflow run <ref>`. Models, tools, working directory and runner are frozen. Workflow environment pass-through values and env/secret references are materialized at dispatch. *Why:* #919 (editing a workflow broke in-flight runs). (`src/workflows/plan.ts:28`, `src/workflows/freeze/freeze.ts:36-100`, `src/workflows/exec/run-workflow.ts:312-345`, `docs/architecture/workflow-engine.md:Frozen plans`, #919)
804
+ - **A run executes a frozen plan** — `akm workflow run <ref>` freezes an irVersion 7 plan (`plan_json`, `plan_hash`, `sourceHash`) at start. Resume executes only the frozen plan and never re-reads the index or the workflow source, other than to warn once if the source changed; each dispatching step re-reads live config for the run's bundle root, its env bindings and its `${ref}` tokens. A stored plan that does not decode is abandoned (status `failed`) with a message naming `akm workflow run <ref>`. Models, tools and runner are frozen. The working directory is not: it is the root of the bundle that owns the run's workflow, found at each step, with `exec.cwd` joined onto it once. Workflow environment pass-through values and env/secret references are materialized at dispatch. *Why:* #919 (editing a workflow broke in-flight runs). (`src/workflows/plan.ts:31`, `src/workflows/freeze/freeze.ts:36-100`, `src/workflows/exec/run-workflow.ts:312-345`, `docs/architecture/workflow-engine.md:Frozen plans`, #919)
805
805
  - **Param defaults are applied at run start** — Every declared `default:` fills a param the run was not given, from flags, task inputs or an object. Explicit values win, params are never required, and supplied-plus-default values are schema-validated. Flags and an object cannot be mixed (`Workflow parameters must use either an object or per-parameter flags, not both`). Params are declared non-secret, with an advisory warning for secret-shaped values. *Why:* #1162, since defaults were previously accepted but never applied and prompts showed `Run parameters: {}`. (`src/workflows/ir/params.ts:89`, `src/workflows/runtime/runs.ts:241-262,337`, #1162)
806
- - **One active run per (ref, scope)** — Starting a ref with an active run in the same scope resumes it. `--new` starts a separate run and leaves the old one untouched. Scope is the nearest `.akm/config.json`, else git root, else the stash if cwd is inside it, else cwd. An active run of the same ref in another scope only produces a warning. *Why:* #485 (zero concurrency control) and #942 (a scheduled task's cwd differed from the shell's). (`src/workflows/authoring/scope-key.ts:12-33`, `src/workflows/runtime/runs.ts:286-306`, #485, #942, #919)
806
+ - **A ref starts a run; a run id continues it** — `akm workflow run <ref>` always starts a new run and warns, naming `run <id>`/`abandon`, when other runs of the ref are active. `akm workflow run <run-id>` continues a run; `akm workflow resume <run-id>` re-activates a blocked or failed one. Runs are not scoped to a directory, so `list` and `status` see every run on the host. `--skip-if-locked` skips a ref start while an active run of the ref is held by a live process. `--new` and `--all-scopes` are deprecated no-ops through 0.10. *Why:* #485 (zero concurrency control), #942 (a scheduled task's cwd differed from the shell's) and #1239 (a run is a run id). (`src/workflows/runtime/runs.ts`, `src/commands/workflow-cli.ts`, #1239)
807
807
  - **One engine drives a run** — `workflow run` takes an O_EXCL lock file per run (`workflow-run-locks/<run id>.lock`). A second process gets `RUN_LEASE_HELD` (exit 75). A dead holder is reclaimed at once; nothing expires by age. A run that is not `active` is refused before dispatch, with a hint to `akm workflow resume`. (`src/workflows/exec/run-workflow.ts:226-239,312-330`)
808
808
  - **Dispatch is at-least-once with unit reuse** — Units are journaled under a content-derived id plus an input hash (`hashVersion` 7). A `completed` unit is reused on resume and never re-dispatched, while failed or missing units are dispatched. An ambiguous crash can duplicate a side effect. (`docs/architecture/workflow-engine.md:Resume skips completed units`, `src/workflows/exec/step-work.ts:513-521`)
809
809
  - **Map fan-out width** — A `map` step with no `concurrency:` freezes width 4. A step's real width is the minimum of its own `concurrency`, the run's frozen `workflow.maxConcurrency` (unset → `min(16, max(1, cores−2))`; set → clamped to [1, 64]), the LLM engine's concurrency (1 for loopback endpoints, 4 for remote, or `engines.<n>.concurrency`), and the host CPU cap re-derived at dispatch. Loopback detection is purely syntactic. *Why:* local model servers fail hard on concurrent inference; one config must work across machines. (`src/workflows/concurrency-policy.ts:12-59`, `src/workflows/resource-limits.ts:16`, `src/workflows/freeze/freeze.ts:96`, `docs/reference/workflow-schema.md:Fan-out`)
810
- - **Exec (shell) units** — `exec.command` is an argv array, never a shell string, and no engine, model or llm is allowed beside it. The default timeout is 600,000 ms unless the unit or `defaults.timeout` sets one (`timeout: none` opts out). Stdout and stderr retention is 8 MiB each: past that, a unit with no `output:` schema succeeds with a `__akm_exec_output_truncated__` marker appended, and a unit with an `output:` schema fails `exec_output_limit`. `exec_context_too_large`, `exec_cwd_escape` and `exec_capture_incomplete` are not retryable. (`src/workflows/resource-limits.ts:31,39`, `src/workflows/freeze/step-values.ts:83-95`, `docs/reference/workflow-schema.md:Exec units`)
810
+ - **Exec (shell) units** — `exec.command` is an argv array, never a shell string, and no engine, model or llm is allowed beside it. The default timeout is 600,000 ms unless the unit or `defaults.timeout` sets one (`timeout: none` opts out). Stdout and stderr retention is 8 MiB each: past that, a unit with no `output:` schema succeeds with a `__akm_exec_output_truncated__` marker appended, and a unit with an `output:` schema fails `exec_output_limit`. `exec_context_too_large` and `exec_capture_incomplete` are not retryable. (`src/workflows/resource-limits.ts:31,39`, `src/workflows/freeze/step-values.ts:83-95`, `docs/reference/workflow-schema.md:Exec units`)
811
811
  - **Failure policy** — Fail-fast is the default, and `on_error: continue` lets the completion gate decide. `retry: {max, on}` requires `max ≥ 0` and a non-empty `on` list of failure reasons (`timeout, spawn_failed, non_zero_exit, parse_error, cooldown, llm_rate_limit, llm_content_filter, llm_invalid_json, content_policy_reject, unsupported_type, no_change, quality_rejected, aborted`). A unit `output` schema miss re-dispatches once with corrective feedback; an exec unit fails immediately instead. Budget ceilings (`max_units`, `max_tokens`) are run-lifetime counters seeded from the journal and are a hard stop that ignores `on_error: continue`. (`src/workflows/parser.ts:84-103,1155-1190`, `docs/reference/workflow-schema.md:Failure policy,Budget ceilings`)
812
812
  - **Gates are fail-closed** — A non-empty `### gate` rubric enables mandatory verification. The judge sees the step's artifact as canonical JSON clipped at 4,000 chars plus the byte-exact rubric. Only a well-formed `complete: true` verdict advances the step. A missing judge, a dispatch failure or a malformed verdict rejects or blocks. `gate.max_loops` (default 1) bounds the evaluator-optimizer retries, and only `command` targets loop. Shell and child-workflow steps are judged once. At freeze, a missing verification engine is a warning, not an error: the step blocks for `akm workflow resume` when reached. *Why:* a gate judging a machine summary like "Executed 3 units" proves nothing; re-running a frozen argv would repeat side effects. (`src/workflows/exec/step-work.ts:54,729-738,975-983,1237`, `src/workflows/freeze/freeze.ts:73-88,114-127`, `src/workflows/parser.ts:369,1115-1120`)
813
813
  - **Child workflows (Experimental, #1165)** — A step declares `unit: { workflow: workflows/<name>, with: {param: literal | {from: ref}} }`. The child is compiled and frozen whole, embedded in the parent's frozen target at freeze, and never re-read at dispatch. Only a composition cycle is checked (`COMPOSITION_INVALID`). `workflow` cannot sit beside `exec/engine/model/llm/env` or inside `map.unit`. The child is published idempotently, keyed by `{parentRunId, parentUnitId, unitInputHash}`, and driven inline with the same engine. A child already `blocked` or `failed` is not re-driven. The child's final status maps onto the composing step, and a completed child returns its last step's output. A top-level `outputs:` key no longer exists. Child runs are hidden from `workflow list` unless `--children`. *Why:* #1096 owner decision B: keep a simple, Experimental form. (`src/workflows/freeze/targets/child-workflow.ts:67-125`, `src/workflows/exec/child-workflow.ts:209-357`, `docs/reference/workflow-schema.md:Child workflows`, #1165)
@@ -824,7 +824,7 @@ Paths are relative to the rules-010 checkout. `repository.ts` is `src/commands/p
824
824
  - `docs/reference/workflow-schema.md` documents `gate.max_loops` as 1–100 and `retry.max` as 0–100. The parser and JSON schema enforce only `≥ 1` and `≥ 0`.
825
825
  - `docs/architecture/workflow-engine.md:65-66` claims three composition bounds (depth, cycle, aggregate embedded bytes) enforced at freeze; `docs/reference/workflow-schema.md:80-84` documents the cycle alone; `src/workflows/freeze` implements only the cycle check (`child-workflow.ts:67-76,88`; `freeze.ts:64` recurses unbounded).
826
826
  - `docs/reference/workflow-schema.md` says a gate rubric requires `workflow.judgeEngine` before freezing. The code falls back to default engine resolution and, if none is available, warns at freeze and blocks the step at run time.
827
- - `docs/guides/scheduling.md:63-64` says `irVersion: 5` (now 6); `workflow-engine.md:56-59` still lists removed `uses:` forms and the `script` target.
827
+ - `workflow-engine.md:56-59` still lists removed `uses:` forms and the `script` target.
828
828
  - The shipped hint files (`cli-hints-short.md:11,42`, `cli-hints-full.md:96-104`) and `docs/reference/cli.md:1792-1795` tell agents to run `--positive` with no reason, which starves distill (a positive with no reason or note is skipped there); `cli.md:1793` shows `akm feedback agents/reviewer --negative` with no `--reason`, a usage error by default.
829
829
  - `docs/architecture/workflow-engine.md` still says "Markdown or GitHub-Actions-style YAML".
830
830
  - The `src/core/config/config-schema.ts` header says `scheduler`, `execution`, `experimental` and reranker policy are strict. All are `.passthrough()`.
@@ -434,10 +434,10 @@ here; any other field the transport cannot carry becomes a secret-free
434
434
  `untranslated-field` notice and dispatch continues.
435
435
 
436
436
  Credentials stay symbolic (an env descriptor, an `apiKeyFile` path, or a
437
- `secret://` reference) in everything `resolveExecution` returns, so a request
438
- and runner can be journaled. `runExecution()` reads the current value at each
439
- dispatch and scrubs it, with every other secret-looking value the child could
440
- see, from the result. Workflow resume calls `buildExecutionFromWire()` on the
437
+ `${ref}` reference such as `${secrets/openai}`) in everything
438
+ `resolveExecution` returns, so a request and runner can be journaled.
439
+ `runExecution()` reads the current value at each dispatch and scrubs it, with
440
+ every other secret-looking value the child could see, from the result. Workflow resume calls `buildExecutionFromWire()` on the
441
441
  journaled `{ request, runner }` and never reads config, `models.json`, or
442
442
  credentials, so a config edit after the freeze cannot change a resumed unit.
443
443
  The prompt-free interactive `akm agent` launch resolves its engine directly.
@@ -446,7 +446,7 @@ through to another configured engine.
446
446
 
447
447
  Task-v3 execution and durable workflow-v4 dispatch use this runtime boundary.
448
448
  Markdown workflows compile through source IR v1; new starts freeze
449
- v4-family `irVersion: 5`. A stored plan that decodes runs whatever release
449
+ v4-family `irVersion: 7` (plans frozen at `irVersion` 5 and 6 still decode). A stored plan that decodes runs whatever release
450
450
  froze it; one that does not is marked abandoned and `akm workflow run <ref>`
451
451
  starts afresh. Only a plan a newer akm froze is refused, naming the upgrade.
452
452
  AKM does not support full GitHub Actions semantics or arbitrary remote action
@@ -556,7 +556,7 @@ activity registry around them; `state.db` writers serialize on SQLite's own
556
556
  | `src/commands/agent/` | contribute/agent command surface |
557
557
  | `src/registry/providers/` | registry provider implementations (static-index, skills-sh) |
558
558
  | `src/output/shapes/`, `src/output/text/` | JSON-envelope and text-output registries per command (#490) — a parallel concern to `src/output/renderers.ts`'s per-asset-type `show` renderers, which is still live and self-registering, not replaced |
559
- | `src/workflows/authoring/` | workflow authoring + scope-key helpers |
559
+ | `src/workflows/authoring/` | workflow authoring |
560
560
  | `src/workflows/runtime/runs.ts` | workflow run persistence (raw SQL lives in `src/storage/repositories/workflow-runs-repository.ts`) |
561
561
  | `src/workflows/runtime/` | run lifecycle: runs, checkin, agent-identity |
562
562
  | `src/llm/client.ts` | OpenAI-compatible chat completions client (stateless, single request/response) |
@@ -11,7 +11,7 @@ The short version: they are not competitors. An akm workflow is a durable
11
11
  workflow asset authored in either of two peer source formats, Markdown `.md`
12
12
  or GitHub-shaped YAML `.yml`, and executed by the Stable
13
13
  `akm workflow run` command. Both adapters compile through source IR version 1.
14
- Every new start freezes the durable plan v4 family's executable `irVersion: 5`
14
+ Every new start freezes the durable plan v4 family's executable `irVersion: 7`
15
15
  before execution. The native engine
16
16
  fans work out to concurrent runner units, retries, gates on typed artifacts,
17
17
  and enforces budget ceilings. Execution is native: akm dispatches each unit to
@@ -254,7 +254,7 @@ run lease), `008`/`009` (unit attempts / claim columns).
254
254
 
255
255
  `akm workflow run` is the one stable native execution surface. Peer source formats use the same source IR v1 before durable freezing: Markdown `.md` and YAML `.yml`.
256
256
  A new start by ref compiles source
257
- IR v1 and freezes the resulting durable-v4-family `irVersion: 5` plan on the
257
+ IR v1 and freezes the resulting durable-v4-family `irVersion: 7` plan on the
258
258
  run row (`plan_json`/`plan_hash`); edits to the source file need a new run.
259
259
  Pre-`irVersion`-5
260
260
  stored plans are rejected rather than keeping a parallel runtime. The
@@ -292,20 +292,14 @@ agent session execute units itself; it was removed in favour of the single
292
292
  native path, since native dispatch already covers ten harnesses — Claude Code
293
293
  among them — for less code than the protocol cost.
294
294
 
295
- ### B.5 Scoping & concurrency guards (two different guards)
296
-
297
- Runs are partitioned by **`scope_key`** — a `sha256` of the nearest project
298
- anchor (`.akm/config.json` root → git root → bundle dir → cwd),
299
- `src/workflows/authoring/scope-key.ts`. Within a `(workflow_ref, scope_key)`
300
- pair, `startWorkflowRun` enforces a **single active run** unless `--force` is
301
- passed internally. The public CLI exposes no parallel-start flag: invoking
302
- `workflow run` by ref continues the active run in that scope. This guard is
303
- scope-local by design (two unrelated projects sharing one `state.db` must be
304
- able to run the same-named workflow independently), so two *different*
305
- scopes can each hold their own active run of the same ref — starting one
306
- warns about an active run in another scope (naming its id and scope) rather
307
- than blocking it, and `akm workflow list --all-scopes` / `status` / `resume`
308
- / `abandon <run-id>` see and act across scopes (#942).
295
+ ### B.5 Run identity & concurrency
296
+
297
+ A run is identified by its run id and a workflow by its bundle-qualified ref;
298
+ runs are not partitioned by directory. `workflow run <ref>` always starts a
299
+ new run, so several runs of one ref can be active at once. A start that finds
300
+ other active runs of the ref warns, naming their ids and the `resume` /
301
+ `abandon` commands, and `--skip-if-locked` skips instead of starting when an
302
+ active run of the ref is held by a live process (#1239).
309
303
 
310
304
  Orchestrated runs add a second, unrelated guard: the **run lease**. `akm
311
305
  workflow run` takes a lease (a random holder id, 90s expiry, renewed between
@@ -383,14 +377,14 @@ orchestration surface.
383
377
 
384
378
  | Dimension | Claude Code workflow | akm workflow |
385
379
  |---|---|---|
386
- | **Artifact** | Imperative JS program (`script`) | Declarative workflow asset (`.md` or `.yml`), compiled through source IR v1 to durable-v4-family `irVersion: 5` |
380
+ | **Artifact** | Imperative JS program (`script`) | Declarative workflow asset (`.md` or `.yml`), compiled through source IR v1 to durable-v4-family `irVersion: 7` |
387
381
  | **Authored by** | The agent, inline, per-task, ephemeral | Human or agent, saved as a reusable bundle asset |
388
382
  | **Who executes work** | The harness runs the script; subagents do the work | The native engine, dispatching to a configured harness |
389
383
  | **Unit of work** | `agent()` — a fresh LLM subagent context | One dispatch or one `map` item under the engine |
390
384
  | **Concurrency** | Massively parallel (≤16 concurrent, ≤1000 total, `pipeline`/`parallel`) | Bounded map concurrency, frozen into the plan |
391
385
  | **Control flow** | Full JS: loops, conditionals, fan-out, budget-scaled | `unit`/`map`/`route`, bare reference strings, and bounded gate retries; no backward routes |
392
386
  | **State store** | Transcript dir (`journal.jsonl`, `agent-*.jsonl`) | SQLite `state.db`; native-engine runs add unit rows and a frozen `plan_json`/`plan_hash` |
393
- | **Scope / lifetime** | One session, one turn-shaped fan-out | Cross-session, per-project `scope_key`, resumable indefinitely |
387
+ | **Scope / lifetime** | One session, one turn-shaped fan-out | Cross-session, host-wide (a run is identified by its id), resumable indefinitely |
394
388
  | **Progress model** | Push: live `/workflows` tree + `task-notification` | Pull via `status`, with near-live NDJSON event polling via `akm log` |
395
389
  | **Resume** | Prefix-cache replay keyed on `runId` (needs determinism) | Durable row resume; native-engine units add journaled replay keyed on content-derived identity |
396
390
  | **Quality gates** | Agent-authored (adversarial verify, judge panels, schemas) | Fail-closed LLM or agent verification of typed artifacts; `gate.max_loops` bounds retries |
@@ -29,10 +29,9 @@ Moved verbatim from `exec-unit.ts`'s module header:
29
29
  > - NO LEAKED CHILDREN. `runManagedSubprocess` spawns DETACHED and runs a
30
30
  > SIGTERM→SIGKILL ladder against the whole process group, so `--timeout`
31
31
  > and Ctrl-C really do stop a running command and its descendants.
32
- > - CONTAINMENT. `exec.cwd` is relative and `..`-free by construction
33
- > (parser and frozen-plan decoder), which is necessary but not sufficient:
34
- > a subdirectory can be a symlink. The RESOLVED path is therefore
35
- > re-checked against the RESOLVED base immediately before spawning.
32
+ > - NO CONTAINMENT. `exec.cwd` is relative to the run's bundle root or
33
+ > absolute, and nothing checks where it leads: a command can reach any
34
+ > directory anyway, so a directory is not a security boundary.
36
35
  > - BOUNDED SPEND. A command is arbitrary code with no resource discipline
37
36
  > of its own, so each resource it spends on akm's behalf has a ceiling in
38
37
  > `workflows/resource-limits.ts`: wall clock (`DEFAULT_EXEC_TIMEOUT_MS` or
@@ -52,8 +51,7 @@ Moved verbatim from `exec-unit.ts`'s module header:
52
51
  > which is why this module may return raw stdout/stderr diagnostics without
53
52
  > knowing anything about redaction.
54
53
  >
55
- > Layering: a LEAF. Node built-ins, `core/common` (for `isWithinAsync`, the
56
- > CONTAINMENT recheck above), `core/spawn-env`, `core/subprocess`,
54
+ > Layering: a LEAF. Node built-ins, `core/spawn-env`, `core/subprocess`,
57
55
  > `core/warn` and the import-free `workflows/resource-limits` (plus erased
58
56
  > types) only, so the executor can consume it without opening an import
59
57
  > cycle.
@@ -127,8 +125,8 @@ Moved verbatim from `runExecUnit`'s doc comment:
127
125
  > `timeout`, cancellation → `aborted`, a child that never started →
128
126
  > `spawn_failed`. All four are pre-existing `AgentFailureReason` members, so
129
127
  > `retry.on` keeps working unchanged. The out-of-taxonomy
130
- > `exec_cwd_escape`, `exec_output_limit`, `exec_context_too_large` and
131
- > `exec_capture_incomplete` are deliberate: each is tampering, a runaway, an
128
+ > `exec_output_limit`, `exec_context_too_large` and
129
+ > `exec_capture_incomplete` are deliberate: each is a runaway, an
132
130
  > authoring bug, or work that ALREADY RAN — never a transient — so no
133
131
  > `retry.on` value can ever re-dispatch one.
134
132
  >
@@ -332,7 +332,7 @@ explicit retention policy removes them.
332
332
 
333
333
  #### Table: `workflow_runs`
334
334
 
335
- New starts persist durable-v4-family plan `irVersion: 5`, the sole executable
335
+ New starts persist durable-v4-family plan `irVersion: 7`, the sole executable
336
336
  plan format. Pre-`irVersion`-5 stored
337
337
  plans are rejected rather than upgraded or replayed through another runtime.
338
338
 
@@ -340,8 +340,8 @@ plans are rejected rather than upgraded or replayed through another runtime.
340
340
  |---|---|---|
341
341
  | `id` | TEXT PRIMARY KEY | UUID v4 |
342
342
  | `workflow_ref` | TEXT NOT NULL | e.g. `workflows/review-todos` |
343
- | `scope_key` | TEXT | Directory hash; isolates runs per project. Gates `list`'s default filter, `akm show`'s active-run nudge, and `startWorkflowRun`'s own-scope uniqueness guard (unchanged, still per-scope) — so two DIFFERENT scopes can each hold their own active run of the same ref; starting one warns about the other rather than blocking it (#942). `akm workflow list --all-scopes` sees across scopes, and `status`/`resume`/`abandon <run-id>` already act on a run regardless of which scope started it |
344
- | `workflow_entry_id` | INTEGER | Optional FK into `index.db entries.id` |
343
+ | `scope_key` | TEXT | Legacy: a directory hash written by releases before 0.10. Never written or read now (NULL on new rows); the column and its index stay so older rows load |
344
+ | `workflow_entry_id` | INTEGER | Legacy: an `index.db entries.id` written by releases before 0.10. Never written now (NULL on new rows) |
345
345
  | `workflow_title` | TEXT NOT NULL | Human-readable title |
346
346
  | `status` | TEXT NOT NULL | `active`, `completed`, `blocked`, `failed` |
347
347
  | `params_json` | TEXT NOT NULL DEFAULT '{}' | Run parameters |
@@ -618,7 +618,7 @@ The JSONL file at `$CACHE/events.jsonl` is no longer read or written by akm.
618
618
  | `curate` | `akm curate` | `query`, `itemCount`, `itemRefs[]` |
619
619
  | `show` | `akm show` | `type`, `name` |
620
620
  | `select` | `akm show`, when a search within the last 60s returned the ref | `query`, `searchTs`, `rankPosition` |
621
- | `env_access` | `akm env run`, and a workflow step's frozen environment | `keys`; `secretNames` from a workflow step (names only, never values) |
621
+ | `env_access` | `akm env run`, and a workflow step's `env:` bindings (read at each step) | `keys` (names only, never values) |
622
622
  | `secret_access` | `akm secret run` | `var` (the variable name, never the value) |
623
623
  | `promoted` | `akm proposal accept`, and drain/triage promotion | `proposalId`, `source`, `sourceRun`, `assetPath`, `eligibilitySource`; `retired: true` for a retire proposal |
624
624
  | `rejected` | `akm proposal reject` | `proposalId`, `source`, `sourceRun`, `reason` |
@@ -36,11 +36,12 @@ and named once, round-trips through ordinary writes, and is dropped by
36
36
 
37
37
  | Format | Where written | Version marker | Older data | Newer data | Gate |
38
38
  | --- | --- | --- | --- | --- | --- |
39
- | `config.json` | `src/core/config/config.ts` | `configVersion` field | `configVersion` is read, never gated on (`readConfigVersion`, `src/core/config/config.ts`): a missing field or `"0.9.0"` — the only value akm has shipped — loads silently, and any other value is named once and read as `0.9.0`. Below the 0.9.15 floor the file is refused, not converted: the retired `stashDir`/`sources[]`/`installed[]` layout fails validation (`AkmConfigSchema`, `src/core/config/config-schema.ts`) and an `extraParams` key that shadows a first-class engine field (`temperature`, `maxTokens`, `enableThinking`, `reasoningEffort`) is rejected by `validateExtraParams`, each telling the user to run `akm migrate apply` under akm 0.9.x or to set the field. A config with no `scheduler.enabled` (0.9.15 wrote none) is read as "keep what is installed": `akm task sync` adopts the akm-written scheduler rows as the list (`tests/integration/upgrade-rehearsal/`'s 0.9.15 origin). 0.9.16's `{kind, ref, sourceId}` `scheduler.enabled` entries are still read, as their `ref`, and written back as plain refs; `akm migrate apply`'s `configFile` step (`normalizeConfigFile`) persists that — `configVersion: "0.9.0"`, unknown keys dropped — to the file once, under a backup | Same as older: the value is named once and the file is read as `0.9.0`; there is no `UNSUPPORTED_CONFIG_VERSION` refusal. `akm health`'s `binary-config-skew` advisory (`src/commands/health/config-skew.ts`) is what says "a newer akm wrote this config, upgrade this install" | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`); `tests/config-v09.test.ts` |
39
+ | `config.json` | `src/core/config/config.ts` | `configVersion` field | `configVersion` is read, never gated on (`readConfigVersion`, `src/core/config/config.ts`): a missing field or `"0.9.0"` — the only value akm has shipped — loads silently, and any other value is named once and read as `0.9.0`. Below the 0.9.15 floor the file is refused, not converted: the retired `stashDir`/`sources[]`/`installed[]` layout fails validation (`AkmConfigSchema`, `src/core/config/config-schema.ts`) and an `extraParams` key that shadows a first-class engine field (`temperature`, `maxTokens`, `enableThinking`, `reasoningEffort`) is rejected by `validateExtraParams`, each telling the user to run `akm migrate apply` under akm 0.9.x or to set the field. A config with no `scheduler.enabled` (0.9.15 wrote none) is read as "keep what is installed": `akm task sync` adopts the akm-written scheduler rows as the list (`tests/integration/upgrade-rehearsal/`'s 0.9.15 origin). 0.9.16's `{kind, ref, sourceId}` `scheduler.enabled` entries are still read, as their `ref`, and written back as plain refs. A `secret://<name>` credential (`engines.*.apiKey`, `embedding.apiKey`, a git bundle's `credential`, any config string) is retired: the reader (`readRetiredSecretRefs`, `src/core/config/config.ts`) takes it as `${<name>}` when `<name>` is already a full ref, else `${secrets/<name>}`, naming the key once. `akm migrate apply`'s `configFile` step (`normalizeConfigFile`) persists that — `configVersion: "0.9.0"`, `secret://` rewritten, unknown keys dropped — to the file once, under a backup | Same as older: the value is named once and the file is read as `0.9.0`; there is no `UNSUPPORTED_CONFIG_VERSION` refusal. `akm health`'s `binary-config-skew` advisory (`src/commands/health/config-skew.ts`) is what says "a newer akm wrote this config, upgrade this install" | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`); `tests/config-v09.test.ts` (including the `secret://` read and rewrite) |
40
40
  | `state.db` | `src/core/state/migrations.ts` (append-only `MIGRATIONS` registry, applied by `src/storage/sqlite-migrations.ts`) | `schema_migrations` ledger table | `openStateDatabase` opens one connection (mkdir, open, standard pragmas), reads the ledger on it, and runs every migration not yet applied forward in one transaction; a missing/empty/table-less file is simply a fresh database. Before a migration that drops schema (`018-drop-dead-lane-schema`, `028-improve-ledger`; `STATE_MIGRATION_SAFETY_BY_ID`) runs against a database that already has migrations applied, the file is copied beside itself to `state.db.pre-<migration id>.bak` via `VACUUM INTO` on the same connection | An older binary opening a `state.db` migrated by a newer akm continues with the schema it knows and warns once (`warnNewerStateLedger`, `src/core/state-db.ts`); it does not refuse; only a ledger that diverges from this akm's registry is refused (`assertMigrationLedger`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) |
41
41
  | `index.db` | `src/storage/repositories/index-schema.ts` (`ensureSchema`; `index-entry-schema.ts` for the DDL and the layout constant) | `index_meta.version` = `CANONICAL_INDEX_DB_VERSION` (26), a layout marker | Readers (`openExistingDatabase` / `openReadonlyExistingDatabase`) serve it as-is and name `akm index` once (`checkIndexLayout`); the writable opener migrates in place — added columns via `ALTER TABLE`, a one-time FTS rebuild from `entries` for the layout-23 content-copy tables, and for layout 24 the drops of the sqlite-vec mirror `entries_vec` (when sqlite-vec loads; dropping a vec0 table needs it) and `entry_fragments_fts`, with `entries.search_text` hashed into `embed_hash` before the column is dropped so no vector is re-embedded, and for layout 25 the declared links (`asset_links`, #935) derived from each entry's stored `document_json` with no file read (only the directories holding workflows and tasks, whose targets no earlier layout stored, lose their incremental cursor and re-drain on the next `akm index`) — never dropping `embeddings`, `utility_scores*`, or `llm_enrichment_cache`. It drops the lazy-extraction queue `graph_extraction_queue`, retired in 0.9.17, and, on any layout, the LLM entity graph (`graph_meta`, `graph_files`, `graph_file_*`) and its `llm_enrichment_cache` rows, retired in 0.9.17-alpha.9 — an older release's `CREATE TABLE IF NOT EXISTS` recreates the graph tables (empty) if it ever opens this index again. It also deletes `llm_enrichment_cache` rows at the default `cache_variant` — the metadata-enhance pass, retired in 0.9.17-alpha.9 — on any layout; memory inference uses its own named variant and is unaffected. A migration leaves `index_meta.vacuumPending`, and the next `akm index` VACUUMs the freed pages. An `entries` table older than layout 21 has its entries-keyed tables recreated: one that lacks a column this release reads, or that still has a transitional column layout 21 removed (`entry_key`, `dir_path`, `stash_dir`, `entry_json`, `entry_type`), as the table 0.9.1 wrote does, with `document_json` empty on every row. A reader finds such a table unservable (`hasCurrentEntriesTable`), so a read rebuilds it inline. Only `SQLITE_CORRUPT` deletes and rebuilds the file (#865) | Refused by readers and the writable opener alike (`INDEX_SCHEMA_INCOMPATIBLE`, "Upgrade akm to use this index.", `newerIndexLayoutError`), leaving the file untouched: a newer layout may lack a column this akm reads, as layout 25 lacks `search_text` | `tests/integration/indexer/index-layout-migration.test.ts` (layout-23 and layout-24 fixtures); `tests/integration/previous-release-corpus.test.ts` (the layout-25 index 0.9.17-alpha.7 wrote, `index-v25.sql`, and the layout-20 index 0.9.1 wrote, `index-v20.sql`); `tests/integration/storage/index-generation-read-boundary.test.ts`; upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) |
42
42
  | Task source (`tasks/*.yml`) | `src/tasks/source/parse-task-source.ts` | `version: 4` (`TASK_SOURCE_V4_VERSION`) | **The named exception to the contract (#987): the runtime reads only v4.** `version: 2` or `version: 3` throws `TASK_SCHEMA_VERSION_UNSUPPORTED`, and a declared `version: 4` document whose `schedule[]` still carries 0.9.15's retired `enabled` key throws `TASK_SOURCE_INVALID`; both messages name `akm migrate apply`, the one place the conversion happens (`scripts/akm-migrate/migrate/task-files.ts`, planning every v2/v3/v4 file straight to v4 with the pure planner `scripts/akm-migrate/migrate/task-to-v4.ts`, rewriting under a backup; `akm upgrade` runs it after an install). It covers every stash the runtime reads tasks from: each enabled configured bundle, and the stash `AKM_BUNDLE_DIR` selects when no bundle names it; the `akm-task` probe still recognizes a root of v2/v3 task files, so one without a recorded adapter is found. Every caller reports the refusal for that one file: `akm task sync` keeps reconciling every other task and leaves the file's installed row as it is | Any other `version` throws `TASK_SCHEMA_VERSION_UNSUPPORTED` naming `akm migrate apply` | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`); `tests/integration/previous-release-corpus.test.ts` (each older shape is refused at runtime and converted by the migrator) |
43
- | Workflow IR (frozen plans) | `src/workflows/freeze/freeze.ts`, read back in `src/workflows/runtime/run-plan.ts` (`readRunPlan`, `decodeWorkflowPlan`) | `irVersion` (`WORKFLOW_PLAN_VERSION`, 6) on the plan and in `plan_ir_version`; `plan_hash` alongside | Every workflow compiles to one plan type. `decodeWorkflowPlan` reads the irVersion 5 shape 0.9.15 and 0.9.16 wrote (its `sourceReadSet` and host-identity fields included) as well as the current one, and keeps keys it does not know. Neither the version nor the hash gates a read: a stored plan that decodes runs whatever `irVersion` froze it, with one warning; one that cannot be decoded is abandoned by `akm workflow run` (status `failed`) with a message naming `akm workflow run <ref>` to start afresh — a status change, never an exception; `status`/`list`/`abandon` never read the plan | A newer akm's plan that still decodes runs the same way; one that does not decode is left untouched and refused with a message naming "Upgrade akm" (`readRunPlan`'s `newer`) | `tests/workflows/run-plan.test.ts`; `tests/integration/workflows/frozen-plan.test.ts` — the rehearsal builds no workflow run |
43
+ | Workflow IR (frozen plans) | `src/workflows/freeze/freeze.ts`, read back in `src/workflows/runtime/run-plan.ts` (`readRunPlan`, `decodeWorkflowPlan`) | `irVersion` (`WORKFLOW_PLAN_VERSION`, 7) on the plan and in `plan_ir_version`; `plan_hash` alongside | Every workflow compiles to one plan type. `decodeWorkflowPlan` reads the irVersion 5 shape 0.9.15 and 0.9.16 wrote (its `sourceReadSet` and host-identity fields included) and the irVersion 6 shape (a shell or command target's `cwdIdentity`, an env-ref's `owner`, `keys` and `secretNames`) as well as the current one, and keeps keys it does not know. The irVersion 6 location fields are kept exactly as stored and never read: a v6 run dispatches by ref, finding the bundle root and the env asset again, but the unit `input_hash` and the child invocation key derive from the whole decoded target (an embedded child plan included), so dropping a field would give an interrupted v6 parent a new key after the upgrade and publish a second child (`irVersion` 7 writes none of them, and the inode and path fields left the shell `contentHash` preimage and the unit `input_hash` for new plans only; stored rows keep their hashes). A runner's `apiKeySecretRef` / `fallbackApiKeySecretRef` in the retired `secret://<name>` form is likewise kept as stored by `decodeFrozenRunnerSpec` and read as `${secrets/<name>}` where it is resolved (`lookupApiKeySecretRefValue`, `resolveLlmCredentialValue`, via `liftRetiredSecretRef`), with no warning. An `irVersion` 7 command target freezes `cwdIdentity: {}`, which the v7 decoder carries and ignores: akm 0.10.0 and earlier decode a command-only v7 plan (they only warn on a version mismatch, and read `cwdIdentity` loosely) and would run it in the invoking directory, but their dispatch (`prepareWorkflowExecution`) refuses an identity it cannot verify with `PATH_ESCAPE_VIOLATION` before anything is spawned. A v7 shell target or `env:` ref already fails their decode. The hash gates nothing and neither does an older version: a stored plan that decodes runs whatever older `irVersion` froze it, with one warning; one that cannot be decoded is abandoned by `akm workflow run` (status `failed`) with a message naming `akm workflow run <ref>` to start afresh — a status change, never an exception; `list`/`abandon` never read the plan, and `status --units` decodes it, read-only and without a warning, only to describe a failed unit | A plan whose `plan_ir_version` is above this release's is left untouched and refused, whether or not it would decode, with a message naming "Upgrade akm" (`readRunPlan`'s `newer`) | `tests/workflows/run-plan.test.ts`; `tests/integration/workflows/legacy-plan-child-identity.test.ts` (a v6 parent resumed after the upgrade recovers its child); `tests/integration/workflows/frozen-plan.test.ts`; `tests/integration/workflows/frozen-identities.test.ts` (a v6 `plan_json` row dispatches by ref) — the rehearsal builds no workflow run |
44
+ | Workflow run rows (`workflow_runs.scope_key`, `workflow_entry_id`) | `src/storage/repositories/workflow-runs-repository.ts` (`insertRun`, `publishChildWorkflowRun`) | No marker; the columns stay in the schema | A row from before 0.10 carries a directory-hash `scope_key` (`dir:v1:…`) and, for a top-level run, an `index.db` `workflow_entry_id`. Neither is read: `list`, `status`, `resume`, `abandon` and the start warning select runs by run id and `workflow_ref` alone, so those rows stay visible from any directory. New rows (0.10, #1239) write NULL in both columns. `bundle rename` rewrites `workflow_ref` on every row naming the old bundle prefix (a legacy unqualified ref is left alone) and lists the renamed bundle's active and blocked runs, whose frozen `plan_json` keeps the old qualified refs | A 0.9.15 binary on the same `state.db` filters runs by `scope_key`, so it does not see runs 0.10 started (owner-accepted, #1239): finish or abandon them before downgrading | `tests/integration/state-db/state-migration-023.test.ts`, `state-migration-024.test.ts` (the column set); `tests/workflows/run-resume-and-new.test.ts` (new rows write NULL) |
44
45
  | Native scheduler rows (crontab, launchd plists, Task Scheduler) | `src/tasks/backends/cron.ts`, `launchd.ts`, `schtasks.ts`; the invocation grammar in `src/tasks/scheduler-invocation.ts` | The row's own invocation shape: current rows set their environment inline (a cron `VAR=value` prefix, a plist `EnvironmentVariables` entry, a PowerShell `$env:` assignment) — always `AKM_BUNDLE_DIR`, the syncing shell's working stash — and carry no `--scheduler-context`; 0.9.0 – 0.9.17-alpha.6 rows carry `--scheduler-context <descriptor>` holding the same values; older rows carry neither. A cron command over 1,000 bytes runs an akm wrapper script (`sh <log dir>/.akm-cron-wrapper-…sh`) that holds the same line | Every shape is listed and parsed (`parseScheduledInvocationArgv`; a spilled cron row from its wrapper script): a row inside akm's own `# akm:task` markers from before `--scheduler-context` (#881), and a `--scheduler-context` row, which keeps firing (the CLI applies its descriptor, below) until the next plain `akm task sync` rewrites it in place — an update that keeps the row's launcher and schedule, never an add or remove. The primary bundle proves a row by the `AKM_BUNDLE_DIR` it carries, inline or in its descriptor (#846); any other bundle owns rows by name. Rows that still carry the pre-0.9.17 workflow execution-evidence marker (`# akm:workflow-evidence` in the crontab, an `akm-workflow-evidence` plist comment or PowerShell literal) are listed like any other and rewritten once without it. Plain `akm task sync` keeps an installed row's own launcher (`installOptionsFor`, `src/tasks/scheduler-sync.ts`) and recomputes its environment; `akm task sync --rebind` repoints the launcher | A row inside akm's markers, `com.akm.task.` label namespace, or `\akm\` task folder whose argv this binary cannot parse (for example one a newer akm wrote) is not listed: sync leaves it alone unless an enabled source renders a row with the same native id, which then replaces it — the row is derived state and its source is the record. A crontab whose akm markers are malformed is refused unmodified (`parseBlocks`, `cron.ts`) | Upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`: the previous release's `--scheduler-context` rows fire under the candidate, then `task sync --dry-run` shows them only as updates); `tests/tasks-sync.test.ts` (`--scheduler-context` rows and spilled rows are updates, never adds) |
45
46
  | Scheduler-context descriptor (`$DATA/tasks/context/<sha256>.json`, written by 0.9.0 – 0.9.17-alpha.6) | No longer written: a row carries its own environment (#987). Read by `src/tasks/scheduler-invocation.ts` (`readLegacySchedulerContext`) | `version: 1`; the environment key set | When a row that names one fires, the CLI reads the file as plain JSON and applies its `AKM_*_DIR` keys and, in one written before 0.9.17, `PATH`; nothing else about the file (name, mode, owner, link) is checked. Sync reads its `AKM_BUNDLE_DIR` to attribute the row. A plain `akm task sync` that reconciles the row rewrites it without the argument; a row a sync leaves as it is (its source failed, a `--bundle` sync did not cover it) keeps naming the file | A key this binary does not know is ignored. A file that is gone or is not JSON fails the run with `INVALID_CONFIG_FILE` naming `akm task sync`; sync cannot attribute such a row to the primary bundle and leaves it in place, and `akm task prune` reports it as `invalid-context` | `tests/tasks-scheduler-invocation.test.ts`; `tests/integration/scheduler-context-launcher.test.ts`; upgrade rehearsal gate (`tests/integration/upgrade-rehearsal/`) fires the previous release's rows under the candidate before and after its plain `task sync` |
46
47
  | Proposal `metadata_json` | `src/storage/repositories/proposals-repository.ts` | No explicit version field; presence/absence of the `changes` key | Rows from before the `changes` key existed (~89% of archived rows on real installs) are treated as a known legacy gap — decoded with an empty change list rather than thrown (`proposals-repository.ts:47-56`). 0.9.17-alpha.9 adds a consolidate pair-pass `retire` proposal's `changes[0].op: "delete"` (no `after`, already a valid `FileChangeOp`) and its `retirement` / `retiredArchive` / `promotionSource` / `promotionSourceHash` metadata keys, plus the retirement continuity check's `retirement.continuityRisk` (rule R3, item 1) — a person can accept a proposal carrying it by id even though it is excluded from bulk accept; an older reader's `validatePresentMetadata` does not enumerate these keys, so it neither inspects nor rejects them — the row decodes with them silently absent, same as any other field an older release predates. 0.9.19 adds `reviewHistory` (the rejections `akm proposal reopen` undid, oldest first, each with its gate verdict) the same way: a proposal reopened by 0.9.19 and read by an older release is an ordinary pending proposal with its history absent, and that release drops the key if it rewrites the row. `retirement.judgedScope` (Unreleased, #1197) optionally records the shared normalized memory scope tuple; `{}` means known unscoped and absence means unknown. Older rows still decode unchanged; acceptance refuses an unknown tuple and asks for rejection/reassessment, rather than inferring judgment from live files. Staged stale decisions use the existing stale-target rejection/ledger retry path. Older readers carry this nested object through but do not enforce it, so do not downgrade before reviewing pending retirements. No format-version bump or database migration is needed for this additive metadata. The quality judge's evidence, `scores` and `judgeReason`, is added inside the existing `gateDecision` object (Unreleased): an older reader's `validatePresentMetadata` checks only the gate fields it knows and carries the object through whole, so the evidence survives a rewrite by an older release unread. An older release also counts a pending proposal's age (retention expiry, `--older-than`) from `createdAt`, so after a downgrade a reopened proposal can expire at the next `akm improve` run (a retire proposal never expires), or be swept by a scheduled `--older-than` bulk action or drain | 0.9.17-alpha.8 and earlier predate the retire proposal shape entirely — they were never taught to read a delete-primary change with no `after`. `akm proposal show` / `diff` / `drain` exit 70 on one, and drain's own nightly pre-pass failing on the first pending retire proposal it meets stops that run's auto-promotion for the whole stash, not just that one proposal. **Downgrade gate:** every pending retire proposal must be accepted or rejected (`akm proposal accept` / `reject`) before downgrading to 0.9.17-alpha.8 or earlier — once decided it is archived, out of the pending set `show`/`diff`/`drain` iterate, and no longer trips this. **Gap** otherwise (no code-level guard; this is operator guidance only). Downgrading also revives the starvation the second review round's B1 fixed forward-only: 0.9.17-alpha.8's `retrieval-scope.ts` counts a `consolidate-pair` ledger row as processed, so the pair pass's own nightly attempts crowd new material back out of every other improve lane's scope, the same way they did before B1 — measured on the real bundle, two alpha.9 nights left only 341 of 2,183 new-only assets still in scope once read under alpha.8. **Gap** here too (the ledger `source` string carries no version of its own; operator guidance only — do not downgrade across a pair-pass run without expecting this). | `tests/proposal-repository-pure.test.ts` — unit only; the rehearsal builds no proposal |
@@ -727,7 +727,7 @@ split first). Recompute when those land.
727
727
 
728
728
  Under the agent-toolkit identity, the following were reviewed and are **not**
729
729
  defects of scope, only subject to the coherence/safety issues above:
730
- `akm env`/`akm secret` (including run-injection and `${secret:}` interpolation),
730
+ `akm env`/`akm secret` (including run-injection and `${ref}` interpolation),
731
731
  `akm agent` and the ten harness integrations, the workflow engine including the
732
732
  `brief`/`report` external-driver protocol, `akm tasks` and its three OS
733
733
  backends, the improve/proposal/collapse-detector loop, and the health
@@ -1118,8 +1118,8 @@ Secret-bearing content MUST preserve current protections:
1118
1118
  - no values in output or telemetry;
1119
1119
  - no shell sourcing of untrusted dotenv content;
1120
1120
  - process injection without command substitution;
1121
- - path containment;
1122
- - dangerous environment-key policy;
1121
+ - write-side path containment;
1122
+ - the non-blocking `dangerous-env-key` lint rule (#1237 removed the install and injection gates);
1123
1123
  - redaction in logs and errors.
1124
1124
 
1125
1125
  ---
@@ -1677,7 +1677,7 @@ grant or silently reduce it.
1677
1677
 
1678
1678
  *(Two v0.2 clauses — untrusted presentation clamping and a catch-all sensitive-content refusal — were withdrawn in v0.3, deviation §4.3c. Catch-all adapters remain explicit-configuration-only and never auto-selected (§9.4): a user who mounts a root with `generic-files` indexes what they pointed it at, deliberately.)*
1679
1679
 
1680
- The dangerous-environment-key policy keeps its origin asymmetry across the config migration **as a port of the existing rule** (today: `registryId`-bearing source → hard block; first-party → warn): an env export from a registry-installed source containing process-hijacking keys MUST still hard-error after `installed[]` is replaced, covered by a port-preservation conformance test. This is existing behavior surviving the migration, not new machinery.
1680
+ *(Amended 2026-10-22, #1237: the dangerous-environment-key policy and its origin asymmetry were removed. Installing a bundle is the trust decision, so env export and injection from any bundle neither block nor warn on process-hijacking keys. The `dangerous-env-key` lint rule remains, non-blocking.)*
1681
1681
 
1682
1682
  ### 28.3 Path safety
1683
1683