akm-cli 0.9.30 → 0.9.32

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 (165) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/dist/commands/feedback-cli.js +21 -13
  5. package/dist/commands/improve/improve-cli.js +33 -12
  6. package/dist/core/errors.js +1 -0
  7. package/dist/core/file-lock.js +10 -1
  8. package/dist/indexer/usage/usage-events.js +6 -2
  9. package/dist/llm/client.js +33 -3
  10. package/dist/scripts/akm-migrate-node.js +6 -1
  11. package/dist/scripts/akm-migrate.js +6 -1
  12. package/docs/README.md +42 -41
  13. package/docs/agents/README.md +15 -0
  14. package/docs/agents/agent-install.md +14 -0
  15. package/docs/architecture/README.md +67 -0
  16. package/docs/architecture/adapters.md +272 -0
  17. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  18. package/docs/architecture/akm-core-principles.md +126 -0
  19. package/docs/architecture/architecture.md +579 -0
  20. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  21. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  22. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  23. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  24. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  25. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  26. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  27. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  28. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  29. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  30. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  31. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  32. package/docs/architecture/decisions/README.md +62 -0
  33. package/docs/architecture/improvement.md +440 -0
  34. package/docs/architecture/internals/classification.md +154 -0
  35. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  36. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  37. package/docs/architecture/internals/health-advisories.md +110 -0
  38. package/docs/architecture/internals/improve-workflow.md +713 -0
  39. package/docs/architecture/internals/indexing.md +498 -0
  40. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  41. package/docs/architecture/internals/search.md +249 -0
  42. package/docs/architecture/internals/storage-locations.md +943 -0
  43. package/docs/architecture/persisted-data-compat.md +59 -0
  44. package/docs/architecture/reviews/env-secret-access.md +319 -0
  45. package/docs/architecture/runtime-boundary-design.md +153 -0
  46. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  47. package/docs/architecture/specs/0.9.0-decisions.md +528 -0
  48. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  49. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  50. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  51. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  52. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  53. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  54. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  55. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  56. package/docs/architecture/specs/di-seams-plan.md +539 -0
  57. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  58. package/docs/architecture/specs/fact-asset-type.md +142 -0
  59. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  60. package/docs/architecture/specs/okf-support.md +148 -0
  61. package/docs/architecture/specs/ref.md +329 -0
  62. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  63. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  64. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  65. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  66. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  67. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  68. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  69. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  70. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  71. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  72. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  73. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  74. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  75. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  76. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  77. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  78. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  79. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  80. package/docs/architecture/specs/workflow-format-unification.md +437 -0
  81. package/docs/architecture/testing/manual-testing-checklist.md +3420 -0
  82. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  83. package/docs/architecture/testing/task-workflow-manual-tests.md +1817 -0
  84. package/docs/architecture/testing/testing-workflow.md +446 -0
  85. package/docs/architecture/workflow-engine.md +274 -0
  86. package/docs/guides/README.md +33 -0
  87. package/docs/guides/author-bundles.md +310 -0
  88. package/docs/guides/author-workflows.md +520 -0
  89. package/docs/guides/bundles.md +321 -0
  90. package/docs/guides/capture-knowledge.md +136 -0
  91. package/docs/guides/claude-code-vs-akm-workflows.md +30 -0
  92. package/docs/guides/concepts.md +93 -0
  93. package/docs/guides/discover-and-load.md +158 -0
  94. package/docs/guides/getting-started.md +240 -0
  95. package/docs/guides/improve-the-library.md +246 -0
  96. package/docs/guides/knowledge-management.md +18 -0
  97. package/docs/guides/recipes/headless-install.md +97 -0
  98. package/docs/guides/recipes/website-source.md +28 -0
  99. package/docs/guides/run-workflows.md +279 -0
  100. package/docs/guides/scheduling.md +123 -0
  101. package/docs/guides/use-with-any-agent.md +129 -0
  102. package/docs/guides/wikis.md +81 -0
  103. package/docs/maintainers/README.md +19 -0
  104. package/docs/maintainers/curate-workmap.md +336 -0
  105. package/docs/maintainers/eval.md +3 -0
  106. package/docs/maintainers/local-development.md +108 -0
  107. package/docs/maintainers/release-checklist.md +103 -0
  108. package/docs/migration/README.md +2 -2
  109. package/docs/migration/release-notes/0.0.13.md +1 -1
  110. package/docs/migration/release-notes/0.1.0.md +1 -1
  111. package/docs/migration/release-notes/0.2.0.md +1 -1
  112. package/docs/migration/release-notes/0.3.0.md +1 -1
  113. package/docs/migration/release-notes/0.5.0.md +1 -1
  114. package/docs/migration/release-notes/0.6.0.md +1 -1
  115. package/docs/migration/release-notes/0.7.0.md +1 -1
  116. package/docs/migration/release-notes/0.7.3.md +2 -2
  117. package/docs/migration/release-notes/0.7.4.md +1 -1
  118. package/docs/migration/release-notes/0.7.5.md +1 -1
  119. package/docs/migration/release-notes/0.8.0.md +1 -1
  120. package/docs/migration/release-notes/0.9.0.md +1 -1
  121. package/docs/migration/release-notes/0.9.14.md +1 -1
  122. package/docs/migration/release-notes/0.9.15.md +1 -1
  123. package/docs/migration/release-notes/0.9.16.md +1 -1
  124. package/docs/migration/release-notes/0.9.17.md +1 -1
  125. package/docs/migration/release-notes/0.9.19.md +1 -1
  126. package/docs/migration/release-notes/0.9.2.md +1 -1
  127. package/docs/migration/release-notes/README.md +27 -31
  128. package/docs/migration/v0.5-to-v0.6.md +759 -0
  129. package/docs/migration/v0.8-to-v0.9.md +1 -1
  130. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  131. package/docs/posts/README.md +30 -0
  132. package/docs/posts/akm-command-tour.md +495 -0
  133. package/docs/posts/akm-logo-sized.webp +0 -0
  134. package/docs/posts/belief-aware-memory-12.md +139 -0
  135. package/docs/posts/config-v2-lessons-16.md +191 -0
  136. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  137. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  138. package/docs/posts/intro-part-01.md +76 -0
  139. package/docs/posts/intro-part-02.md +221 -0
  140. package/docs/posts/intro-part-03.md +190 -0
  141. package/docs/posts/intro-part-04.md +221 -0
  142. package/docs/posts/llm-mode-reflect-13.md +166 -0
  143. package/docs/posts/openpalm-built-on-akm.md +86 -0
  144. package/docs/posts/progressive-disclosure-06.md +129 -0
  145. package/docs/posts/proposal-safety-net-15.md +112 -0
  146. package/docs/posts/release-0.5.0.md +247 -0
  147. package/docs/posts/release-0.6.0.md +117 -0
  148. package/docs/posts/release-0.7.0.md +159 -0
  149. package/docs/posts/release-0.8.0.md +332 -0
  150. package/docs/posts/scattered-skills-05.md +100 -0
  151. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  152. package/docs/posts/team-skills-07.md +198 -0
  153. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  154. package/docs/posts/workflows-vaults-09.md +321 -0
  155. package/docs/product-surface.md +54 -0
  156. package/docs/reference/README.md +1 -2
  157. package/docs/reference/asset-types.md +168 -0
  158. package/docs/reference/cli.md +5 -6
  159. package/docs/reference/data-and-telemetry.md +6 -0
  160. package/docs/reference/env-and-secrets.md +126 -0
  161. package/docs/reference/memory.md +124 -0
  162. package/docs/reference/refs.md +156 -0
  163. package/docs/reference/registry.md +489 -0
  164. package/docs/reference/website-sources.md +141 -0
  165. package/package.json +12 -15
