akm-cli 0.9.0-rc.0 → 0.9.0-rc.1
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 +123 -0
- package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
- package/dist/commands/env/env-binding.js +95 -0
- package/dist/commands/env/env-cli.js +8 -65
- package/dist/commands/sources/migration-help.js +7 -4
- package/dist/commands/workflow-cli.js +276 -12
- package/dist/core/asset/asset-spec.js +58 -1
- package/dist/core/config/config-schema.js +21 -0
- package/dist/core/json-schema.js +142 -0
- package/dist/indexer/db/db.js +2 -1
- package/dist/indexer/walk/matchers.js +39 -0
- package/dist/integrations/agent/builders.js +7 -5
- package/dist/integrations/agent/model-aliases.js +9 -0
- package/dist/integrations/agent/profiles.js +72 -5
- package/dist/integrations/agent/runner-dispatch.js +25 -1
- package/dist/integrations/agent/spawn.js +137 -14
- package/dist/integrations/harnesses/aider/agent-builder.js +113 -0
- package/dist/integrations/harnesses/aider/index.js +58 -0
- package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
- package/dist/integrations/harnesses/amazonq/agent-builder.js +153 -0
- package/dist/integrations/harnesses/amazonq/index.js +59 -0
- package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
- package/dist/integrations/harnesses/claude/agent-builder.js +45 -6
- package/dist/integrations/harnesses/claude/index.js +25 -23
- package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
- package/dist/integrations/harnesses/codex/agent-builder.js +137 -0
- package/dist/integrations/harnesses/codex/index.js +63 -0
- package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
- package/dist/integrations/harnesses/copilot/agent-builder.js +122 -0
- package/dist/integrations/harnesses/copilot/index.js +60 -0
- package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
- package/dist/integrations/harnesses/gemini/agent-builder.js +121 -0
- package/dist/integrations/harnesses/gemini/index.js +60 -0
- package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
- package/dist/integrations/harnesses/index.js +26 -4
- package/dist/integrations/harnesses/opencode/index.js +15 -16
- package/dist/integrations/harnesses/opencode-sdk/harness.js +65 -0
- package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +581 -91
- package/dist/integrations/harnesses/openhands/agent-builder.js +126 -0
- package/dist/integrations/harnesses/openhands/index.js +58 -0
- package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
- package/dist/integrations/harnesses/pi/agent-builder.js +104 -0
- package/dist/integrations/harnesses/pi/index.js +58 -0
- package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
- package/dist/integrations/harnesses/types.js +7 -0
- package/dist/integrations/session-logs/index.js +24 -11
- package/dist/output/renderers.js +3 -2
- package/dist/output/shapes/passthrough.js +4 -0
- package/dist/output/text/helpers.js +212 -1
- package/dist/output/text/workflow.js +3 -1
- package/dist/schemas/akm-config.json +14225 -0
- package/dist/schemas/akm-workflow.json +328 -0
- package/dist/scripts/migrate-storage.js +1034 -6973
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +768 -10
- package/dist/storage/repositories/workflow-runs-repository.js +189 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/authoring/authoring.js +123 -10
- package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
- package/dist/workflows/cli.js +4 -0
- package/dist/workflows/db.js +135 -0
- package/dist/workflows/exec/brief.js +484 -0
- package/dist/workflows/exec/native-executor.js +975 -0
- package/dist/workflows/exec/param-secrets.js +115 -0
- package/dist/workflows/exec/report.js +1295 -0
- package/dist/workflows/exec/run-workflow.js +596 -0
- package/dist/workflows/exec/scheduler.js +100 -0
- package/dist/workflows/exec/step-work.js +1156 -0
- package/dist/workflows/exec/unit-writer.js +23 -0
- package/dist/workflows/exec/watch.js +116 -0
- package/dist/workflows/exec/worktree.js +171 -0
- package/dist/workflows/ir/compile.js +388 -0
- package/dist/workflows/ir/params.js +54 -0
- package/dist/workflows/ir/plan-hash.js +33 -0
- package/dist/workflows/ir/schema.js +4 -0
- package/dist/workflows/parser.js +3 -1
- package/dist/workflows/program/expressions.js +369 -0
- package/dist/workflows/program/parser.js +760 -0
- package/dist/workflows/program/project.js +105 -0
- package/dist/workflows/program/schema.js +54 -0
- package/dist/workflows/renderer.js +82 -5
- package/dist/workflows/runtime/agent-identity.js +59 -14
- package/dist/workflows/runtime/runs.js +206 -36
- package/dist/workflows/runtime/unit-checkin.js +45 -0
- package/dist/workflows/runtime/workflow-asset-loader.js +64 -1
- package/dist/workflows/validate-summary.js +24 -3
- package/dist/workflows/validator.js +1 -1
- package/docs/data-and-telemetry.md +2 -1
- package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
- package/package.json +1 -1
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { defaultRendererRegistry } from "../../core/asset/asset-registry.js";
|
|
12
12
|
import { SCRIPT_EXTENSIONS } from "../../core/asset/asset-spec.js";
|
|
13
13
|
import { looksLikeWorkflow } from "../../workflows/parser.js";
|
|
14
|
+
import { looksLikeWorkflowProgram } from "../../workflows/program/parser.js";
|
|
15
|
+
import { WORKFLOW_PROGRAM_RENDERER_NAME } from "../../workflows/program/project.js";
|
|
14
16
|
import { registerMatcher } from "./file-context.js";
|
|
15
17
|
// ---------------------------------------------------------------------------
|
|
16
18
|
// Private data
|
|
@@ -178,6 +180,35 @@ function classifyBySmartMd(ctx) {
|
|
|
178
180
|
}
|
|
179
181
|
return { type: "knowledge", specificity: 5 };
|
|
180
182
|
}
|
|
183
|
+
/** YAML workflow *programs* (redesign addendum, R1): `.yaml`/`.yml` files. */
|
|
184
|
+
const WORKFLOW_PROGRAM_EXTENSIONS = new Set([".yaml", ".yml"]);
|
|
185
|
+
/**
|
|
186
|
+
* Classify YAML workflow programs. Two claims, mirroring the markdown rules:
|
|
187
|
+
*
|
|
188
|
+
* - any `.yaml`/`.yml` under a `workflows/` dir (the directory rule —
|
|
189
|
+
* specificity 15 when `workflows` is the immediate parent, 10 for a
|
|
190
|
+
* deeper ancestor, same ladder as `matchDirectoryHint`);
|
|
191
|
+
* - anywhere else, a content probe via `looksLikeWorkflowProgram`
|
|
192
|
+
* (`version: 1` + `steps:` at column 0) at specificity 19, the same
|
|
193
|
+
* level as `classifyBySmartMd`'s `looksLikeWorkflow` claim.
|
|
194
|
+
*
|
|
195
|
+
* The fact does NOT go through `toMatchResult` — the workflow TYPE maps to
|
|
196
|
+
* the markdown renderer by default, so this classifier names the
|
|
197
|
+
* `workflow-program-yaml` renderer on its result directly.
|
|
198
|
+
*/
|
|
199
|
+
function classifyByWorkflowProgram(ctx) {
|
|
200
|
+
if (!WORKFLOW_PROGRAM_EXTENSIONS.has(ctx.ext))
|
|
201
|
+
return null;
|
|
202
|
+
if (isTypedDirDocFile(ctx.fileName))
|
|
203
|
+
return null;
|
|
204
|
+
if (ctx.parentDir === "workflows")
|
|
205
|
+
return { type: "workflow", specificity: 15 };
|
|
206
|
+
if (ctx.ancestorDirs.includes("workflows"))
|
|
207
|
+
return { type: "workflow", specificity: 10 };
|
|
208
|
+
if (looksLikeWorkflowProgram(ctx.content()))
|
|
209
|
+
return { type: "workflow", specificity: 19 };
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
181
212
|
function classifyByWiki(ctx) {
|
|
182
213
|
if (ctx.ext !== ".md")
|
|
183
214
|
return null;
|
|
@@ -223,12 +254,20 @@ export function smartMdMatcher(ctx) {
|
|
|
223
254
|
export function wikiMatcher(ctx) {
|
|
224
255
|
return toMatchResult(ctx, classifyByWiki);
|
|
225
256
|
}
|
|
257
|
+
export function workflowProgramMatcher(ctx) {
|
|
258
|
+
const fact = classifyByWorkflowProgram(ctx);
|
|
259
|
+
if (!fact)
|
|
260
|
+
return null;
|
|
261
|
+
// Named directly (not via rendererNameFor) — see classifyByWorkflowProgram.
|
|
262
|
+
return { type: fact.type, specificity: fact.specificity, renderer: WORKFLOW_PROGRAM_RENDERER_NAME };
|
|
263
|
+
}
|
|
226
264
|
const builtinMatchers = [
|
|
227
265
|
extensionMatcher,
|
|
228
266
|
directoryMatcher,
|
|
229
267
|
parentDirHintMatcher,
|
|
230
268
|
smartMdMatcher,
|
|
231
269
|
wikiMatcher,
|
|
270
|
+
workflowProgramMatcher,
|
|
232
271
|
];
|
|
233
272
|
export function registerBuiltinMatchers() {
|
|
234
273
|
for (const matcher of builtinMatchers) {
|
|
@@ -77,11 +77,13 @@ const BUILTIN_BUILDERS = (() => {
|
|
|
77
77
|
* A *custom* profile (unknown platform) falls back to the default builder —
|
|
78
78
|
* that generic `--system-prompt`/`--model`/`--` shape is the documented
|
|
79
79
|
* contract for user-defined wrappers. A *known built-in agent CLI* with no
|
|
80
|
-
* dedicated builder
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* dedicated builder is a loud `ConfigError` instead: the default flag shape
|
|
81
|
+
* is wrong for those CLIs (aider, for one, treats positionals as file names),
|
|
82
|
+
* so the old silent fallback produced a broken command that "ran" and failed
|
|
83
|
+
* downstream. As of the P2 harness-adapter integration every built-in profile
|
|
84
|
+
* has a registry-derived builder, so this branch only fires if a future
|
|
85
|
+
* builtin profile ships without one. Custom builders injected via tests can
|
|
86
|
+
* be passed as `registry`.
|
|
85
87
|
*/
|
|
86
88
|
export function getCommandBuilder(platform, registry = BUILTIN_BUILDERS) {
|
|
87
89
|
const found = registry[platform];
|
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
* always resolve to the full name for determinism)
|
|
12
12
|
*/
|
|
13
13
|
const BUILTIN_ALIASES = [
|
|
14
|
+
{
|
|
15
|
+
// Anthropic's Mythos-class tier above Opus — the recommended resolution
|
|
16
|
+
// target for the `deep` workflow tier (see docs/features/workflows.md).
|
|
17
|
+
alias: "fable",
|
|
18
|
+
platforms: {
|
|
19
|
+
claude: "claude-fable-5",
|
|
20
|
+
opencode: "opencode/claude-fable-5",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
14
23
|
{
|
|
15
24
|
alias: "opus",
|
|
16
25
|
platforms: {
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
// every lane's read-back (GRR). It is a provenance tag, never a secret.
|
|
10
10
|
const COMMON_PASSTHROUGH = ["HOME", "PATH", "USER", "LANG", "LC_ALL", "TERM", "TMPDIR", "AKM_EVENT_SOURCE"];
|
|
11
11
|
/**
|
|
12
|
-
* Built-in profiles for the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Built-in profiles for the agent CLIs akm knows out of the box: the five the
|
|
13
|
+
* v1 spec calls out explicitly, plus the P2 harness adapters (copilot, pi,
|
|
14
|
+
* amazonq, openhands — plan §"Capability matrix"). The fields here are
|
|
15
|
+
* conservative defaults — every value is overridable from user config.
|
|
15
16
|
*
|
|
16
17
|
* For headless/automation use (propose, reflect, tasks), use the '-headless' variant.
|
|
17
18
|
*/
|
|
@@ -56,9 +57,42 @@ const BUILTINS = {
|
|
|
56
57
|
envPassthrough: [...COMMON_PASSTHROUGH, "OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
57
58
|
parseOutput: "text",
|
|
58
59
|
},
|
|
60
|
+
// ── P2 harness-adapter profiles (plan §"Capability matrix") ────────────────
|
|
61
|
+
copilot: {
|
|
62
|
+
name: "copilot",
|
|
63
|
+
bin: "copilot",
|
|
64
|
+
args: [],
|
|
65
|
+
stdio: "interactive",
|
|
66
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "GH_TOKEN", "GITHUB_TOKEN"],
|
|
67
|
+
parseOutput: "text",
|
|
68
|
+
},
|
|
69
|
+
pi: {
|
|
70
|
+
name: "pi",
|
|
71
|
+
bin: "pi",
|
|
72
|
+
args: [],
|
|
73
|
+
stdio: "interactive",
|
|
74
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "PI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
75
|
+
parseOutput: "text",
|
|
76
|
+
},
|
|
77
|
+
amazonq: {
|
|
78
|
+
name: "amazonq",
|
|
79
|
+
bin: "q",
|
|
80
|
+
args: [],
|
|
81
|
+
stdio: "interactive",
|
|
82
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "AWS_PROFILE", "AWS_REGION"],
|
|
83
|
+
parseOutput: "text",
|
|
84
|
+
},
|
|
85
|
+
openhands: {
|
|
86
|
+
name: "openhands",
|
|
87
|
+
bin: "openhands",
|
|
88
|
+
args: [],
|
|
89
|
+
stdio: "interactive",
|
|
90
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "LLM_MODEL", "LLM_API_KEY", "LLM_BASE_URL"],
|
|
91
|
+
parseOutput: "text",
|
|
92
|
+
},
|
|
59
93
|
};
|
|
60
94
|
/**
|
|
61
|
-
* Headless variants of the
|
|
95
|
+
* Headless variants of the base profiles for automation use (propose, reflect, tasks).
|
|
62
96
|
*
|
|
63
97
|
* These profiles use `stdio: "captured"` and `parseOutput: "json"` so the
|
|
64
98
|
* agent's response can be read from stdout. They share the same `bin` and
|
|
@@ -110,9 +144,42 @@ const HEADLESS_BUILTINS = {
|
|
|
110
144
|
envPassthrough: [...COMMON_PASSTHROUGH, "OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
111
145
|
parseOutput: "json",
|
|
112
146
|
},
|
|
147
|
+
// ── P2 harness-adapter headless variants (plan §"Capability matrix") ───────
|
|
148
|
+
"copilot-headless": {
|
|
149
|
+
name: "copilot-headless",
|
|
150
|
+
bin: "copilot",
|
|
151
|
+
args: [],
|
|
152
|
+
stdio: "captured",
|
|
153
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "GH_TOKEN", "GITHUB_TOKEN"],
|
|
154
|
+
parseOutput: "json",
|
|
155
|
+
},
|
|
156
|
+
"pi-headless": {
|
|
157
|
+
name: "pi-headless",
|
|
158
|
+
bin: "pi",
|
|
159
|
+
args: [],
|
|
160
|
+
stdio: "captured",
|
|
161
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "PI_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
162
|
+
parseOutput: "json",
|
|
163
|
+
},
|
|
164
|
+
"amazonq-headless": {
|
|
165
|
+
name: "amazonq-headless",
|
|
166
|
+
bin: "q",
|
|
167
|
+
args: [],
|
|
168
|
+
stdio: "captured",
|
|
169
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "AWS_PROFILE", "AWS_REGION"],
|
|
170
|
+
parseOutput: "json",
|
|
171
|
+
},
|
|
172
|
+
"openhands-headless": {
|
|
173
|
+
name: "openhands-headless",
|
|
174
|
+
bin: "openhands",
|
|
175
|
+
args: [],
|
|
176
|
+
stdio: "captured",
|
|
177
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "LLM_MODEL", "LLM_API_KEY", "LLM_BASE_URL"],
|
|
178
|
+
parseOutput: "json",
|
|
179
|
+
},
|
|
113
180
|
};
|
|
114
181
|
/**
|
|
115
|
-
* Names of the
|
|
182
|
+
* Names of the primary built-in profiles. Stable, sorted. Does NOT
|
|
116
183
|
* include the `-headless` variants (those are resolvable by name but are
|
|
117
184
|
* excluded from detection/enumeration flows).
|
|
118
185
|
*/
|
|
@@ -28,8 +28,32 @@
|
|
|
28
28
|
* (X2) can wrap `executeRunner` without changing this contract.
|
|
29
29
|
*/
|
|
30
30
|
import { assertNever } from "../../core/assert.js";
|
|
31
|
-
import { runOpencodeSdk } from "../harnesses/opencode-sdk/index.js";
|
|
31
|
+
import { closeServer as disposeOpencodeSdkServers, runOpencodeSdk } from "../harnesses/opencode-sdk/index.js";
|
|
32
32
|
import { runAgent } from "./spawn.js";
|
|
33
|
+
/**
|
|
34
|
+
* Release every long-lived resource the dispatch runners CACHE for reuse, so a
|
|
35
|
+
* one-shot process (the CLI) can exit cleanly once dispatching is done.
|
|
36
|
+
*
|
|
37
|
+
* The `sdk` runner keeps a per-env registry of `opencode serve` CHILD
|
|
38
|
+
* PROCESSES (see `opencode-sdk/sdk-runner.ts`), started lazily and reused
|
|
39
|
+
* across units within a process. Each live child is an OS handle that keeps
|
|
40
|
+
* Bun's event loop open — and the registry's own teardown is wired ONLY to
|
|
41
|
+
* `process.once('exit')`, which never fires while such a child holds the loop
|
|
42
|
+
* open. That is a deadlock: a successful `akm workflow run` that dispatched via
|
|
43
|
+
* the SDK path would hang the CLI (owner finding 4) because the process is
|
|
44
|
+
* never idle enough for the exit hook to run and close the children it is
|
|
45
|
+
* waiting on.
|
|
46
|
+
*
|
|
47
|
+
* The engine therefore calls this in its run `finally` (every exit path —
|
|
48
|
+
* success, gate rejection, failure, abort) to drain the registry deterministically
|
|
49
|
+
* BEFORE relying on the event loop to drain. The close is synchronous and
|
|
50
|
+
* idempotent; when no SDK server was ever started it is a no-op, so the `agent`
|
|
51
|
+
* / `llm` dispatch paths pay nothing. Servers are re-created lazily on the next
|
|
52
|
+
* dispatch, so calling this between independent dispatch invocations is safe.
|
|
53
|
+
*/
|
|
54
|
+
export function disposeDispatchResources() {
|
|
55
|
+
disposeOpencodeSdkServers();
|
|
56
|
+
}
|
|
33
57
|
/**
|
|
34
58
|
* Dispatch a {@link RunnerSpec} to its runner and return the raw
|
|
35
59
|
* {@link AgentRunResult}. `opts` is the {@link RunAgentOptions} for the profile
|
|
@@ -143,21 +143,93 @@ function buildChildEnv(profile, options) {
|
|
|
143
143
|
}
|
|
144
144
|
return env;
|
|
145
145
|
}
|
|
146
|
+
const STREAM_READ_TIMEOUT = Symbol("stream-read-timeout");
|
|
146
147
|
async function readStream(stream, opts) {
|
|
147
148
|
if (!stream)
|
|
148
|
-
return "";
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
return { text: "", timedOut: false };
|
|
150
|
+
const reader = stream.getReader();
|
|
151
|
+
const decoder = new TextDecoder();
|
|
152
|
+
let text = "";
|
|
153
|
+
if (!opts?.timeoutMs) {
|
|
154
|
+
try {
|
|
155
|
+
while (true) {
|
|
156
|
+
const chunk = await reader.read();
|
|
157
|
+
if (chunk.done)
|
|
158
|
+
break;
|
|
159
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
160
|
+
}
|
|
161
|
+
text += decoder.decode();
|
|
162
|
+
return { text, timedOut: false };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return { text, timedOut: false, error };
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
try {
|
|
169
|
+
reader.releaseLock();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
/* ignore */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
152
176
|
// Race the stream read against a timeout so a process that is killed via
|
|
153
177
|
// SIGTERM/SIGKILL but whose pipe endpoints stay open (e.g. background
|
|
154
178
|
// threads still holding the fd) cannot block the caller indefinitely.
|
|
155
|
-
// On timeout we return whatever
|
|
156
|
-
|
|
179
|
+
// On timeout we return whatever was decoded before the pipe stopped draining.
|
|
180
|
+
const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
|
|
181
|
+
const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
|
|
182
|
+
let timer;
|
|
157
183
|
const timeoutPromise = new Promise((resolve) => {
|
|
158
|
-
|
|
184
|
+
timer = setTimeoutImpl(() => {
|
|
185
|
+
timer = undefined;
|
|
186
|
+
resolve(STREAM_READ_TIMEOUT);
|
|
187
|
+
}, opts.timeoutMs);
|
|
188
|
+
if (typeof timer !== "number")
|
|
189
|
+
timer.unref?.();
|
|
159
190
|
});
|
|
160
|
-
|
|
191
|
+
try {
|
|
192
|
+
while (true) {
|
|
193
|
+
const chunk = await Promise.race([reader.read(), timeoutPromise]);
|
|
194
|
+
if (chunk === STREAM_READ_TIMEOUT) {
|
|
195
|
+
void reader.cancel().catch(() => { });
|
|
196
|
+
return { text, timedOut: true };
|
|
197
|
+
}
|
|
198
|
+
if (chunk.done)
|
|
199
|
+
break;
|
|
200
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
201
|
+
}
|
|
202
|
+
text += decoder.decode();
|
|
203
|
+
return { text, timedOut: false };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return { text, timedOut: false, error };
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (timer !== undefined) {
|
|
210
|
+
clearTimeoutImpl(timer);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
reader.releaseLock();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
/* ignore */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function streamFailureMessage(profileName, stdout, stderr) {
|
|
221
|
+
const failures = [];
|
|
222
|
+
if (stdout.error)
|
|
223
|
+
failures.push(`stdout read failed: ${stdout.error instanceof Error ? stdout.error.message : String(stdout.error)}`);
|
|
224
|
+
if (stderr.error)
|
|
225
|
+
failures.push(`stderr read failed: ${stderr.error instanceof Error ? stderr.error.message : String(stderr.error)}`);
|
|
226
|
+
if (stdout.timedOut)
|
|
227
|
+
failures.push("stdout drain timed out");
|
|
228
|
+
if (stderr.timedOut)
|
|
229
|
+
failures.push("stderr drain timed out");
|
|
230
|
+
if (failures.length === 0)
|
|
231
|
+
return undefined;
|
|
232
|
+
return `agent CLI "${profileName}" output capture failed: ${failures.join("; ")}`;
|
|
161
233
|
}
|
|
162
234
|
/**
|
|
163
235
|
* Spawn the agent CLI described by `profile` with `prompt` (forwarded as
|
|
@@ -184,6 +256,17 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
184
256
|
const parseOutput = options.parseOutput ?? profile.parseOutput;
|
|
185
257
|
const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
|
|
186
258
|
const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
|
|
259
|
+
// Observability seam — ids/status only, best-effort (see RunAgentOptions.onEvent).
|
|
260
|
+
const emitSpawnEvent = (type, data) => {
|
|
261
|
+
if (!options.onEvent)
|
|
262
|
+
return;
|
|
263
|
+
try {
|
|
264
|
+
options.onEvent({ type, data });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Observability must never break the dispatch.
|
|
268
|
+
}
|
|
269
|
+
};
|
|
187
270
|
// Build argv via the platform-specific builder when dispatch params are
|
|
188
271
|
// provided; fall back to the legacy positional-prompt form otherwise.
|
|
189
272
|
let builtArgv;
|
|
@@ -247,6 +330,10 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
247
330
|
error: err instanceof Error ? err.message : String(err),
|
|
248
331
|
};
|
|
249
332
|
}
|
|
333
|
+
emitSpawnEvent("spawn_start", {
|
|
334
|
+
profile: profile.name,
|
|
335
|
+
...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
|
|
336
|
+
});
|
|
250
337
|
// Hard timeout. We prefer SIGTERM, then SIGKILL if SIGTERM is ignored,
|
|
251
338
|
// but the subprocess only exposes a single .kill() — one signal is enough
|
|
252
339
|
// for the structured-failure contract.
|
|
@@ -267,11 +354,13 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
267
354
|
timedOut = true;
|
|
268
355
|
killGroup(proc, "SIGTERM");
|
|
269
356
|
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
270
|
-
setTimeoutImpl(() => {
|
|
357
|
+
const sigkillTimer = setTimeoutImpl(() => {
|
|
271
358
|
if (!proc || proc.exitCode !== null)
|
|
272
359
|
return;
|
|
273
360
|
killGroup(proc, "SIGKILL");
|
|
274
361
|
}, 5000);
|
|
362
|
+
if (typeof sigkillTimer !== "number")
|
|
363
|
+
sigkillTimer.unref?.();
|
|
275
364
|
}, timeoutMs);
|
|
276
365
|
}
|
|
277
366
|
// Cooperative cancel: same SIGTERM→SIGKILL discipline as the timeout, but
|
|
@@ -308,11 +397,19 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
308
397
|
// When there is no kill timer, allow up to 30 s for streams to drain.
|
|
309
398
|
const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
|
|
310
399
|
const stdoutPromise = stdioMode === "captured"
|
|
311
|
-
? readStream(proc.stdout ?? null, {
|
|
312
|
-
|
|
400
|
+
? readStream(proc.stdout ?? null, {
|
|
401
|
+
timeoutMs: streamDrainTimeoutMs,
|
|
402
|
+
setTimeoutFn: setTimeoutImpl,
|
|
403
|
+
clearTimeoutFn: clearTimeoutImpl,
|
|
404
|
+
})
|
|
405
|
+
: Promise.resolve({ text: "", timedOut: false });
|
|
313
406
|
const stderrPromise = stdioMode === "captured"
|
|
314
|
-
? readStream(proc.stderr ?? null, {
|
|
315
|
-
|
|
407
|
+
? readStream(proc.stderr ?? null, {
|
|
408
|
+
timeoutMs: streamDrainTimeoutMs,
|
|
409
|
+
setTimeoutFn: setTimeoutImpl,
|
|
410
|
+
clearTimeoutFn: clearTimeoutImpl,
|
|
411
|
+
})
|
|
412
|
+
: Promise.resolve({ text: "", timedOut: false });
|
|
316
413
|
// Optional stdin payload (captured mode only).
|
|
317
414
|
//
|
|
318
415
|
// BUG-H1: race the stdin write/close against `proc.exited` and the
|
|
@@ -352,6 +449,12 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
352
449
|
// will not block indefinitely.
|
|
353
450
|
await Promise.allSettled([stdoutPromise, stderrPromise]);
|
|
354
451
|
const durationMs = Date.now() - start;
|
|
452
|
+
emitSpawnEvent("spawn_exit", {
|
|
453
|
+
profile: profile.name,
|
|
454
|
+
...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
|
|
455
|
+
exitCode: null,
|
|
456
|
+
status: "spawn_failed",
|
|
457
|
+
});
|
|
355
458
|
return {
|
|
356
459
|
ok: false,
|
|
357
460
|
exitCode: null,
|
|
@@ -364,7 +467,15 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
364
467
|
}
|
|
365
468
|
clearTimeoutImpl(timer);
|
|
366
469
|
abortSignal?.removeEventListener("abort", onAbort);
|
|
367
|
-
|
|
470
|
+
emitSpawnEvent("spawn_exit", {
|
|
471
|
+
profile: profile.name,
|
|
472
|
+
...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
|
|
473
|
+
exitCode,
|
|
474
|
+
status: aborted ? "aborted" : timedOut ? "timeout" : exitCode !== 0 ? "non_zero_exit" : "ok",
|
|
475
|
+
});
|
|
476
|
+
const [stdoutRead, stderrRead] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
477
|
+
const stdout = stdoutRead.text;
|
|
478
|
+
const stderr = stderrRead.text;
|
|
368
479
|
const durationMs = Date.now() - start;
|
|
369
480
|
if (aborted) {
|
|
370
481
|
return {
|
|
@@ -388,6 +499,18 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
388
499
|
error: `agent CLI "${profile.name}" timed out after ${timeoutMs ?? 0}ms`,
|
|
389
500
|
};
|
|
390
501
|
}
|
|
502
|
+
const captureFailure = streamFailureMessage(profile.name, stdoutRead, stderrRead);
|
|
503
|
+
if (captureFailure) {
|
|
504
|
+
return {
|
|
505
|
+
ok: false,
|
|
506
|
+
exitCode,
|
|
507
|
+
stdout,
|
|
508
|
+
stderr,
|
|
509
|
+
durationMs,
|
|
510
|
+
reason: "spawn_failed",
|
|
511
|
+
error: captureFailure,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
391
514
|
if (exitCode !== 0) {
|
|
392
515
|
return {
|
|
393
516
|
ok: false,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Aider CLI agent command builder (P2, plan §"The adapter contract" step 2 /
|
|
6
|
+
* §"Capability matrix").
|
|
7
|
+
*
|
|
8
|
+
* Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
|
|
9
|
+
* headless argv the `aider` CLI expects. Per the capability matrix the
|
|
10
|
+
* headless invocation is:
|
|
11
|
+
*
|
|
12
|
+
* aider -m "<p>" --yes-always
|
|
13
|
+
*
|
|
14
|
+
* with `--model <m>` for model selection, NO structured-output mode (Aider is
|
|
15
|
+
* the "none" tier — prompt-injected schema + embedded-JSON extraction), and
|
|
16
|
+
* NO session flag (resume is chat-history files, not an id).
|
|
17
|
+
*
|
|
18
|
+
* Platform-specific mapping decisions (all localized here, per the adapter
|
|
19
|
+
* contract):
|
|
20
|
+
*
|
|
21
|
+
* - **prompt** — `--message` is Aider's one-shot non-interactive mode (the
|
|
22
|
+
* matrix's `-m`; the long form is used for self-documenting argv). Unlike
|
|
23
|
+
* claude/codex/pi there is NO positional prompt and NO `--` end-of-options
|
|
24
|
+
* separator to hide behind: the prompt is a *flag value*. The glued
|
|
25
|
+
* `--message=<payload>` form is therefore used so a payload that begins
|
|
26
|
+
* with `-`/`--` binds to the flag lexically and can never be parsed as a
|
|
27
|
+
* separate option by Aider's argument parser.
|
|
28
|
+
* - **--yes-always** — always emitted: auto-confirms every interactive
|
|
29
|
+
* prompt (create file? run command? …), which is what makes a captured,
|
|
30
|
+
* unattended run possible. This is the matrix's documented headless shape.
|
|
31
|
+
* - **--no-pretty** — always emitted: disables colored/pretty terminal
|
|
32
|
+
* rendering so captured stdout is clean text for `./result-extractor.ts`
|
|
33
|
+
* (the same "dispatch is the captured path" reasoning as the Claude
|
|
34
|
+
* builder's unconditional `--print` and Codex's unconditional `--json`).
|
|
35
|
+
* - **systemPrompt** — Aider has no system-prompt flag (its nearest concept
|
|
36
|
+
* is conventions files via `--read`, which take a path, not text). Folded
|
|
37
|
+
* into the message payload — system text first, blank line, then the task —
|
|
38
|
+
* mirroring the codex builder's treatment.
|
|
39
|
+
* - **schema** — the matrix places Aider in the "via prompt+validate" tier
|
|
40
|
+
* with *no* structured output mode at all (plan §"Structured-output
|
|
41
|
+
* normalization", tier "none"): there is no schema flag and no JSON output
|
|
42
|
+
* flag, so the JSON Schema is injected into the message payload using the
|
|
43
|
+
* exact directive wording of the engine's prompt assembly
|
|
44
|
+
* (`step-work.ts` `buildUnitPrompt`) and the pi builder, so all
|
|
45
|
+
* dispatch paths speak one dialect. Downstream, embedded-JSON extraction +
|
|
46
|
+
* the engine's shared retry-until-valid loop supply the validation Aider
|
|
47
|
+
* lacks. No temp schema file is written — that is Codex's native-schema
|
|
48
|
+
* mechanism (`--output-schema`), which Aider does not have.
|
|
49
|
+
* - **tools** — deliberately unconsumed. Aider has no per-tool allowlist
|
|
50
|
+
* flag; tool-ish behaviour is governed by its own switches (`--yes-always`,
|
|
51
|
+
* git integration, shell-command confirmation). A restrictive policy is
|
|
52
|
+
* therefore dropped rather than approximated — never silently widened.
|
|
53
|
+
* - **resume/session** — NOT expressible: Aider persists context in
|
|
54
|
+
* chat-history files (`.aider.chat.history.md`), not session ids, so there
|
|
55
|
+
* is no flag-shaped `HarnessResumeSupport` to export and the extractor
|
|
56
|
+
* never yields a `sessionId`. akm's `workflow_run_units` remains the
|
|
57
|
+
* durable source of truth; resume works even against a harness with no
|
|
58
|
+
* session model (plan §"Session, MCP, and identity across harnesses" —
|
|
59
|
+
* Aider is the plan's named example).
|
|
60
|
+
* - **effort** — stays unconsumed (reserved; the shared request contract's
|
|
61
|
+
* "no builder consumes it yet" note stays true).
|
|
62
|
+
*
|
|
63
|
+
* NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
|
|
64
|
+
* follow-up integration task (as is the registry entry declaring
|
|
65
|
+
* `structuredOutput: "none"` and no `resume`). Exported standalone so that
|
|
66
|
+
* task only adds a registry entry.
|
|
67
|
+
*/
|
|
68
|
+
import { assertNotFlag } from "../../agent/builder-shared.js";
|
|
69
|
+
import { resolveModel } from "../../agent/model-aliases.js";
|
|
70
|
+
/** Canonical harness/platform id used for model-alias resolution. */
|
|
71
|
+
export const AIDER_PLATFORM = "aider";
|
|
72
|
+
/**
|
|
73
|
+
* Assemble the `--message` payload: optional system text, the task prompt,
|
|
74
|
+
* and — when a schema is requested — the same schema directive the workflow
|
|
75
|
+
* engine's prompt assembly uses (Aider has no native structured output, so
|
|
76
|
+
* the prompt is the only channel; plan §"Structured-output normalization",
|
|
77
|
+
* tier "none").
|
|
78
|
+
*/
|
|
79
|
+
function buildMessagePayload(req) {
|
|
80
|
+
const sections = [];
|
|
81
|
+
if (req.systemPrompt)
|
|
82
|
+
sections.push(req.systemPrompt);
|
|
83
|
+
sections.push(req.prompt);
|
|
84
|
+
if (req.schema) {
|
|
85
|
+
sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
|
|
86
|
+
}
|
|
87
|
+
return sections.join("\n\n");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Aider builder.
|
|
91
|
+
* Command shape:
|
|
92
|
+
* aider [--model <m>] --yes-always --no-pretty --message=<[system\n\n]prompt[\n\nschema directive]>
|
|
93
|
+
*/
|
|
94
|
+
export const aiderBuilder = {
|
|
95
|
+
platform: AIDER_PLATFORM,
|
|
96
|
+
build(profile, req) {
|
|
97
|
+
assertNotFlag(req.systemPrompt, "systemPrompt");
|
|
98
|
+
assertNotFlag(req.model, "model");
|
|
99
|
+
const args = [...profile.args];
|
|
100
|
+
if (req.model) {
|
|
101
|
+
const resolved = resolveModel(req.model, AIDER_PLATFORM, profile.modelAliases, profile.globalModelAliases);
|
|
102
|
+
args.push("--model", resolved);
|
|
103
|
+
}
|
|
104
|
+
// Headless essentials (matrix shape): auto-confirm everything, and keep
|
|
105
|
+
// captured stdout free of pretty/ANSI rendering for the extractor.
|
|
106
|
+
args.push("--yes-always");
|
|
107
|
+
args.push("--no-pretty");
|
|
108
|
+
// Glued form: the payload is a flag VALUE (no positional prompt exists),
|
|
109
|
+
// so `=` binding keeps a dash-leading payload from parsing as an option.
|
|
110
|
+
args.push(`--message=${buildMessagePayload(req)}`);
|
|
111
|
+
return { argv: [profile.bin, ...args] };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
/**
|
|
5
|
+
* Aider CLI harness (P2 integration, plan §"The adapter contract").
|
|
6
|
+
*
|
|
7
|
+
* Per-harness barrel gathering the Aider integration surfaces:
|
|
8
|
+
* - agent command builder → ./agent-builder.ts (aiderBuilder)
|
|
9
|
+
* - result extractor → ./result-extractor.ts (aiderResultExtractor)
|
|
10
|
+
*
|
|
11
|
+
* It also defines {@link AiderHarness}, the {@link AkmHarness} descriptor that
|
|
12
|
+
* `HARNESS_REGISTRY` registers. Dispatch-only: no native session-log reader or
|
|
13
|
+
* config importer yet.
|
|
14
|
+
*/
|
|
15
|
+
import { BaseHarness } from "../types.js";
|
|
16
|
+
import { aiderBuilder } from "./agent-builder.js";
|
|
17
|
+
import { aiderResultExtractor } from "./result-extractor.js";
|
|
18
|
+
export { AIDER_PLATFORM, aiderBuilder } from "./agent-builder.js";
|
|
19
|
+
export { aiderResultExtractor } from "./result-extractor.js";
|
|
20
|
+
function caps(c) {
|
|
21
|
+
return {
|
|
22
|
+
sessionLogs: false,
|
|
23
|
+
agentDispatch: false,
|
|
24
|
+
detection: false,
|
|
25
|
+
configImport: false,
|
|
26
|
+
runtimeIdentity: false,
|
|
27
|
+
v1Migration: false,
|
|
28
|
+
...c,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Aider.
|
|
33
|
+
*
|
|
34
|
+
* Canonical id is `'aider'`; no alias or distinct runtime identity.
|
|
35
|
+
*/
|
|
36
|
+
export class AiderHarness extends BaseHarness {
|
|
37
|
+
id = "aider";
|
|
38
|
+
displayName = "Aider";
|
|
39
|
+
aliases = [];
|
|
40
|
+
agentBuilder = aiderBuilder;
|
|
41
|
+
resultExtractor = aiderResultExtractor;
|
|
42
|
+
// ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
|
|
43
|
+
// akm spawns the `aider` CLI locally per unit ⇒ local-runner.
|
|
44
|
+
pattern = "local-runner";
|
|
45
|
+
// No structured-output mode at all (the matrix's "none — parse output"):
|
|
46
|
+
// akm injects the schema into the prompt and extracts embedded JSON.
|
|
47
|
+
structuredOutput = "none";
|
|
48
|
+
// No `resume`: Aider persists context in chat-history files
|
|
49
|
+
// (`.aider.chat.history.md`), not session ids — the plan's named example of
|
|
50
|
+
// a harness with no session model. akm's `workflow_run_units` remains the
|
|
51
|
+
// durable resume source of truth.
|
|
52
|
+
// No `identityEnv`: the matrix lists Aider's identity markers as uncertain,
|
|
53
|
+
// and Aider stamps no session var onto child processes.
|
|
54
|
+
capabilities = caps({
|
|
55
|
+
agentDispatch: true,
|
|
56
|
+
detection: true,
|
|
57
|
+
});
|
|
58
|
+
}
|