akm-cli 0.9.29 → 0.9.31

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 (162) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +10 -10
  3. package/ROADMAP.md +112 -0
  4. package/dist/commands/improve/improve-cli.js +33 -12
  5. package/dist/commands/sources/plugin-upgrade.js +46 -23
  6. package/dist/core/file-lock.js +10 -1
  7. package/dist/llm/client.js +33 -3
  8. package/dist/scripts/akm-migrate-node.js +5 -1
  9. package/dist/scripts/akm-migrate.js +5 -1
  10. package/docs/README.md +42 -41
  11. package/docs/agents/README.md +15 -0
  12. package/docs/agents/agent-install.md +14 -0
  13. package/docs/architecture/README.md +67 -0
  14. package/docs/architecture/adapters.md +272 -0
  15. package/docs/architecture/akm-architecture-decision-history.md +2055 -0
  16. package/docs/architecture/akm-core-principles.md +126 -0
  17. package/docs/architecture/architecture.md +579 -0
  18. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +550 -0
  19. package/docs/architecture/decisions/0001-no-interpolation-attached-structured-context.md +72 -0
  20. package/docs/architecture/decisions/0002-unit-reuse-and-input-hash-scope.md +142 -0
  21. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +189 -0
  22. package/docs/architecture/decisions/0004-task-input-contract-and-flag-coercion.md +97 -0
  23. package/docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md +108 -0
  24. package/docs/architecture/decisions/0006-task-source-version-routing.md +124 -0
  25. package/docs/architecture/decisions/0007-workflow-composition-bounds.md +83 -0
  26. package/docs/architecture/decisions/0008-task-binding-normalization.md +88 -0
  27. package/docs/architecture/decisions/0009-child-run-publication-column-parity.md +80 -0
  28. package/docs/architecture/decisions/0010-driver-protocol-cut.md +36 -0
  29. package/docs/architecture/decisions/0011-engine-run-loop-invariants.md +125 -0
  30. package/docs/architecture/decisions/README.md +62 -0
  31. package/docs/architecture/improvement.md +440 -0
  32. package/docs/architecture/internals/classification.md +154 -0
  33. package/docs/architecture/internals/fresh-host-rebuild-runbook.md +40 -0
  34. package/docs/architecture/internals/functional-contract-patterns.md +340 -0
  35. package/docs/architecture/internals/health-advisories.md +110 -0
  36. package/docs/architecture/internals/improve-workflow.md +713 -0
  37. package/docs/architecture/internals/indexing.md +498 -0
  38. package/docs/architecture/internals/registry-network-boundary.md +57 -0
  39. package/docs/architecture/internals/search.md +249 -0
  40. package/docs/architecture/internals/storage-locations.md +943 -0
  41. package/docs/architecture/persisted-data-compat.md +59 -0
  42. package/docs/architecture/reviews/env-secret-access.md +319 -0
  43. package/docs/architecture/runtime-boundary-design.md +153 -0
  44. package/docs/architecture/specs/0.9.0-close-out-plan.md +361 -0
  45. package/docs/architecture/specs/0.9.0-decisions.md +528 -0
  46. package/docs/architecture/specs/0.9.0-docs-code-drift-register.md +745 -0
  47. package/docs/architecture/specs/0.9.0-open-items-register.md +135 -0
  48. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +740 -0
  49. package/docs/architecture/specs/0.9.0-release-surface-review.md +636 -0
  50. package/docs/architecture/specs/agent-command-engine-model-design.md +513 -0
  51. package/docs/architecture/specs/akm-0.9.0-bundle-adapter-spec.md +501 -0
  52. package/docs/architecture/specs/akm-0.9.0-ref-grammar-decision.md +291 -0
  53. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +2059 -0
  54. package/docs/architecture/specs/di-seams-plan.md +539 -0
  55. package/docs/architecture/specs/driver-protocol-keep-or-cut.md +275 -0
  56. package/docs/architecture/specs/fact-asset-type.md +142 -0
  57. package/docs/architecture/specs/improve-collapse-churn-detector-design.md +634 -0
  58. package/docs/architecture/specs/okf-support.md +148 -0
  59. package/docs/architecture/specs/ref.md +329 -0
  60. package/docs/architecture/specs/reviews/task-workflow-unification/README.md +72 -0
  61. package/docs/architecture/specs/reviews/task-workflow-unification/findings-consolidated.md +152 -0
  62. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-alternatives.md +352 -0
  63. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-bunqueue.md +300 -0
  64. package/docs/architecture/specs/reviews/task-workflow-unification/library-eval-openworkflow.md +402 -0
  65. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-1.md +758 -0
  66. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-2.md +645 -0
  67. package/docs/architecture/specs/reviews/task-workflow-unification/opus-review-3.md +602 -0
  68. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-adversarial.md +27 -0
  69. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-architecture.md +27 -0
  70. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-coverage.md +74 -0
  71. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r1-security.md +15 -0
  72. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-adversarial.md +132 -0
  73. package/docs/architecture/specs/reviews/task-workflow-unification/panel-r2-verification.md +147 -0
  74. package/docs/architecture/specs/stash-conventions-code-spec.md +275 -0
  75. package/docs/architecture/specs/stash-organization-conventions.md +377 -0
  76. package/docs/architecture/specs/task-workflow-format-unification.md +721 -0
  77. package/docs/architecture/specs/workflow-engine-buy-vs-build.md +231 -0
  78. package/docs/architecture/specs/workflow-format-unification.md +437 -0
  79. package/docs/architecture/testing/manual-testing-checklist.md +3420 -0
  80. package/docs/architecture/testing/okf-v0.2-conformance-runbook.md +924 -0
  81. package/docs/architecture/testing/task-workflow-manual-tests.md +1817 -0
  82. package/docs/architecture/testing/testing-workflow.md +446 -0
  83. package/docs/architecture/workflow-engine.md +274 -0
  84. package/docs/guides/README.md +33 -0
  85. package/docs/guides/author-bundles.md +310 -0
  86. package/docs/guides/author-workflows.md +520 -0
  87. package/docs/guides/bundles.md +321 -0
  88. package/docs/guides/capture-knowledge.md +136 -0
  89. package/docs/guides/claude-code-vs-akm-workflows.md +30 -0
  90. package/docs/guides/concepts.md +93 -0
  91. package/docs/guides/discover-and-load.md +158 -0
  92. package/docs/guides/getting-started.md +240 -0
  93. package/docs/guides/improve-the-library.md +246 -0
  94. package/docs/guides/knowledge-management.md +18 -0
  95. package/docs/guides/recipes/headless-install.md +97 -0
  96. package/docs/guides/recipes/website-source.md +28 -0
  97. package/docs/guides/run-workflows.md +279 -0
  98. package/docs/guides/scheduling.md +123 -0
  99. package/docs/guides/use-with-any-agent.md +129 -0
  100. package/docs/guides/wikis.md +81 -0
  101. package/docs/maintainers/README.md +19 -0
  102. package/docs/maintainers/curate-workmap.md +336 -0
  103. package/docs/maintainers/eval.md +3 -0
  104. package/docs/maintainers/local-development.md +108 -0
  105. package/docs/maintainers/release-checklist.md +103 -0
  106. package/docs/migration/README.md +2 -2
  107. package/docs/migration/release-notes/0.0.13.md +1 -1
  108. package/docs/migration/release-notes/0.1.0.md +1 -1
  109. package/docs/migration/release-notes/0.2.0.md +1 -1
  110. package/docs/migration/release-notes/0.3.0.md +1 -1
  111. package/docs/migration/release-notes/0.5.0.md +1 -1
  112. package/docs/migration/release-notes/0.6.0.md +1 -1
  113. package/docs/migration/release-notes/0.7.0.md +1 -1
  114. package/docs/migration/release-notes/0.7.3.md +2 -2
  115. package/docs/migration/release-notes/0.7.4.md +1 -1
  116. package/docs/migration/release-notes/0.7.5.md +1 -1
  117. package/docs/migration/release-notes/0.8.0.md +1 -1
  118. package/docs/migration/release-notes/0.9.0.md +1 -1
  119. package/docs/migration/release-notes/0.9.14.md +1 -1
  120. package/docs/migration/release-notes/0.9.15.md +1 -1
  121. package/docs/migration/release-notes/0.9.16.md +1 -1
  122. package/docs/migration/release-notes/0.9.17.md +1 -1
  123. package/docs/migration/release-notes/0.9.19.md +1 -1
  124. package/docs/migration/release-notes/0.9.2.md +1 -1
  125. package/docs/migration/release-notes/README.md +27 -31
  126. package/docs/migration/v0.5-to-v0.6.md +759 -0
  127. package/docs/migration/v0.8-to-v0.9.md +1 -1
  128. package/docs/plans/0.9.2-agent-command-workflow-plan.md +423 -0
  129. package/docs/posts/README.md +30 -0
  130. package/docs/posts/akm-command-tour.md +495 -0
  131. package/docs/posts/akm-logo-sized.webp +0 -0
  132. package/docs/posts/belief-aware-memory-12.md +139 -0
  133. package/docs/posts/config-v2-lessons-16.md +191 -0
  134. package/docs/posts/improve-loop-autonomous-10.md +225 -0
  135. package/docs/posts/improve-pipeline-debugging-0803.md +179 -0
  136. package/docs/posts/intro-part-01.md +76 -0
  137. package/docs/posts/intro-part-02.md +221 -0
  138. package/docs/posts/intro-part-03.md +190 -0
  139. package/docs/posts/intro-part-04.md +221 -0
  140. package/docs/posts/llm-mode-reflect-13.md +166 -0
  141. package/docs/posts/openpalm-built-on-akm.md +86 -0
  142. package/docs/posts/progressive-disclosure-06.md +129 -0
  143. package/docs/posts/proposal-safety-net-15.md +112 -0
  144. package/docs/posts/release-0.5.0.md +247 -0
  145. package/docs/posts/release-0.6.0.md +117 -0
  146. package/docs/posts/release-0.7.0.md +159 -0
  147. package/docs/posts/release-0.8.0.md +332 -0
  148. package/docs/posts/scattered-skills-05.md +100 -0
  149. package/docs/posts/task-assets-persistent-workflows-11.md +222 -0
  150. package/docs/posts/team-skills-07.md +198 -0
  151. package/docs/posts/wiki-agent-knowledge-08.md +223 -0
  152. package/docs/posts/workflows-vaults-09.md +321 -0
  153. package/docs/product-surface.md +54 -0
  154. package/docs/reference/README.md +1 -2
  155. package/docs/reference/asset-types.md +168 -0
  156. package/docs/reference/cli.md +5 -6
  157. package/docs/reference/env-and-secrets.md +126 -0
  158. package/docs/reference/memory.md +124 -0
  159. package/docs/reference/refs.md +156 -0
  160. package/docs/reference/registry.md +489 -0
  161. package/docs/reference/website-sources.md +141 -0
  162. package/package.json +12 -15
