akm-cli 0.7.5 → 0.8.0-rc2
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/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -13,5 +13,7 @@
|
|
|
13
13
|
export { DEFAULT_AGENT_TIMEOUT_MS, listAgentProfileNames, listResolvedAgentProfiles, parseAgentConfig, requireAgentProfile, resolveAgentProfile, resolveDefaultProfileName, resolveProfileFromConfig, } from "./config";
|
|
14
14
|
export { defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect";
|
|
15
15
|
export { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles";
|
|
16
|
-
export { buildProposePrompt, buildReflectPrompt, parseAgentProposalPayload, stripJsonFences } from "./prompts";
|
|
16
|
+
export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, parseAgentProposalPayload, stripJsonFences, } from "./prompts";
|
|
17
|
+
export { runWithAgentRunner, selectAgentRunner } from "./runners";
|
|
18
|
+
export { runAgentSdk } from "./sdk-runner";
|
|
17
19
|
export { runAgent } from "./spawn";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared proposal-agent pipeline for `akm propose` and `akm reflect`.
|
|
3
|
+
*
|
|
4
|
+
* Both commands share the same core spawn step: resolve a profile, build a
|
|
5
|
+
* prompt, run the agent, and return a structured result. This module extracts
|
|
6
|
+
* that shared step so the two command implementations stay focused on their
|
|
7
|
+
* own pre-processing (prompt building) and post-processing (proposal creation).
|
|
8
|
+
*/
|
|
9
|
+
import { runWithAgentRunner } from "./runners";
|
|
10
|
+
/**
|
|
11
|
+
* Run the agent for a proposal-producing command (propose or reflect).
|
|
12
|
+
*
|
|
13
|
+
* When `profile.sdkMode` is true, routes to {@link runAgentSdk} (no CLI
|
|
14
|
+
* binary required). Otherwise, when `draftFilePath` is set the agent is
|
|
15
|
+
* spawned in interactive mode so it can use its file tools to write the draft
|
|
16
|
+
* directly. When no draft path is provided the agent is spawned in captured
|
|
17
|
+
* mode and output is read from stdout.
|
|
18
|
+
*/
|
|
19
|
+
export async function runProposalAgentPipeline(opts) {
|
|
20
|
+
const result = await runWithAgentRunner({
|
|
21
|
+
profile: opts.profile,
|
|
22
|
+
prompt: opts.prompt,
|
|
23
|
+
llmConfig: opts.llmConfig,
|
|
24
|
+
runOptions: {
|
|
25
|
+
stdio: opts.draftFilePath ? "interactive" : "captured",
|
|
26
|
+
parseOutput: "text",
|
|
27
|
+
...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
ok: result.ok,
|
|
32
|
+
stdout: result.stdout ?? "",
|
|
33
|
+
stderr: result.stderr ?? "",
|
|
34
|
+
durationMs: result.durationMs,
|
|
35
|
+
exitCode: result.exitCode,
|
|
36
|
+
error: result.error,
|
|
37
|
+
reason: result.reason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -3,6 +3,8 @@ const COMMON_PASSTHROUGH = ["HOME", "PATH", "USER", "LANG", "LC_ALL", "TERM", "T
|
|
|
3
3
|
* Built-in profiles for the five agent CLIs the v1 spec calls out
|
|
4
4
|
* explicitly. The fields here are conservative defaults — every value is
|
|
5
5
|
* overridable from user config.
|
|
6
|
+
*
|
|
7
|
+
* For headless/automation use (propose, reflect, tasks), use the '-headless' variant.
|
|
6
8
|
*/
|
|
7
9
|
const BUILTINS = {
|
|
8
10
|
opencode: {
|
|
@@ -46,15 +48,75 @@ const BUILTINS = {
|
|
|
46
48
|
parseOutput: "text",
|
|
47
49
|
},
|
|
48
50
|
};
|
|
49
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Headless variants of the five base profiles for automation use (propose, reflect, tasks).
|
|
53
|
+
*
|
|
54
|
+
* These profiles use `stdio: "captured"` and `parseOutput: "json"` so the
|
|
55
|
+
* agent's response can be read from stdout. They share the same `bin` and
|
|
56
|
+
* `envPassthrough` as the corresponding base profile but are intentionally
|
|
57
|
+
* kept out of `BUILTIN_AGENT_PROFILE_NAMES` (and therefore out of CLI
|
|
58
|
+
* detection/enumeration) to avoid showing up as separate installable profiles.
|
|
59
|
+
*
|
|
60
|
+
* Users may reference them by name via `--profile opencode-headless` or by
|
|
61
|
+
* setting `agent.default: "opencode-headless"` in config.json.
|
|
62
|
+
*/
|
|
63
|
+
const HEADLESS_BUILTINS = {
|
|
64
|
+
"opencode-headless": {
|
|
65
|
+
name: "opencode-headless",
|
|
66
|
+
bin: "opencode",
|
|
67
|
+
args: ["run"],
|
|
68
|
+
stdio: "captured",
|
|
69
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "OPENCODE_API_KEY", "OPENCODE_CONFIG"],
|
|
70
|
+
parseOutput: "json",
|
|
71
|
+
},
|
|
72
|
+
"claude-headless": {
|
|
73
|
+
name: "claude-headless",
|
|
74
|
+
bin: "claude",
|
|
75
|
+
args: [],
|
|
76
|
+
stdio: "captured",
|
|
77
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "ANTHROPIC_API_KEY", "CLAUDE_CONFIG"],
|
|
78
|
+
parseOutput: "json",
|
|
79
|
+
},
|
|
80
|
+
"codex-headless": {
|
|
81
|
+
name: "codex-headless",
|
|
82
|
+
bin: "codex",
|
|
83
|
+
args: [],
|
|
84
|
+
stdio: "captured",
|
|
85
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "OPENAI_API_KEY", "CODEX_CONFIG"],
|
|
86
|
+
parseOutput: "json",
|
|
87
|
+
},
|
|
88
|
+
"gemini-headless": {
|
|
89
|
+
name: "gemini-headless",
|
|
90
|
+
bin: "gemini",
|
|
91
|
+
args: [],
|
|
92
|
+
stdio: "captured",
|
|
93
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
94
|
+
parseOutput: "json",
|
|
95
|
+
},
|
|
96
|
+
"aider-headless": {
|
|
97
|
+
name: "aider-headless",
|
|
98
|
+
bin: "aider",
|
|
99
|
+
args: ["--no-auto-commits"],
|
|
100
|
+
stdio: "captured",
|
|
101
|
+
envPassthrough: [...COMMON_PASSTHROUGH, "OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
102
|
+
parseOutput: "json",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Names of the five primary built-in profiles. Stable, sorted. Does NOT
|
|
107
|
+
* include the `-headless` variants (those are resolvable by name but are
|
|
108
|
+
* excluded from detection/enumeration flows).
|
|
109
|
+
*/
|
|
50
110
|
export const BUILTIN_AGENT_PROFILE_NAMES = Object.freeze(Object.keys(BUILTINS).sort());
|
|
51
|
-
/** Returns the built-in profile by name, or `undefined` if not
|
|
111
|
+
/** Returns the built-in profile by name (including headless variants), or `undefined` if not found. */
|
|
52
112
|
export function getBuiltinAgentProfile(name) {
|
|
53
|
-
return BUILTINS[name];
|
|
113
|
+
return BUILTINS[name] ?? HEADLESS_BUILTINS[name];
|
|
54
114
|
}
|
|
55
115
|
/**
|
|
56
|
-
* Return a deep copy of every built-in profile keyed by name.
|
|
57
|
-
*
|
|
116
|
+
* Return a deep copy of every primary built-in profile keyed by name.
|
|
117
|
+
* Headless variants are NOT included — use `getBuiltinAgentProfile(name)`
|
|
118
|
+
* to look them up by name. Callers should not assume reference equality with
|
|
119
|
+
* subsequent calls.
|
|
58
120
|
*/
|
|
59
121
|
export function listBuiltinAgentProfiles() {
|
|
60
122
|
const out = {};
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
* during validation. We carry it through if the agent supplies it.
|
|
24
24
|
*/
|
|
25
25
|
import { TYPE_DIRS } from "../../core/asset-spec";
|
|
26
|
+
import { parseEmbeddedJsonResponse, stripCodeFences, stripThinkBlocks } from "../../core/parse";
|
|
26
27
|
/**
|
|
27
28
|
* Per-asset-type frontmatter / authoring hints surfaced in the prompt so
|
|
28
29
|
* the agent can produce content that passes proposal validation. Kept tiny:
|
|
29
30
|
* full schema docs live in `docs/` — these are nudges, not contracts.
|
|
30
31
|
*/
|
|
31
32
|
const TYPE_HINTS = {
|
|
32
|
-
lesson: "lesson assets MUST start with frontmatter containing `description` and `when_to_use` keys (both non-empty). Body
|
|
33
|
+
lesson: "lesson assets MUST start with frontmatter containing `description` and `when_to_use` keys (both non-empty). Body: 1–3 short paragraphs of practical guidance. A lesson is NOT a restatement of the source asset — it answers: When should I reach for this? What goes wrong without it? What did real use reveal that the asset itself doesn't say?",
|
|
33
34
|
skill: "skill assets are stored as `skills/<name>/SKILL.md`. Frontmatter typically includes `name`, `description`, and `when_to_use`.",
|
|
34
35
|
command: "command assets are markdown with optional frontmatter (`name`, `description`). The body is the prompt template the user invokes.",
|
|
35
36
|
agent: "agent assets are markdown with frontmatter describing the agent role (`name`, `description`, optional `tools`, `model`).",
|
|
@@ -80,7 +81,15 @@ function fileWriteContract(draftFilePath) {
|
|
|
80
81
|
export function buildReflectPrompt(input) {
|
|
81
82
|
const sections = [];
|
|
82
83
|
if (input.ref && input.type && input.name) {
|
|
83
|
-
|
|
84
|
+
// Change 2 — type-conditioned goal framing
|
|
85
|
+
const isLesson = input.type === "lesson";
|
|
86
|
+
const isSkill = input.type === "skill";
|
|
87
|
+
const goalSentence = isLesson
|
|
88
|
+
? `Your task is to distill what usage signals reveal about this ${input.type} asset — when to reach for it, what goes wrong without it, and what real use has revealed that the asset itself does not say. Do not reproduce the source content; your proposal must add information the source does not contain.`
|
|
89
|
+
: isSkill
|
|
90
|
+
? "Your task is to review this skill asset, identify what the feedback and related distilled lessons show is broken, missing, unclear, or durable enough to promote into long-term documentation, and produce a single improved proposal. If the strongest evidence points to companion reference material rather than the main SKILL.md, you may instead propose a skill-adjacent knowledge doc such as `knowledge:skills/<skill>/references/<topic>`."
|
|
91
|
+
: `Your task is to review this ${input.type} asset, identify what the feedback signals as broken, missing, or unclear, and produce an improved version. Do not reproduce the source content unchanged; your proposal must correct or add something the source lacks.`;
|
|
92
|
+
sections.push(goalSentence);
|
|
84
93
|
sections.push(`Target ref: ${input.ref}`);
|
|
85
94
|
sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
|
|
86
95
|
}
|
|
@@ -92,6 +101,23 @@ export function buildReflectPrompt(input) {
|
|
|
92
101
|
if (input.task?.trim()) {
|
|
93
102
|
sections.push(`Task / focus: ${input.task.trim()}`);
|
|
94
103
|
}
|
|
104
|
+
// Change 3 & 4 — feedback moved before asset content; missing else branch added
|
|
105
|
+
if (input.feedback && input.feedback.length > 0) {
|
|
106
|
+
sections.push("Recent feedback / signals:");
|
|
107
|
+
for (const line of input.feedback)
|
|
108
|
+
sections.push(`- ${line}`);
|
|
109
|
+
}
|
|
110
|
+
else if (!input.ref) {
|
|
111
|
+
sections.push("Recent feedback / signals:");
|
|
112
|
+
sections.push("- (no feedback events recorded)");
|
|
113
|
+
}
|
|
114
|
+
else if (input.type === "skill" && input.relatedLessons && input.relatedLessons.length > 0) {
|
|
115
|
+
sections.push("No direct feedback events were recorded. Limit substantive changes to what is justified by the related distilled lessons below; do not speculate beyond that evidence.");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// ref is set but no feedback — explicitly constrain scope to schema compliance
|
|
119
|
+
sections.push("No usage feedback recorded. Limit your proposal to schema and structural improvements only: missing required frontmatter fields, unclear `when_to_use`, ambiguous description, or broken formatting. Do not speculate about runtime weaknesses you have not observed.");
|
|
120
|
+
}
|
|
95
121
|
if (input.assetContent?.trim()) {
|
|
96
122
|
sections.push("Current asset content (verbatim):");
|
|
97
123
|
sections.push("```");
|
|
@@ -104,21 +130,27 @@ export function buildReflectPrompt(input) {
|
|
|
104
130
|
else {
|
|
105
131
|
sections.push("(No existing asset content was supplied.)");
|
|
106
132
|
}
|
|
107
|
-
if (input.feedback && input.feedback.length > 0) {
|
|
108
|
-
sections.push("Recent feedback / signals:");
|
|
109
|
-
for (const line of input.feedback)
|
|
110
|
-
sections.push(`- ${line}`);
|
|
111
|
-
}
|
|
112
|
-
else if (!input.ref) {
|
|
113
|
-
sections.push("Recent feedback / signals:");
|
|
114
|
-
sections.push("- (no feedback events recorded)");
|
|
115
|
-
}
|
|
116
133
|
if (input.schemaHints && input.schemaHints.length > 0) {
|
|
117
134
|
sections.push("Schema / lint hints to address:");
|
|
118
135
|
for (const line of input.schemaHints)
|
|
119
136
|
sections.push(`- ${line}`);
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
if (input.relatedLessons && input.relatedLessons.length > 0) {
|
|
139
|
+
sections.push("Related distilled lessons to evaluate for consolidation:");
|
|
140
|
+
for (const lesson of input.relatedLessons) {
|
|
141
|
+
sections.push(`Lesson ref: ${lesson.ref}`);
|
|
142
|
+
sections.push("```");
|
|
143
|
+
sections.push(lesson.content.trimEnd());
|
|
144
|
+
sections.push("```");
|
|
145
|
+
}
|
|
146
|
+
sections.push("Evaluate whether these lessons contain strong evidence of factual, repeatable guidance that should be promoted into long-term skill documentation.");
|
|
147
|
+
sections.push("Promote only guidance that is durable, generally applicable, and supported by repeated evidence. Do not copy anecdotal details, one-off incidents, or duplicate wording verbatim.");
|
|
148
|
+
sections.push("If the guidance belongs in the main skill instructions, update the skill proposal. If it belongs in a companion reference document, return a `knowledge:skills/<skill>/references/<topic>` proposal instead.");
|
|
149
|
+
}
|
|
150
|
+
if (input.avoidPatterns && input.avoidPatterns.length > 0) {
|
|
151
|
+
sections.push(`## Avoid These Patterns\nPrevious assets in this run produced these errors — do not repeat them:\n${input.avoidPatterns.map((e) => `- ${e}`).join("\n")}`);
|
|
152
|
+
}
|
|
153
|
+
sections.push("Produce a single proposal that addresses the feedback and respects the asset-type contract. If the proposal's frontmatter is missing `when_to_use`, you MUST generate one — a one-line trigger sentence describing exactly when a user should reach for this asset.");
|
|
122
154
|
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
123
155
|
return sections.join("\n\n");
|
|
124
156
|
}
|
|
@@ -141,6 +173,33 @@ export function buildProposePrompt(input) {
|
|
|
141
173
|
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
142
174
|
return sections.join("\n\n");
|
|
143
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Build the prompt for the schema repair pass in `akm improve`. Asks the
|
|
178
|
+
* agent to add the minimal required frontmatter to an asset that failed
|
|
179
|
+
* validation — without rewriting the body.
|
|
180
|
+
*/
|
|
181
|
+
export function buildSchemaRepairPrompt(input) {
|
|
182
|
+
const sections = [];
|
|
183
|
+
sections.push(`This ${input.type} asset failed schema validation with the error: "${input.reason}". ` +
|
|
184
|
+
`Your task is to fix the schema issue by adding or correcting the missing/invalid field(s) ` +
|
|
185
|
+
`while preserving all existing content.`);
|
|
186
|
+
sections.push(`Target ref: ${input.ref}`);
|
|
187
|
+
sections.push(`Schema requirements for ${input.type} assets: ${hintForType(input.type)}`);
|
|
188
|
+
const CONTENT_CAP = 3000;
|
|
189
|
+
const body = input.assetContent.trimEnd();
|
|
190
|
+
const truncated = body.length > CONTENT_CAP;
|
|
191
|
+
sections.push("Current asset content (first 3000 chars — sufficient to generate missing frontmatter):");
|
|
192
|
+
sections.push("```");
|
|
193
|
+
sections.push(truncated ? `${body.slice(0, CONTENT_CAP)}\n... [truncated]` : body);
|
|
194
|
+
sections.push("```");
|
|
195
|
+
sections.push("Produce the minimal fix: add ONLY the missing required frontmatter field(s). " +
|
|
196
|
+
"Do not rewrite the body unless it is empty. " +
|
|
197
|
+
"If `description` is missing, generate a concise one-sentence description from the content. " +
|
|
198
|
+
"If `when_to_use` is missing, generate a one-line trigger sentence. " +
|
|
199
|
+
"Preserve all existing frontmatter keys and the full body verbatim.");
|
|
200
|
+
sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
|
|
201
|
+
return sections.join("\n\n");
|
|
202
|
+
}
|
|
144
203
|
/**
|
|
145
204
|
* Parse agent stdout into a proposal payload. The agent contract requires a
|
|
146
205
|
* single JSON object; anything else is reported as a parse error so callers
|
|
@@ -151,7 +210,8 @@ export function buildProposePrompt(input) {
|
|
|
151
210
|
* 2. Prose preamble / postamble around the JSON object (handled by `extractEmbeddedJson`).
|
|
152
211
|
*/
|
|
153
212
|
export function parseAgentProposalPayload(stdout) {
|
|
154
|
-
|
|
213
|
+
// Strip <think> blocks and fences, then attempt full parse with embedded fallback.
|
|
214
|
+
const trimmed = stripCodeFences(stripThinkBlocks(stdout)).trim();
|
|
155
215
|
if (!trimmed)
|
|
156
216
|
throw new Error("agent produced empty output");
|
|
157
217
|
let parsed;
|
|
@@ -162,7 +222,7 @@ export function parseAgentProposalPayload(stdout) {
|
|
|
162
222
|
// Agent output contains prose before/after the JSON object (e.g. a local
|
|
163
223
|
// LLM that narrates before responding). Try extracting the first balanced
|
|
164
224
|
// top-level `{…}` from the text rather than failing immediately.
|
|
165
|
-
const embedded =
|
|
225
|
+
const embedded = parseEmbeddedJsonResponse(trimmed);
|
|
166
226
|
if (!embedded)
|
|
167
227
|
throw directErr;
|
|
168
228
|
parsed = embedded;
|
|
@@ -182,66 +242,11 @@ export function parseAgentProposalPayload(stdout) {
|
|
|
182
242
|
}
|
|
183
243
|
return out;
|
|
184
244
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Extract the first balanced top-level `{…}` object from `text`. Used as a
|
|
187
|
-
* fallback when direct `JSON.parse` fails due to surrounding prose. Kept
|
|
188
|
-
* local to `agent/` (mirrors `parseEmbeddedJsonResponse` in `src/llm/client.ts`
|
|
189
|
-
* without importing across the one-way boundary — v1 spec §9.7).
|
|
190
|
-
*/
|
|
191
|
-
function extractEmbeddedJson(text) {
|
|
192
|
-
for (let start = 0; start < text.length; start++) {
|
|
193
|
-
if (text[start] !== "{")
|
|
194
|
-
continue;
|
|
195
|
-
let depth = 0;
|
|
196
|
-
let inString = false;
|
|
197
|
-
let escaped = false;
|
|
198
|
-
for (let i = start; i < text.length; i++) {
|
|
199
|
-
const ch = text[i];
|
|
200
|
-
if (inString) {
|
|
201
|
-
if (escaped) {
|
|
202
|
-
escaped = false;
|
|
203
|
-
}
|
|
204
|
-
else if (ch === "\\") {
|
|
205
|
-
escaped = true;
|
|
206
|
-
}
|
|
207
|
-
else if (ch === '"') {
|
|
208
|
-
inString = false;
|
|
209
|
-
}
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (ch === '"') {
|
|
213
|
-
inString = true;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
if (ch === "{")
|
|
217
|
-
depth++;
|
|
218
|
-
if (ch === "}") {
|
|
219
|
-
depth--;
|
|
220
|
-
if (depth === 0) {
|
|
221
|
-
try {
|
|
222
|
-
return JSON.parse(text.slice(start, i + 1));
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
break;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
245
|
/**
|
|
234
246
|
* Strip `\`\`\`json … \`\`\`` fences and `<think>…</think>` reasoning blocks
|
|
235
|
-
* from agent output.
|
|
236
|
-
*
|
|
247
|
+
* from agent output. Thin wrapper around `core/parse` helpers, kept exported
|
|
248
|
+
* for backward compatibility (re-exported from `integrations/agent/index.ts`).
|
|
237
249
|
*/
|
|
238
250
|
export function stripJsonFences(text) {
|
|
239
|
-
|
|
240
|
-
.trim()
|
|
241
|
-
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
242
|
-
.trim();
|
|
243
|
-
const fenced = stripped.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
|
|
244
|
-
if (fenced)
|
|
245
|
-
return fenced[1] ?? stripped;
|
|
246
|
-
return stripped;
|
|
251
|
+
return stripCodeFences(stripThinkBlocks(text));
|
|
247
252
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { runAgentSdk } from "./sdk-runner";
|
|
2
|
+
import { runAgent } from "./spawn";
|
|
3
|
+
const spawnAgentRunner = {
|
|
4
|
+
name: "spawn-agent-runner",
|
|
5
|
+
supports(profile) {
|
|
6
|
+
return profile.sdkMode !== true;
|
|
7
|
+
},
|
|
8
|
+
run(request) {
|
|
9
|
+
return runAgent(request.profile, request.prompt, request.runOptions ?? {});
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
const sdkAgentRunner = {
|
|
13
|
+
name: "sdk-agent-runner",
|
|
14
|
+
supports(profile) {
|
|
15
|
+
return profile.sdkMode === true;
|
|
16
|
+
},
|
|
17
|
+
run(request) {
|
|
18
|
+
return runAgentSdk(request.profile, request.prompt ?? "", request.runOptions ?? {}, request.llmConfig);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export const defaultAgentRunners = [spawnAgentRunner, sdkAgentRunner];
|
|
22
|
+
export function selectAgentRunner(profile, runners = defaultAgentRunners) {
|
|
23
|
+
const runner = runners.find((candidate) => candidate.supports(profile));
|
|
24
|
+
if (!runner) {
|
|
25
|
+
throw new Error(`No agent runner available for profile "${profile.name}".`);
|
|
26
|
+
}
|
|
27
|
+
return runner;
|
|
28
|
+
}
|
|
29
|
+
export async function runWithAgentRunner(request, runners = defaultAgentRunners) {
|
|
30
|
+
return selectAgentRunner(request.profile, runners).run(request);
|
|
31
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode SDK agent runner — uses embedded @opencode-ai/sdk instead of
|
|
3
|
+
* Bun.spawn. Requires no agent CLI binary to be installed. The user provides
|
|
4
|
+
* an OpenAI-compatible endpoint (or inherits from config.llm) for the SDK.
|
|
5
|
+
*/
|
|
6
|
+
// Singleton server — started once per process, reused across calls
|
|
7
|
+
let _server = null;
|
|
8
|
+
/**
|
|
9
|
+
* Close the singleton OpenCode SDK server and reset the handle.
|
|
10
|
+
* Primarily for use in tests to ensure clean teardown between test runs.
|
|
11
|
+
*/
|
|
12
|
+
export function closeServer() {
|
|
13
|
+
try {
|
|
14
|
+
_server?.server.close();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* ignore */
|
|
18
|
+
}
|
|
19
|
+
_server = null;
|
|
20
|
+
}
|
|
21
|
+
async function getOrStartServer(profile, llmConfig) {
|
|
22
|
+
if (_server)
|
|
23
|
+
return _server;
|
|
24
|
+
const { createOpencode } = await import("@opencode-ai/sdk").catch(() => {
|
|
25
|
+
throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
|
|
26
|
+
});
|
|
27
|
+
// Resolve endpoint and model: profile fields take precedence over config.llm
|
|
28
|
+
const endpoint = profile.endpoint ?? llmConfig?.endpoint;
|
|
29
|
+
const apiKey = profile.apiKey ?? llmConfig?.apiKey;
|
|
30
|
+
const model = profile.model;
|
|
31
|
+
const sdkConfig = {};
|
|
32
|
+
if (model)
|
|
33
|
+
sdkConfig.model = model;
|
|
34
|
+
if (endpoint || apiKey) {
|
|
35
|
+
// Configure a custom OpenAI-compatible provider
|
|
36
|
+
sdkConfig.provider = {
|
|
37
|
+
"akm-custom": {
|
|
38
|
+
npm: "@ai-sdk/openai-compatible",
|
|
39
|
+
options: {
|
|
40
|
+
baseURL: endpoint?.replace(/\/chat\/completions$/, "").replace(/\/$/, ""),
|
|
41
|
+
...(apiKey ? { apiKey } : {}),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
// Use the custom provider's model if not already qualified
|
|
46
|
+
if (model && !model.includes("/")) {
|
|
47
|
+
sdkConfig.model = `akm-custom/${model}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
_server = (await createOpencode(Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {}));
|
|
51
|
+
process.once("exit", () => {
|
|
52
|
+
closeServer();
|
|
53
|
+
});
|
|
54
|
+
if (!_server)
|
|
55
|
+
throw new Error("Failed to initialise OpenCode SDK server.");
|
|
56
|
+
return _server;
|
|
57
|
+
}
|
|
58
|
+
export async function runAgentSdk(profile, prompt, _opts = {}, llmConfig) {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
let client;
|
|
61
|
+
try {
|
|
62
|
+
({ client } = await getOrStartServer(profile, llmConfig));
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: String(e),
|
|
69
|
+
durationMs: Date.now() - start,
|
|
70
|
+
exitCode: 1,
|
|
71
|
+
reason: "spawn_failed",
|
|
72
|
+
error: String(e),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// One session per call — do NOT reuse (history accumulates, token costs grow)
|
|
76
|
+
const sessionRes = await client.session.create({ body: { title: "akm" } });
|
|
77
|
+
const sessionId = sessionRes.data?.id;
|
|
78
|
+
if (!sessionId) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: "Failed to create session",
|
|
83
|
+
durationMs: Date.now() - start,
|
|
84
|
+
exitCode: 1,
|
|
85
|
+
reason: "spawn_failed",
|
|
86
|
+
error: "Failed to create OpenCode session",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const result = await client.session.prompt({
|
|
91
|
+
path: { id: sessionId },
|
|
92
|
+
body: { parts: [{ type: "text", text: prompt }] },
|
|
93
|
+
});
|
|
94
|
+
const parts = result.data?.parts ?? [];
|
|
95
|
+
const textPart = parts.find((p) => p.type === "text");
|
|
96
|
+
const stdout = textPart?.text ?? "";
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
stdout,
|
|
100
|
+
stderr: "",
|
|
101
|
+
durationMs: Date.now() - start,
|
|
102
|
+
exitCode: 0,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
stdout: "",
|
|
109
|
+
stderr: String(e),
|
|
110
|
+
durationMs: Date.now() - start,
|
|
111
|
+
exitCode: 1,
|
|
112
|
+
reason: "non_zero_exit",
|
|
113
|
+
error: String(e),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
// Clean up session to prevent disk accumulation in ~/.local/share/opencode/
|
|
118
|
+
await client.session.delete({ path: { id: sessionId } }).catch(() => { });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
* NEVER imports an LLM SDK. Agents are reachable only via shell-out;
|
|
12
12
|
* this is a pre-emptive guarantee against the #222 invariant.
|
|
13
13
|
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
14
17
|
import { DEFAULT_AGENT_TIMEOUT_MS } from "./config";
|
|
15
18
|
/**
|
|
16
19
|
* Kill the process group of `proc` with `signal`, falling back to
|
|
@@ -39,6 +42,39 @@ export function killGroup(proc, signal) {
|
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
const DEFAULT_TIMEOUT_MS = DEFAULT_AGENT_TIMEOUT_MS;
|
|
45
|
+
/**
|
|
46
|
+
* Supplement `existingPath` with well-known user binary directories when
|
|
47
|
+
* running in a scheduler context (cron/launchd) where PATH is stripped.
|
|
48
|
+
*
|
|
49
|
+
* Detection heuristic: if the current PATH does not contain the user's home
|
|
50
|
+
* directory, we are likely in a stripped scheduler env. In an interactive
|
|
51
|
+
* shell the user's home almost always appears (e.g. ~/.bun/bin, ~/.cargo/bin).
|
|
52
|
+
*
|
|
53
|
+
* Only directories that actually exist on disk are prepended, and only if
|
|
54
|
+
* they are not already present, so interactive-shell PATH ordering is never
|
|
55
|
+
* disturbed.
|
|
56
|
+
*/
|
|
57
|
+
export function supplementPathForSchedulerContext(existingPath) {
|
|
58
|
+
const home = os.homedir();
|
|
59
|
+
// If PATH already contains the home directory, we are in an interactive
|
|
60
|
+
// shell — skip supplementation entirely.
|
|
61
|
+
if (existingPath.split(path.delimiter).some((d) => d.startsWith(home))) {
|
|
62
|
+
return existingPath;
|
|
63
|
+
}
|
|
64
|
+
const candidates = [
|
|
65
|
+
path.join(home, ".bun", "bin"),
|
|
66
|
+
path.join(home, ".cargo", "bin"),
|
|
67
|
+
path.join(home, ".local", "bin"),
|
|
68
|
+
"/opt/homebrew/bin",
|
|
69
|
+
"/opt/homebrew/sbin",
|
|
70
|
+
"/usr/local/bin",
|
|
71
|
+
];
|
|
72
|
+
const existing = new Set(existingPath.split(path.delimiter).filter(Boolean));
|
|
73
|
+
const toAdd = candidates.filter((d) => !existing.has(d) && fs.existsSync(d));
|
|
74
|
+
if (toAdd.length === 0)
|
|
75
|
+
return existingPath;
|
|
76
|
+
return [...toAdd, existingPath].filter(Boolean).join(path.delimiter);
|
|
77
|
+
}
|
|
42
78
|
function resolveSpawnFn(options) {
|
|
43
79
|
if (options.spawn)
|
|
44
80
|
return options.spawn;
|
|
@@ -54,6 +90,10 @@ function resolveSpawnFn(options) {
|
|
|
54
90
|
* • Every name in `profile.envPassthrough`.
|
|
55
91
|
* • Every entry in `profile.env`.
|
|
56
92
|
* • Every entry in `options.env` (highest precedence).
|
|
93
|
+
*
|
|
94
|
+
* PATH is supplemented with well-known user binary directories when running
|
|
95
|
+
* in a scheduler context (cron/launchd) where the inherited PATH is stripped.
|
|
96
|
+
* See {@link supplementPathForSchedulerContext}.
|
|
57
97
|
*/
|
|
58
98
|
function buildChildEnv(profile, options) {
|
|
59
99
|
const source = options.envSource ?? process.env;
|
|
@@ -63,6 +103,11 @@ function buildChildEnv(profile, options) {
|
|
|
63
103
|
if (value !== undefined)
|
|
64
104
|
env[name] = value;
|
|
65
105
|
}
|
|
106
|
+
// Supplement PATH after passthrough so the scheduler-context fix applies to
|
|
107
|
+
// the value actually coming from the environment source.
|
|
108
|
+
if (env.PATH !== undefined) {
|
|
109
|
+
env.PATH = supplementPathForSchedulerContext(env.PATH);
|
|
110
|
+
}
|
|
66
111
|
if (profile.env) {
|
|
67
112
|
for (const [k, v] of Object.entries(profile.env))
|
|
68
113
|
env[k] = v;
|
|
@@ -109,7 +154,8 @@ async function readStream(stream, opts) {
|
|
|
109
154
|
*/
|
|
110
155
|
export async function runAgent(profile, prompt, options = {}) {
|
|
111
156
|
const stdioMode = options.stdio ?? profile.stdio;
|
|
112
|
-
|
|
157
|
+
// null = explicitly disabled (no kill timer). undefined = inherit from profile/default.
|
|
158
|
+
const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : (profile.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
113
159
|
const parseOutput = options.parseOutput ?? profile.parseOutput;
|
|
114
160
|
const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
|
|
115
161
|
const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
|
|
@@ -153,26 +199,34 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
153
199
|
// BUG-M3: only flag `timedOut` when the child has not already exited. A
|
|
154
200
|
// timer firing in the same microtask as `proc.exited` resolving could
|
|
155
201
|
// otherwise label a clean exit as a timeout.
|
|
202
|
+
//
|
|
203
|
+
// When timeoutMs is null the kill timer is skipped entirely — the task runs
|
|
204
|
+
// until the process exits naturally. Intended for long-running local-model
|
|
205
|
+
// tasks where wall-clock time is unpredictable.
|
|
156
206
|
let timedOut = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
timedOut = true;
|
|
161
|
-
killGroup(proc, "SIGTERM");
|
|
162
|
-
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
163
|
-
setTimeoutImpl(() => {
|
|
207
|
+
let timer;
|
|
208
|
+
if (timeoutMs !== null) {
|
|
209
|
+
timer = setTimeoutImpl(() => {
|
|
164
210
|
if (proc.exitCode !== null)
|
|
165
211
|
return;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
212
|
+
timedOut = true;
|
|
213
|
+
killGroup(proc, "SIGTERM");
|
|
214
|
+
// Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
|
|
215
|
+
setTimeoutImpl(() => {
|
|
216
|
+
if (proc.exitCode !== null)
|
|
217
|
+
return;
|
|
218
|
+
killGroup(proc, "SIGKILL");
|
|
219
|
+
}, 5000);
|
|
220
|
+
}, timeoutMs);
|
|
221
|
+
}
|
|
169
222
|
// Stream-drain timeout: the overall wall-clock budget plus a 2 s grace
|
|
170
223
|
// period. When a process is killed via SIGTERM/SIGKILL (from our timeout
|
|
171
224
|
// handler or from outside) some runtimes keep the pipe write-end open in
|
|
172
225
|
// background threads, which would cause `Response.text()` to block forever.
|
|
173
|
-
// Capping stream draining
|
|
174
|
-
//
|
|
175
|
-
|
|
226
|
+
// Capping stream draining ensures the caller never hangs past the wall
|
|
227
|
+
// budget regardless of subprocess pipe behaviour.
|
|
228
|
+
// When there is no kill timer, allow up to 30 s for streams to drain.
|
|
229
|
+
const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
|
|
176
230
|
const stdoutPromise = stdioMode === "captured"
|
|
177
231
|
? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
|
|
178
232
|
: Promise.resolve("");
|
|
@@ -209,7 +263,8 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
209
263
|
exitCode = await proc.exited;
|
|
210
264
|
}
|
|
211
265
|
catch (err) {
|
|
212
|
-
|
|
266
|
+
if (timer !== undefined)
|
|
267
|
+
clearTimeoutImpl(timer);
|
|
213
268
|
// BUG-H2: drain stream readers before the early return so they don't
|
|
214
269
|
// surface as unhandled rejections after the function resolves.
|
|
215
270
|
// The streams already carry a built-in drain timeout so this allSettled
|
|
@@ -237,7 +292,7 @@ export async function runAgent(profile, prompt, options = {}) {
|
|
|
237
292
|
stderr,
|
|
238
293
|
durationMs,
|
|
239
294
|
reason: "timeout",
|
|
240
|
-
error: `agent CLI "${profile.name}" timed out after ${timeoutMs}ms`,
|
|
295
|
+
error: `agent CLI "${profile.name}" timed out after ${timeoutMs ?? 0}ms`,
|
|
241
296
|
};
|
|
242
297
|
}
|
|
243
298
|
if (exitCode !== 0) {
|