@@ -0,0 +1,142 @@
1
+ # 0002 — Unit reuse and the input-hash scope
2
+
3
+ ## Context
4
+
5
+ A resumed workflow run must recompute the same units the original run
6
+ dispatched, byte-for-byte, so that journaled rows can stand in for a live
7
+ dispatch instead of re-executing. That guarantee depends on two things
8
+ staying strictly separated: which computations are PURE functions of
9
+ `(frozen plan, params, journaled results)`, and exactly which fields feed
10
+ the hash that decides whether a journaled unit is still valid to reuse.
11
+ `src/workflows/exec/step-work.ts` is the one module both the fresh-execution
12
+ path (`native-executor.ts`) and the resume/replay path share for these
13
+ decisions, specifically so the two paths cannot drift apart.
14
+
15
+ ## Decision
16
+
17
+ ### The module's purity contract
18
+
19
+ Moved verbatim from `step-work.ts`'s module header:
20
+
21
+ > Shared step semantics — the ONE implementation of a step's orchestration
22
+ > decisions, consumed by the engine loop (`run-workflow.ts` +
23
+ > `native-executor.ts`) on both the fresh-execution and the resume/replay
24
+ > path. The cardinal rule here is *no duplicated semantics*: work-list
25
+ > computation, prompt assembly, reducer/artifact promotion, output-schema
26
+ > validation, artifact-judged gate summaries, gate-feedback recovery, and
27
+ > route evaluation live here so a first run and a resumed run of the same
28
+ > frozen plan produce byte-identical unit graphs.
29
+ >
30
+ > **What is PURE here.** `computeStepWorkList` — given the frozen step plan
31
+ > and a `WorkListInput` (params, prior step outputs, gate-loop number + its
32
+ > recovered feedback) — is a pure function: same inputs ⇒ same unit ids,
33
+ > input hashes, and fully-resolved prompts. It takes NO clock, NO IO, and NO
34
+ > journal (journal-derived state, i.e. the recovered gate feedback, is
35
+ > passed in). This is the load-bearing guarantee that a resumed run
36
+ > recomputes exactly the units the original run dispatched, so journaled
37
+ > rows can be reused instead of re-executed. So are the reducer/artifact
38
+ > helpers (`buildEvidence`, `projectStepOutput`, `validateStepArtifact`,
39
+ > `buildArtifactSummary`), the gate-feedback recovery (`recoverGateFeedback`
40
+ > / `activeGateLoop`), and route evaluation (`evaluateRoute` and its
41
+ > bookkeeping).
42
+ >
43
+ > **What does IO here.** The gate-evaluation journaling
44
+ > (`journalGateEvaluationStart` / `journalGateEvaluationFinish`) writes
45
+ > `workflow_run_units` rows through the serialized writer queue — an
46
+ > engine-driven judge call is an LLM call and is journaled like a unit. It
47
+ > lives here (not in the engine loop) so every caller journals gate
48
+ > evaluations through the identical writer.
49
+ >
50
+ > This module NEVER dispatches a unit and NEVER writes step rows: dispatch
51
+ > is the executor's job (`native-executor.ts`), advancing the gated spine is
52
+ > the engine loop's job (`run-workflow.ts` via `completeWorkflowStep`).
53
+
54
+ ### The input-hash preimage
55
+
56
+ Moved verbatim from `computeUnitInputHash`'s doc comment:
57
+
58
+ > The canonical dispatch-input envelope (reviewer finding #1). Every field
59
+ > here is a PLAN-FROZEN input that changes what the backend is actually
60
+ > asked to do, so a completed unit is reused ONLY when all of them match; a
61
+ > change to any of them re-dispatches. `canonicalJsonString` sorts keys
62
+ > recursively, so the preimage is order-independent, and this is the ONE
63
+ > place a unit's inputHash is computed (every caller goes through
64
+ > `computeStepWorkList`) — a hash that is byte-identical across a fresh run
65
+ > and a resume is structural, not coincidental.
66
+ >
67
+ > Unit identity (workflow-format-unification, spec §2.3/§4) hashes the
68
+ > FROZEN TEMPLATE BYTES (`template.instructions`, byte-exact, never an
69
+ > instantiated/interpolated string) + the canonical item JSON + the
70
+ > declared-input artifacts + the params snapshot — instead of a
71
+ > resolved/spliced prompt string, since there is no more splicing.
72
+ >
73
+ > Included beyond the template/runner/model/schema baseline: resolved
74
+ > timeoutMs, named environment bindings, and isolation — each reaches
75
+ > dispatch and a changed one yields a materially different call. `env`
76
+ > carries NAMES ONLY, never resolved values: hashing a resolved secret would
77
+ > leak it into a durable hash oracle and would spuriously re-dispatch on
78
+ > every secret rotation. `retry`/`onError` are DELIBERATELY excluded — they
79
+ > govern failed-unit re-dispatch and step-level failure reduction, not a
80
+ > COMPLETED unit's inputs/output, so a completed row stays valid across
81
+ > policy changes.
82
+ >
83
+ > `gateFeedback` IS included (conditionally, so a no-feedback unit's
84
+ > preimage is byte-identical to before): it is appended to the prompt by
85
+ > `buildUnitPrompt`, so a gate loop's retry is materially a different ask
86
+ > than the rejected attempt. Replay-safe: feedback is re-derived from the
87
+ > journaled gate decision, so a resumed retry re-hashes identically.
88
+ >
89
+ > `taskInputs` IS included, on the same conditional terms (R-R15,
90
+ > `hashVersion` 7): a reference binding's RESOLVED value reaches the unit's
91
+ > prompt / `AKM_TASK_INPUTS` / `childParams`, so a changed upstream value is
92
+ > a materially different ask even though the binding's authored shape inside
93
+ > `frozenTarget` is unchanged. Hashing it makes a resume whose journaled
94
+ > upstream output was altered fail loudly as replay divergence instead of
95
+ > silently reusing the stale row. The key is absent for a unit whose target
96
+ > carries no `inputBindings`, so a binding-free unit's preimage keeps the
97
+ > shape it had — only the version fields moved.
98
+ >
99
+ > Command targets carry their frozen argv/script/cwd/timeout identity in the
100
+ > same target slot used by agent, SDK, and direct-LLM work. The complete
101
+ > current target is hashed once, so a completed unit is reused only for the
102
+ > exact durable request that originally produced it.
103
+ >
104
+ > Ambient config is deliberately excluded because it is not consulted
105
+ > during execution. The frozen target and named environment bindings are
106
+ > the runtime identity boundary; only the values behind those names remain
107
+ > live.
108
+
109
+ ## Consequences
110
+
111
+ - Any new field added to a unit's dispatch plan must be triaged against
112
+ this scope explicitly: does it change what the backend is asked to do
113
+ (include it), is it a policy about failure handling rather than a
114
+ completed unit's inputs/output (exclude it, like `retry`/`onError`), or is
115
+ it a resolved secret value (exclude the value, include only the name)?
116
+ Getting this wrong either lets a materially different request reuse a
117
+ stale journaled row, or spuriously invalidates every journaled row on an
118
+ unrelated change.
119
+ - `hashVersion` (currently **7**) is the version prefix mixed into this hash;
120
+ changing what this preimage covers requires bumping it. P4's R-R15 carried
121
+ advisory — a reference binding's RESOLVED value sitting outside this hash —
122
+ is exactly such a scope change, and it is now **resolved**: `taskInputs`
123
+ (the resolved values of a composed task's reference-kind `inputBindings`) is
124
+ a conditional preimage field, present only for a unit whose frozen target
125
+ carries `inputBindings`, and closing it came with the bump this rule
126
+ mandates. The 0.9.2 line's released tags (`v0.9.2-alpha.1`…`alpha.4`) all
127
+ carry 5, so the durable, user-visible step is 5 → 7. The "left unfixed
128
+ rather than folded in as a quiet scope change" note in
129
+ `docs/plans/specs/p4-deletions-closeout.md` §8 R-R15 (and p3b §3.6's "why
130
+ `hashVersion` stays 6") records the state BEFORE that bump and is
131
+ superseded by this entry.
132
+ - Because the whole module is import-cycle-free with respect to dispatch
133
+ (it never dispatches, never writes step rows), the fresh-execution and
134
+ resume/replay paths cannot silently diverge by one of them bypassing this
135
+ module's decisions.
136
+
137
+ ## Provenance
138
+
139
+ - Source: `src/workflows/exec/step-work.ts`, module header (the Purity
140
+ Contract) and `computeUnitInputHash`'s doc comment (reviewer finding #1).
141
+ - Spec: workflow-format-unification, spec §2.3/§4.
142
+ - Extracted: P4 (`docs/plans/specs/p4-deletions-closeout.md` §4.2), 2026-08-27.
@@ -0,0 +1,189 @@
1
+ # 0003 — The exec unit's child-environment allowlist and provenance
2
+
3
+ ## Context
4
+
5
+ `src/workflows/exec/exec-unit.ts` is the one place a frozen workflow spawns
6
+ a shell command as a unit. A spawned child inherits none of AKM's own
7
+ process safety by default — no argv-injection protection, no environment
8
+ isolation, no output bound — so every one of those has to be a deliberate
9
+ design choice in this one module rather than an accident of whatever
10
+ `child_process` defaults to.
11
+
12
+ ## Decision
13
+
14
+ ### The module's invariants
15
+
16
+ Moved verbatim from `exec-unit.ts`'s module header:
17
+
18
+ > The `exec` unit runner — the ONE place a frozen workflow spawns a shell
19
+ > command as a unit. The invariants it exists to hold:
20
+ >
21
+ > - ARGV, NEVER A SHELL STRING. `IrExecSpec.command` is an argv ARRAY and
22
+ > the format has no shell-string spelling at all; the child is spawned
23
+ > directly, so `;`, `|`, `&&`, `$(…)`, backticks, `>` and `*` are inert
24
+ > literal argument BYTES. A workflow that wants a pipeline names the
25
+ > interpreter itself (`["bash", "-lc", "a | b"]`), visibly in frontmatter.
26
+ > - NON-BLOCKING. Everything on this path is async. A synchronous call here
27
+ > blocks the event loop and with it every concurrently-scheduled unit,
28
+ > the run's lease heartbeat, and abort handling.
29
+ > - NO LEAKED CHILDREN. `runManagedSubprocess` spawns DETACHED and runs a
30
+ > SIGTERM→SIGKILL ladder against the whole process group, so `--timeout`
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.
36
+ > - BOUNDED SPEND. A command is arbitrary code with no resource discipline
37
+ > of its own, so each resource it spends on akm's behalf has a ceiling in
38
+ > `workflows/resource-limits.ts`: wall clock (`DEFAULT_EXEC_TIMEOUT_MS` or
39
+ > the authored `timeout:`), retained output
40
+ > (`WORKFLOW_MAX_EXEC_OUTPUT_BYTES` per pipe) and the context environment
41
+ > (`execContextLimits`, checked BEFORE the spawn so an oversized artifact
42
+ > yields an actionable akm error, not a bare `E2BIG`).
43
+ > - ALLOWLISTED ENVIRONMENT. The child does NOT inherit akm's environment:
44
+ > it starts EMPTY and receives exactly `EXEC_DEFAULT_ENV_PASSTHROUGH` plus
45
+ > the unit's `exec.passEnv`, then the resolved `env:` bindings, then the
46
+ > engine-authored `AKM_*` context. See `childEnv`.
47
+ >
48
+ > Secrets: `env` values reaching this module are already resolved from
49
+ > `env:` bindings by NAME (`resolveEnvBinding`) — the plan never carries
50
+ > inline secrets and the input hash only ever carries names. The caller
51
+ > scrubs the outcome with `redactUnitOutcome` BEFORE anything is journaled,
52
+ > which is why this module may return raw stdout/stderr diagnostics without
53
+ > knowing anything about redaction.
54
+ >
55
+ > Layering: a LEAF. Node built-ins, `core/common` (for `isWithinAsync`, the
56
+ > CONTAINMENT recheck above), `core/spawn-env`, `core/subprocess`,
57
+ > `core/warn` and the import-free `workflows/resource-limits` (plus erased
58
+ > types) only, so the executor can consume it without opening an import
59
+ > cycle.
60
+
61
+ ### The default allowlist and why it is an allowlist
62
+
63
+ Moved verbatim from `EXEC_DEFAULT_ENV_PASSTHROUGH`'s doc comment and
64
+ `childEnv`'s doc comment:
65
+
66
+ > The DEFAULT environment allowlist for an exec unit's child — the single
67
+ > definition of the EXEC list (the docs describe it, the tests assert
68
+ > against it, and `exec.passEnv` extends it per unit). The win32
69
+ > process-creation names are not re-spelled here: they are spread from
70
+ > `WIN32_SPAWN_ENV_FLOOR`, which owns them.
71
+ >
72
+ > The child starts from an EMPTY environment and receives only these names,
73
+ > matching how an agent harness child is already built
74
+ > (`profile.envPassthrough` → `collectAllowlistedEnv`) — and literally
75
+ > extending the same `COMMON_SPAWN_ENV_PASSTHROUGH` baseline those profiles
76
+ > start from, so the two child-spawn allowlists share one floor. Every
77
+ > entry earns its place by being load-bearing for ordinary commands on some
78
+ > supported platform: `PATH` (command resolution), `HOME` (the config/cache
79
+ > root essentially every toolchain reads), `USER`/`LOGNAME` (process
80
+ > identity for git/ssh), `SHELL` (tools that re-exec a login shell),
81
+ > `LANG`/`LC_ALL`/`LC_CTYPE` (text encoding — without a locale a command
82
+ > falls back to the C locale and mangles non-ASCII stdout, which IS this
83
+ > unit's artifact), `TERM` (some CLIs abort or emit raw escape bytes with no
84
+ > TERM), `TZ` (timestamps a command prints would otherwise silently switch
85
+ > to the host default), `TMPDIR` (POSIX scratch space), the win32 spawn
86
+ > floor plus `APPDATA`/`LOCALAPPDATA`/`ProgramData`/`ProgramFiles` (Windows
87
+ > toolchain roots not on the floor itself), and `AKM_EVENT_SOURCE`
88
+ > (provenance, never a secret: an exec unit that calls `akm` must record
89
+ > machine traffic rather than user demand, exactly as the agent passthrough
90
+ > list does — DRIFT-6).
91
+ >
92
+ > Deliberately ABSENT and reachable only through `exec.passEnv` / `env:`:
93
+ > credentials of every kind, cloud/CI vars, and the proxy family
94
+ > (`HTTP_PROXY` & friends) — proxy URLs routinely embed credentials, which
95
+ > is why akm's redaction policy already treats URL-shaped passthrough
96
+ > values as credential-bearing.
97
+ >
98
+ > The child's environment resolves in three layers with fixed precedence:
99
+ > (1) the BASE — the default allowlist plus the unit's `exec.passEnv`
100
+ > names; (2) the unit's resolved `env:` bindings; (3) the engine-authored
101
+ > `AKM_*` context, LAST so a workflow-supplied binding can never shadow the
102
+ > ids/item the engine is telling the command the truth about.
103
+ >
104
+ > **Why the default is an allowlist.** Not because it stops an attacker: a
105
+ > command that runs at all can read the same credentials off disk that the
106
+ > environment would have handed it, and a workflow source is executed code
107
+ > either way (`docs/guides/run-workflows.md`, "workflow sources are
108
+ > executed code"). The allowlist earns its place for three narrower, real
109
+ > reasons: it bounds ACCIDENTAL exposure — the ambient shell of whoever ran
110
+ > `akm workflow run` (or the CI job that did) routinely carries tokens for
111
+ > unrelated services, and a third-party workflow step that merely prints
112
+ > its environment should not get them for free; it makes the environment
113
+ > surface EXPLICIT and REVIEWABLE — what a command can see is this constant
114
+ > plus lines in the frontmatter diff, rather than "whatever the invoking
115
+ > shell happened to export"; and it matches the convention akm already
116
+ > applies to spawned children — `profile.envPassthrough` in
117
+ > `integrations/agent/spawn.ts` has always built agent-harness children
118
+ > this way, and the SAME `collectAllowlistedEnv` does it here, so there is
119
+ > one mechanism to review instead of two.
120
+
121
+ ### Capture and overflow semantics
122
+
123
+ Moved verbatim from `runExecUnit`'s doc comment:
124
+
125
+ > Run one exec unit and map its process outcome onto the dispatch
126
+ > vocabulary: non-zero exit → `non_zero_exit`, wall-clock expiry →
127
+ > `timeout`, cancellation → `aborted`, a child that never started →
128
+ > `spawn_failed`. All four are pre-existing `AgentFailureReason` members, so
129
+ > `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
132
+ > authoring bug, or work that ALREADY RAN — never a transient — so no
133
+ > `retry.on` value can ever re-dispatch one.
134
+ >
135
+ > **An INCOMPLETE STDOUT capture is a failure, never a partial artifact.**
136
+ > `exitCode === 0` is not on its own proof that stdout was fully read: a
137
+ > pipe can error, and the stream-drain timeout can fire while the command
138
+ > LEADER has already exited 0 because a background descendant still holds
139
+ > the stdout fd open. Both leave a PREFIX, and promoting it would hand the
140
+ > next step, the gate judge and `steps.<id>.output` a silently truncated
141
+ > artifact. So the unit fails instead, through the same shared classifier
142
+ > (`streamCaptureFailure`) the agent spawn path uses. Only STDOUT is fatal
143
+ > here — stderr is a diagnostic channel that never contributes to the
144
+ > artifact, so a stderr drain that did not finish leaves the unit's actual
145
+ > result intact. The stdout reason is `exec_capture_incomplete`,
146
+ > deliberately OUTSIDE the `retry.on` taxonomy, for the same reason
147
+ > `journal_write_failed` is: the command RAN TO COMPLETION and exited 0 —
148
+ > what failed is akm's record of it. A retryable reason here would let
149
+ > `retry.on: [spawn_failed]` re-dispatch byte-identical argv for a command
150
+ > that already deployed, already published, already migrated.
151
+ >
152
+ > **Output OVERFLOW does not fail a command that passed.** Crossing
153
+ > `WORKFLOW_MAX_EXEC_OUTPUT_BYTES` is a different condition: the reader DID
154
+ > drain the pipe to its end, it just stopped RETAINING, so the child never
155
+ > blocked and its exit code is real. Overflow splits by what the unit
156
+ > PROMISED about its output: no declared `output:` schema → success, with
157
+ > the artifact carrying a `WORKFLOW_EXEC_OUTPUT_TRUNCATED_MARKER` block
158
+ > naming both byte counts; a declared `output:` schema →
159
+ > `exec_output_limit`, since a truncated JSON prefix cannot parse and
160
+ > promoting it would corrupt every downstream reference to the typed
161
+ > artifact. stderr overflow never fails anything.
162
+
163
+ ## Consequences
164
+
165
+ - Adding a new name to `EXEC_DEFAULT_ENV_PASSTHROUGH` is a real security
166
+ and portability decision, not a drive-by convenience edit — every
167
+ existing entry earns its place with a documented reason above, and a new
168
+ one should be held to the same bar.
169
+ - The allowlist mechanism (`collectAllowlistedEnv`) is deliberately the
170
+ SAME one `integrations/agent/spawn.ts` uses for agent-harness children —
171
+ a divergence between the two lists is a drift to catch, not a place to
172
+ special-case exec units.
173
+ - The engine-authored `AKM_*` context is applied LAST specifically so no
174
+ authored `env:` binding can ever shadow it; any future third environment
175
+ layer must preserve that ordering guarantee or a workflow step gains the
176
+ ability to spoof its own run/step/unit identity to the engine.
177
+ - `exec_capture_incomplete` and `exec_output_limit` must never be added to
178
+ any `retry.on` taxonomy — doing so would let a step's own retry policy
179
+ re-run a command whose side effects already happened.
180
+
181
+ ## Provenance
182
+
183
+ - Source: `src/workflows/exec/exec-unit.ts` — module header (invariants),
184
+ `EXEC_DEFAULT_ENV_PASSTHROUGH`'s doc comment, `childEnv`'s doc comment,
185
+ `runExecUnit`'s doc comment.
186
+ - Related: DRIFT-6 (`AKM_EVENT_SOURCE` provenance parity with the agent
187
+ passthrough list); `docs/guides/run-workflows.md` ("workflow sources are
188
+ executed code").
189
+ - Extracted: P4 (`docs/plans/specs/p4-deletions-closeout.md` §4.2), 2026-08-27.
@@ -0,0 +1,97 @@
1
+ # 0004 — The shared input contract, generalized from workflow params
2
+
3
+ ## Context
4
+
5
+ Before task source v4, workflow parameters (`WorkflowParameterPlan` /
6
+ `WorkflowParameterFlag` in `src/workflows/ir/params.ts`) had their own
7
+ pure validation/coercion vocabulary, and nothing else in the codebase
8
+ needed the same shape. Task source v4 introduced typed `inputs:`
9
+ declarations on tasks (P2a), which need the identical kind of contract —
10
+ declared names, types, defaults, required-ness, JSON-schema validation —
11
+ but for a different caller (a task) with its own diagnostics vocabulary
12
+ (different error codes and messages than a workflow parameter flag). Rather
13
+ than duplicate the logic, P2a generalized the workflow-only module into a
14
+ shared one.
15
+
16
+ ## Decision
17
+
18
+ Moved verbatim from `src/execution/input-contract.ts`'s module header:
19
+
20
+ > The shared input contract (P2a Lane B, docs/plans/specs/
21
+ > p2a-task-source-v4.md §4, §1.3 D3, and the disambiguations
22
+ > D3-N1/D3-N2/D3-N3 in §1.5).
23
+ >
24
+ > This module GENERALIZES the pure module `src/workflows/ir/params.ts`
25
+ > (`WorkflowParameterPlan`, `WorkflowParameterFlag`,
26
+ > `materializeWorkflowParameterFlags`, `validateWorkflowParams`,
27
+ > `assertRunParamsSatisfyPlan`) into a contract-shaped vocabulary that both
28
+ > workflow params AND task source v4's `inputs:` declarations can consume.
29
+ > `src/workflows/ir/params.ts` becomes a THIN CONSUMER of this module: it
30
+ > adapts a `WorkflowParameterPlan` into an `InputContract`
31
+ > (`contractFromPlan`), supplies the workflow-specific diagnostics
32
+ > vocabulary, and re-exports its three wrappers under their existing names
33
+ > — byte-identical messages, codes, and hints
34
+ > (`tests/workflows/workflow-param-flags.test.ts`,
35
+ > `tests/integration/workflows/params-validation.test.ts`).
36
+ >
37
+ > `TaskInputBinding` is declared here for P2b (task source v4 `inputs:`
38
+ > delivery). P2a only ever produces `kind: "literal"` bindings, and nothing
39
+ > consumes them yet — P2a's contract for inputs is VALIDATION only
40
+ > (docs/plans/specs/p2a-task-source-v4.md §0).
41
+ >
42
+ > **D3-N1 (import boundary):** `src/execution/**` must never import
43
+ > `src/workflows/**` — `tests/architecture/import-cycle-ratchet.test.ts`
44
+ > runs a shrink-only, EMPTY baseline, and an `execution -> workflows` edge
45
+ > would close a cycle the moment `workflows/ir/params.ts` imports back into
46
+ > this module. `INPUT_NAME_PATTERN` is therefore DEFINED here (identical
47
+ > source/flags to today's `PROGRAM_PARAM_NAME_PATTERN`) rather than
48
+ > imported from the workflow program vocabulary; `src/workflows/program/
49
+ > schema.ts` becomes a re-export of this constant instead. Permitted
50
+ > imports: `node:crypto`, `src/core/errors`, `src/core/json-schema`, and
51
+ > `src/execution/**` — see `tests/execution/input-contract.test.ts`'s
52
+ > purity scan, which reads this file as text/AST and asserts the allowlist.
53
+ >
54
+ > **D3-N2 (canonical JSON):** `canonicalInputJson`/`canonicalInputHash`
55
+ > implement the SAME recursive key-sort + `JSON.stringify` as
56
+ > `canonicalJson` (`src/workflows/ir/plan-hash.ts`) locally, rather than
57
+ > importing it — `plan-hash.ts` also imports `./schema-v4` and
58
+ > `../resource-limits`, so importing it would drag workflow IR into
59
+ > `src/execution/**` and violate D3-N1. `tests/execution/
60
+ > input-contract.test.ts` asserts BYTE EQUALITY with `canonicalJson` over a
61
+ > fixture set covering nested objects, arrays, `null`, unicode keys, and
62
+ > insertion-order permutations, so the duplication cannot drift unnoticed.
63
+ >
64
+ > **D3-N3 (injected diagnostics vocabulary):** `materializeInputFlags`
65
+ > contains no literal user-facing string. Every message/code it raises is
66
+ > produced by a caller-supplied `InputFlagDiagnostics` — `params.ts`'s
67
+ > `WORKFLOW_PARAMETER_DIAGNOSTICS` reproduces today's five
68
+ > workflow-parameter strings/codes exactly; `src/tasks/source/
69
+ > task-input-diagnostics.ts` supplies the task vocabulary.
70
+ >
71
+ > Pure module: no IO, no engine imports.
72
+
73
+ ## Consequences
74
+
75
+ - Workflow params and task `inputs:` share exactly one validation/coercion
76
+ implementation; a bug fix or a new capability (e.g. a new JSON-schema
77
+ subset feature) lands once and both callers get it.
78
+ - The two callers can still speak entirely different user-facing vocabulary
79
+ (different error codes, different hint text) because the diagnostics are
80
+ injected, not hardcoded — `materializeInputFlags` itself never needs to
81
+ know which caller it is serving.
82
+ - `src/execution/**` staying import-free of `src/workflows/**` is a
83
+ standing constraint, not a one-time fact: any future change that makes
84
+ this module (or anything it imports) reach into `src/workflows/**` closes
85
+ an import cycle and fails `tests/architecture/import-cycle-ratchet.test.ts`'s
86
+ shrink-only, empty baseline.
87
+ - The canonical-JSON duplication between here and `workflows/ir/plan-hash.ts`
88
+ is deliberate, not an oversight — a future refactor that "de-duplicates"
89
+ it by importing across the D3-N1 boundary is the violation, not the
90
+ duplication itself.
91
+
92
+ ## Provenance
93
+
94
+ - Source: `src/execution/input-contract.ts`, module header.
95
+ - Spec: `docs/plans/specs/p2a-task-source-v4.md` §4, §1.3 D3, §1.5
96
+ (D3-N1/D3-N2/D3-N3).
97
+ - Extracted: P4 (`docs/plans/specs/p4-deletions-closeout.md` §4.2), 2026-08-27.
@@ -0,0 +1,108 @@
1
+ # 0005 — The D8 result-vocabulary re-code and its legacy read mapping
2
+
3
+ ## Context
4
+
5
+ Before P1b's D8 vocabulary re-code, a task's result `target.kind` used
6
+ `"prompt"` for a prepared command/agent-LLM result and one shared
7
+ `"command"` for every native (shell/script) result, which conflated two
8
+ materially different execution shapes under one label and mislabeled an
9
+ LLM-routed dispatch as a literal "prompt". D8 renamed the written
10
+ vocabulary; the harder problem was that `task_history` is a durable,
11
+ already-populated table — existing rows on disk still carry the OLD labels,
12
+ and `readTaskHistory` has to keep returning a coherent shape for both
13
+ generations of rows forever.
14
+
15
+ ## Decision
16
+
17
+ Moved verbatim from `src/tasks/run/task-result.ts`'s module header:
18
+
19
+ > `TaskRunResult` — the shape every dispatch arm returns — plus the small
20
+ > cluster of helpers that build one directly: `preparedResultTarget` (the D8
21
+ > result-vocabulary projection of a freshly prepared execution),
22
+ > `finishDisabledTask` (the disabled-task short-circuit), and
23
+ > `exitCodeForStatus` (the OS-scheduler exit-code mapping). `RunTaskOptions`
24
+ > — the public options bag `runTask()`, `load-task.ts`, and every dispatch
25
+ > arm read from — lives here too, alongside the other public-surface types
26
+ > the compat shim (`src/tasks/runner.ts`) re-exports.
27
+ >
28
+ > D8 (§5.3, §6 F-2): `preparedResultTarget`'s prepared-command arm now
29
+ > returns `{kind:"command", engine}` (formerly `{kind:"prompt", engine}`);
30
+ > its native arm now returns the bare `{kind:"shell"}` / `{kind:"script"}`
31
+ > (formerly one shared `{kind:"command"}`) — the arm-specific `cmd` is
32
+ > added by `run-native-task.ts` once it has actually built the argv,
33
+ > mirroring the pre-P1b shape where the bare disabled-task projection never
34
+ > carried `cmd` either.
35
+
36
+ And from `src/tasks/run/task-history.ts`'s module header (the WRITE half —
37
+ the READ half's mapping rule is kept in the code itself, not moved; see
38
+ below):
39
+
40
+ > The `task_history` read/write boundary: `appendHistory` (write) and
41
+ > `readTaskHistory` / `taskHistoryRowToResult` (read).
42
+ >
43
+ > D8 (spec §5.3, §6 F-2) result-vocabulary re-code, implemented entirely at
44
+ > this read/write boundary. WRITE: every row `appendHistory` writes now
45
+ > carries `targetVocab: 2` in its metadata, and the new target_kind strings
46
+ > ("command" for a prepared command/agent-LLM result, "shell", "script",
47
+ > "workflow" unchanged).
48
+
49
+ ## Consequences
50
+
51
+ - Every NEW row written after D8 is unambiguous — `targetVocab: 2` plus the
52
+ new `target_kind` strings mean a reader never has to guess which
53
+ generation a row belongs to.
54
+ - Every OLD row (written before D8, no `targetVocab` marker at all) must
55
+ keep reading correctly forever — this is not a migration that runs once
56
+ and finishes; there is no "convert task_history in place" step, because
57
+ the table is an append-only historical log, not authoring state. The
58
+ read-side mapping rule is therefore a PERMANENT part of the codebase
59
+ (`docs/plans/specs/p4-deletions-closeout.md` row B-51: "it reads old rows
60
+ forever. Deleting it is a review-blocking violation.").
61
+ - The exact legacy mapping — kept in `src/tasks/run/task-history.ts` itself
62
+ as a short invariant comment, not summarized here, because a maintainer
63
+ reading `taskHistoryRowToResult` needs it right there — is: a legacy row
64
+ (no `targetVocab` marker) maps `"prompt"` → `{kind:"command", engine}`,
65
+ `"command"` → `{kind:"shell"}`, `"workflow"` unchanged, and anything else
66
+ (including the new vocabulary's own strings written WITHOUT a marker,
67
+ which no production writer ever does) → `"unknown"`. The P0-pinned null
68
+ fallbacks survive: a workflow row's `ref` falls back to `""`, the
69
+ command/prompt arm's `engine` falls back to `null`.
70
+ - `SAFE_TASK_ATTEMPT_ERROR_CODES` (`src/tasks/run/attempt-lifecycle.ts`) is a
71
+ related but separate allowlist — it decides which error CODES are safe to
72
+ surface verbatim in a `detail.error` column, not which result-kind
73
+ strings are legal. Do not conflate the two when reading either file.
74
+ - **Removed 2026-08-28** (review finding against this phase's own close-out
75
+ spec, `docs/plans/specs/p4-deletions-closeout.md`): `finishDisabledTask`
76
+ and `preparedResultTarget`, quoted above as part of the moved essay, are no
77
+ longer in `src/tasks/run/task-result.ts`. `finishDisabledTask`'s only
78
+ caller — `run-task.ts`'s `shouldSkipUnactivatedTask` — was deleted by the
79
+ SAME phase's P4-N6 (task source v4 has no document-level `enabled` to skip
80
+ at fire time), which left `finishDisabledTask` fully dead; `preparedResultTarget`
81
+ was in turn `finishDisabledTask`'s only caller, so it went dead with it.
82
+ Deleting them also retired the `"disabled"` `TaskRunStatus` member
83
+ `finishDisabledTask` was the sole producer of, and the now-unreachable
84
+ `result.status === "disabled"` branch in `src/commands/tasks/tasks.ts`.
85
+ This does not touch the D8 vocabulary or the legacy read mapping this ADR
86
+ is about — every OTHER dispatch arm still builds its own
87
+ `TaskRunResult.target` per the Decision above, and `task-history.ts`'s read
88
+ side is unchanged.
89
+ - **Superseded 2026-09-01**: the "PERMANENT part of the codebase" ruling
90
+ above (row B-51) is overturned. `task_history` is DB-owned data, and the
91
+ legacy->current remap is deterministic and total, so it belongs in a
92
+ one-time schema migration, not three permanently-recurring read-side
93
+ branches (the mapping had been independently re-implemented at
94
+ `src/tasks/run/task-history.ts`, `src/commands/health/improve-metrics.ts`'s
95
+ `isAgentTaskHistoryRow`, and `src/commands/health/windows.ts`). State
96
+ migration `025-task-history-vocabulary-backfill`
97
+ (`src/core/state/migrations.ts`) now rewrites every legacy row's
98
+ `target_kind` (and stamps `targetVocab: 2`) the first time a pre-migration
99
+ state.db is opened by this or a later release; all three read sites were
100
+ deleted and now read `target_kind` directly in the current vocabulary.
101
+
102
+ ## Provenance
103
+
104
+ - Source: `src/tasks/run/task-result.ts` module header;
105
+ `src/tasks/run/task-history.ts` module header (WRITE half only — the READ
106
+ half's mapping rule stays in the code as a permanent invariant).
107
+ - Spec: `docs/plans/specs/p1b-model-extraction.md` §5.3, §6 F-2.
108
+ - Extracted: P4 (`docs/plans/specs/p4-deletions-closeout.md` §4.2), 2026-08-27.
@@ -0,0 +1,124 @@
1
+ # 0006 — Task source version routing, from three generations to one
2
+
3
+ ## Context
4
+
5
+ Between P2a and P4, `src/tasks/source/parse-task-source.ts` was the one
6
+ place every task document's `version:` field got routed to a parser.
7
+ Through that window there were three live generations to route between:
8
+ task v2 (retired at the router itself, with a dedicated unsupported-version
9
+ error), task v3 (the shipped grammar every existing task file on disk still
10
+ used), and task source v4 (the new grammar P2a introduced). The router had
11
+ to dispatch to the right parser without a second parse, a re-serialization,
12
+ or a synthetic document — an invariant P1b §4.3 established for the
13
+ front end and every later phase carried forward unchanged.
14
+
15
+ Two things about that routing were the module's own, deliberately preserved
16
+ warts, recorded as such at the time rather than fixed:
17
+
18
+ 1. The bounded YAML front end's pre-version failures (source not a string,
19
+ source too large, YAML parse/warning/expansion) render with a fixed
20
+ label — `"task v3 source"` — even when the document under parse turns out
21
+ to declare `version: 4`, because `root.version` cannot be read until the
22
+ front end has already succeeded or failed. P2a's spec explicitly deferred
23
+ fixing this: "P4 owns the final label once v3 is gone."
24
+ 2. For any `version:` other than `2`, `3`, or `4`, the router fell through to
25
+ `parseTaskV3Document`'s OWN wording — "version is required and must be
26
+ exactly 3." — a message that names the wrong number for a document that
27
+ was never v3 in the first place. This was also deliberately deferred:
28
+ "P4 owns the final version-error text."
29
+
30
+ ## Decision
31
+
32
+ Moved verbatim from `src/tasks/source/parse-task-source.ts`'s module header,
33
+ as it read from P2a through the end of P3b (immediately before P4 §3.2
34
+ rewrote it):
35
+
36
+ > The task source version router (spec docs/plans/specs/p2a-task-source-v4.md
37
+ > §3.4, §1.5 D2-N2).
38
+ >
39
+ > Runs the bounded YAML front end ONCE (`readBoundedTaskSourceYaml`), reads
40
+ > `root.version`, and dispatches into `parseTaskV3Document` (unmodified,
41
+ > exported from `../source-v3`) or `parseTaskSourceV4Document` with that
42
+ > SAME `{root, lineAt}` — no second parse, no re-serialization, no synthetic
43
+ > document (the P1b §4.3 invariant this phase carries forward).
44
+ >
45
+ > D2-N2's exact routing table:
46
+ >
47
+ > | root `version` | routed to | observable result |
48
+ > |---|---|---|
49
+ > | `4` | `parseTaskSourceV4Document` | new grammar |
50
+ > | `3` | `parseTaskV3Document` | byte-identical to today |
51
+ > | `2` | `parseTaskV3Document` | byte-identical (raises `taskV2UnsupportedError` itself) |
52
+ > | absent/other | `parseTaskV3Document` | byte-identical (its own preserved wording) |
53
+ >
54
+ > Only `version: 4` needs an explicit branch: `parseTaskV3Document` already
55
+ > raises `taskV2UnsupportedError` for `version: 2` and its own "version is
56
+ > required.../must be exactly 3" wording for everything else
57
+ > (`source-v3.ts:720-722`) — a DELIBERATELY preserved wart, not fixed here
58
+ > (P4 owns the final version-error text).
59
+ >
60
+ > The front end's own pre-version failures (source not a string, source too
61
+ > large, YAML parse/warning/expansion) ALWAYS render with the "task v3
62
+ > source" label, even when the document later declares `version: 4` —
63
+ > `root.version` cannot be read until the front end already succeeded. This
64
+ > is a deliberate, spec-recorded wart (§3.4): "P4 owns the final label once
65
+ > v3 is gone."
66
+
67
+ P4 §3.2 (`docs/plans/specs/p4-deletions-closeout.md`) removed task source v3
68
+ acceptance from `src` entirely (§3 family A2) and, in the same commit,
69
+ rewrote the router to close both warts. The terminal routing table, in
70
+ place since:
71
+
72
+ | root `version` | outcome |
73
+ |---|---|
74
+ | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
75
+ | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (rows B-14/B-15) |
76
+ | absent / not a number | `parseTaskSourceV4Document`'s own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
77
+
78
+ A missing or non-numeric `version:` still routes into the v4 parser rather
79
+ than the generic unsupported-version rejection — that part of the shape is
80
+ unchanged from the original design — because a document with no readable
81
+ version was never task v2 or v3 in the first place, and the v4 parser's own
82
+ field error names the one grammar `src` still accepts.
83
+
84
+ ## Consequences
85
+
86
+ - **Wart 1 closed (row B-17).** The front end's pre-version failure label is
87
+ now `"task source"` — version-agnostic, since at that point in parsing no
88
+ version has been read yet — instead of the `"task v3 source"` label that
89
+ used to leak the retired generation's name into an error about a document
90
+ that might turn out to be `version: 4`.
91
+ - **Wart 2 closed (row B-16).** A document with an unreadable `version:`
92
+ (missing, or not a number) now gets the v4 parser's own "is required and
93
+ must be 4." / "must be exactly 4." wording, naming the grammar `src`
94
+ actually accepts, instead of `parseTaskV3Document`'s inherited "must be
95
+ exactly 3." — which named a generation the document was never going to be.
96
+ - **The routing table shrank from three live branches to one.** `version: 3`
97
+ and `version: 2` no longer parse at all — both now raise
98
+ `TASK_SCHEMA_VERSION_UNSUPPORTED` with the migrator hint (rows B-14, B-15),
99
+ where they used to route to `parseTaskV3Document` and execute
100
+ byte-identically to any other release. This is the observable break every
101
+ task v3 (and v2) file on disk hits; `docs/migration/v0.9.1-to-v0.9.2.md`
102
+ and `CHANGELOG.md` document the remedy (`akm migrate apply`).
103
+ - **The router no longer imports `../source-v3` for a parser call at all.**
104
+ `parseTaskV3Document` moved to the frozen, vendored migrator copy
105
+ (`scripts/akm-migrate/migrate/task-source-v3-frozen.ts`) — the router's
106
+ only remaining job is: is this `version: 4`, or not.
107
+ - `ParsedTaskSource` collapsed from a two-member discriminated union
108
+ (`{version: 3, v3: ...} | {version: 4, v4: ...}`) to the single-member
109
+ shape `Readonly<{version: 4; v4: TaskSourceV4Document}>` — every consumer
110
+ that used to branch on `parsed.version === 4` simplified to its v4 arm.
111
+
112
+ ## Provenance
113
+
114
+ - Source: `src/tasks/source/parse-task-source.ts` module header, as it read
115
+ from P2a (`git show 0f16f70fdc61:src/tasks/source/parse-task-source.ts`)
116
+ through immediately before P4 §3.2's rewrite.
117
+ - Spec: `docs/plans/specs/p2a-task-source-v4.md` §3.4, §1.5 D2-N2 (the
118
+ original routing design and both recorded warts);
119
+ `docs/plans/specs/p4-deletions-closeout.md` §3.2.2, §4.2 (the rewrite that
120
+ closed both warts and required this ADR).
121
+ - Extracted: P4, 2026-08-28 (review finding against this phase's own
122
+ close-out spec — the index row for this ADR existed from P4's ADR-move
123
+ commit onward, but the file itself was not authored until this
124
+ correction).