@@ -0,0 +1,1817 @@
1
+ # Manual tests — `akm task` and `akm workflow` (0.9.2)
2
+
3
+ This document is a focused manual-QA suite for the `akm task` and `akm
4
+ workflow` subsystems shipped in PR #844 ("Task/workflow final refactor:
5
+ typed task inputs, child workflows, v3 retirement"). It sits alongside
6
+ [`manual-testing-checklist.md`](./manual-testing-checklist.md) (the
7
+ general-purpose runbook, whose §14/§15 give a lighter pass over the same
8
+ commands) as the deeper, field-level reference for this specific feature
9
+ area — use it when validating a task/workflow release or investigating a
10
+ regression in scheduling, typed inputs, child workflows, or the
11
+ task↔workflow seam.
12
+
13
+ **Verified against:** `akm` `0.9.2-alpha.5`, commit `fb7bef3f` (branch
14
+ `release/0.9.2`). Every expected result marked "verified live" below was
15
+ captured by actually running that exact command against that exact commit
16
+ during authoring/review of this document, via `bun src/cli.ts` in an
17
+ isolated sandbox (see [Setup](#setup--one-sandbox-for-every-non-destructive-test)
18
+ below) — not inferred from source or docs. Anything not run live is marked
19
+ `UNVERIFIED` in bold, inline, at the point it applies. **Re-verify the exact
20
+ JSON field names, error strings, and exit codes here before relying on them
21
+ against a later release** — none of this is covered by automated tests
22
+ (that's the point of a manual suite), so it can drift silently.
23
+
24
+ The two defects discovered during that live pass were fixed before stable:
25
+ #846 is now a cross-bundle scheduler-isolation regression, and #847 is now an
26
+ abandon/resume durable-spine regression. Their corrected expectations below
27
+ are pinned by automated integration coverage on the stable candidate; the
28
+ remaining "verified live" labels still refer to the alpha.5 run above.
29
+
30
+ ## Read this first — the one safety-critical finding
31
+
32
+ **`akm task sync` (and, to a lesser extent, `akm task add`) write to the
33
+ real, global, per-OS-user scheduler (cron / launchd / schtasks) — no
34
+ `AKM_*_DIR` environment variable isolates it.** Setting `AKM_BUNDLE_DIR`,
35
+ `AKM_DATA_DIR`, etc. isolates task *files* and akm's own state, but the
36
+ scheduler is one shared, host-global resource with no concept of your
37
+ sandbox.
38
+
39
+ Before #846 was fixed, `task sync`'s orphan-removal used bundle-name equality
40
+ without also confirming the installed entry's resolved bundle path. Two
41
+ unregistered bundles with the same directory basename could therefore make
42
+ one bundle inspect another's scheduler entry as an orphan. Stable 0.9.2
43
+ requires both the matching name and matching resolved owner path for this
44
+ primary-bundle case, and refuses to remove an entry whose owner path cannot be
45
+ established. DT-5 below pins that boundary. **There is still no `--dry-run`
46
+ for `task add`/`task sync`, so scheduler tests remain destructive.**
47
+
48
+ Practical rule for every test below that touches `add`/`sync`:
49
+
50
+ 1. Never run them on a machine with real akm-managed scheduler entries
51
+ unless you have taken a `crontab -l` (or platform equivalent) backup
52
+ immediately beforehand and can restore it.
53
+ 2. Prefer an explicit, collision-proof `--bundle <name>` so evidence and
54
+ cleanup are unambiguous, even though #846 now protects basename collisions.
55
+ 3. Prefer a disposable container/VM with an empty scheduler.
56
+ 4. Every destructive test below states its own backup/restore commands and
57
+ uses an obviously-throwaway id (`akm-manual-test-*`). Follow them exactly;
58
+ do not substitute `akm task sync` as a "cleanup" step (it can remove
59
+ entries the test didn't create).
60
+
61
+ `akm task doctor` is read-only but **still reads the real OS scheduler**
62
+ regardless of `AKM_*_DIR` isolation — seeing your host's real cron entries
63
+ in `doctor` output during an "isolated" test is expected, not a bug.
64
+
65
+ ## Setup — one sandbox for every non-destructive test
66
+
67
+ ```sh
68
+ export AKM_SANDBOX="$(mktemp -d /tmp/akm-task-workflow-qa.XXXXXX)"
69
+ export AKM_BUNDLE_DIR="$AKM_SANDBOX/bundle"
70
+ export AKM_CONFIG_DIR="$AKM_SANDBOX/config"
71
+ export AKM_DATA_DIR="$AKM_SANDBOX/data"
72
+ export AKM_CACHE_DIR="$AKM_SANDBOX/cache"
73
+ export AKM_STATE_DIR="$AKM_SANDBOX/state"
74
+ mkdir -p "$AKM_BUNDLE_DIR"/{tasks,workflows,commands} \
75
+ "$AKM_CONFIG_DIR" "$AKM_DATA_DIR" "$AKM_CACHE_DIR" "$AKM_STATE_DIR"
76
+ cat > "$AKM_CONFIG_DIR/config.json" <<'EOF'
77
+ {"configVersion":"0.9.0","semanticSearchMode":"off","registries":[]}
78
+ EOF
79
+ cd "$AKM_SANDBOX" # workflow list/status scope by cwd — see WF-8
80
+
81
+ # Pick ONE binary and use it consistently for a given test run:
82
+ akm() { bun /home/founder3/code/github/itlackey/akm/src/cli.ts "$@"; } # repo checkout
83
+ # akm() { "$AKM_SANDBOX/npm/bin/akm" "$@"; } # after: npm i -g --prefix "$AKM_SANDBOX/npm" akm-cli@0.9.2
84
+ ```
85
+
86
+ This hand-written `config.json` is enough for every `task` and `workflow`
87
+ command below — they resolve directly against `AKM_BUNDLE_DIR` without the
88
+ bundle needing to be a registered source. **The one exception is TASK-14**
89
+ (migration), where `migrate status`/`apply` require the bundle to be
90
+ registered via `akm setup` instead; that test gives its own setup variant.
91
+
92
+ **Cleanup for every non-destructive test:** `rm -rf "$AKM_SANDBOX"`. Nothing
93
+ outside it is touched. Destructive tests (final section) have their own,
94
+ separate cleanup that includes restoring the OS scheduler.
95
+
96
+ ---
97
+
98
+ ## Summary table
99
+
100
+ | ID | Name | Destructive | Platform | Live-verified | Expected-to-fail pending |
101
+ |---|---|:---:|---|:---:|---|
102
+ | TASK-1 | Typed input, default provenance | No | Any | Yes | — |
103
+ | TASK-2 | Provenance from explicit flag | No | Any | Yes | — |
104
+ | TASK-3 | Wrong-type input rejected | No | Any | Yes | — |
105
+ | TASK-4 | Missing required input: `explain` lenient, `run` strict | No | Any | Yes | — |
106
+ | TASK-5 | Undeclared input flag rejected | No | Any | Yes | — |
107
+ | TASK-6 | Reserved flag name collision | No | Any | Yes | — |
108
+ | TASK-7 | Secret-shaped default redacted in `explain` | No | Any | Yes | — |
109
+ | TASK-8 | Schedule-binding provenance | No | Any | Yes | — |
110
+ | TASK-9 | Required input w/o default bricks a schedule entry | No | Any | Yes | — |
111
+ | TASK-10 | `run` happy path; inputs validated then discarded | No | Any | Yes | — |
112
+ | TASK-11 | `history` reflects a real run | No | Any | Yes | — |
113
+ | TASK-12 | `run` on nonexistent task id (exit 1, not 2) | No | Any | Yes | — |
114
+ | TASK-13 | Pre-v4 stored task is rejected with a migration hint | No | Any | Yes | — |
115
+ | TASK-14 | v3→v4 migration end to end | No | Any | Yes | — |
116
+ | TASK-15 | `doctor` reports backend/paths | No | Linux verified; macOS/Windows **UNVERIFIED** | Partial | — |
117
+ | TASK-16 | Ineligible-checkout guard fires before any scheduler write | No | Any | Partial | — |
118
+ | WF-1 | Author and run an exec workflow end to end | No | Any | Yes | — |
119
+ | WF-2 | Default `create` template needs a judge engine | No | Any | Yes | — |
120
+ | WF-3 | Child workflow: `plan` expansion + `run` execution | No | Any | Yes | — |
121
+ | WF-4 | `plan` makes zero durable writes | No | Any | Yes | — |
122
+ | WF-5 | Failing step surfaces a clear diagnostic | No | Any | Yes | — |
123
+ | WF-6 | Blocked run → `abandon` → `resume` | No | Any | Alpha.5 bug reproduced; stable regression automated | — |
124
+ | WF-7 | Active (paused) run → `abandon` → `resume` (contrast to WF-6) | No | Any | Yes | — |
125
+ | WF-8 | `list` is cwd-scoped; `status <id>` is not | No | Any | Yes | — |
126
+ | WF-9 | Pre-`irVersion`-5 stored plan is retired | No | Any | Yes | — |
127
+ | WF-10 | Removed `inherit_env` is rejected | No | Any | Yes | — |
128
+ | INT-1 | A task that runs a workflow | No | Any | Yes | — |
129
+ | INT-2 | A workflow step that targets a task (+ typed input binding) | No | Any | Yes | — |
130
+ | INT-3 | Failure propagation: `task history` vs `workflow status` | No | Any | Yes | — |
131
+ | INT-4 | Identity across the seam: abandon then re-invoke | No | Any | Yes | — |
132
+ | INT-5 | Shipped `improve/*.yml` tasks as a real-world case | No | Any | Yes | — |
133
+ | DT-1 | `task add` happy path installs a real cron entry | **Yes** | Linux (cron) | Partial | — |
134
+ | DT-2 | `task add --disabled` writes a disabled binding | **Yes** | Linux (cron) | Partial | — |
135
+ | DT-3 | `task sync` drift reconciliation (shell-target task) | **Yes** | Linux (cron), VM recommended | No — **UNVERIFIED** | — |
136
+ | DT-4 | `task sync` reconciling a workflow-targeting task's schedule | **Yes** | Linux (cron), VM recommended | No — **UNVERIFIED** | — |
137
+ | DT-5 | Cross-bundle name collision cannot make `sync` remove another bundle's entries | **Yes** | Linux (cron), VM required | Automated; manual platform run **UNVERIFIED** | — |
138
+ | DT-6 | macOS launchd equivalents of DT-1..DT-3 | **Yes** | macOS — **PLATFORM-GATED, UNVERIFIED** | No | Issue #770 (no automated coverage) |
139
+ | DT-7 | Windows schtasks equivalents of DT-1..DT-3 | **Yes** | Windows — **PLATFORM-GATED, UNVERIFIED** | No | Issue #770 (no automated coverage) |
140
+
141
+ ---
142
+
143
+ ## Tasks
144
+
145
+ Scope: `akm task add|run|explain|history|sync|doctor`, non-destructive
146
+ subset. Destructive/`sync`/`add` scheduler-mutation tests are in
147
+ [Destructive & platform-gated](#destructive--platform-gated-tests) at the
148
+ end.
149
+
150
+ ### TASK-1 — Typed input, default provenance
151
+
152
+ **Why it matters:** the most common real path — a task with typed inputs a
153
+ user never overrides. If `explain`'s provenance tracking is wrong here,
154
+ every other provenance claim is suspect too.
155
+
156
+ **Preconditions:** common sandbox above.
157
+
158
+ **Setup:**
159
+ ```sh
160
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-echo.yml" <<'EOF'
161
+ version: 4
162
+ name: Manual test echo
163
+ description: Echoes a greeting for manual QA
164
+ inputs:
165
+ who:
166
+ type: string
167
+ default: world
168
+ loud:
169
+ type: boolean
170
+ default: false
171
+ run: echo "hello $AKM_TASK_INPUTS"
172
+ schedule: "@daily"
173
+ EOF
174
+ ```
175
+
176
+ **Command:** `akm task explain manual-test-echo --format json`
177
+
178
+ **Expected result (verified live):** exit `0`;
179
+ `.suppliedInputs.who == {"value":"world","provenance":"default"}`;
180
+ `.suppliedInputs.loud == {"value":false,"provenance":"default"}`;
181
+ `.inputDeclarations.who.default == "world"`;
182
+ `.schedule[0].cron == "@daily"`, `.schedule[0].enabled == true`.
183
+
184
+ **Cleanup:** keep `manual-test-echo.yml` for TASK-2/TASK-10/TASK-11 if
185
+ running the suite in order, otherwise `rm "$AKM_BUNDLE_DIR/tasks/manual-test-echo.yml"`.
186
+
187
+ ### TASK-2 — Provenance from an explicit flag
188
+
189
+ **Why it matters:** provenance is the whole point of `explain` — a user
190
+ debugging "why did my scheduled run use X" needs to trust this field.
191
+
192
+ **Preconditions:** `manual-test-echo.yml` from TASK-1.
193
+
194
+ **Command:** `akm task explain manual-test-echo --who Alice --format json`
195
+
196
+ **Expected result (verified live):** exit `0`;
197
+ `.suppliedInputs.who == {"value":"Alice","provenance":"flag"}`;
198
+ `.suppliedInputs.loud` unchanged (`provenance: "default"`).
199
+
200
+ **Cleanup:** none (read-only).
201
+
202
+ ### TASK-3 — Wrong-type input value is rejected
203
+
204
+ **Why it matters:** typed inputs are only useful if bad values are caught
205
+ before reaching a command/script — the core defect-prevention claim of the
206
+ release.
207
+
208
+ **Preconditions:** `manual-test-echo.yml` from TASK-1.
209
+
210
+ **Command:** `akm task explain manual-test-echo --loud notabool`
211
+
212
+ **Expected result (verified live, exact):** exit `2`; stderr:
213
+ ```json
214
+ {"ok":false,"error":"Task input \"--loud\" must be boolean.","code":"INPUT_BINDING_INVALID","hint":"Check the step's with: keys against the target's declared inputs."}
215
+ ```
216
+ Also run `akm task run manual-test-echo --loud notabool` and confirm the
217
+ same `code`/exit `2` — `run` and `explain` share one validator.
218
+
219
+ **Cleanup:** none.
220
+
221
+ ### TASK-4 — Missing `required: true` input: `explain` is lenient, `run` is not
222
+
223
+ **Why it matters:** a real trap — a user who runs `explain` on a
224
+ half-configured task sees no error and may assume it's fine to schedule,
225
+ then discovers at `run` time that it fails.
226
+
227
+ **Setup:**
228
+ ```sh
229
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-required.yml" <<'EOF'
230
+ version: 4
231
+ name: Manual test required input
232
+ inputs:
233
+ ticket:
234
+ type: string
235
+ required: true
236
+ run: echo "ticket=$AKM_TASK_INPUTS"
237
+ EOF
238
+ ```
239
+
240
+ **Commands and expected results (both verified live):**
241
+ 1. `akm task explain manual-test-required --format json` → exit `0`,
242
+ `.suppliedInputs == {}` (no error — `ticket` is simply absent).
243
+ 2. `akm task run manual-test-required` → exit `2`:
244
+ ```json
245
+ {"ok":false,"error":"Task input flags do not satisfy the task's declared input schemas:\n - inputs.ticket: is required","code":"INPUT_BINDING_INVALID"}
246
+ ```
247
+
248
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-test-required.yml"`.
249
+
250
+ ### TASK-5 — Undeclared input flag is rejected
251
+
252
+ **Why it matters:** typo-tolerance — a fat-fingered flag name should get a
253
+ clear rejection, not a silent no-op.
254
+
255
+ **Preconditions:** `manual-test-echo.yml` from TASK-1.
256
+
257
+ **Command:** `akm task explain manual-test-echo --bogus x`
258
+
259
+ **Expected result (verified live, exact):** exit `2`:
260
+ ```json
261
+ {"ok":false,"error":"Unknown task input \"--bogus\". Input flags must exactly match a declared task input.","code":"UNKNOWN_FLAG","hint":"Declared inputs: --loud, --who."}
262
+ ```
263
+
264
+ ### TASK-6 — Declaring an input that collides with a reserved `task run` flag
265
+
266
+ **Why it matters:** guards against a task input silently shadowing a
267
+ built-in flag (e.g. `bundle`, `format`); the docs say this is rejected at
268
+ parse time.
269
+
270
+ **Setup:**
271
+ ```sh
272
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-reserved.yml" <<'EOF'
273
+ version: 4
274
+ name: Manual test reserved flag collision
275
+ inputs:
276
+ bundle:
277
+ type: string
278
+ default: x
279
+ run: echo hi
280
+ EOF
281
+ ```
282
+
283
+ **Command:** `akm task explain manual-test-reserved`
284
+
285
+ **Expected result (verified live, exact):** exit `2`, `.code ==
286
+ "TASK_SOURCE_INVALID"`, error text is `Invalid task source v4 at
287
+ <path>:5: inputs.bundle collides with akm task run's own --bundle flag;
288
+ declare the input under a different name.`
289
+
290
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-test-reserved.yml"`.
291
+
292
+ ### TASK-7 — Secret-shaped input default is redacted in `explain`
293
+
294
+ **Why it matters:** `explain` is documented as secret-free — a
295
+ security-conscious user needs to trust this before pasting `explain` output
296
+ into a bug report or chat.
297
+
298
+ **Setup:**
299
+ ```sh
300
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-secret.yml" <<'EOF'
301
+ version: 4
302
+ name: Manual test secret redaction
303
+ inputs:
304
+ api_token:
305
+ type: string
306
+ default: "sk-abcdef123456"
307
+ run: echo "token=$AKM_TASK_INPUTS"
308
+ EOF
309
+ ```
310
+
311
+ **Command:** `akm task explain manual-test-secret`
312
+
313
+ **Expected result (verified live):** exit `0`; the literal value
314
+ `sk-abcdef123456` does **not** appear anywhere in stdout;
315
+ `.inputDeclarations.api_token.default == "<redacted>"` and
316
+ `.redacted == true`; `.suppliedInputs.api_token.value == "<redacted>"` and
317
+ `.redacted == true`. (A heuristic warning about the secret-suggesting name
318
+ is also printed on stderr — expected, not a failure.)
319
+
320
+ Verify outside akm: `grep -c sk-abcdef123456 <(akm task explain manual-test-secret)` must print `0`.
321
+
322
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-test-secret.yml"`.
323
+
324
+ ### TASK-8 — Schedule-binding provenance is distinct from manual-run provenance
325
+
326
+ **Why it matters:** exercises the third provenance value
327
+ (`schedule-binding`) and the fact that a schedule entry's own literal
328
+ inputs are shown per-entry, not merged into top-level `suppliedInputs`.
329
+
330
+ **Setup:**
331
+ ```sh
332
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-schedbind.yml" <<'EOF'
333
+ version: 4
334
+ name: Manual test schedule-binding provenance
335
+ inputs:
336
+ scope:
337
+ type: string
338
+ enum: [changed, all]
339
+ default: changed
340
+ run: echo "scope=$AKM_TASK_INPUTS"
341
+ schedule:
342
+ - cron: "0 8 * * 1"
343
+ enabled: true
344
+ inputs: { scope: all }
345
+ EOF
346
+ ```
347
+
348
+ **Command:** `akm task explain manual-test-schedbind`
349
+
350
+ **Expected result (verified live, exact):** exit `0`;
351
+ `.suppliedInputs.scope == {"value":"changed","provenance":"default"}`
352
+ (reflecting the bare call);
353
+ `.schedule[0].inputs.scope == {"value":"all","provenance":"schedule-binding"}`;
354
+ `.schedule[0].source == "schedule[0].cron"`.
355
+
356
+ Then `akm task run manual-test-schedbind --scheduled --scope all` → exit `0`
357
+ (the exact invocation tail `sync` would compile for this binding).
358
+
359
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-test-schedbind.yml"`.
360
+
361
+ ### TASK-9 — Required input with no default bricks the whole schedule entry at parse time
362
+
363
+ **Why it matters:** the sharpest footgun in typed inputs. Adding a
364
+ `required: true` input to an already-scheduled task, without also giving
365
+ the schedule entry a literal value, doesn't fail narrowly — **the whole task
366
+ file fails to parse**, for `explain`, `run`, even with the flag supplied
367
+ manually.
368
+
369
+ **Setup:**
370
+ ```sh
371
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-badsched.yml" <<'EOF'
372
+ version: 4
373
+ name: Manual test bad schedule required input
374
+ inputs:
375
+ ticket:
376
+ type: string
377
+ required: true
378
+ run: echo "ticket=$AKM_TASK_INPUTS"
379
+ schedule: "0 8 * * 1"
380
+ EOF
381
+ ```
382
+
383
+ **Commands and expected (both verified live, identical failure):**
384
+ 1. `akm task explain manual-test-badsched`
385
+ 2. `akm task run manual-test-badsched --ticket OPS-1` (supplying the flag
386
+ does **not** help)
387
+
388
+ Both: exit `2`, `.code == "TASK_SOURCE_INVALID"`, error text is `Invalid
389
+ task source v4 at <path>:8: schedule does not satisfy the task's declared
390
+ inputs once defaults are applied: inputs.ticket: is required. A scheduled
391
+ run supplies no input flags — give this schedule entry an inputs: value for
392
+ each input named above, or declare a default: on the input instead (a
393
+ required: true input may not carry one).`
394
+
395
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-test-badsched.yml"`.
396
+
397
+ ### TASK-10 — `run` happy path, and inputs are validated then discarded for non-workflow targets
398
+
399
+ **Why it matters:** for `run:`/`scripts/`/`commands/` targets, input values
400
+ are validated then **discarded** — they do not reach `AKM_TASK_INPUTS` or
401
+ the process environment. A user expecting `--who Bob` to show up in a shell
402
+ script via `$AKM_TASK_INPUTS` will be surprised.
403
+
404
+ **Preconditions:** `manual-test-echo.yml` from TASK-1.
405
+
406
+ **Command:** `akm task run manual-test-echo --who Bob --format json`
407
+
408
+ **Expected result (verified live):** exit `0`; `.result.status ==
409
+ "completed"`, `.result.detail.exitCode == 0`; `.result.log` names a file
410
+ under `$AKM_CACHE_DIR/tasks/logs/manual-test-echo/`.
411
+
412
+ Verify outside akm: `cat "$(akm task run manual-test-echo --who Bob --format json | jq -r .result.log)"`
413
+ prints `hello ` — literally empty after "hello ", **not** "hello Bob". This
414
+ is the falsifiable proof that `--who Bob` was validated and discarded, not
415
+ delivered.
416
+
417
+ ### TASK-11 — `history` reflects a real run
418
+
419
+ **Why it matters:** `history` is the only place a user reviews what
420
+ happened after the fact (e.g. debugging a failed scheduled run days later)
421
+ — it must agree with what `run` itself reported.
422
+
423
+ **Preconditions:** run TASK-10 first so at least one history row exists.
424
+
425
+ **Commands:** `akm task history --format json` and `akm task history --id
426
+ manual-test-echo --format json`
427
+
428
+ **Expected result (verified live):** exit `0` both; `.rows[0].id ==
429
+ "manual-test-echo"`, `.rows[0].status == "completed"`,
430
+ `.rows[0].detail.exitCode == 0`, `.rows[0].log` matches TASK-10's log path
431
+ exactly.
432
+
433
+ Also: `akm task history --id does-not-exist` → exit `0`, `.rows == []`
434
+ (empty result, not an error).
435
+
436
+ ### TASK-12 — `run` on a nonexistent task id
437
+
438
+ **Why it matters:** distinguishes "usage error" (exit 2) from "resource not
439
+ found" (exit 1) — a real difference for scripts/CI wrapping `akm task run`.
440
+
441
+ **Command:** `akm task run nonexistent-task-xyz`
442
+
443
+ **Expected result (verified live, exact):** exit `1` (not 2);
444
+ ```json
445
+ {"ok":false,"error":"Task \"nonexistent-task-xyz\" was not found in the configured \"akm\" component.","code":"ASSET_NOT_FOUND","hint":"Run `akm search <query>` or `akm index` to refresh the index."}
446
+ ```
447
+
448
+ ### TASK-13 — A pre-v4 stored task is unusable, and the error names the fix
449
+
450
+ **Why it matters:** the headline real-user-impact change in alpha.5.
451
+ Anyone upgrading from an older akm with existing task files hits this on
452
+ the very next `run`/`sync`.
453
+
454
+ **Setup** (uses the project's own v2 fixture; v3 behaves identically with a
455
+ different version number in the message):
456
+ ```sh
457
+ cp /home/founder3/code/github/itlackey/akm/tests/fixtures/manual-qa/bundle/tasks/manual-success.yml \
458
+ "$AKM_BUNDLE_DIR/tasks/manual-success.yml"
459
+ ```
460
+
461
+ **Commands:** `akm task run manual-success` and `akm task explain
462
+ manual-success`
463
+
464
+ **Expected result (verified live, exact, both commands identical):** exit
465
+ `2`;
466
+ ```json
467
+ {"ok":false,"error":"TASK_SCHEMA_VERSION_UNSUPPORTED: Task at <path> uses task schema version 2; this release reads only version 4. Run `akm migrate apply` to convert it.","code":"TASK_SCHEMA_VERSION_UNSUPPORTED","hint":"Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`."}
468
+ ```
469
+ The hint says "task-v3 to task-source-v4" even for a v2 file — that's the
470
+ shipped wording (the v2→v3→v4 chain is one hint), not a bug.
471
+
472
+ **Cleanup:** `rm "$AKM_BUNDLE_DIR/tasks/manual-success.yml"`.
473
+
474
+ ### TASK-14 — v3→v4 migration end to end: status, dry-run, apply, backup, and post-migration runnability
475
+
476
+ **Why it matters:** TASK-13 shows the wall a real user hits; this shows
477
+ whether `akm migrate` actually gets them through it. The most important
478
+ test in this document for real upgrade risk.
479
+
480
+ **Setup — this test needs the bundle registered as a source**
481
+ (`migrate status`/`apply` scan registered bundle *sources*, not just
482
+ `AKM_BUNDLE_DIR`'s contents — a hand-written `config.json` is NOT enough
483
+ here, unlike every other test in this document):
484
+ ```sh
485
+ akm setup --config '{"semanticSearchMode":"off","registries":[]}' \
486
+ --dir "$AKM_BUNDLE_DIR" --no-init --format json
487
+ # NOTE: the scratch/tmp path triggers a "transient directory" refusal in
488
+ # `akm setup` — set AKM_FORCE_SETUP_TMP_STASH=1 first (expected, not a bug).
489
+
490
+ cat > "$AKM_BUNDLE_DIR/tasks/manual-test-v3.yml" <<'EOF'
491
+ version: 3
492
+ name: v3 fixture for migration test
493
+ run: echo "v3 task"
494
+ akm:
495
+ schedule: "0 6 * * *"
496
+ EOF
497
+ ```
498
+
499
+ **Commands and expected results (all verified live):**
500
+ 1. `akm migrate status --format json` → exit `0`;
501
+ `.taskV4Migration.files[0].status == "changed"`,
502
+ `.reason == "task-converted"`.
503
+ 2. `akm migrate apply --dry-run --format json` → exit `0`; identical
504
+ `status`/`reason` fields; the file on disk is byte-identical to before —
505
+ verify with `diff` against the pre-dry-run content.
506
+ 3. `akm migrate apply --format json` → exit `0`; JSON includes
507
+ `.taskV4Applied == 1` and a `.taskV4BackupPath` under
508
+ `$AKM_DATA_DIR/backups/task-v4/<timestamp>-<uuid>/`.
509
+ 4. Verify outside akm: `cat "$AKM_BUNDLE_DIR/tasks/manual-test-v3.yml"` now
510
+ shows `version: 4` with the `akm.schedule` value hoisted to a top-level
511
+ `schedule: 0 6 * * *` key; the backup path holds the original v3 bytes.
512
+ 5. `akm task explain manual-test-v3 --format json` → exit `0`,
513
+ `.sourceVersion == 4`, `.schedule[0].cron == "0 6 * * *"` — the task is
514
+ now actually runnable, closing the loop from TASK-13.
515
+
516
+ **Cleanup:** `rm -f "$AKM_BUNDLE_DIR/tasks/manual-test-v3.yml"`.
517
+
518
+ ### TASK-15 — `doctor` on this platform
519
+
520
+ **Why it matters:** `doctor` is the first-stop diagnostic a user runs when
521
+ scheduling seems broken; its reported backend/paths must match reality.
522
+
523
+ **Command:** `akm task doctor --format json`
524
+
525
+ **Expected result (verified live on Linux/cron):** exit `0`;
526
+ `.backend == "cron"`; `.logDir`/`.historyDir` are under
527
+ `$AKM_CACHE_DIR/tasks/...` (cache-dir isolation applies to logs/history
528
+ even though the scheduler itself isn't isolated); `.akm.eligible == false`
529
+ and `.akm.kind == "checkout"` when run via `bun src/cli.ts` (a repo checkout
530
+ is never scheduler-eligible — see TASK-16); `.remediation == "akm task sync
531
+ --rebind"` is present whenever any bindings are ineligible.
532
+
533
+ Verify outside akm: cross-check `.bindings[].taskIds` against `crontab -l |
534
+ grep -oP '(?<=# akm:task )\S+(?= BEGIN)'` — the two lists should match
535
+ (read-only both sides).
536
+
537
+ **`UNVERIFIED`: the exact `doctor` output shape on macOS (`launchd`) and
538
+ Windows (`schtasks`) backends — only Linux/cron was exercised live for this
539
+ document.** Platform-gated: run the macOS/Windows equivalent on those OSes;
540
+ expect `.backend` to read `"launchd"` or `"schtasks"` respectively (inferred
541
+ from source naming, not observed).
542
+
543
+ ### TASK-16 — Ineligible-invocation guard fires before any scheduler write
544
+
545
+ **Why it matters:** the one `add`/`sync`-adjacent test that's genuinely safe
546
+ on a real machine, because it fails *before* touching the scheduler.
547
+
548
+ **Preconditions:** run from a **repo checkout** (`bun src/cli.ts`), not an
549
+ npm-global/standalone install — those are scheduler-eligible and this test
550
+ would not short-circuit the same way (see [DT-1](#dt-1--task-add-happy-path-installs-a-real-cron-entry-linux-cron)
551
+ instead).
552
+
553
+ **Command:** `akm task sync` in the common sandbox, with
554
+ `manual-test-echo.yml` (a `schedule:`-bearing task) present.
555
+
556
+ **Expected result:** exit `2`, `.code == "INVALID_FLAG_VALUE"`, error text
557
+ `Refusing to reconcile native scheduler bindings from an ineligible
558
+ checkout invocation (<argv...>).` (Source:
559
+ `src/commands/tasks/tasks.ts`, `resolveAndValidateSchedulerInvocation`.)
560
+
561
+ **Historical note:** the alpha.5 live run encountered #846's unrelated-entry
562
+ fingerprint guard before this invocation guard. Stable 0.9.2's owner-path
563
+ scoping prevents that cross-bundle detour, so the expected result above is the
564
+ one this test now requires. Its exact wording remains **UNVERIFIED live** in
565
+ this manual suite, although the guard has automated coverage.
566
+
567
+ Verify outside akm: `crontab -l` (or platform equivalent) is byte-identical
568
+ before and after.
569
+
570
+ ---
571
+
572
+ ## Workflows
573
+
574
+ Scope: `akm workflow status|list|create|resume|abandon|run|plan`. Every
575
+ fixture below uses `unit: { exec: { command: [...] } }` shell steps — no
576
+ engine/LLM credentials are needed except WF-6, which needs an engine
577
+ *configured* (not reachable) to force a deterministic judge-infrastructure
578
+ failure.
579
+
580
+ ### WF-1 — Author and run an exec workflow end to end
581
+
582
+ **Why it matters:** the single most common real workflow — create a
583
+ document, wire up steps, run it — must work with zero ceremony.
584
+
585
+ **Setup:**
586
+ ```sh
587
+ akm workflow create demo --format json
588
+ ```
589
+ **Expected:** exit `0`, JSON `{"ok": true, "ref": "bundle//workflows/demo",
590
+ "path": ".../bundle/workflows/demo.md", "bundleDir": "<AKM_SANDBOX>/bundle",
591
+ "shape": "workflow-create", "schemaVersion": 1}`. Note the field is
592
+ **`bundleDir`**, not `stashDir` (renamed this release — a script still
593
+ reading `stashDir` here gets `undefined`).
594
+
595
+ Overwrite `$AKM_BUNDLE_DIR/workflows/demo.md` (the default template needs a
596
+ judge engine — see WF-2):
597
+ ```markdown
598
+ ---
599
+ type: workflow
600
+ description: Exec-only smoke test workflow (no engine required)
601
+ updated: 2026-01-01
602
+ tags: [example]
603
+ steps:
604
+ - id: first-step
605
+ unit:
606
+ exec:
607
+ command: ["bash", "-lc", "echo hello-first"]
608
+ - id: second-step
609
+ inputs: [steps.first-step.output]
610
+ unit:
611
+ exec:
612
+ command: ["bash", "-lc", "echo hello-second"]
613
+ ---
614
+
615
+ # Demo Workflow
616
+
617
+ ## first-step
618
+
619
+ Print a greeting.
620
+
621
+ ## second-step
622
+
623
+ Print a second greeting, using the first step's artifact as context.
624
+ ```
625
+
626
+ **Command:** `akm workflow run workflows/demo --format json`
627
+
628
+ **Expected result (verified live):** exit `0`; `.run.status ==
629
+ "completed"`; `.run.currentStepId == null`; `.stepsProcessed == 2`,
630
+ `.done == true`; `.executed[0].stepId == "first-step"`,
631
+ `.executed[0].ok == true`; same shape for `"second-step"`. Two advisory
632
+ warnings about missing `output:` schemas print on stderr — non-fatal,
633
+ stdout remains valid JSON.
634
+
635
+ Then `akm workflow status <run-id> --format json`: `.workflow.steps[0]
636
+ .evidence.output == "hello-first"`; `.workflow.steps[1].evidence.output ==
637
+ "hello-second"`.
638
+
639
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"` (or reuse for WF-4/WF-7/WF-8, which
640
+ reference this fixture).
641
+
642
+ ### WF-2 — Default `create` template needs a judge engine (first-run trap)
643
+
644
+ **Why it matters:** `akm workflow create <name>` scaffolds a template whose
645
+ second step ships with a non-empty `### gate` rubric by default. A
646
+ brand-new user who runs `create` then immediately `plan`s/`run`s without
647
+ configuring anything hits an opaque-looking failure.
648
+
649
+ **Preconditions:** fresh sandbox, no `engines`/`workflow.judgeEngine` in
650
+ config.json.
651
+
652
+ **Commands:**
653
+ ```sh
654
+ akm workflow create raw --format json
655
+ akm workflow plan workflows/raw --format json
656
+ ```
657
+
658
+ **Expected result (verified live, exact):** `create` exits `0`. `plan`
659
+ (untouched template) **fails**: exit **`78`**;
660
+ ```json
661
+ {"ok": false, "error": "This workflow declares completion criteria but no verification engine is configured. Set workflow.judgeEngine to a named LLM or agent engine.", "code": "INVALID_CONFIG_FILE"}
662
+ ```
663
+ Note the message says *what* to set but not *where* (`config.json`) or what
664
+ a valid value looks like — a real gap for a scaffolded template's
665
+ out-of-the-box failure, worth flagging in review even though it's not
666
+ incorrect.
667
+
668
+ ### WF-3 — Child workflow: `plan` shows expansion, `run` executes it
669
+
670
+ **Why it matters:** child workflows are the headline capability of this
671
+ release. A parent step composing another workflow must show up correctly in
672
+ the frozen plan graph before anything runs, and actually drive the child to
673
+ completion, folding its exported output back into the parent step's
674
+ artifact.
675
+
676
+ **Setup:**
677
+
678
+ `$AKM_BUNDLE_DIR/workflows/child.md`:
679
+ ```markdown
680
+ ---
681
+ type: workflow
682
+ description: Child workflow composed by parent.yml (exec-only, no engine required)
683
+ updated: 2026-01-01
684
+ tags: [example]
685
+ params:
686
+ greeting: { type: string, description: Greeting text bound from the parent step }
687
+ steps:
688
+ - id: greet
689
+ unit:
690
+ exec:
691
+ command: ["bash", "-lc", "echo child-said-hello"]
692
+ outputs:
693
+ message:
694
+ from: steps.greet.output
695
+ ---
696
+
697
+ # Child Workflow
698
+
699
+ ## greet
700
+
701
+ Print a fixed greeting.
702
+ ```
703
+
704
+ `$AKM_BUNDLE_DIR/workflows/parent.yml` (composition requires a
705
+ GitHub-shaped `.yml` parent — Markdown frontmatter has no `uses:` key):
706
+ ```yaml
707
+ name: Parent composes child
708
+ on:
709
+ workflow_dispatch: {}
710
+ jobs:
711
+ main:
712
+ runs-on: [self-hosted]
713
+ steps:
714
+ - id: dispatch
715
+ uses: workflows/child
716
+ with:
717
+ greeting: hello-from-parent
718
+ ```
719
+
720
+ **Part A — `plan` (no writes, no execution):** `akm workflow plan
721
+ workflows/parent --format json`
722
+
723
+ **Expected result (verified live):** exit `0`; `.steps[0].targetKind ==
724
+ "child-workflow"`; `.steps[0].inputBindings == [{"name": "greeting", "kind":
725
+ "literal", "value": "hello-from-parent"}]`; `.steps[0].expansion.via ==
726
+ "child"`, `.expansion.childRef == "bundle//workflows/child"`,
727
+ `.expansion.childVia == "direct"`, `.expansion.childOutputs ==
728
+ ["message"]`; `.steps[0].expansion.steps` contains the child's own frozen
729
+ graph (one entry, `stepId: "greet"`, `targetKind: "shell"`).
730
+
731
+ **Part B — `run` (actually executes):** `akm workflow run workflows/parent
732
+ --format json`, then `akm workflow status <run-id> --format json`
733
+
734
+ **Expected result (verified live):** `run` exits `0`, `.run.status ==
735
+ "completed"`; `status`'s `.workflow.steps[0].evidence.output == {"message":
736
+ "child-said-hello"}` — the child's declared `outputs:` export, promoted as
737
+ the parent step's own artifact; `status` has a top-level **`.children`**
738
+ array (not nested under `.workflow`): one entry, `status: "completed"`,
739
+ `workflowRef: "bundle//workflows/child"`, `spawnedByUnitId:
740
+ "dispatch:solo"`, `stepId: "dispatch"`.
741
+
742
+ **Part C — list visibility:** `akm workflow list --format json` (count N),
743
+ then `akm workflow list --children --format json` (count N+1).
744
+
745
+ **Expected:** the second count is exactly one more — the child run is
746
+ hidden from the default `list` and appears only with `--children`.
747
+
748
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
749
+
750
+ ### WF-4 — `plan` makes zero durable writes
751
+
752
+ **Why it matters:** `plan` is documented as making zero durable writes. If a
753
+ future change accidentally makes it write a run row, it would silently
754
+ pollute `list`/state for every user who reviews a workflow before running
755
+ it — a very common habit.
756
+
757
+ **Preconditions:** fresh sandbox; reuse `demo.md` from WF-1.
758
+
759
+ **Commands:**
760
+ ```sh
761
+ akm workflow list --format json # baseline
762
+ sqlite3 "$AKM_DATA_DIR/state.db" "select count(*) from workflow_runs;" # expect 0
763
+ akm workflow plan workflows/demo --format json
764
+ sqlite3 "$AKM_DATA_DIR/state.db" "select count(*) from workflow_runs;" # must still be 0
765
+ akm workflow list --format json # must still be []
766
+ ```
767
+
768
+ **Expected result (verified live):** `plan` exits `0`, `.ok == true`,
769
+ `.published == false`. `select count(*)` is `0` both before and after.
770
+ `workflow list` after `plan` is byte-identical to before: `{"runs": [],
771
+ "shape": "workflow-list", "schemaVersion": 1}`.
772
+
773
+ `sqlite3` is required for the row-count check; the `list` before/after diff
774
+ alone is sufficient if unavailable.
775
+
776
+ ### WF-5 — A failing step surfaces a clear diagnostic
777
+
778
+ **Why it matters:** most real workflow authoring involves a step failing.
779
+ The failure needs to be loud, specific, and land in a stable place
780
+ (`status`), not just flash by in `run`'s stdout.
781
+
782
+ **Setup:**
783
+
784
+ `$AKM_BUNDLE_DIR/workflows/fails.md`:
785
+ ```markdown
786
+ ---
787
+ type: workflow
788
+ description: Exec-only workflow whose second step always fails (no engine required)
789
+ updated: 2026-01-01
790
+ tags: [example]
791
+ steps:
792
+ - id: ok-step
793
+ unit:
794
+ exec:
795
+ command: ["bash", "-lc", "echo ok"]
796
+ - id: fail-step
797
+ inputs: [steps.ok-step.output]
798
+ unit:
799
+ exec:
800
+ command: ["bash", "-lc", "echo boom 1>&2; exit 1"]
801
+ ---
802
+
803
+ # Fails Workflow
804
+
805
+ ## ok-step
806
+
807
+ Print ok.
808
+
809
+ ## fail-step
810
+
811
+ Always exits 1.
812
+ ```
813
+
814
+ **Command:** `akm workflow run workflows/fails --format json`
815
+
816
+ **Expected result (verified live, exact):** exit **`1`** (not 0, not 2 — a
817
+ run-outcome failure, distinct from a usage error); `.run.status ==
818
+ "failed"`, `.run.currentStepId == "fail-step"`; `.executed[1].ok == false`,
819
+ `.executed[1].failedUnits == 1`; `.executed[1].summary` contains the exact
820
+ substring `fail-step:solo (non_zero_exit)` and the captured stderr text
821
+ `boom`.
822
+
823
+ Then `akm workflow status <run-id> --units --format json`:
824
+ `.workflow.steps[1].status == "failed"`;
825
+ `.workflow.steps[1].evidence.units[0].failureReason == "non_zero_exit"`.
826
+
827
+ ### WF-6 — Blocked run: both direct `resume` and `abandon` → `resume` work (#847 regression)
828
+
829
+ **Why it matters:** `akm workflow abandon --help` promises "mark it failed
830
+ so it stops counting as active (resume can reopen it)". Alpha.5 violated that
831
+ promise for a run blocked by a judge-infrastructure failure: abandon left the
832
+ step blocked, and resume rejected that honest failed-run spine as corrupt.
833
+ Stable 0.9.2 accepts the three legitimate failed-run current-step states and
834
+ normalizes each back to pending on resume. This test keeps the exact #847
835
+ reproduction as a regression.
836
+
837
+ **How "blocked" is produced without any real LLM/API key:** a workflow with
838
+ a non-empty `### gate` requires `workflow.judgeEngine` to be *configured*
839
+ (present in `config.json`) before it can freeze — nothing requires it to be
840
+ *reachable*. Pointing it at a closed local port makes every gate evaluation
841
+ throw a network error, which is a judge-infrastructure failure (not a
842
+ verdict), and that path blocks the step. No credentials, no network,
843
+ reproduces every time.
844
+
845
+ **Setup:**
846
+ ```sh
847
+ cat > "$AKM_CONFIG_DIR/config.json" <<'EOF'
848
+ {
849
+ "configVersion": "0.9.0",
850
+ "semanticSearchMode": "off",
851
+ "registries": [],
852
+ "engines": {
853
+ "unreachable": { "kind": "llm", "endpoint": "http://127.0.0.1:1/v1/chat/completions", "model": "does-not-matter" }
854
+ },
855
+ "workflow": { "judgeEngine": "unreachable" }
856
+ }
857
+ EOF
858
+ ```
859
+
860
+ `$AKM_BUNDLE_DIR/workflows/gated.md`:
861
+ ```markdown
862
+ ---
863
+ type: workflow
864
+ description: Exec step with a gate rubric, judged by an unreachable engine (simulates judge-infrastructure failure -> blocked)
865
+ updated: 2026-01-01
866
+ tags: [example]
867
+ steps:
868
+ - id: produce
869
+ unit:
870
+ exec:
871
+ command: ["bash", "-lc", "echo produced"]
872
+ gate: { max_loops: 1 }
873
+ ---
874
+
875
+ # Gated Workflow
876
+
877
+ ## produce
878
+
879
+ Print the word "produced".
880
+
881
+ ### gate
882
+
883
+ - The artifact is exactly "produced".
884
+ ```
885
+
886
+ **Part A — plain resume works (contrast case):**
887
+ ```sh
888
+ akm workflow run workflows/gated --format json --timeout 30s
889
+ ```
890
+ **Expected (verified live):** exit `1`; `.run.status == "blocked"`;
891
+ `.workflow.steps[0].notes` contains `could not be verified: the
892
+ verification judge failed (Network error:` and names the exact resume
893
+ command.
894
+
895
+ ```sh
896
+ akm workflow resume <run-id> --format json
897
+ ```
898
+ **Expected (verified live):** exit `0`; `.run.status` becomes `"active"`;
899
+ `.workflow.steps[0].status` becomes `"pending"` (evidence cleared — the
900
+ next `run` re-evaluates the gate, not the exec unit).
901
+
902
+ **Part B — abandon then resume.** Start a **fresh** run (do not
903
+ reuse Part A's run — re-running against the ref resumes the existing active
904
+ run rather than starting a new one; see the note at the end of this test):
905
+ ```sh
906
+ akm workflow run workflows/gated --format json --timeout 30s # capture the NEW run id
907
+ akm workflow abandon <run-id> --format json
908
+ akm workflow resume <run-id> --format json
909
+ ```
910
+
911
+ **Expected result (stable regression, automated):**
912
+ - `abandon` succeeds, exit `0`. `.run.status` becomes `"failed"`; the current
913
+ step honestly remains `"blocked"` until a resume is requested.
914
+ - `resume` succeeds, exit `0`. `.run.status` becomes `"active"` and
915
+ `.workflow.steps[0].status` becomes `"pending"`, with the blocked notes and
916
+ evidence cleared so the gate can be evaluated again.
917
+ - No corrupt-spine error is emitted. A subsequent `run <run-id>` re-enters
918
+ the current step normally.
919
+
920
+ **Note on `run <ref>` semantics:** `akm workflow run workflows/<name>` does
921
+ not always start a brand-new run — if an **active** run already exists for
922
+ that ref in the current scope, it resumes that run instead, consistent with
923
+ `run --help` ("Start **or resume** a workflow...") but easy to miss.
924
+
925
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
926
+
927
+ ### WF-7 — Active (paused) run → `abandon` → `resume` round-trips cleanly (companion to WF-6)
928
+
929
+ **Why it matters:** pins the other legitimate abandoned-run spine: an active
930
+ run leaves its current step pending, while WF-6 leaves it blocked. Resume must
931
+ normalize both without weakening corruption detection for impossible shapes.
932
+ This is a deliberate contrast pair with WF-6, not a duplicate — keep both.
933
+
934
+ **Preconditions:** fresh sandbox, no `engines` config needed. Use `demo.md`
935
+ from WF-1.
936
+
937
+ **Commands:**
938
+ ```sh
939
+ akm workflow run workflows/demo --max-steps=1 --format json
940
+ # capture run id; .run.status is "active", .run.currentStepId "second-step"
941
+ akm workflow abandon <run-id> --format json
942
+ akm workflow resume <run-id> --format json
943
+ ```
944
+
945
+ **Expected result (verified live):**
946
+ - After `run --max-steps=1`: `.run.status == "active"`,
947
+ `.workflow.steps[0].status == "completed"`,
948
+ `.workflow.steps[1].status == "pending"`.
949
+ - `abandon` succeeds, exit `0`, `.run.status` becomes `"failed"`,
950
+ `.workflow.steps[1].status` **stays `"pending"`** — this is the case that
951
+ does *not* produce the WF-6 bug, because the step status already agrees
952
+ with the run's new status.
953
+ - `resume` succeeds, exit **`0`**, `.run.status` becomes `"active"` again,
954
+ `.workflow.steps[1].status` stays `"pending"`. No corrupt-spine error.
955
+
956
+ ### WF-8 — Scope semantics: `list` is cwd-scoped by default; `--all-scopes`
957
+ and the cross-scope start warning (#942)
958
+
959
+ **Why it matters:** `list`/`status --help` say refs/runs "resolve within
960
+ the current scope" — easy to read past. A user who runs a workflow from one
961
+ directory, `cd`s elsewhere, then runs `akm workflow list` and sees
962
+ `{"runs": []}` could plausibly conclude the run vanished — this is exactly
963
+ what happened in production: a scheduled task's cwd (`$HOME`) and a human
964
+ shell's cwd hashed to different scope keys, each `akm workflow run`
965
+ independently believed no run was active, and each started one. Scope is
966
+ still the **working directory**, hashed into `scopeKey` — not the bundle,
967
+ not the config dir — but the envelope now says which scope it searched, and
968
+ `--all-scopes` can search every scope.
969
+
970
+ **Setup:** fresh sandbox at `$AKM_SANDBOX`, plus `mkdir -p
971
+ "$AKM_SANDBOX-elsewhere"`. Use `demo.md` from WF-1.
972
+
973
+ **Commands:**
974
+ ```sh
975
+ cd "$AKM_SANDBOX"
976
+ akm workflow run workflows/demo --format json # note RUNID
977
+ akm workflow list --format json # from $AKM_SANDBOX
978
+
979
+ cd "$AKM_SANDBOX-elsewhere"
980
+ akm workflow list --format json # same env vars, different cwd
981
+ akm workflow list --all-scopes --format json # same env vars, different cwd
982
+ akm workflow status "$RUNID" --format json # by id, from elsewhere
983
+ akm workflow run workflows/demo --format json # same ref, from elsewhere
984
+ ```
985
+
986
+ **Expected result (verified live):**
987
+ - `list` from `$AKM_SANDBOX` returns `.runs` length **1** and a top-level
988
+ `.scopeKey` starting `"dir:v1:..."` — the scope it searched, which every
989
+ returned run's own `.scopeKey` also matches.
990
+ - `list` from the elsewhere directory (identical `AKM_*` env vars, only
991
+ `cwd` differs) returns **`{"runs": [], "scopeKey": "dir:v1:..."}`**
992
+ (a *different* hash than the first call), exit `0` — not an error, and no
993
+ longer indistinguishable from "nothing anywhere": the envelope names the
994
+ scope that came up empty.
995
+ - `list --all-scopes` from the elsewhere directory returns `.runs` length
996
+ **1** (the run started in `$AKM_SANDBOX`) and a top-level `.scopeKey` of
997
+ **`null`** — "every scope" — while the run's own `.scopeKey` field still
998
+ names where it actually started.
999
+ - `status $RUNID` from elsewhere **still succeeds** and returns the full
1000
+ run — status-by-id is not scope-filtered the way `list` is.
1001
+ - `run workflows/demo` from elsewhere **starts a second, independent run**
1002
+ (the scope-local concurrency guard is unchanged — a deliberate
1003
+ per-project partition, see `storage-locations.md`) but its envelope now
1004
+ carries a `warnings[]` entry naming `$RUNID` and `$AKM_SANDBOX`'s scope
1005
+ key, with the same `akm workflow run <id>` / `akm workflow abandon <id>`
1006
+ remedy the same-scope guard already gives — so the operator sees the
1007
+ other run instead of two runs silently drifting.
1008
+
1009
+ ### WF-9 — Migration: a pre-`irVersion`-5 stored plan is retired
1010
+
1011
+ **Why it matters:** every user upgrading from 0.9.1 or an early 0.9.2 alpha
1012
+ with an in-flight workflow run hits this. Requires the `sqlite3` CLI (or any
1013
+ SQLite client) to hand-edit one column of one row.
1014
+
1015
+ **Setup:**
1016
+
1017
+ `$AKM_BUNDLE_DIR/workflows/retiring.md`:
1018
+ ```markdown
1019
+ ---
1020
+ type: workflow
1021
+ description: Exec-only two-step workflow used for the pre-irVersion-5 retirement test
1022
+ updated: 2026-01-01
1023
+ tags: [example]
1024
+ steps:
1025
+ - id: first-step
1026
+ unit:
1027
+ exec:
1028
+ command: ["bash", "-lc", "echo hello-first"]
1029
+ - id: second-step
1030
+ inputs: [steps.first-step.output]
1031
+ unit:
1032
+ exec:
1033
+ command: ["bash", "-lc", "echo hello-second"]
1034
+ ---
1035
+
1036
+ # Retiring Workflow
1037
+
1038
+ ## first-step
1039
+
1040
+ Print a greeting.
1041
+
1042
+ ## second-step
1043
+
1044
+ Print a second greeting.
1045
+ ```
1046
+
1047
+ **Commands:**
1048
+ ```sh
1049
+ OUT=$(akm workflow run workflows/retiring --max-steps=1 --format json)
1050
+ RUNID=$(echo "$OUT" | python3 -c "import json,sys; print(json.load(sys.stdin)['run']['id'])")
1051
+ sqlite3 "$AKM_DATA_DIR/state.db" "UPDATE workflow_runs SET plan_ir_version = 4 WHERE id = '$RUNID';"
1052
+ akm workflow status "$RUNID" --format json # read-only surfaces must still work
1053
+ akm workflow resume "$RUNID" --format json # execution surfaces must fail closed
1054
+ akm workflow abandon "$RUNID" --format json # abandon must still work
1055
+ ```
1056
+
1057
+ **Expected result (verified live, every line confirmed):**
1058
+ - Before tampering: `.run.status == "active"`.
1059
+ - After tampering, `status` reports `.run.planIrVersion == 4`,
1060
+ `.run.executionSupport == "unsupported-version"`, `.run.status`
1061
+ unchanged (`"active"`) — reading the run does not itself fail.
1062
+ - `resume` fails, exit **`2`**:
1063
+ ```json
1064
+ {"ok": false, "error": "Workflow run <RUNID> was frozen as workflow plan irVersion 4; pre-irVersion-5 plans cannot execute after the 0.9.2 upgrade. Complete them before upgrading, or run 'akm workflow abandon <RUNID>' and start a new run from the authored workflow. 'akm workflow status' and 'akm workflow list' still work on this run.", "code": "WORKFLOW_IR_VERSION_UNSUPPORTED", "hint": "Abandon the run with `akm workflow abandon <id>`, then start it again from the workflow source — a frozen plan this akm cannot execute is not re-executable in place."}
1065
+ ```
1066
+ This message names the run id twice, the exact `abandon` command, and
1067
+ states the only path forward — the strongest error message found across
1068
+ this whole document; treat it as the bar WF-6's corrupt-spine error and
1069
+ WF-10's `inherit_env` rejection should be held to.
1070
+ - `abandon` **succeeds**, exit `0`, `.run.status` becomes `"failed"` —
1071
+ unlike WF-6's blocked-abandon case, this does not corrupt the step spine
1072
+ (the step here is `"pending"`, a status abandon's `"failed"` reconciles
1073
+ with cleanly).
1074
+
1075
+ ### WF-10 — Migration: removed `inherit_env` is rejected
1076
+
1077
+ **Why it matters:** `inherit_env: true` was breaking-removed in
1078
+ `0.9.2-alpha.1` in favor of `exec.pass_env`. Anyone porting an older
1079
+ exec-unit workflow forward will paste this key in and needs an unambiguous
1080
+ signal.
1081
+
1082
+ **Setup:**
1083
+
1084
+ `$AKM_BUNDLE_DIR/workflows/inherit-env.md`:
1085
+ ```markdown
1086
+ ---
1087
+ type: workflow
1088
+ description: Exec step authored with the removed inherit_env option
1089
+ updated: 2026-01-01
1090
+ tags: [example]
1091
+ steps:
1092
+ - id: step1
1093
+ unit:
1094
+ exec:
1095
+ command: ["bash", "-lc", "echo hi"]
1096
+ inherit_env: true
1097
+ ---
1098
+
1099
+ # Inherit Env Workflow
1100
+
1101
+ ## step1
1102
+
1103
+ Print hi; this step's exec unit illegally authors `inherit_env: true`.
1104
+ ```
1105
+
1106
+ **Commands:** `akm workflow plan workflows/inherit-env --format json` and
1107
+ `akm workflow run workflows/inherit-env --format json`
1108
+
1109
+ **Expected result (verified live, identical for both):** exit **`2`**;
1110
+ ```json
1111
+ {"ok": false, "error": "Workflow source has 1 error(s):\n <path>/workflows/inherit-env.md:11 — Unknown Step \"step1\" \"exec\" key \"inherit_env\". Allowed keys: command, cwd, pass_env.", "code": "WORKFLOW_SOURCE_INVALID", "hint": "Run `akm lint` to see the failing source location, or `akm workflow plan <ref>` to compile it without writing."}
1112
+ ```
1113
+ This is a generic "unknown field" rejection, not a dedicated migration
1114
+ message — it lists `pass_env` among allowed keys (a usable hint for someone
1115
+ who already knows the replacement) but never says "`inherit_env` was
1116
+ removed" or points at
1117
+ [`docs/migration/v0.9.1-to-v0.9.2.md#workflow-cutover`](../../migration/v0.9.1-to-v0.9.2.md#workflow-cutover).
1118
+ Compare against WF-9's message, which names the concept, reason, and exact
1119
+ fix — worth a documentation-quality discussion, not necessarily a blocking
1120
+ bug.
1121
+
1122
+ **Positive control:** replace `inherit_env: true` with `pass_env: [PATH]`
1123
+ in the same fixture and re-run `plan`. Expected (verified live): `.ok ==
1124
+ true`, and the frozen step's `.environment` array gains `{"kind":
1125
+ "pass-through", "name": "PATH"}`.
1126
+
1127
+ ---
1128
+
1129
+ ## Integration (task ↔ workflow seam)
1130
+
1131
+ Scope: only scenarios where `akm task` and `akm workflow` genuinely meet —
1132
+ a task whose target is a workflow, a workflow step that targets a task, and
1133
+ the shared identity/failure machinery. The destructive `task sync` seam
1134
+ tests (a workflow-targeting task's schedule, and the #846 cross-bundle
1135
+ regression) are in [Destructive & platform-gated](#destructive--platform-gated-tests).
1136
+
1137
+ ### INT-1 — A task that runs a workflow (the headline case)
1138
+
1139
+ **Why it matters:** `akm task` is documented as scheduling "recurring
1140
+ commands, prompts, and workflows" — a task whose target IS a workflow is
1141
+ the one thing that makes `task` and `workflow` the same feature area
1142
+ instead of two unrelated CLIs.
1143
+
1144
+ **Destructive:** No — uses `task run` directly (verified to make zero
1145
+ scheduler writes).
1146
+
1147
+ **Setup:**
1148
+ ```sh
1149
+ cat > "$AKM_BUNDLE_DIR/workflows/leaf.yml" <<'EOF'
1150
+ name: Manual test leaf workflow
1151
+ on:
1152
+ workflow_dispatch:
1153
+ jobs:
1154
+ main:
1155
+ runs-on: [self-hosted]
1156
+ steps:
1157
+ - id: work
1158
+ run: printf leaf-ok
1159
+ EOF
1160
+
1161
+ cat > "$AKM_BUNDLE_DIR/tasks/akm-manual-test-wf-task.yml" <<'EOF'
1162
+ version: 4
1163
+ name: Manual test task -> workflow
1164
+ uses: workflows/leaf
1165
+ schedule:
1166
+ - cron: "0 3 * * *"
1167
+ enabled: false
1168
+ EOF
1169
+ akm index
1170
+ ```
1171
+
1172
+ **Steps and expected results (all verified live):**
1173
+ 1. `akm task explain akm-manual-test-wf-task --format json` → exit `0`,
1174
+ `.target == {"kind":"workflow","ref":"workflows/leaf"}`.
1175
+ 2. `akm task run akm-manual-test-wf-task --format json` (record
1176
+ `.result.detail.runId`) → exit `0`, `.ok == true`, `.result.status ==
1177
+ "completed"`, `.result.target == {"kind":"workflow","ref":"bundle//workflows/leaf"}`,
1178
+ `.result.detail.runId` is a UUID.
1179
+ 3. `akm workflow list --format json` → `.runs[]` contains an entry with
1180
+ `.id` equal to step 2's runId, `.workflowRef ==
1181
+ "bundle//workflows/leaf"`, `.status == "completed"`.
1182
+ 4. `akm workflow status <runId> --format json` → `.run.status ==
1183
+ "completed"`, `.workflow.steps[0].id == "work"`, `.status ==
1184
+ "completed"`, `.evidence.output == "leaf-ok"`.
1185
+ 5. `akm task history --id akm-manual-test-wf-task --format json` →
1186
+ `.rows[0].detail.runId` equals step 2's runId, `.rows[0].target.ref ==
1187
+ "bundle//workflows/leaf"`.
1188
+
1189
+ **Cross-check outside akm:** `crontab -l | grep -c akm-manual-test-wf-task`
1190
+ before and after step 2 — both must print `0`. `task run` never touches the
1191
+ OS scheduler.
1192
+
1193
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"` (no scheduler entries were ever
1194
+ created).
1195
+
1196
+ ### INT-2 — A workflow step that targets a task (the inverse), including typed input binding
1197
+
1198
+ **Why it matters:** the reverse composition — the alpha.5
1199
+ `FrozenChildWorkflowTarget`/task-bindings machinery. If a workflow step
1200
+ `uses: tasks/<ref>` doesn't correctly expand at `plan` time, a published
1201
+ workflow's step graph silently diverges from what actually runs.
1202
+
1203
+ **Destructive:** No — `workflow plan` performs zero durable writes.
1204
+
1205
+ **Setup:** reuses `workflows/leaf.yml` and
1206
+ `tasks/akm-manual-test-wf-task.yml` from INT-1, plus:
1207
+ ```sh
1208
+ cat > "$AKM_BUNDLE_DIR/workflows/task-wrapped.yml" <<'EOF'
1209
+ name: Manual test workflow wrapping a task
1210
+ on:
1211
+ workflow_dispatch:
1212
+ jobs:
1213
+ main:
1214
+ runs-on: [self-hosted]
1215
+ steps:
1216
+ - id: dispatch
1217
+ uses: tasks/akm-manual-test-wf-task
1218
+ EOF
1219
+ akm index
1220
+ ```
1221
+
1222
+ **Steps and expected results:**
1223
+ 1. `akm workflow plan workflows/task-wrapped --format json` — expected
1224
+ (verified live): `.steps[0].stepId == "dispatch"`, `.targetKind ==
1225
+ "child-workflow"`, `.expansion.via == "child"`, `.expansion.childVia ==
1226
+ "task"`, `.expansion.childTaskRef ==
1227
+ "bundle//tasks/akm-manual-test-wf-task"`, `.expansion.childRef ==
1228
+ "bundle//workflows/leaf"`, `.expansion.steps[0].stepId == "work"` and
1229
+ `.targetKind == "shell"`; exit `0`.
1230
+ 2. `akm workflow run workflows/task-wrapped --format json` (this DOES
1231
+ publish a run — acceptable, scoped to isolated bundle/state dirs, not
1232
+ the OS scheduler), then `akm workflow status <runId> --format json`.
1233
+ **Expected result (verified live — this replaces an UNVERIFIED marker
1234
+ from an earlier draft of this test):** exit `0`, final `.run.status ==
1235
+ "completed"`. Because `leaf.yml` declares no `outputs:` block, the
1236
+ parent step's `.workflow.steps[0].evidence.output` is **not** the
1237
+ child's step output directly — it is a run pointer: `{"runId":
1238
+ "<child-run-uuid>", "status": "completed"}`. The full child summary
1239
+ lives in the top-level `.children[0]`: `{"runId": "<same uuid>",
1240
+ "workflowRef": "bundle//workflows/leaf", "status": "completed",
1241
+ "spawnedByUnitId": "dispatch:solo", "stepId": "dispatch", ...}` — same
1242
+ shape as WF-3's `.children` array. (Contrast with WF-3, where the child
1243
+ workflow *does* declare `outputs:`, and that gets promoted into the
1244
+ parent step's `evidence.output` instead of a bare run pointer.)
1245
+
1246
+ **Bonus check — typed input binding across the seam** (exercises the PR's
1247
+ "fail-closed input bindings"):
1248
+ ```sh
1249
+ cat > "$AKM_BUNDLE_DIR/tasks/akm-manual-test-typed-input-task.yml" <<'EOF'
1250
+ version: 4
1251
+ name: Manual test typed-input task (run target)
1252
+ inputs:
1253
+ note:
1254
+ type: string
1255
+ required: true
1256
+ run: printf "note=%s" "$AKM_TASK_INPUTS"
1257
+ schedule:
1258
+ - cron: "0 3 * * *"
1259
+ enabled: false
1260
+ inputs:
1261
+ note: scheduled-default
1262
+ EOF
1263
+
1264
+ cat > "$AKM_BUNDLE_DIR/workflows/wrap-typed-input-task.yml" <<'EOF'
1265
+ name: Manual test workflow binding a literal input into a task step
1266
+ on:
1267
+ workflow_dispatch:
1268
+ jobs:
1269
+ main:
1270
+ runs-on: [self-hosted]
1271
+ steps:
1272
+ - id: dispatch
1273
+ uses: tasks/akm-manual-test-typed-input-task
1274
+ with:
1275
+ note: hello-seam
1276
+ EOF
1277
+ akm index
1278
+ akm workflow plan workflows/wrap-typed-input-task --format json
1279
+ ```
1280
+ **Expected (verified live):** `.steps[0].inputBindings == [{"name":"note","kind":"literal","value":"hello-seam"}]`.
1281
+
1282
+ Note: a **required** task input with no `default:` must be given a matching
1283
+ `schedule[].inputs.<name>` entry or the task source is rejected outright
1284
+ (`TASK_SOURCE_INVALID`) — a task-only rule, not part of the seam, but it
1285
+ will trip up anyone building this fixture from scratch.
1286
+
1287
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
1288
+
1289
+ ### INT-3 — Failure propagation: `task history` vs `workflow status`
1290
+
1291
+ **Why it matters:** operability. When a scheduled task's workflow fails at
1292
+ 3am, does the on-call person get the answer from `task history` (what a
1293
+ cron failure notification points at), or must they already know to pivot to
1294
+ `workflow status <runId>`?
1295
+
1296
+ **Destructive:** No (`task run` only).
1297
+
1298
+ **Setup:**
1299
+ ```sh
1300
+ cat > "$AKM_BUNDLE_DIR/workflows/fail-leaf.yml" <<'EOF'
1301
+ name: Manual test failing leaf workflow
1302
+ on:
1303
+ workflow_dispatch:
1304
+ jobs:
1305
+ main:
1306
+ runs-on: [self-hosted]
1307
+ steps:
1308
+ - id: boom
1309
+ run: exit 1
1310
+ EOF
1311
+
1312
+ cat > "$AKM_BUNDLE_DIR/tasks/akm-manual-test-fail-task.yml" <<'EOF'
1313
+ version: 4
1314
+ name: Manual test task -> failing workflow
1315
+ uses: workflows/fail-leaf
1316
+ schedule:
1317
+ - cron: "0 3 * * *"
1318
+ enabled: false
1319
+ EOF
1320
+ akm index
1321
+ ```
1322
+
1323
+ **Steps and expected (verified live) results:**
1324
+ 1. `akm task run akm-manual-test-fail-task --format json` → exit **`1`**,
1325
+ `.ok == false`, `.result.status == "failed"`, `.result.target ==
1326
+ {"kind":"workflow","ref":"bundle//workflows/fail-leaf"}`.
1327
+ **`.result.detail` is exactly `{"runId": "<uuid>"}` — no `error`/`message`
1328
+ field at all.** (`run-workflow-task.ts` only attaches `detail.error` for
1329
+ a thrown error, gate rejection, or timeout — a workflow that reaches
1330
+ `status: "failed"` through ordinary step failure sets none of those.)
1331
+ 2. The task's own log file (path from step 1's `.result.log`) contains only:
1332
+ ```
1333
+ [akm task] task=akm-manual-test-fail-task kind=workflow ref=bundle//workflows/fail-leaf
1334
+ run_id=<uuid> status=failed
1335
+ workflow_title=fail-leaf
1336
+ ```
1337
+ No mention of which step failed or why.
1338
+ 3. `akm task history --id akm-manual-test-fail-task --format json` → same
1339
+ shape as step 1's `.result` — still no reason.
1340
+ 4. `akm workflow status <runId> --format json` → `.workflow.steps[0].notes`
1341
+ contains the actual reason: `Executed 1 unit(s) for step "boom" via
1342
+ workflow orchestration: 0 succeeded, 1 failed. Failures: boom:solo
1343
+ (non_zero_exit). First failure diagnostic (boom:solo): exec unit
1344
+ "boom:solo" ran "/usr/bin/sh" (2 arguments) and it exited 1`.
1345
+
1346
+ **Conclusion (falsifiable, and it falsifies the "should"):** a normal
1347
+ workflow-side failure is legible ONLY from `workflow status`, never from
1348
+ `task history`/the task log/the task's own JSON result. This is a real,
1349
+ reproducible operability gap, not a hypothetical.
1350
+
1351
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
1352
+
1353
+ ### INT-4 — Identity/ownership across the seam: abandon then re-invoke
1354
+
1355
+ **Why it matters:** if a task's scheduled invocation drives a workflow run
1356
+ and a human intervenes with `workflow abandon` (e.g. because it's stuck),
1357
+ the next scheduled firing must do something predictable.
1358
+
1359
+ **Destructive:** No (`task run`/`workflow abandon` only, isolated state DB).
1360
+
1361
+ **Setup:**
1362
+ ```sh
1363
+ cat > "$AKM_BUNDLE_DIR/workflows/two-step.yml" <<'EOF'
1364
+ name: Manual test two-step workflow
1365
+ on:
1366
+ workflow_dispatch:
1367
+ jobs:
1368
+ main:
1369
+ runs-on: [self-hosted]
1370
+ steps:
1371
+ - id: first
1372
+ run: printf step-one-ok
1373
+ - id: second
1374
+ run: printf step-two-ok
1375
+ EOF
1376
+
1377
+ cat > "$AKM_BUNDLE_DIR/tasks/akm-manual-test-abandon-task.yml" <<'EOF'
1378
+ version: 4
1379
+ name: Manual test task -> two-step workflow (stops after 1 step)
1380
+ uses: workflows/two-step
1381
+ maxSteps: 1
1382
+ schedule:
1383
+ - cron: "0 3 * * *"
1384
+ enabled: false
1385
+ EOF
1386
+ akm index
1387
+ ```
1388
+ (`maxSteps: 1` lets a "scheduled" invocation stop mid-run, so there's
1389
+ something to abandon.)
1390
+
1391
+ **Steps and expected results (all verified live):**
1392
+ 1. `akm task run akm-manual-test-abandon-task --format json` (first
1393
+ firing) → `.result.status == "active"`, exit `0` (a paused run is not a
1394
+ failure), `.result.detail.runId` = **runA**. `akm workflow status runA`
1395
+ confirms `.run.status == "active"`, step `"first"` is `"completed"`,
1396
+ `"second"` is `"pending"`.
1397
+ 2. `akm workflow abandon runA --format json` → exit `0`, `.run.status ==
1398
+ "failed"`.
1399
+ 3. `akm task run akm-manual-test-abandon-task --format json` (second
1400
+ firing, as if the scheduler fired it again) → **a brand-new run id,
1401
+ "runB" != "runA"**, `.result.detail.runId == runB`, `.result.status ==
1402
+ "active"` again. The task log shows step `"first"` executing again from
1403
+ scratch — it is not resuming runA's position.
1404
+ 4. `akm workflow list --format json` → both `runA` (`"failed"`) and `runB`
1405
+ (`"active"`) present as separate rows for the same `workflowRef`.
1406
+
1407
+ **Conclusion (falsifiable and confirmed):** the next scheduled invocation
1408
+ starts fresh, never resumes and never errors — a direct consequence of
1409
+ `getActiveRunRowForScope`'s SQL filter
1410
+ (`src/storage/repositories/workflow-runs-repository.ts`) only ever
1411
+ attaching to `status = 'active'` rows. One nuance: this auto-attach path is
1412
+ also `AND parent_run_id IS NULL`, so it applies to a top-level run a *task*
1413
+ starts (INT-1) but not to a run started as a **child** of a parent workflow
1414
+ (INT-2) — a child run is only ever driven by its parent. Abandoning a child
1415
+ run's semantics from the *task* side is **UNVERIFIED** — not exercised
1416
+ here, plausibly a second interesting gap (a task can only ever directly own
1417
+ a top-level run).
1418
+
1419
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
1420
+
1421
+ ### INT-5 — The shipped `improve` tasks as a real-world case
1422
+
1423
+ **Why it matters:** `src/assets/tasks/improve/*.yml` are framed as "real
1424
+ examples of tasks that drive akm's own workflows." Testing only synthetic
1425
+ fixtures never catches a real schema drift between what `akm task add`
1426
+ authors and what these shipped files contain.
1427
+
1428
+ **Finding that reshapes this test:** none of the five shipped
1429
+ `src/assets/tasks/improve/*.yml` (or `src/assets/tasks/core/*.yml`) files
1430
+ actually target an `akm workflow` asset. Every one is `run: akm <command>
1431
+ ...` — a shell target. Verified live for `akm-improve-catchup.yml`:
1432
+ `.target == {"kind":"shell"}`. So the literal task→workflow seam (INT-1)
1433
+ has **no shipped real-world example** as of alpha.5 — "workflow" in the
1434
+ PR/brief is used in the English sense (the `improve` pipeline is a
1435
+ multi-step process), not the `akm workflow` asset sense. The direction a
1436
+ real shipped task asset DOES exercise unmodified is INT-2's shape — a
1437
+ workflow step wrapping a real task via `uses: tasks/`.
1438
+
1439
+ **Destructive:** No (`workflow plan` only — zero durable writes).
1440
+
1441
+ **Setup:**
1442
+ ```sh
1443
+ cp /home/founder3/code/github/itlackey/akm/src/assets/tasks/improve/akm-improve-catchup.yml \
1444
+ "$AKM_BUNDLE_DIR/tasks/akm-improve-catchup.yml"
1445
+
1446
+ cat > "$AKM_BUNDLE_DIR/workflows/wrap-real-improve-task.yml" <<'EOF'
1447
+ name: Manual test workflow wrapping the real shipped improve-catchup task
1448
+ on:
1449
+ workflow_dispatch:
1450
+ jobs:
1451
+ main:
1452
+ runs-on: [self-hosted]
1453
+ steps:
1454
+ - id: dispatch
1455
+ uses: tasks/akm-improve-catchup
1456
+ EOF
1457
+ akm index
1458
+ ```
1459
+
1460
+ **Steps and expected results (verified live):**
1461
+ 1. `akm task explain akm-improve-catchup --format json` → exit `0`,
1462
+ `.target.kind == "shell"`, `.description == "Manual recovery —
1463
+ consolidation + triage drain (run on demand via \`akm task run
1464
+ akm-improve-catchup\`)"`, `.schedule[0] == {"ordinal":0,"cron":"0 4 * * *","enabled":false,"source":"schedule[0].cron","inputs":{}}`
1465
+ — the real shipped file describes a schedule but cannot activate itself;
1466
+ this host's `scheduler.enabled` list does not name it.
1467
+ 2. `akm workflow plan workflows/wrap-real-improve-task --format json` →
1468
+ `.steps[0].targetKind == "shell"`, `.steps[0].expansion ==
1469
+ {"via":"task","taskRef":"tasks/akm-improve-catchup"}`.
1470
+
1471
+ **Do not** `workflow run` this composed workflow, and do not `task run
1472
+ akm-improve-catchup` directly — both would invoke the real `akm improve
1473
+ --strategy catchup` pipeline against a live agent engine, which is slow,
1474
+ may require credentials, and is out of scope for a seam test. `workflow
1475
+ plan` already proves the seam without executing anything.
1476
+
1477
+ **Cleanup:** `rm -rf "$AKM_SANDBOX"`.
1478
+
1479
+ ---
1480
+
1481
+ ## Destructive & platform-gated tests
1482
+
1483
+ **Everything in this section writes to the real OS scheduler (cron /
1484
+ launchd / schtasks). Re-read [Read this first](#read-this-first--the-one-safety-critical-finding)
1485
+ before running any of it.** Mandatory for every test below:
1486
+
1487
+ 1. Back up first: `crontab -l > ~/akm-crontab-backup-$(date +%s).txt` (or
1488
+ the platform equivalent). If this shows entries you don't recognize as
1489
+ your own throwaway fixtures, **stop** — you are not on a disposable
1490
+ scheduler.
1491
+ 2. Use an obviously-throwaway id/name (`akm-manual-test-<unix-timestamp>`
1492
+ or similarly namespaced) so a stray leftover is identifiable later.
1493
+ 3. Restore explicitly at the end: `crontab <backup-file>`, then verify with
1494
+ `crontab -l`.
1495
+ 4. **Never use `akm task sync` as a cleanup step** — it performs a broad
1496
+ reconcile, while cleanup should remove only the exact test fixture. DT-5
1497
+ separately proves #846's owner-path isolation. Clean up by hand.
1498
+ 5. Prefer a disposable OS account, VM, or container over a real machine.
1499
+
1500
+ `add` writes only its own binding (narrower blast radius); `sync` does a
1501
+ full reconcile and can attempt to remove unrelated real entries.
1502
+
1503
+ ### DT-1 — `task add` happy path installs a real cron entry [Linux: cron]
1504
+
1505
+ **Setup:** `export TID="akm-manual-test-$(date +%s)"`; back up crontab per
1506
+ above.
1507
+
1508
+ **Command:**
1509
+ ```sh
1510
+ akm task add "$TID" --schedule "@daily" --command "echo akm-manual-test" --bundle "$AKM_BUNDLE_DIR"
1511
+ ```
1512
+ Note: from a repo checkout this still requires `--rebind` (see TASK-16) —
1513
+ from an npm-global/standalone install it should succeed without it.
1514
+
1515
+ **`UNVERIFIED`: the exact success JSON shape.** This command was not
1516
+ executed live in review of this document (the coding-agent sandbox's own
1517
+ destructive-operation classifier refused all `task add` invocations —
1518
+ itself corroborating evidence the command is correctly recognized as
1519
+ touching the real system). Confirm on a disposable VM/container before
1520
+ relying on the shape below.
1521
+
1522
+ **Expected:** exit `0`; a task YAML at `$AKM_BUNDLE_DIR/tasks/$TID.yml`
1523
+ with `version: 4`.
1524
+
1525
+ **Verify OUTSIDE akm (mandatory — the whole point of the test):**
1526
+ ```sh
1527
+ crontab -l | grep -A2 "# akm:task $TID BEGIN"
1528
+ ```
1529
+ Expected format (pattern only, confirmed live against this machine's
1530
+ existing real entries):
1531
+ ```
1532
+ # akm:task <TID> BEGIN
1533
+ <cron-expr> AKM_BUNDLE_DIR=<working stash> <absolute-node-or-bun-path> <absolute-akm-launcher> task run <TID> --bundle <bundle> --scheduled > <logDir>/<TID>.log 2>&1
1534
+ # akm:task <TID> END
1535
+ ```
1536
+ (Rows written by 0.9.0 – 0.9.17-alpha.6 carry `--scheduler-context <path>`
1537
+ after the launcher instead of the inline `AKM_BUNDLE_DIR=`.)
1538
+ A disabled binding's line is prefixed `# akm:disabled ` instead of being a
1539
+ live cron line.
1540
+
1541
+ **Cleanup (do NOT use `akm task sync`):**
1542
+ ```sh
1543
+ crontab -l | sed "/# akm:task $TID BEGIN/,/# akm:task $TID END/d" | crontab -
1544
+ rm -f "$AKM_BUNDLE_DIR/tasks/$TID.yml"
1545
+ crontab -l | grep -c "$TID" # must print 0
1546
+ ```
1547
+
1548
+ ### DT-2 — `task add --disabled` writes a disabled binding [Linux: cron]
1549
+
1550
+ Same as DT-1 with `--disabled` added. Expected difference: the generated
1551
+ task YAML has `schedule: [{cron: "@daily", enabled: false}]`; the crontab
1552
+ line is written as `# akm:disabled ...`, matching real disabled entries
1553
+ observed on this host. Verify and clean up identically to DT-1.
1554
+ **UNVERIFIED for the same reason as DT-1.**
1555
+
1556
+ ### DT-3 — `task sync` drift reconciliation, shell-target task [Linux: cron; VM/disposable-account strongly recommended]
1557
+
1558
+ **Why it matters:** the actual "scheduler and bundle disagree" scenario —
1559
+ a high-risk mutation path that also exercises #846's owner-path scoping.
1560
+
1561
+ **Mandatory precondition:**
1562
+ ```sh
1563
+ crontab -l > "$AKM_SANDBOX/crontab-backup-$(date +%Y%m%d-%H%M%S).txt" 2>&1
1564
+ # If this shows real akm:task entries you don't recognize, STOP.
1565
+ ```
1566
+
1567
+ **Setup:** install a throwaway task via DT-1 (`$TID`), then edit its cron
1568
+ schedule by hand in `crontab -e` (change the minute field) without touching
1569
+ the YAML — manufacturing drift between the scheduler and the bundle.
1570
+
1571
+ **Command:** `akm task sync --bundle "$AKM_BUNDLE_DIR"`
1572
+
1573
+ **Expected:** `sync` detects the drifted binding and rewrites it to match
1574
+ the YAML's declared schedule (the YAML is the source of truth). Exit `0`.
1575
+ **`UNVERIFIED`: the exact JSON field name reporting "which entries
1576
+ changed"** — not executed live in review of this document; inspect
1577
+ `finalizeSchedulerSyncPlan`'s result shape in `src/commands/tasks/tasks.ts`
1578
+ before relying on a specific field name, or run once in a container and
1579
+ record the real output.
1580
+
1581
+ **Verify outside akm:** `crontab -l | grep -A2 "# akm:task $TID BEGIN"`
1582
+ shows the schedule restored to the YAML's value, not the hand-edited one.
1583
+
1584
+ **Cleanup:** same manual crontab-block removal as DT-1, plus delete the
1585
+ YAML, plus restore the mandatory backup.
1586
+
1587
+ ### DT-4 — `task sync` reconciling a workflow-targeting task's schedule [Linux: cron; VM/disposable-account strongly recommended]
1588
+
1589
+ **Why it matters:** `task sync`'s own one-line help ("preflight and
1590
+ reconcile a bundle's task/workflow schedules") is the only place in the CLI
1591
+ that names both features together. This is DT-3's counterpart for a task
1592
+ whose target is a workflow rather than a shell command — not a duplicate:
1593
+ the target kind is different and both paths need independent coverage.
1594
+
1595
+ **Mandatory precondition:** `crontab -l > "$AKM_SANDBOX/crontab-backup-$(date +%Y%m%d-%H%M%S).txt" 2>&1` — stop if it shows unrecognized real entries.
1596
+
1597
+ **Setup** (disposable container/VM with an empty crontab only; use a
1598
+ collision-proof bundle name so the resulting scheduler evidence is easy to
1599
+ identify and clean up):
1600
+ ```sh
1601
+ export SEAM_BUNDLE_NAME="akm-manual-test-seam-$(date +%s)"
1602
+ cat > "$AKM_BUNDLE_DIR/workflows/leaf.yml" <<'EOF'
1603
+ name: Manual test leaf workflow
1604
+ on:
1605
+ workflow_dispatch:
1606
+ jobs:
1607
+ main:
1608
+ runs-on: [self-hosted]
1609
+ steps:
1610
+ - id: work
1611
+ run: printf leaf-ok
1612
+ EOF
1613
+ cat > "$AKM_BUNDLE_DIR/tasks/akm-manual-test-drift.yml" <<'EOF'
1614
+ version: 4
1615
+ name: Manual test drift task -> workflow
1616
+ uses: workflows/leaf
1617
+ schedule:
1618
+ - cron: "0 3 * * *"
1619
+ enabled: true
1620
+ EOF
1621
+ akm index
1622
+ ```
1623
+
1624
+ **Steps and expected results:**
1625
+ 1. `akm task sync --bundle "$SEAM_BUNDLE_NAME"` (first install) → expected
1626
+ exit `0`, `.installed` contains `"akm-manual-test-drift"`, `.updated ==
1627
+ []`, `.removed == []`. Cross-check: `crontab -l | grep -A1 "akm:task
1628
+ akm-manual-test-drift BEGIN"` shows `0 3 * * *`, `--bundle
1629
+ "$SEAM_BUNDLE_NAME"`, and `task run akm-manual-test-drift --scheduled`.
1630
+ 2. Simulate OS-side drift without touching the bundle file:
1631
+ `crontab -l | sed 's/^0 3 \* \* \*\(.*akm-manual-test-drift.*\)/0 4 * * *\1/' | crontab -`.
1632
+ Cross-check the entry now shows `0 4 * * *`.
1633
+ 3. `akm task sync --bundle "$SEAM_BUNDLE_NAME"` again → expected exit `0`,
1634
+ `.updated` contains `"akm-manual-test-drift"` (the bundle's `.yml` wins),
1635
+ `.installed == []`, `.removed == []`. Cross-check the entry shows `0 3 *
1636
+ * *` again.
1637
+ 4. `rm "$AKM_BUNDLE_DIR/tasks/akm-manual-test-drift.yml" && akm task sync
1638
+ --bundle "$SEAM_BUNDLE_NAME"` (the product's documented removal
1639
+ mechanism — there is no `task remove` subcommand;
1640
+ `docs/reference/tasks.md:391`: *"Delete the `.yml` source and sync to
1641
+ remove its derived binding(s)."*) → expected exit `0`, `.removed`
1642
+ contains `"akm-manual-test-drift"`. Cross-check: `crontab -l | grep -c
1643
+ akm-manual-test-drift` prints `0`.
1644
+
1645
+ **`UNVERIFIED` — this scenario's step-by-step outputs were designed from
1646
+ traced source (`belongsToBundle`, `src/tasks/scheduler-sync.ts`) but not
1647
+ executed live: the coding-agent sandbox's destructive-action classifier
1648
+ refused every `task sync` call attempted during review, which is itself a
1649
+ useful signal about how this surface should be treated.** Confirm the exact
1650
+ `.installed`/`.updated`/`.removed` values on a real disposable scheduler
1651
+ before relying on this test.
1652
+
1653
+ **Cleanup:** if step 4 was reached the crontab is already clean; regardless,
1654
+ restore the mandatory backup and verify with `crontab -l`.
1655
+
1656
+ ### DT-5 — Cross-bundle name collision cannot make `sync` remove another bundle's entries (#846 regression) [Linux: cron; disposable VM/container REQUIRED]
1657
+
1658
+ **Why it matters:** alpha.5 could inspect and remove an unrelated scheduler
1659
+ entry when two primary bundles derived the same basename. Stable 0.9.2
1660
+ requires a name match *and* a resolved owner-path match before an entry is in
1661
+ scope, and refuses ownership when the path cannot be established. (Since
1662
+ 0.9.17-alpha.7 every row names the path inline as `AKM_BUNDLE_DIR=`; an
1663
+ older row names it in its `--scheduler-context` descriptor, and is refused
1664
+ when that file is missing or unreadable.) This test reproduces the
1665
+ original collision safely and proves task A survives bundle B's sync.
1666
+
1667
+ **`UNVERIFIED` — even this fully self-contained, two-throwaway-bundle
1668
+ reproduction was refused by the coding-agent sandbox's destructive-action
1669
+ classifier before any `task sync` call executed during this document's
1670
+ review.** This test is designed from traced source, not run live. Whoever
1671
+ runs it for real needs an environment without that guardrail — a
1672
+ disposable VM/container is exactly that, and is required here regardless.
1673
+
1674
+ **Mandatory precondition:** `crontab -l > "$AKM_SANDBOX/crontab-backup-$(date +%Y%m%d-%H%M%S).txt" 2>&1`.
1675
+
1676
+ **Setup — two throwaway bundles that deliberately collide by name:**
1677
+ ```sh
1678
+ export BASE=/tmp/akm-seam-collide-$(date +%s)
1679
+ mkdir -p "$BASE/a/bundle"/{tasks,workflows} "$BASE/a"/{config,data,cache,state}
1680
+ mkdir -p "$BASE/b-collide/bundle"/{tasks,workflows} "$BASE/b-collide"/{config,data,cache,state}
1681
+ for d in a b-collide; do
1682
+ cat > "$BASE/$d/config/config.json" <<'EOF'
1683
+ {"configVersion":"0.9.0","semanticSearchMode":"off","registries":[]}
1684
+ EOF
1685
+ done
1686
+ # Both bundle directories are literally named "bundle" — same basename means
1687
+ # deriveBundleId (src/core/write-source.ts:1254) derives the SAME name for
1688
+ # both, because de-dup only checks each bundle's OWN config.json, never the
1689
+ # other bundle's. This is the collision, reproduced on purpose.
1690
+
1691
+ cat > "$BASE/a/bundle/tasks/akm-manual-test-collide-a.yml" <<'EOF'
1692
+ version: 4
1693
+ name: Collision test task A (should survive if scoping is correct)
1694
+ run: printf collide-a-ok
1695
+ schedule:
1696
+ - cron: "1 1 * * *"
1697
+ enabled: true
1698
+ EOF
1699
+
1700
+ cat > "$BASE/b-collide/bundle/tasks/akm-manual-test-collide-b.yml" <<'EOF'
1701
+ version: 4
1702
+ name: Collision test task B (does not declare task A)
1703
+ run: printf collide-b-ok
1704
+ schedule:
1705
+ - cron: "2 2 * * *"
1706
+ enabled: true
1707
+ EOF
1708
+ ```
1709
+
1710
+ **Steps and expected results:**
1711
+ 1. Install bundle A with **no** `--bundle` flag (name comes from the
1712
+ directory-basename default):
1713
+ ```sh
1714
+ export AKM_BUNDLE_DIR="$BASE/a/bundle" AKM_CONFIG_DIR="$BASE/a/config" \
1715
+ AKM_DATA_DIR="$BASE/a/data" AKM_CACHE_DIR="$BASE/a/cache" AKM_STATE_DIR="$BASE/a/state"
1716
+ akm index && akm task sync
1717
+ ```
1718
+ Expected: exit `0`, `.installed` contains `"akm-manual-test-collide-a"`.
1719
+ Cross-check: `crontab -l | grep akm-manual-test-collide-a` shows `1 1 *
1720
+ * *`.
1721
+ 2. Install bundle B (the colliding one), same pattern, no `--bundle` flag:
1722
+ ```sh
1723
+ export AKM_BUNDLE_DIR="$BASE/b-collide/bundle" AKM_CONFIG_DIR="$BASE/b-collide/config" \
1724
+ AKM_DATA_DIR="$BASE/b-collide/data" AKM_CACHE_DIR="$BASE/b-collide/cache" AKM_STATE_DIR="$BASE/b-collide/state"
1725
+ akm index && akm task sync
1726
+ ```
1727
+ Expected: `.removed == []` for task A and `.installed` contains
1728
+ `"akm-manual-test-collide-b"`. Cross-check: `crontab -l | grep -c
1729
+ akm-manual-test-collide-a` prints `1`, and its line still shows `1 1 * *
1730
+ *`. **This is the falsifiable core of the regression:** a removal of task A
1731
+ is a release-blocking recurrence of #846.
1732
+ 3. **Negative control**, proving scoping DOES work when names differ.
1733
+ Reinstall task A (repeat step 1), then repeat step 2 with a
1734
+ non-colliding directory name, e.g. `"$BASE/b-safe/bundle-safe"`.
1735
+ Expected: `.removed == []`, task A's entry is still untouched. This proves
1736
+ the non-colliding case stays unchanged alongside the collision fix.
1737
+
1738
+ **Cleanup:** `crontab "$AKM_SANDBOX/crontab-backup-"*.txt` to restore, then
1739
+ verify with `crontab -l` that only the original entries remain. `rm -rf
1740
+ "$BASE"`.
1741
+
1742
+ ### DT-6 — macOS launchd equivalents of DT-1..DT-3 [PLATFORM-GATED — cannot run on this Linux machine]
1743
+
1744
+ Everything above assumed `backend == "cron"`. On macOS, `akm task doctor`
1745
+ should report `.backend == "launchd"` (inferred from source, not observed).
1746
+ Re-run DT-1–DT-3 there with:
1747
+ - Verify outside akm: `launchctl list | grep <TID>` and inspect the
1748
+ generated plist under whatever path `doctor`'s `logDir`-equivalent
1749
+ reports on that host.
1750
+ - Cleanup: `launchctl bootout`/`unload` the job by label before deleting
1751
+ the plist and the task YAML — do not rely on `akm task sync` for
1752
+ cleanup, same rationale as DT-1.
1753
+
1754
+ **Entirely UNVERIFIED — confirm before shipping.** Per issue #770 this
1755
+ path has no automated coverage, which is exactly why it's high-risk manual
1756
+ surface.
1757
+
1758
+ ### DT-7 — Windows schtasks equivalents of DT-1..DT-3 [PLATFORM-GATED — cannot run on this Linux machine]
1759
+
1760
+ Same structure as DT-6 but for `schtasks`. Verify outside akm with
1761
+ `schtasks /query /tn <TID> /v /fo list`. Cleanup with `schtasks /delete /tn
1762
+ <TID> /f` before deleting the task YAML. **Entirely UNVERIFIED** — not
1763
+ executed, platform absent here; per #770 this is uncovered by automation.
1764
+
1765
+ ---
1766
+
1767
+ ## Corrections made during adversarial review
1768
+
1769
+ This document was assembled from three independently-authored drafts and
1770
+ adversarially spot-checked against a live build before merging. Two
1771
+ specific corrections from that review:
1772
+
1773
+ 1. **The claim "`akm workflow --help` USAGE omits `plan`" does not
1774
+ reproduce on commit `fb7bef3f`.** Live-checked against both `bun
1775
+ src/cli.ts workflow --help` and a freshly rebuilt `dist/akm workflow
1776
+ --help`: both list `status|list|create|resume|abandon|run|plan` in the
1777
+ USAGE line and describe `plan` in the COMMANDS table. One source draft's
1778
+ claim to the contrary appears to have been made against a stale `dist/`
1779
+ build before it noticed and rebuilt (the same draft separately warns
1780
+ "the checked-out `dist/` was stale... lacked `workflow plan`"). The
1781
+ correct, still-true finding kept in this document instead is: `akm task
1782
+ --help` genuinely has no `remove` subcommand (`add|run|explain|history|
1783
+ sync|doctor` only) — confirmed live.
1784
+ 2. **INT-2's parent-side child-run evidence shape**, marked `UNVERIFIED` in
1785
+ the source draft, was resolved live during review (see INT-2 step 2
1786
+ above): for a child dispatched via `uses: tasks/<ref>` where the child
1787
+ workflow declares no `outputs:`, the parent step's `evidence.output` is
1788
+ a bare `{runId, status}` pointer, while the full child summary is
1789
+ carried in the top-level `.children[]` array — the same array shape
1790
+ WF-3 already demonstrates for a direct (non-task) child dispatch whose
1791
+ child workflow *does* declare `outputs:`.
1792
+
1793
+ Live alpha.5 spot-checks performed (in addition to the two above): TASK-3,
1794
+ TASK-6, TASK-9, TASK-12, TASK-13, WF-2, WF-5 (incl. `status --units`),
1795
+ WF-3/INT-2's `.children` shape, WF-6/#847's original reproduction, WF-9's
1796
+ sqlite table/column names and exact error message, and INT-1 end to end. All
1797
+ matched their source draft's claimed exact fields, error codes, and exit codes
1798
+ with no further discrepancies found. Stable 0.9.2 changes WF-6 and DT-5 to
1799
+ the fixed regression expectations documented above; both are now automated.
1800
+ No test was found using
1801
+ non-falsifiable language ("works correctly", "runs successfully", "no
1802
+ errors"); the three source drafts were already disciplined about naming
1803
+ exact exit codes, JSON fields, and error strings. No true duplicate tests
1804
+ were found across the three drafts — the deliberate contrast pairs (WF-6 vs
1805
+ WF-7; DT-3 vs DT-4) were kept, and no accidental duplication existed thanks
1806
+ to the drafts' explicit scope carve-outs.
1807
+
1808
+ ## A note on the existing bundle checklist
1809
+
1810
+ The AKM bundle asset `knowledge/projects/akm/akm-manual-testing-checklist`
1811
+ (frontmatter `updated: 2026-05-17`) has stale Tasks (§5) and Workflows (§7)
1812
+ sections: it references `akm tasks list` (the command is `task`, singular
1813
+ — `akm tasks list` does not exist) and `akm workflow next` (no such
1814
+ subcommand exists in 0.9.2; the current surface is `status|list|create|
1815
+ resume|abandon|run|plan`). Recommend replacing those two sections with a
1816
+ pointer to this document rather than trying to patch them in place. This
1817
+ document does not edit that bundle asset — only its author/owner should.