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