akm-cli 0.9.2-alpha.3 → 0.9.2-alpha.5

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 (151) hide show
  1. package/CHANGELOG.md +664 -0
  2. package/STABILITY.md +23 -5
  3. package/dist/assets/hints/cli-hints-full.md +12 -7
  4. package/dist/assets/tasks/core/extract.yml +3 -5
  5. package/dist/assets/tasks/core/improve.yml +3 -5
  6. package/dist/assets/tasks/core/index-refresh.yml +3 -5
  7. package/dist/assets/tasks/core/sync.yml +3 -5
  8. package/dist/assets/tasks/core/version-check.yml +3 -5
  9. package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
  10. package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
  11. package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
  12. package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
  13. package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
  14. package/dist/cli/unknown-flags.js +12 -1
  15. package/dist/cli.js +8 -1
  16. package/dist/commands/command/command-execution.js +23 -2
  17. package/dist/commands/health/improve-metrics.js +38 -0
  18. package/dist/commands/health/plugin-staleness.js +219 -0
  19. package/dist/commands/health/type-directory-check.js +167 -0
  20. package/dist/commands/health/windows.js +8 -4
  21. package/dist/commands/health.js +71 -9
  22. package/dist/commands/improve/extract.js +22 -1
  23. package/dist/commands/lint/index.js +1 -1
  24. package/dist/commands/migrate-cli.js +130 -24
  25. package/dist/commands/proposal/validators/proposal-validators.js +7 -2
  26. package/dist/commands/read/remember-cli.js +6 -1
  27. package/dist/commands/remember.js +70 -0
  28. package/dist/commands/tasks/explain.js +304 -0
  29. package/dist/commands/tasks/tasks-cli.js +185 -3
  30. package/dist/commands/tasks/tasks.js +233 -45
  31. package/dist/commands/workflow/plan.js +159 -0
  32. package/dist/commands/workflow-cli.js +94 -2
  33. package/dist/core/activation-policy.js +2 -12
  34. package/dist/core/adapter/adapters/akm-lint.js +7 -4
  35. package/dist/core/adapter/adapters/akm-metadata.js +26 -14
  36. package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
  37. package/dist/core/errors.js +45 -0
  38. package/dist/core/json-schema.js +15 -5
  39. package/dist/core/state/migrations.js +57 -0
  40. package/dist/core/state-db.js +16 -14
  41. package/dist/core/subprocess.js +47 -13
  42. package/dist/execution/guarded-source.js +44 -0
  43. package/dist/execution/input-contract.js +250 -0
  44. package/dist/execution/target-ref.js +63 -0
  45. package/dist/indexer/usage/usage-events.js +14 -3
  46. package/dist/integrations/agent/execution-lowering.js +12 -1
  47. package/dist/integrations/harnesses/claude/session-log.js +85 -25
  48. package/dist/integrations/session-logs/pre-filter.js +152 -2
  49. package/dist/output/shapes/passthrough.js +2 -0
  50. package/dist/output/text/helpers.js +1 -1
  51. package/dist/output/text/migrate.js +12 -3
  52. package/dist/output/text/workflow-format.js +192 -10
  53. package/dist/output/text/workflow.js +2 -1
  54. package/dist/registry/semver.js +4 -0
  55. package/dist/runtime.js +1 -0
  56. package/dist/scripts/akm-migrate-node.js +11838 -10118
  57. package/dist/scripts/akm-migrate.js +11828 -10117
  58. package/dist/setup/steps/tasks.js +34 -17
  59. package/dist/storage/repositories/task-history-repository.js +5 -1
  60. package/dist/storage/repositories/workflow-runs-repository.js +144 -6
  61. package/dist/tasks/backends/launchd.js +31 -84
  62. package/dist/tasks/embedded.js +13 -7
  63. package/dist/tasks/model/invocation.js +4 -0
  64. package/dist/tasks/prepare/prepare-script-target.js +9 -0
  65. package/dist/tasks/prepare/prepare-support.js +154 -0
  66. package/dist/tasks/prepare/prepare.js +117 -0
  67. package/dist/tasks/prepare/prepared-execution.js +4 -0
  68. package/dist/tasks/prepare/script-capture.js +80 -0
  69. package/dist/tasks/run/attempt-lifecycle.js +165 -0
  70. package/dist/tasks/run/load-task.js +117 -0
  71. package/dist/tasks/run/provenance.js +20 -0
  72. package/dist/tasks/run/run-command-task.js +92 -0
  73. package/dist/tasks/run/run-native-task.js +222 -0
  74. package/dist/tasks/run/run-task.js +99 -0
  75. package/dist/tasks/run/run-workflow-task.js +222 -0
  76. package/dist/tasks/run/task-history.js +134 -0
  77. package/dist/tasks/run/task-log.js +179 -0
  78. package/dist/tasks/run/task-result.js +19 -0
  79. package/dist/tasks/scheduler-binding.js +66 -2
  80. package/dist/tasks/scheduler-invocation.js +63 -3
  81. package/dist/tasks/scheduler-sync.js +55 -12
  82. package/dist/tasks/source/bounded-document.js +455 -0
  83. package/dist/tasks/source/parse-task-source.js +59 -0
  84. package/dist/tasks/source/project-v4.js +62 -0
  85. package/dist/tasks/source/task-input-diagnostics.js +36 -0
  86. package/dist/tasks/source/task-source-v4.js +626 -0
  87. package/dist/tasks/source-v3.js +10 -733
  88. package/dist/tasks/task-run-reserved-flags.js +79 -0
  89. package/dist/workflows/authoring/authoring.js +17 -8
  90. package/dist/workflows/exec/child-invocation.js +34 -0
  91. package/dist/workflows/exec/child-workflow.js +370 -0
  92. package/dist/workflows/exec/exec-unit.js +50 -170
  93. package/dist/workflows/exec/frozen-judge.js +19 -2
  94. package/dist/workflows/exec/native-executor.js +49 -27
  95. package/dist/workflows/exec/param-secrets.js +12 -0
  96. package/dist/workflows/exec/run-workflow.js +48 -59
  97. package/dist/workflows/exec/step-work.js +222 -80
  98. package/dist/workflows/exec/unit-dispatch.js +72 -0
  99. package/dist/workflows/freeze/child-output-references.js +94 -0
  100. package/dist/workflows/freeze/environment.js +174 -0
  101. package/dist/workflows/freeze/identity.js +22 -0
  102. package/dist/workflows/freeze/resolve-steps.js +78 -0
  103. package/dist/workflows/freeze/source-freeze.js +57 -0
  104. package/dist/workflows/freeze/step-values.js +68 -0
  105. package/dist/workflows/freeze/targets/child-workflow.js +206 -0
  106. package/dist/workflows/freeze/targets/command.js +81 -0
  107. package/dist/workflows/freeze/targets/script.js +57 -0
  108. package/dist/workflows/freeze/targets/shell.js +31 -0
  109. package/dist/workflows/freeze/targets/task.js +179 -0
  110. package/dist/workflows/freeze/task-bindings.js +180 -0
  111. package/dist/workflows/ir/compile.js +59 -11
  112. package/dist/workflows/ir/environment-v4.js +3 -3
  113. package/dist/workflows/ir/freeze-v4.js +41 -7
  114. package/dist/workflows/ir/params.js +58 -131
  115. package/dist/workflows/ir/plan-hash.js +3 -3
  116. package/dist/workflows/ir/schema-v4.js +246 -17
  117. package/dist/workflows/parser.js +74 -2
  118. package/dist/workflows/program/schema.js +5 -2
  119. package/dist/workflows/resource-limits.js +20 -0
  120. package/dist/workflows/runtime/plan-classifier.js +19 -5
  121. package/dist/workflows/runtime/run-outputs.js +103 -0
  122. package/dist/workflows/runtime/runs.js +114 -9
  123. package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
  124. package/dist/workflows/source-files.js +5 -5
  125. package/dist/workflows/source-ir/compare.js +17 -0
  126. package/dist/workflows/source-ir/compile.js +7 -3
  127. package/dist/workflows/source-ir/github-yaml.js +64 -17
  128. package/dist/workflows/source-ir/schema.js +69 -21
  129. package/dist/workflows/source-ir/semantics.js +7 -25
  130. package/dist/workflows/source-ir/triggers.js +79 -0
  131. package/dist/workflows/source-ir/uses.js +33 -7
  132. package/docs/migration/README.md +1 -1
  133. package/docs/migration/release-notes/0.9.2.md +87 -11
  134. package/docs/migration/release-notes/README.md +2 -2
  135. package/docs/migration/v0.8-to-v0.9.md +9 -7
  136. package/docs/migration/v0.9.0-troubleshooting.md +14 -7
  137. package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
  138. package/docs/reference/README.md +1 -1
  139. package/docs/reference/cli.md +140 -46
  140. package/docs/reference/configuration.md +3 -3
  141. package/docs/reference/supported-formats.md +9 -5
  142. package/docs/reference/tasks.md +338 -75
  143. package/docs/reference/workflow-schema.md +281 -8
  144. package/docs/reference/workflows.md +57 -7
  145. package/package.json +1 -1
  146. package/schemas/akm-task.json +173 -118
  147. package/schemas/akm-workflow.json +28 -0
  148. package/dist/tasks/runner.js +0 -941
  149. package/dist/tasks/runtime-v3.js +0 -281
  150. package/dist/workflows/ir/source-freeze-v4.js +0 -506
  151. package/dist/workflows/source-ir/ordering.js +0 -38
package/CHANGELOG.md CHANGED
@@ -6,6 +6,670 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Fixed
10
+
11
+ - **Scheduled tasks on Windows ran but recorded no output.** A task fired by
12
+ Task Scheduler logged `exit_code=0` with an empty log: the command really
13
+ ran, but nothing it printed was captured. Captured runs asked for their own
14
+ process group, which is what lets a timeout reap the whole descendant tree
15
+ on macOS/Linux; on Windows that same flag instead means "start with no
16
+ console", and a console host started that way (`powershell.exe`, `cmd.exe`)
17
+ allocates its own console and thereby replaces the pipes it was handed, so
18
+ its output went nowhere. Windows got no reaping benefit in exchange — the
19
+ group kill it enables is a POSIX-only call — so captured Windows runs no
20
+ longer ask for it. A run whose output capture is incomplete for any other
21
+ reason now says so in the task log instead of leaving a silent gap.
22
+ - **A scheduled shell task could fail instantly with exit code 1 on Windows.**
23
+ Two defects on the default Windows task shell (`powershell`): a
24
+ scheduler-fired run restores the PATH captured at install time, which can be
25
+ minimal, and `powershell.exe` is not on it (it lives in a `WindowsPowerShell`
26
+ subdirectory, not `System32`), so the spawn failed outright; and rebinding a
27
+ bare leading `akm` produced a quoted path, which PowerShell parses as a
28
+ string rather than a command to run. Both shells are now resolved to
29
+ absolute paths on Windows, and a rebound invocation carries PowerShell's
30
+ call operator. `cmd`-shell tasks additionally pass their hand-quoted command
31
+ line through verbatim, since `cmd /s /c` does not read a standard argv.
32
+ - **`state.db` could fail to open on macOS** with "this platform has no
33
+ descriptor-backed path." The descriptor-alias optimization used to bind a
34
+ SQLite open to its exact held inode isn't reliably available everywhere —
35
+ Windows never has one, and macOS's `/dev/fd` is a small fixed-size table
36
+ that a process holding higher file-descriptor numbers (as a bundled
37
+ standalone binary routinely does) can miss. The surrounding identity checks
38
+ (dev/ino/uid, re-verified immediately before and after every open) are the
39
+ real protection; a missing alias now falls back to the plain path on any
40
+ platform instead of throwing, matching the fallback Windows already used.
41
+ - The Windows build shipped no embedded template assets, because the
42
+ build-time asset copy anchored its rewrites on forward slashes while the
43
+ glob yields platform-native separators.
44
+
45
+ ## [0.9.2-alpha.5] - 2026-08-28
46
+
47
+ ### Breaking changes & migration
48
+
49
+ - **Durable workflow plans bump to `irVersion` 5.** A stored run frozen
50
+ before this release (`irVersion` 4 or earlier) can no longer `resume`,
51
+ `next`, `complete`, or `run` — those fail closed with `UsageError` code
52
+ `WORKFLOW_IR_VERSION_UNSUPPORTED`, naming the run and pointing at
53
+ `akm workflow abandon`. `akm workflow status`, `akm workflow list`, and
54
+ `akm workflow abandon` keep working on those runs — no data is lost, and
55
+ their step spine is untouched by abandoning. **Before upgrading**, run
56
+ `akm workflow list --active` and either let in-flight runs finish or
57
+ abandon them; after upgrading, recover a blocked run with
58
+ `akm workflow abandon <id>` followed by `akm workflow run <ref>` to start
59
+ fresh from the current authored source. There is no second executor and no
60
+ compatibility replay layer for a pre-`irVersion`-5 plan. The unit and gate
61
+ input-hash prefixes bump alongside it, from `hashVersion` 5 to
62
+ `hashVersion` 7, so that a freshly frozen plan's units are never
63
+ content-addressed the same way an old, no-longer-executable plan's were.
64
+ (`hashVersion` 6 existed only inside this release's own development and
65
+ never shipped in any version — the durable step a released install sees is
66
+ 5 → 7.) The unit preimage also gains one **conditional** field,
67
+ `taskInputs`: the *resolved* values of a task-composing step's input
68
+ bindings, present only for a unit whose frozen target carries
69
+ `inputBindings` — a binding-free unit's preimage keeps exactly the shape it
70
+ had. Hashing the resolved values, not just the frozen binding expression,
71
+ is what makes a resumed run whose upstream step output changed under a
72
+ `{from: "steps.<id>.output"}` binding fail loudly as a replay divergence
73
+ instead of silently reusing the completed unit's stale result.
74
+ See [Migrating from akm 0.9.1 to 0.9.2](docs/migration/v0.9.1-to-v0.9.2.md#workflow-cutover).
75
+ - A workflow step that passes `with:` to a `tasks/<ref>` target whose task
76
+ declares **no** `inputs:` (a `version: 4` task with no `inputs:` key at
77
+ all) is now **rejected** (`UsageError` code
78
+ `COMPOSITION_INVALID`, exit 2) instead of having the authored mapping
79
+ silently dropped at freeze — for any authored shape, including `with: {}`.
80
+ When the target's task source **does** declare `inputs:`, `with:` now
81
+ **binds** them instead: a literal value, or a `{from: "steps.<id>.output…"}`
82
+ reference resolved just before the unit dispatches (the reference grammar
83
+ also accepts `{from: "params.<name>"}`, but a composing step's own
84
+ document can never declare `params:`, so that form is not reachable in
85
+ this release). See [Task input bindings](docs/reference/tasks.md#typed-inputs-and-output)
86
+ for the full grammar. `with:` on `uses: akm/command` is unaffected — it is
87
+ still that builtin's own action-argument bag, never an input binding.
88
+ - **New rejection:** `with:` on a workflow step targeting `uses:
89
+ commands/<ref>` or `uses: scripts/<ref>` is now **rejected**
90
+ (`COMPOSITION_INVALID`, exit 2) instead of being silently discarded at
91
+ freeze — neither target is a binding surface. Remove the `with:` block from
92
+ any such step; a command/script-composing step never accepted its values
93
+ in the first place, so this closes a defect rather than a feature.
94
+ - Composing a task source v4 document from a workflow step's `uses:
95
+ tasks/<ref>`, where the task's own target is a workflow, is no longer
96
+ deferred: it now **freezes and dispatches** normally, the same as a
97
+ direct `uses: workflows/<ref>` step. The prior release's
98
+ `TASK_SOURCE_INVALID` "arrives in a later 0.9.x release" rejection for this
99
+ case is gone.
100
+ - Task-source validation errors raised through the shared `sourceError`
101
+ funnel (field- and semantic-level checks: missing/invalid fields, schedule
102
+ conflicts, and similar) now report code **`TASK_SOURCE_INVALID`** instead of
103
+ `INVALID_FLAG_VALUE`. YAML syntax, size, structure, and expansion failures
104
+ (malformed YAML, oversized source, unsupported YAML constructs, and
105
+ alias/tag/depth/node-count limits) are raised earlier, before that funnel is
106
+ reached — early in this release these still reported `INVALID_FLAG_VALUE`,
107
+ but by 0.9.2's release they report `TASK_SOURCE_INVALID` too (the terminal
108
+ diagnostics ratchet, see the Changed entry below): task-source failures no
109
+ longer split across two codes, so **scripts that were branching on both
110
+ `TASK_SOURCE_INVALID` and `INVALID_FLAG_VALUE` for a task-source error can
111
+ drop the `INVALID_FLAG_VALUE` arm**. Every such error's message prefix is
112
+ `Invalid task source at <path>[:<line>]: …` — not `Invalid task v3 source`,
113
+ since the label no longer names a specific schema generation (task source
114
+ v4 is the only version `src/` accepts by release; see the "Task v3 sources
115
+ no longer parse" entry below). The envelope's `error` message text and exit
116
+ code 2 are unchanged for every task-source error. The envelope's `hint`
117
+ field and the `detail` text `akm lint` and the akm-task adapter report for
118
+ the same failure change from `… Run \`akm <command> --help\` to see
119
+ accepted values.` to `… Fix the task source at the reported path and line,
120
+ then re-run.`
121
+ - **Task-history / JSON-output `target.kind` vocabulary changed.** A prepared
122
+ command (agent/LLM) run now reports `"command"` (formerly the confusingly
123
+ inverted `"prompt"`); the former shared `"command"` string for the native
124
+ arm splits into `"shell"` and `"script"`, now distinguishable in history;
125
+ `"workflow"` and `"unknown"` are unchanged. **Consumers branching on
126
+ `"prompt"` must handle `"command"`** — this affects `akm task run`'s and
127
+ `akm task history`'s JSON output (`result.target.kind` /
128
+ `rows[].target.kind`) and any code reading `task_history.target_kind`
129
+ directly. Rows written by earlier akm versions are read back **mapped** to
130
+ the new vocabulary (legacy `"prompt"` → `{kind:"command", engine}`, legacy
131
+ `"command"` → `{kind:"shell"}`), so `akm task history` output stays uniform
132
+ across vintages. New rows carry a `targetVocab: 2` marker inside their
133
+ `metadata_json`, which akm versions before this one reject as an unknown
134
+ metadata field — a mixed-version fleet must upgrade every `akm` that writes
135
+ task history before an older one reads it.
136
+ - **Task source v4 (`version: 4`) is the task source grammar.** By 0.9.2's
137
+ release, `version: 4` is the *only* version `src/` accepts — see the
138
+ "Task v3 sources no longer parse" entry below for the cutover, the
139
+ `TASK_SCHEMA_VERSION_UNSUPPORTED` rejection, and the migration path. What
140
+ follows describes the v4 grammar itself. Scheduling is **optional**: a
141
+ task source v4 document with no `schedule:` parses, is runnable with
142
+ `akm task run`, and is **skipped** by `akm task sync` (zero bindings, zero
143
+ failures) instead of being rejected for missing a trigger — this is now
144
+ the *only* scheduling grammar; the second syntax task v3 offered
145
+ (`akm.schedule` / a document's top-level `on:`) is retired along with v3
146
+ itself. Task source v4 removes the `akm:` options bag and the `on:`
147
+ trigger block outright;
148
+ every field they carried is a top-level key instead: `akm.description` →
149
+ `description`, `akm.when_to_use` → `when_to_use`, `akm.tags` → `tags`,
150
+ `akm.agent` → `agent`, `akm.engine` → `engine`, `akm.model` → `model`,
151
+ `akm.inference` → `inference`, `akm.outputSchema` → `output`,
152
+ `akm.tools` → `tools`, `akm.timeout` → `timeout`, `akm.redact` →
153
+ `redact`, `akm.maxSteps` → `maxSteps`, `akm.maxRetries` → `maxRetries`,
154
+ `akm.schedule` → top-level `schedule:`, and `akm.enabled` → each
155
+ `schedule:` entry's own `enabled` (v3's single document-level flag
156
+ becomes per-binding in task source v4, defaulting to `true`; the
157
+ document-level `enabled` skip that read it is gone with v3, since a v4
158
+ document has no document-level `enabled` key to read). The GitHub-action
159
+ `uses:` target (`owner/repo@ref`) is removed outright — see the "GitHub
160
+ Action locators are no longer recognized anywhere" entry below.
161
+ `with:` is legal in task source v4 only alongside
162
+ `uses: akm/command`; every other target uses the new typed `inputs:`
163
+ declarations instead of `with:`. A declared `inputs:` name may not collide
164
+ with a flag `akm task run` already declares for itself (`bundle`, `format`,
165
+ `detail`, `shape`, `output`, `scheduled`, `quiet`, `verbose`, `help`,
166
+ `no-quiet`, `no-verbose`) or with `target`, the spelling `akm task`
167
+ retired in 0.9 and still answers with a rename hint in every spelling —
168
+ such a document now fails `TASK_SOURCE_INVALID` at parse time, since the
169
+ colliding name would otherwise route a caller's value into `akm task run`'s
170
+ own flag, or into that rename hint, instead of the declared input. `akm
171
+ task run <id>` now accepts exact-name input flags for a task source v4
172
+ document's declared `inputs:` (an undeclared flag name fails
173
+ `UNKNOWN_FLAG`; a bad value or an unsatisfied `required: true` declaration
174
+ fails `INPUT_BINDING_INVALID`; both exit 2 with the usual JSON error
175
+ envelope).
176
+ Where those materialized values go depends on the task's own target: for
177
+ `uses: workflows/<ref>` they become the child run's params (the existing
178
+ `with:` → params path); for a `run:`, `scripts/<ref>`, or `commands/<ref>`
179
+ target they are validated and then **discarded** — `akm task run`'s own
180
+ flags never populate an `AKM_TASK_INPUTS` environment variable or a
181
+ `## Task inputs` prompt block. Those two surfaces are a separate delivery
182
+ path: they carry a **workflow step's** `with:` binding into a task it
183
+ composes via `uses: tasks/<ref>` (see the `with:`-binding bullet above),
184
+ not `akm task run`'s own CLI flags. `schedule[].inputs` on a `version: 4`
185
+ task source are compiled the same way `akm task run`'s flags are: `akm
186
+ task sync` builds them into the scheduler binding's own invocation tail
187
+ instead of only validating and discarding them, so a scheduled run is
188
+ subject to the identical workflow-target-only delivery rule. `akm task
189
+ add` now authors task source v4 (see the "Task v3 sources no longer
190
+ parse" entry below for the `--params` → typed `inputs:` change). A
191
+ `version: 4` task source is now a valid workflow-step target (see above).
192
+ The published [task schema](schemas/akm-task.json) now publishes only the
193
+ single `version: 4` shape — the `version: 3` arm and its `githubActionRef`
194
+ definition are removed; a `version: 2` or `version: 3` document validates
195
+ against nothing in this schema and is converted by `akm migrate apply`
196
+ instead. **Binding a task's inputs adds nothing to the hash preimage of a
197
+ step that binds none**: a unit whose frozen target carries no
198
+ `inputBindings` has exactly the preimage *shape* it had before this
199
+ feature — no `taskInputs` key at all. Its hash *value* still moves, because
200
+ every unit and gate hash in this release re-versions once (`hashVersion`
201
+ 5 → 7, see the `irVersion` 5 entry above), and no pre-`irVersion`-5 plan can
202
+ execute here to be compared against.
203
+ - **A task's `output:` is legal only with a command target.** `output:`
204
+ alongside `run:`, `uses: scripts/<ref>`, or `uses: workflows/<ref>` now
205
+ fails `TASK_SOURCE_INVALID` (exit 2) at parse instead of being accepted and
206
+ never enforced: those runtimes decide success from the process exit code or
207
+ from a child run's own status and consume no task-level response schema, so
208
+ an authored contract there was silently unenforced. `uses: commands/<ref>`
209
+ and `uses: akm/command` — the targets that forward it as the model's
210
+ response schema — are unchanged. The published
211
+ [task schema](schemas/akm-task.json) enforces the same rule, so an editor
212
+ validating against it no longer green-lights a document `akm task run`
213
+ refuses to load. Migration handles this for you: `akm migrate apply` drops
214
+ an `akm.outputSchema` that sat on one of those three targets (it was inert
215
+ in v3 as well — nothing ever read it there) and reports the drop as a
216
+ notice on that file's plan entry rather than blocking the file.
217
+ - **A `schedule:` entry must be able to satisfy the task's declared
218
+ `inputs:`.** A scheduled firing supplies no input flags, so a task
219
+ declaring a `required: true` input — which may not also carry a
220
+ `default:` — and a `schedule:` entry that names no value for it could only
221
+ ever install a binding that fails at every firing. Such a document now
222
+ fails `TASK_SOURCE_INVALID` (exit 2) at parse, naming the unsatisfied
223
+ input, instead of syncing cleanly and failing once per fire. This covers
224
+ every entry shape: the `schedule: "<cron>"` string shorthand and a list
225
+ entry with no `inputs:` key are held to the same contract as one that
226
+ authors `inputs:`. Give the entry an `inputs:` value for each named input,
227
+ or declare a `default:` on the input instead. Manual-only tasks are
228
+ unaffected — a `required: true` input with no `schedule:` is still valid
229
+ and is supplied per run with `akm task run <id> --<name> <value>`.
230
+ - **`akm workflow create --json` renames its `stashDir` envelope field to
231
+ `bundleDir`.** The success envelope now reads
232
+ `{ok, ref, path, bundleDir}`; the value (the owning bundle's directory) is
233
+ unchanged. Scripts reading `stashDir` off `akm workflow create --json`
234
+ must read `bundleDir`. This was the last `stash`-vocabulary field on a
235
+ 0.9.2 command envelope; the indexer's internal `IndexOptions.stashDir` is
236
+ not a CLI surface and is unchanged.
237
+ - **Task v3 sources no longer parse.** A task document with `version: 3`
238
+ (or `version: 2`) fails with `UsageError` code
239
+ `TASK_SCHEMA_VERSION_UNSUPPORTED` (exit 2) instead of executing — the v3
240
+ parser is gone from `src/`; it survives only vendored inside the
241
+ `akm-migrate` executable, which is how the migrator still reads what it
242
+ converts. Run `akm migrate apply --dry-run`, review every `changed` /
243
+ `skipped` / `blocked` result, then `akm migrate apply` — the command now
244
+ runs **both** generations in one pass: task-v2 → task-v3, then
245
+ task-v3 → task source v4, against the same tree. `akm task add` authors
246
+ task source v4 directly; a task's `--params` becomes typed `inputs:` with
247
+ defaults instead of a `with:` bag. A task's enabled state is now per
248
+ schedule binding (`schedule[].enabled`) rather than a document-level
249
+ `akm.enabled` flag — `akm task add --disabled` writes
250
+ `schedule: [{cron: …, enabled: false}]` instead of a document-level
251
+ `akm.enabled: false`. See
252
+ [Migrating task v3 to task source v4](docs/migration/v0.9.1-to-v0.9.2.md#migrating-task-v3-to-task-source-v4).
253
+ - **GitHub Action locators are no longer recognized anywhere.** A task's
254
+ `uses: owner/repo[/path]@rev` is now a source error at parse
255
+ (`TASK_SOURCE_INVALID`); a workflow step's `uses: owner/repo[/path]@rev`
256
+ now fails with reason `unsupported-uses-target` instead of
257
+ `remote-action-acquisition-out-of-scope`. Nothing acquired or executed a
258
+ remote action in any akm release — this deletes the *recognition* of the
259
+ shape, not a capability that ever worked. The migrator still names the
260
+ target explicitly when it blocks a file
261
+ (`github-action-target-removed`).
262
+ - **Multi-job YAML is rejected at the adapter boundary.** A GitHub-shaped
263
+ workflow document whose `jobs:` map does not contain exactly one job now
264
+ fails at the source adapter with reason `multi-job-unsupported`, surfaced
265
+ from `akm workflow run` (and `akm workflow plan`) as `UsageError` code
266
+ `COMPOSITION_INVALID` (exit 2). Previously such a document parsed and
267
+ ordered its jobs cleanly and was refused only later, in two different
268
+ places, with two different shapes (one thrown error, one `ok: false`
269
+ compile result). Split a multi-job document into separate single-job
270
+ workflows and compose them with a child-workflow step
271
+ (`uses: workflows/<ref>`). Every other workflow-source compile failure now
272
+ reports `UsageError` code `WORKFLOW_SOURCE_INVALID` rather than
273
+ `INVALID_FLAG_VALUE`.
274
+ - **The second task scheduling syntax is removed.** `akm.schedule` and a
275
+ task document's top-level `on:` are gone along with task v3 (see "Task v3
276
+ sources no longer parse" above); task source v4's optional top-level
277
+ `schedule:` is the one canonical scheduling form, and a task with no
278
+ `schedule:` is manual-only and fully composable as a workflow-step
279
+ target.
280
+
281
+ ### Changed
282
+
283
+ - **`INVALID_FLAG_VALUE` is now rare in task or workflow domain failures,
284
+ with two named exceptions.** Every task-source, workflow-source,
285
+ target-classification, and composition failure now reports a
286
+ phase-specific code — `TASK_SOURCE_INVALID`, `TARGET_REF_INVALID`,
287
+ `COMPOSITION_INVALID`, `WORKFLOW_SOURCE_INVALID`, `INPUT_BINDING_INVALID`,
288
+ `TASK_SCHEMA_VERSION_UNSUPPORTED`, or `TASK_TARGET_UNSUPPORTED` — **except**
289
+ a task's workflow-target `env:` composition rejection (a `uses:
290
+ workflows/<ref>` task that also authors `env:`) and a workflow child-ref
291
+ asset-resolution failure (`Workflow source target <ref> was not found.`),
292
+ both deliberately preserved as `INVALID_FLAG_VALUE` so an existing pinned
293
+ test's code and message stay byte-unchanged. The remaining
294
+ `INVALID_FLAG_VALUE` sites in the task/workflow domains (38 total, across
295
+ `src/tasks/**` and `src/workflows/**`) are these two preserved exceptions
296
+ plus scalar CLI-argument parsing (a cron expression, a task id, a workflow
297
+ parameter flag) and one code-allowlist membership entry — genuine
298
+ flag-value validation or a pinned exception, not a re-codable
299
+ task/workflow source or composition failure. **Scripts branching on
300
+ `code` for a task/workflow domain error should switch on the specific
301
+ code above rather than assuming `INVALID_FLAG_VALUE` — except for the two
302
+ named exceptions, which still report `INVALID_FLAG_VALUE`.** Exit codes
303
+ are unchanged (2 for every one of these).
304
+ - **A typed task-input or workflow-param flag no longer echoes the supplied
305
+ value in a validation error.** `akm task run <ref> --<input> <value>`
306
+ against a `type:`-declared input used to report
307
+ `must be <types>; received "<value>"` on a coercion failure, and a value
308
+ that failed its declared `enum:`/`minimum:`/`maximum:` constraint reported
309
+ the value in that message too — both are closed now, since a typed flag
310
+ can carry a credential and this detail lands in stderr envelopes that get
311
+ pasted into CI logs and issue reports. The declared constraint (the
312
+ allowed list or the bound) is still named, since it comes from the
313
+ author's own schema rather than the caller's data. Error and exit codes
314
+ are unchanged.
315
+
316
+ ### Added
317
+
318
+ - **Child workflows.** A workflow step can now compose another workflow —
319
+ directly (`uses: workflows/<ref>`) or through a task source v4 document
320
+ whose own target is a workflow (`uses: tasks/<ref>`) — instead of
321
+ failing to freeze. `with:` on the composing step binds the child's
322
+ declared `params:`. Composition is bounded: depth (8 levels), a
323
+ composition cycle, and aggregate embedded plan bytes (1 MiB total across
324
+ one root freeze) are all checked at **freeze**, before the parent run is
325
+ published, and fail with `UsageError` code `COMPOSITION_INVALID`. The
326
+ child workflow is compiled, validated, and frozen **completely** — its own
327
+ complete plan embedded inside the parent's — before the parent run exists,
328
+ so editing the child's source afterward cannot affect an already-frozen
329
+ parent, and the child's transitive sources join the parent's guarded
330
+ source read set. A composing step's own `env:` is rejected at freeze
331
+ (`UsageError` code `COMPOSITION_INVALID`) rather than silently dropped —
332
+ a child run carries its own frozen environment inside its own plan, so a
333
+ parent-level `env:` on the composing step has nothing to apply to.
334
+ Running a step that composes a child workflow now drives
335
+ the child to completion — see **Child workflows now execute** and
336
+ **Workflow `outputs:`** below. (Correction: an earlier development
337
+ increment of this same 0.9.2 release briefly made an unexecuted composing
338
+ step fail closed with `UsageError` code
339
+ `WORKFLOW_CHILD_EXECUTION_UNSUPPORTED`. That code never reached a release
340
+ and is gone from the shipped 0.9.2 — it is listed here only because a
341
+ 0.9.2 pre-release snapshot may otherwise be the sole place it was seen.)
342
+ See
343
+ [Workflow Schema: Child workflows](docs/reference/workflow-schema.md#child-workflows).
344
+ - **Child workflows now execute.** Running a step whose target is a child
345
+ workflow drives that child inline, in the parent's own process, with the
346
+ same engine `akm workflow run` uses — publication is idempotent, so a
347
+ retried or resumed composing step reuses the same child rather than
348
+ starting a new one. The child's final status maps onto the composing
349
+ step and the parent run: `completed` promotes the child's exported result
350
+ as the step's output and the parent continues; `failed` fails the step
351
+ and the run; `blocked` blocks the step and the run, with recovery notes
352
+ naming the exact sequence — `akm workflow resume <childRunId>`, then
353
+ `akm workflow resume <parentRunId>` and `akm workflow run <parentRunId>`.
354
+ `akm workflow status` on a run that composes children now renders a
355
+ `children:` tree; `akm workflow list` excludes child runs by default
356
+ (`--children` includes them), and a child run id always works directly
357
+ with `status`/`resume`/`abandon`/`run`. See
358
+ [Workflow Schema: Child execution](docs/reference/workflow-schema.md#child-execution)
359
+ and [Running Workflows: Child runs](https://github.com/itlackey/akm/blob/main/docs/guides/run-workflows.md#child-runs).
360
+ - **Workflow `outputs:`.** A workflow may declare a run-level export in its
361
+ Markdown frontmatter — `outputs: {<name>: {from: steps.<id>.output(.<seg>)*,
362
+ schema?}}`, up to 64 entries — resolved once, from persisted step
363
+ evidence, at run completion. An unresolvable reference, a truncated
364
+ step artifact, or a schema violation rolls the completion back
365
+ (`UsageError` code `WORKFLOW_OUTPUT_INVALID`): the run stays `active` and
366
+ its final step stays `pending` rather than completing with missing
367
+ exports. A run with no `outputs:` declaration exports `{runId, status}`
368
+ instead. This is a Markdown-frontmatter-only key — a GitHub-shaped
369
+ workflow's closed root key set has no extension surface for it, the same
370
+ reason it cannot declare `params:` either. See
371
+ [Workflow Schema: Workflow outputs](docs/reference/workflow-schema.md#workflow-outputs).
372
+ - **`akm workflow plan <ref>`** (Evolving) — compiles, resolves, and freezes
373
+ a workflow exactly as starting a run would, then stops: zero durable
374
+ writes, no published run, no event, no lease. Prints the canonical step
375
+ graph, per-step frozen target kinds, task/child expansion, input
376
+ bindings, the source read set, and freeze-time lowering notices —
377
+ secret-free by construction (no resolved reference value, request
378
+ content, script bytes, or credential is ever printed). Defaults to a
379
+ human-readable summary; `--format json` returns the full envelope. See
380
+ [CLI reference: workflow plan](docs/reference/cli.md#workflow-plan).
381
+ - **`akm task explain <ref> [input flags]`** — read-only task introspection.
382
+ Prints the task's source path and version, its declared `inputs:` (with
383
+ defaults — a secret-shaped default prints as `<redacted>`), the supplied
384
+ values with provenance (`default` | `flag` | `schedule-binding`, likewise
385
+ redacted when secret-shaped), the resolved target kind/ref, effective
386
+ execution settings with field-level provenance, and schedule bindings.
387
+ Never spawns anything, writes history, or touches the scheduler; never
388
+ prints an `env:` value, a credential, a prompt body, a `run:` string, or
389
+ `with.content`. It accepts the task's own declared input flags and nothing
390
+ else: `--scheduled` — which `explain` neither declares nor implements —
391
+ fails `UNKNOWN_FLAG` (exit 2) rather than being silently discarded, in
392
+ every spelling (`--scheduled`, `--scheduled=false`, …). See
393
+ [CLI reference: task](docs/reference/cli.md#task).
394
+ - **`AKM_TASK_INPUTS`** — the exec-context environment variable a
395
+ task-composed step's shell/script target receives: canonical JSON of its
396
+ resolved, schema-validated `inputs:` bindings. Present only when the
397
+ bindings are non-empty; subject to the same per-platform size ceiling as
398
+ `AKM_INPUTS` / `AKM_PARAMS`. See
399
+ [Context reaching the command](docs/reference/workflow-schema.md#context-reaching-the-command).
400
+ - **A v3 → task source v4 migrator**: the separate `akm-migrate` executable
401
+ (installed alongside `akm`) gains `task-v4-status` / `task-v4-apply
402
+ [--dry-run]`, a second, independent generation of the same dry-run-first,
403
+ `changed | skipped | blocked` migration planner — options bag flattened to
404
+ top-level keys. A `with:` authored on any target other than
405
+ `uses: akm/command` is `blocked` for manual review, alongside a
406
+ github-action-targeted `uses:` (blocked reason
407
+ `github-action-target-removed`) and anything else ambiguous: the migrator
408
+ translates structure, never intent, so `inputs:` is never invented on a
409
+ file's behalf — declaring it is an authoring decision left to the person
410
+ editing the migrated file. Nothing is overwritten without a backup. A
411
+ `changed` file can carry an informational **notice** for a translation that
412
+ is faithful but not one-to-one — a manual-dispatch-only trigger that v4
413
+ expresses as "no `schedule:`", and an `akm.outputSchema` dropped because v4
414
+ accepts `output:` only with a command target — so read the notices on a
415
+ dry-run plan, not just the outcomes. A v3 document that was never valid in
416
+ the first place (an empty `on:`, or a `workflow_dispatch:` carrying
417
+ `inputs:`) is `blocked` as `invalid-v3-task` rather than being converted
418
+ into runnable v4 bytes. By
419
+ 0.9.2's release this generation runs automatically as the second half of
420
+ `akm migrate status` / `akm migrate apply [--dry-run]` (see "Task v3
421
+ sources no longer parse" above) — `task-v4-status`/`task-v4-apply` remain
422
+ as the standalone, single-generation entry points the frozen migrator
423
+ always exposes. See the
424
+ [0.9.1 to 0.9.2 migration guide](docs/migration/v0.9.1-to-v0.9.2.md#migrating-task-v3-to-task-source-v4).
425
+
426
+ ### Fixed
427
+
428
+ - **A flag value for a parameter or input declaring both `array` and a
429
+ scalar type is no longer forced into an array.** `akm workflow run <ref>
430
+ --<param> <value>` (and, new in this release, `akm task run <id>
431
+ --<input> <value>`) unconditionally grouped a supplied value into an array
432
+ whenever the declaration mentioned `array` at all, so
433
+ `type: ["array", "string"]` with `--x hello` delivered `["hello"]` instead
434
+ of the permitted string, and `type: ["array", "null"]` could never produce
435
+ `null` — silently, since the altered value still satisfied the array
436
+ branch. A single, non-bracketed value now tries the union's scalar
437
+ alternatives first. An `array`-only declaration, the JSON-array shorthand
438
+ (`--x '["a","b"]'`), and grouping a repeated flag are all unchanged.
439
+ - **`akm task <subcommand> --target=<value>` now answers with the 0.9 rename
440
+ hint instead of ignoring the flag.** The retired-spelling check compared
441
+ whole argv tokens, so it caught a bare `--target` but not `--target=team`;
442
+ because `target` is exempt from the generic unknown-flag gate on `task`
443
+ subcommands precisely so that check can answer, the `=`-spelling was
444
+ rejected by nothing at all and the bundle the caller named was silently
445
+ dropped. It now fails with `UsageError` code `INVALID_FLAG_VALUE` (exit 2)
446
+ naming `--bundle`, in every spelling.
447
+ - **The embedded `akm` hint sheet no longer teaches a task format this
448
+ release rejects.** Its "Scheduled Tasks" section still told readers to
449
+ author `version: 3` with `akm.enabled` and `akm.timeout`; it now describes
450
+ task source v4 (`version: 4`, per-entry `schedule[].enabled`, top-level
451
+ `timeout`, typed `inputs:`/`output:`) and points at `akm migrate apply`.
452
+ The `stash`-terminology doc lint now scans the shipped hint assets too, so
453
+ the embedded help cannot drift out of the active-docs vocabulary again.
454
+ - **The macOS native scheduler backend no longer refuses a real
455
+ `launchctl` inventory.** Its loaded-service reader enforced a narrow,
456
+ hand-written grammar over `launchctl print`'s full output and rejected
457
+ the entire read — surfacing as `INVALID_CONFIG_FILE` from every akm
458
+ scheduler command — the moment any line fell outside it, which real
459
+ `launchctl` output on a real Mac routinely does. It now scans for akm's
460
+ own `com.akm.task.*` labels and ignores everything else, which is what
461
+ every caller actually needed. Caught by the gated native-scheduler
462
+ suite's first run against macOS.
463
+ - **The Windows-built package no longer ships without its embedded
464
+ template assets.** The build's asset-copy step matched paths against a
465
+ forward-slash pattern, but path separators on Windows are backslashes,
466
+ so the match silently failed and `dist/assets/` was never populated —
467
+ the packaged npm tarball built on Windows carried no templates at all,
468
+ and any command rendering one (for example `akm health --format html`)
469
+ crashed with `ERR_MODULE_NOT_FOUND`. Also caught by the gated
470
+ native-scheduler suite's first run, this time against Windows.
471
+
472
+ ## [0.9.2-alpha.4] - 2026-08-26
473
+
474
+ ### Added
475
+
476
+ - **`akm health`: flag assets whose resolved type disagrees with their
477
+ directory** (#837). Adds a `type-directory-disagreement` advisory that
478
+ compares every indexed asset's resolved type against the type its
479
+ `DIR_TYPE_MAP` directory declares (`memories/`, `knowledge/`, `commands/`,
480
+ `agents/`, `workflows/`, `facts/`, `lessons/`, `sessions/`,
481
+ `instructions/`, `scripts/`, `env/`, `secrets/`, `tasks/`). This is the
482
+ diagnostic that would have caught #824 (three `memories/` files silently
483
+ indexed as commands) the day it was introduced. Since `knowledge/` +
484
+ `$ARGUMENTS` and `agents/` + `agent:` frontmatter are deliberate command
485
+ overrides, the check never hard-fails: every disagreement is reported as a
486
+ warning naming the winning classifier signal, with a `knownGoodOverride`
487
+ flag so a sanctioned override reads differently from an unexplained one.
488
+ - **`akm health`: report the Claude harness plugin's version and warn when
489
+ it's stale or out of range** (#838). Adds a `plugin-version` advisory that
490
+ reports each installed Claude Code `akm` plugin's version, warns when a
491
+ newer tag is published upstream (naming the update command), and warns
492
+ when the plugin's own declared `AKM_VERSION_RANGE` no longer admits the
493
+ running CLI — meaning the plugin has silently disabled itself. Makes an
494
+ outbound `git ls-remote` when network is available to check for a newer
495
+ tag; per owner decision, this is read-only and degrades to a benign pass
496
+ (no plugin, no marketplace clone, unreadable manifest, malformed range, or
497
+ a failed remote lookup) rather than crashing or blocking offline use.
498
+
499
+ ### Changed
500
+
501
+ - **Extract: LLM prompt is now built from parent-origin events only —
502
+ "harvest-without-prompting hybrid" (#840).** #830 folds a session's
503
+ subagent transcripts into its event stream for hashing and inline-ref
504
+ harvesting; the prompt sent to the extraction LLM previously included that
505
+ folded subagent content too, competing with the parent's own transcript
506
+ for the 80,000-char pre-filter budget. #840's design-determination doc
507
+ (`docs/plans/subagent-extraction-design.md`) measured that this "fold"
508
+ approach evicts up to 28.6% of parent-origin content on real sessions to
509
+ make room for subagent noise that mostly gets evicted anyway, while a
510
+ "harvest-without-prompting hybrid" — keep folding for hashing/inline-ref
511
+ purposes, but filter the prompt down to `data.events` whose `filePath`
512
+ matches the session's own (`data.ref.filePath`) — matches or beats the
513
+ folded prompt's size with zero eviction on every session measured, and
514
+ recovers the exact same inline refs (`akm remember`/`akm feedback` calls
515
+ the agent made inside a subagent), because that harvesting already runs on
516
+ the raw stream independent of what reaches the prompt. Only
517
+ `runPreLlmSessionGates`'s call into `preFilterSession` changed; folding
518
+ (`session-log.ts`) and `buildExtractPrompt` are untouched.
519
+ - **No forced re-extraction wave.** `hashSessionContent` still hashes the
520
+ full folded `data` (parent + subagents), computed before the
521
+ parent-origin view is built — no previously-computed session hash
522
+ changes, so no session already extracted under the fold prompt shape is
523
+ automatically re-processed. Use `--force` to re-process a specific
524
+ session under the new, parent-only prompt shape.
525
+ - **`processes.extract.maxTotalChars` is unchanged in meaning and default**
526
+ — it still caps the single-call prompt built from parent-origin events;
527
+ it simply no longer has to compete against subagent-origin noise for
528
+ that budget.
529
+ - **`minContentChars`** (the raw-size skip gate, #595/#596) is still
530
+ measured on the FULL folded `data.events` (parent + subagents),
531
+ deliberately left unchanged: narrowing it to parent-origin chars would
532
+ newly skip delegation-heavy sessions with a thin parent transcript
533
+ before extraction runs at all, even though their subagent-origin work is
534
+ still fully harvested via inline refs. The full-stream measurement is
535
+ today's existing behavior; the worst case it preserves is an LLM call
536
+ over a small parent-only prompt, not a missed extraction.
537
+ - #839's task-notification dedupe (which stubs a parent's
538
+ `<task-notification>` only when the matching subagent's own event ALSO
539
+ survives into the same kept prompt set) composes safely with this
540
+ change without modification: subagent-origin events never reach
541
+ `preFilterSession` on this path, so the dedupe's own scoping check
542
+ naturally makes it a no-op — the parent's notification (the only
543
+ remaining trace of delegated work in the prompt) survives untouched.
544
+
545
+ ### Fixed
546
+
547
+ - **`akm remember` synthesizes a description when the caller doesn't supply
548
+ one** (#835). Both the zero-flag hot path and the structured-args path
549
+ (e.g. `--tag`-only, with no `--description`/`--enrich`) previously wrote
550
+ memories with no `description:` and no `tags:`. akm's indexer covers only
551
+ synthesized frontmatter/headings, never body prose, so those memories were
552
+ retrievable only by whatever words survived into the auto-generated
553
+ filename — effectively write-only. Verified on a real stash: 272/3169
554
+ memories lacked a description, 100% of those written via `akm remember`.
555
+ The new `synthesizeMemoryDescription` (ported from akm-eval's
556
+ `firstSentencesCapped` rule, which independently arrived at the same fix)
557
+ is deterministic and makes no LLM call: it accumulates whole sentences
558
+ from the body up to `DESCRIPTION_MAX_CHARS`, skipping a leading markdown
559
+ heading so the description doesn't just repeat the title. Wired into both
560
+ write paths as a fallback only — a caller-supplied `--description` (or one
561
+ derived by `--enrich`) is never overwritten. Closes the write-only-memories
562
+ gap on 0.9.1 indexes.
563
+ - **Extract: deduped the doubled subagent conclusion in the extraction prompt**
564
+ (#839). After #830 folded a session's subagent transcripts into its event
565
+ stream, a completed subagent's final report could appear twice in the same
566
+ extraction prompt: once as the subagent's own folded final message, once as
567
+ the parent's `<task-notification>` record of that same call (#836 measured
568
+ ~92-99% textual overlap on a real pair; reproduced here as a byte-identical
569
+ match after decoding the XML entities Claude Code escapes into `<result>`).
570
+ The parent's notification copy is now stubbed to `[subagent <agentId>
571
+ completed: <description>]` when its `<result>` is a near-duplicate
572
+ (Dice-bigram similarity ≥ 0.9) of a folded subagent transcript's own text;
573
+ the subagent's original is untouched, per #839's owner-decided direction
574
+ (the inverse — dropping the subagent's own terminal event — was evaluated
575
+ and rejected in #836 because some subagent transcripts consist only of
576
+ that one event). Matching is scoped by `<task-id>` to the one subagent
577
+ transcript it names and still requires content similarity, so an earlier
578
+ notification for a *resumed* agent (Claude Code re-notifies the same
579
+ task-id on each stop) that carries a genuinely different, intermediate
580
+ result is left alone.
581
+ **Scoped to the final, post-budget kept set — not the raw stream** (#840's
582
+ design-determination doc flagged this as a hazard while this PR was in
583
+ flight): the dedupe only fires when the subagent's own event ALSO survives
584
+ into the same kept set as the notification. #840 measured that today's
585
+ recency-biased 80k budget already evicts one side of nearly every raw
586
+ duplicate pair before dedupe would matter (0 of 89 raw pairs across four
587
+ real sessions had both sides survive); an unconditional raw-stream stub
588
+ would, under that same eviction pattern, sometimes delete a parent's
589
+ notification whose subagent copy never made the cut in the first place —
590
+ and would unconditionally delete the *only* surviving trace of delegated
591
+ work under #840's recommended future design (prompting from parent-origin
592
+ events only). Verified against the real session #836 and #839 both cite
593
+ (`4a0d9e9b…`): under the actual 80,000-char budget, 0 notifications are
594
+ stubbed today (consistent with #840's finding) because the cited pair's
595
+ subagent copy doesn't survive the budget; with the budget cap lifted,
596
+ 1 of 10 raw duplicate pairs in that session both survive AND still exceed
597
+ the 0.9 similarity bar after the pre-filter's independent per-event
598
+ 2000-char truncation (the other 9 exceed that per-event cap and truncate
599
+ down far enough to fall below the bar — a conservative miss, never a wrong
600
+ stub). The fix is real and correct for sessions/pairs small enough to avoid
601
+ both eviction and truncation, and is structurally inert wherever it would
602
+ be unsafe to fire.
603
+ Implemented in the pre-filter (`preFilterSession`), which runs AFTER
604
+ `hashSessionContent` — so **no `contentHash` moves and no re-extraction
605
+ wave is triggered** (unlike #830's own folding change, which changed the
606
+ raw event stream #602's hash covers).
607
+ - **Extract: regression-tested the no-double-extraction guarantee** (#839).
608
+ Discovery-mode extraction over a project with a parent + subagent
609
+ transcripts now has an explicit end-to-end test proving exactly one
610
+ session is processed, that `--session-id agent-<hash>` resolves to the
611
+ not-found result rather than an extraction, and that folded subagent
612
+ content is attributed only to the parent's session/contentHash. Pins
613
+ behavior already true since #830 (`listSessions()` excludes `subagents/`
614
+ dirs for both discovery and `--session-id` lookup); nothing tested it
615
+ end-to-end before.
616
+
617
+ ### Documentation
618
+
619
+ - **Measured whether subagent-transcript folding (#830) duplicates the
620
+ parent's own summary, and disclosed the one-time re-extraction cost
621
+ (#833).** Using the actual reader/pre-filter/prompt-builder code against 3
622
+ real sessions on this machine — no LLM calls; `contentHash`,
623
+ `preFilterSession`, and `buildExtractPrompt` are deterministic:
624
+ - Raw event counts grow 2x-12x once subagent transcripts are folded in
625
+ (measured: 1209 -> 14224; 1583 -> 6738, the exact session cited in
626
+ #829/#833's "1583 -> 6738" figure; 155 -> 2408). `contentHash` is
627
+ computed over that stream, so every previously-extracted session's hash
628
+ changes and the next `--since` run re-extracts all of them once, each
629
+ with a larger prompt (+1.2% to +5.2% prompt chars across the 3 sessions,
630
+ since the 80,000-char pre-filter budget caps how much of the growth
631
+ actually reaches the LLM).
632
+ - The result is a genuine tradeoff, not a clean win or loss. **Benefit:**
633
+ inline `akm remember`/`akm feedback` calls made *by subagents* are
634
+ recovered regardless of the budget cap (inline-ref extraction runs on
635
+ the raw event stream, not the pre-filtered one) — up to 162 refs
636
+ recovered on the largest session measured (was 2 without folding),
637
+ fixing #829's "delegated work is never harvested" defect. **Cost:** on
638
+ sessions whose raw content is near or under the pre-filter's character
639
+ budget, folding evicts a large share of the parent's own kept content to
640
+ make room for subagent tool-call trace — parent-origin kept events
641
+ dropped 27% and 71% respectively on the two smaller sessions measured.
642
+ On the largest session the budget was already saturated by the parent's
643
+ own tail, so folding changed nothing there. Duplication is real, not
644
+ hypothetical: on the smallest session, one subagent's conclusion appears
645
+ twice in the same prompt sent to the extraction LLM — once via its own
646
+ folded final message, once via the parent's own record of that
647
+ delegated call's result, which independently already captured ~92% of
648
+ the same text verbatim.
649
+ - A narrowing that drops a subagent transcript's terminal event (its
650
+ apparent "final report") to avoid this specific duplication was
651
+ considered and rejected: the existing #830 regression fixture has a
652
+ subagent transcript whose *only* event is that terminal turn (a single
653
+ delegated `akm remember` call) — the same rule would drop the only
654
+ content in short single-step delegations, undoing the harvesting #830
655
+ added.
656
+ - **Decision: keep folding as shipped.** The data does not cleanly favor
657
+ removing or narrowing it, and the one narrowing considered would cost
658
+ more than it fixes. #829's phantom-session exclusion is unaffected
659
+ either way.
660
+ - Recorded the fold-vs-link subagent-extraction design determination in
661
+ `docs/plans/subagent-extraction-design.md` (#840). Measured four candidates
662
+ (fold+dedupe as shipped, link-only, a harvest-without-prompting hybrid, and
663
+ chunked map-reduce extraction) on the same real sessions #836 used plus one
664
+ added for scale. Headline: the hybrid recovers 100% of #830's inline-ref
665
+ harvesting (162/162, 38/38, 1/1, 8/8 across the four sessions) with zero
666
+ parent-content eviction (vs 27.5%/28.6% evicted under fold on two of the
667
+ four), and #839's dedupe was measured to have zero effect on the actual
668
+ LLM prompt on all four sessions (the flagged duplicate content is already
669
+ evicted by the recency-biased budget before dedupe would matter). Chunked
670
+ extraction was measured at 9x-229x more LLM calls per session on real
671
+ data and is not recommended. No behavior changes shipped in this PR.
672
+
9
673
  ## [0.9.2-alpha.3] - 2026-08-26
10
674
 
11
675
  ### Fixed