akm-cli 0.7.4 → 0.8.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/{CHANGELOG.md → .github/CHANGELOG.md} +34 -1
  2. package/.github/LICENSE +374 -0
  3. package/dist/cli/parse-args.js +86 -0
  4. package/dist/cli.js +1223 -650
  5. package/dist/commands/agent-dispatch.js +107 -0
  6. package/dist/commands/agent-support.js +62 -0
  7. package/dist/commands/config-cli.js +68 -84
  8. package/dist/commands/consolidate.js +812 -0
  9. package/dist/commands/curate.js +1 -0
  10. package/dist/commands/distill-promotion-policy.js +658 -0
  11. package/dist/commands/distill.js +224 -39
  12. package/dist/commands/eval-cases.js +40 -0
  13. package/dist/commands/events.js +12 -24
  14. package/dist/commands/graph.js +222 -0
  15. package/dist/commands/health.js +376 -0
  16. package/dist/commands/help/help-accept.md +9 -0
  17. package/dist/commands/help/help-improve.md +53 -0
  18. package/dist/commands/help/help-proposals.md +15 -0
  19. package/dist/commands/help/help-propose.md +17 -0
  20. package/dist/commands/help/help-reject.md +8 -0
  21. package/dist/commands/history.js +3 -30
  22. package/dist/commands/improve.js +1161 -0
  23. package/dist/commands/info.js +2 -2
  24. package/dist/commands/init.js +2 -2
  25. package/dist/commands/install-audit.js +5 -1
  26. package/dist/commands/installed-stashes.js +118 -138
  27. package/dist/commands/knowledge.js +133 -0
  28. package/dist/commands/lint/agent-linter.js +46 -0
  29. package/dist/commands/lint/base-linter.js +291 -0
  30. package/dist/commands/lint/command-linter.js +46 -0
  31. package/dist/commands/lint/default-linter.js +13 -0
  32. package/dist/commands/lint/index.js +145 -0
  33. package/dist/commands/lint/knowledge-linter.js +13 -0
  34. package/dist/commands/lint/memory-linter.js +58 -0
  35. package/dist/commands/lint/registry.js +33 -0
  36. package/dist/commands/lint/skill-linter.js +42 -0
  37. package/dist/commands/lint/task-linter.js +47 -0
  38. package/dist/commands/lint/types.js +1 -0
  39. package/dist/commands/lint/vault-key-rules.js +67 -0
  40. package/dist/commands/lint/workflow-linter.js +53 -0
  41. package/dist/commands/lint.js +1 -0
  42. package/dist/commands/migration-help.js +2 -2
  43. package/dist/commands/proposal.js +8 -7
  44. package/dist/commands/propose.js +106 -43
  45. package/dist/commands/reflect.js +167 -41
  46. package/dist/commands/registry-search.js +2 -2
  47. package/dist/commands/remember.js +55 -1
  48. package/dist/commands/schema-repair.js +130 -0
  49. package/dist/commands/search.js +21 -5
  50. package/dist/commands/show.js +135 -55
  51. package/dist/commands/source-add.js +10 -10
  52. package/dist/commands/source-manage.js +11 -19
  53. package/dist/commands/tasks.js +385 -0
  54. package/dist/commands/url-checker.js +39 -0
  55. package/dist/commands/vault.js +173 -87
  56. package/dist/core/action-contributors.js +25 -0
  57. package/dist/core/asset-ref.js +4 -0
  58. package/dist/core/asset-registry.js +5 -17
  59. package/dist/core/asset-spec.js +11 -1
  60. package/dist/core/common.js +100 -0
  61. package/dist/core/concurrent.js +22 -0
  62. package/dist/core/config.js +240 -127
  63. package/dist/core/events.js +87 -123
  64. package/dist/core/frontmatter.js +0 -6
  65. package/dist/core/markdown.js +17 -0
  66. package/dist/core/memory-improve.js +678 -0
  67. package/dist/core/parse.js +155 -0
  68. package/dist/core/paths.js +101 -3
  69. package/dist/core/proposal-validators.js +61 -0
  70. package/dist/core/proposals.js +49 -38
  71. package/dist/core/state-db.js +731 -0
  72. package/dist/core/time.js +51 -0
  73. package/dist/core/warn.js +59 -1
  74. package/dist/indexer/db-search.js +86 -472
  75. package/dist/indexer/db.js +418 -59
  76. package/dist/indexer/ensure-index.js +133 -0
  77. package/dist/indexer/graph-boost.js +247 -94
  78. package/dist/indexer/graph-db.js +201 -0
  79. package/dist/indexer/graph-dedup.js +99 -0
  80. package/dist/indexer/graph-extraction.js +417 -74
  81. package/dist/indexer/index-context.js +10 -0
  82. package/dist/indexer/indexer.js +480 -298
  83. package/dist/indexer/llm-cache.js +47 -0
  84. package/dist/indexer/matchers.js +124 -160
  85. package/dist/indexer/memory-inference.js +63 -29
  86. package/dist/indexer/metadata-contributors.js +26 -0
  87. package/dist/indexer/metadata.js +196 -197
  88. package/dist/indexer/path-resolver.js +89 -0
  89. package/dist/indexer/ranking-contributors.js +204 -0
  90. package/dist/indexer/ranking.js +74 -0
  91. package/dist/indexer/search-hit-enrichers.js +22 -0
  92. package/dist/indexer/search-source.js +24 -9
  93. package/dist/indexer/semantic-status.js +2 -16
  94. package/dist/indexer/walker.js +25 -0
  95. package/dist/integrations/agent/builders.js +109 -0
  96. package/dist/integrations/agent/config.js +203 -3
  97. package/dist/integrations/agent/index.js +5 -2
  98. package/dist/integrations/agent/model-aliases.js +63 -0
  99. package/dist/integrations/agent/profiles.js +67 -5
  100. package/dist/integrations/agent/prompts.js +114 -29
  101. package/dist/integrations/agent/sdk-runner.js +120 -0
  102. package/dist/integrations/agent/spawn.js +158 -34
  103. package/dist/integrations/lockfile.js +10 -18
  104. package/dist/integrations/session-logs/index.js +65 -0
  105. package/dist/integrations/session-logs/providers/claude-code.js +56 -0
  106. package/dist/integrations/session-logs/providers/opencode.js +52 -0
  107. package/dist/integrations/session-logs/types.js +1 -0
  108. package/dist/llm/call-ai.js +74 -0
  109. package/dist/llm/client.js +63 -86
  110. package/dist/llm/feature-gate.js +27 -16
  111. package/dist/llm/graph-extract.js +297 -64
  112. package/dist/llm/memory-infer.js +52 -71
  113. package/dist/llm/metadata-enhance.js +39 -22
  114. package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
  115. package/dist/output/cli-hints-full.md +277 -0
  116. package/dist/output/cli-hints-short.md +65 -0
  117. package/dist/output/cli-hints.js +2 -309
  118. package/dist/output/renderers.js +226 -257
  119. package/dist/output/shapes.js +109 -96
  120. package/dist/output/text.js +274 -36
  121. package/dist/registry/providers/skills-sh.js +61 -49
  122. package/dist/registry/providers/static-index.js +44 -48
  123. package/dist/registry/resolve.js +8 -16
  124. package/dist/setup/setup.js +510 -11
  125. package/dist/sources/provider-factory.js +2 -1
  126. package/dist/sources/providers/filesystem.js +16 -23
  127. package/dist/sources/providers/git.js +45 -4
  128. package/dist/sources/providers/website.js +15 -22
  129. package/dist/sources/website-ingest.js +4 -0
  130. package/dist/tasks/backends/cron.js +200 -0
  131. package/dist/tasks/backends/exec-utils.js +25 -0
  132. package/dist/tasks/backends/index.js +32 -0
  133. package/dist/tasks/backends/launchd-template.xml +19 -0
  134. package/dist/tasks/backends/launchd.js +184 -0
  135. package/dist/tasks/backends/schtasks-template.xml +29 -0
  136. package/dist/tasks/backends/schtasks.js +212 -0
  137. package/dist/tasks/parser.js +198 -0
  138. package/dist/tasks/resolveAkmBin.js +84 -0
  139. package/dist/tasks/runner.js +432 -0
  140. package/dist/tasks/schedule.js +208 -0
  141. package/dist/tasks/schema.js +13 -0
  142. package/dist/tasks/validator.js +59 -0
  143. package/dist/wiki/index-template.md +12 -0
  144. package/dist/wiki/ingest-workflow-template.md +54 -0
  145. package/dist/wiki/log-template.md +8 -0
  146. package/dist/wiki/schema-template.md +61 -0
  147. package/dist/wiki/wiki-templates.js +12 -0
  148. package/dist/wiki/wiki.js +10 -61
  149. package/dist/workflows/authoring.js +5 -25
  150. package/dist/workflows/db.js +9 -0
  151. package/dist/workflows/renderer.js +8 -3
  152. package/dist/workflows/runs.js +73 -88
  153. package/dist/workflows/scope-key.js +76 -0
  154. package/dist/workflows/validator.js +1 -1
  155. package/dist/workflows/workflow-template.md +24 -0
  156. package/docs/README.md +5 -2
  157. package/docs/migration/release-notes/0.7.0.md +1 -1
  158. package/docs/migration/release-notes/0.7.4.md +1 -1
  159. package/docs/migration/release-notes/0.7.5.md +20 -0
  160. package/docs/migration/release-notes/0.8.0.md +43 -0
  161. package/package.json +4 -3
  162. package/dist/templates/wiki-templates.js +0 -100
@@ -27,14 +27,30 @@ import { ConfigError } from "../../core/errors";
27
27
  import { warn } from "../../core/warn";
28
28
  import { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles";
29
29
  /** Keys recognised at the top level of an `agent` config block. */
30
- const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles"]);
30
+ const KNOWN_AGENT_KEYS = new Set(["default", "timeoutMs", "profiles", "processes"]);
31
31
  /** Keys recognised on a profile entry. */
32
- const KNOWN_PROFILE_KEYS = new Set(["bin", "args", "stdio", "env", "envPassthrough", "timeoutMs", "parseOutput"]);
32
+ const KNOWN_PROFILE_KEYS = new Set([
33
+ "bin",
34
+ "args",
35
+ "stdio",
36
+ "env",
37
+ "envPassthrough",
38
+ "timeoutMs",
39
+ "parseOutput",
40
+ "sdkMode",
41
+ "model",
42
+ "endpoint",
43
+ "apiKey",
44
+ "commandBuilder",
45
+ "modelAliases",
46
+ ]);
33
47
  /**
34
48
  * Default hard timeout for an agent CLI. Spec §12.2 calls for a hard
35
49
  * timeout; 60s matches the example value in `docs/configuration.md`.
36
50
  */
37
51
  export const DEFAULT_AGENT_TIMEOUT_MS = 60_000;
52
+ /** Keys recognised on a `processes[<name>]` object entry. */
53
+ const KNOWN_PROCESS_ENTRY_KEYS = new Set(["profile", "timeoutMs"]);
38
54
  /**
39
55
  * Parse a raw value (typically `rawConfig.agent` from `JSON.parse`) into a
40
56
  * normalised {@link AgentConfig}. Returns `undefined` when the value is not
@@ -83,6 +99,11 @@ export function parseAgentConfig(value) {
83
99
  if (profiles)
84
100
  out.profiles = profiles;
85
101
  }
102
+ if ("processes" in raw) {
103
+ const processes = parseProcessesMap(raw.processes);
104
+ if (processes)
105
+ out.processes = processes;
106
+ }
86
107
  return out;
87
108
  }
88
109
  function parseAgentProfilesMap(value) {
@@ -98,6 +119,124 @@ function parseAgentProfilesMap(value) {
98
119
  }
99
120
  return Object.keys(out).length > 0 ? out : undefined;
100
121
  }
122
+ /**
123
+ * Parse one entry in `agent.processes`. Accepts a string (profile name) or an
124
+ * object with optional `profile` and `timeoutMs` fields. Returns `undefined`
125
+ * and emits a warning for entries that are neither valid strings nor valid
126
+ * objects (warn-and-ignore).
127
+ */
128
+ export function parseProcessEntry(value, name) {
129
+ if (typeof value === "string") {
130
+ if (!value.trim()) {
131
+ warn(`[akm] Ignoring agent.processes."${name}": string value must be non-empty (a profile name).`);
132
+ return undefined;
133
+ }
134
+ return value.trim();
135
+ }
136
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
137
+ warn(`[akm] Ignoring agent.processes."${name}": expected a string (profile name) or an object with optional "profile" and "timeoutMs".`);
138
+ return undefined;
139
+ }
140
+ const raw = value;
141
+ // Warn on unknown keys (warn-and-ignore contract).
142
+ for (const key of Object.keys(raw)) {
143
+ if (!KNOWN_PROCESS_ENTRY_KEYS.has(key)) {
144
+ warn(`[akm] Ignoring unknown agent.processes."${name}" key: "${key}"`);
145
+ }
146
+ }
147
+ const out = {};
148
+ if ("profile" in raw) {
149
+ if (typeof raw.profile === "string" && raw.profile.trim()) {
150
+ out.profile = raw.profile.trim();
151
+ }
152
+ else if (raw.profile !== undefined) {
153
+ warn(`[akm] Ignoring agent.processes."${name}".profile: expected a non-empty string.`);
154
+ }
155
+ }
156
+ if ("timeoutMs" in raw) {
157
+ if (raw.timeoutMs === null) {
158
+ // null = unlimited — explicit, valid.
159
+ out.timeoutMs = null;
160
+ }
161
+ else if (typeof raw.timeoutMs === "number" &&
162
+ Number.isFinite(raw.timeoutMs) &&
163
+ Number.isInteger(raw.timeoutMs) &&
164
+ raw.timeoutMs > 0) {
165
+ out.timeoutMs = raw.timeoutMs;
166
+ }
167
+ else {
168
+ warn(`[akm] Ignoring agent.processes."${name}".timeoutMs: expected a positive integer (milliseconds) or null (unlimited).`);
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+ /**
174
+ * Parse the `agent.processes` map. Returns `undefined` when the value is not
175
+ * a valid object; per-entry validation errors are warn-and-ignored (per spec §9.2).
176
+ */
177
+ export function parseProcessesMap(value) {
178
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
179
+ warn("[akm] Ignoring agent.processes: expected an object.");
180
+ return undefined;
181
+ }
182
+ const out = {};
183
+ for (const [name, raw] of Object.entries(value)) {
184
+ const parsed = parseProcessEntry(raw, name);
185
+ if (parsed !== undefined)
186
+ out[name] = parsed;
187
+ }
188
+ return Object.keys(out).length > 0 ? out : undefined;
189
+ }
190
+ /**
191
+ * Resolve the agent profile and effective timeout for a named process.
192
+ *
193
+ * Resolution order:
194
+ * 1. `config.processes[processName]` — if a string, that is the profile name;
195
+ * if an object, extract `profile` (and optionally `timeoutMs`).
196
+ * 2. Profile name falls back to `config.default` when not specified in the
197
+ * process entry.
198
+ * 3. `timeoutMs` falls back: `process.timeoutMs` (null = unlimited) →
199
+ * profile.timeoutMs → agent.timeoutMs → DEFAULT_AGENT_TIMEOUT_MS.
200
+ *
201
+ * Returns `{ profile, timeoutMs }` where `timeoutMs` is `undefined` when the
202
+ * resolved timeout is `null` (unlimited) or when no timeout is set at any
203
+ * layer (callers treat `undefined` as the DEFAULT_AGENT_TIMEOUT_MS default).
204
+ *
205
+ * Throws {@link ConfigError} (via {@link requireAgentProfile}) when the agent
206
+ * block is missing or the resolved profile cannot be used.
207
+ */
208
+ export function resolveProcessAgentProfile(processName, agentConfig) {
209
+ let profileName;
210
+ let processTimeoutMs; // null = unlimited from config
211
+ const processEntry = agentConfig?.processes?.[processName];
212
+ if (processEntry !== undefined) {
213
+ if (typeof processEntry === "string") {
214
+ profileName = processEntry;
215
+ }
216
+ else {
217
+ profileName = processEntry.profile;
218
+ processTimeoutMs = processEntry.timeoutMs;
219
+ }
220
+ }
221
+ // Profile name falls back to agent.default when not set in the process entry.
222
+ const resolvedProfile = requireAgentProfile(agentConfig, profileName);
223
+ // Timeout resolution: process entry → profile → agent-level → undefined (caller applies DEFAULT).
224
+ let resolvedTimeoutMs;
225
+ if (processTimeoutMs === null) {
226
+ // null = explicit "unlimited" — surface as undefined so callers omit the timer.
227
+ resolvedTimeoutMs = undefined;
228
+ }
229
+ else if (processTimeoutMs !== undefined) {
230
+ resolvedTimeoutMs = processTimeoutMs;
231
+ }
232
+ else if (resolvedProfile.timeoutMs !== undefined) {
233
+ resolvedTimeoutMs = resolvedProfile.timeoutMs;
234
+ }
235
+ else if (agentConfig?.timeoutMs !== undefined) {
236
+ resolvedTimeoutMs = agentConfig.timeoutMs;
237
+ }
238
+ return { profile: resolvedProfile, timeoutMs: resolvedTimeoutMs };
239
+ }
101
240
  function parseAgentProfileConfig(name, value) {
102
241
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
103
242
  warn(`[akm] Ignoring agent.profiles."${name}": expected an object.`);
@@ -171,6 +310,52 @@ function parseAgentProfileConfig(name, value) {
171
310
  else if (raw.parseOutput !== undefined) {
172
311
  warn(`[akm] Ignoring agent.profiles."${name}".parseOutput: expected "text" or "json".`);
173
312
  }
313
+ if (raw.sdkMode === true || raw.sdkMode === false) {
314
+ out.sdkMode = raw.sdkMode;
315
+ }
316
+ else if (raw.sdkMode !== undefined) {
317
+ warn(`[akm] Ignoring agent.profiles."${name}".sdkMode: expected a boolean.`);
318
+ }
319
+ if (typeof raw.model === "string" && raw.model.trim()) {
320
+ out.model = raw.model.trim();
321
+ }
322
+ else if (raw.model !== undefined) {
323
+ warn(`[akm] Ignoring agent.profiles."${name}".model: expected a non-empty string.`);
324
+ }
325
+ if (typeof raw.endpoint === "string" && raw.endpoint.trim()) {
326
+ out.endpoint = raw.endpoint.trim();
327
+ }
328
+ else if (raw.endpoint !== undefined) {
329
+ warn(`[akm] Ignoring agent.profiles."${name}".endpoint: expected a non-empty string.`);
330
+ }
331
+ if (typeof raw.apiKey === "string" && raw.apiKey.trim()) {
332
+ out.apiKey = raw.apiKey.trim();
333
+ }
334
+ else if (raw.apiKey !== undefined) {
335
+ warn(`[akm] Ignoring agent.profiles."${name}".apiKey: expected a non-empty string.`);
336
+ }
337
+ if (typeof raw.commandBuilder === "string" && raw.commandBuilder.trim()) {
338
+ out.commandBuilder = raw.commandBuilder.trim();
339
+ }
340
+ else if (raw.commandBuilder !== undefined) {
341
+ warn(`[akm] Ignoring agent.profiles."${name}".commandBuilder: expected a non-empty string.`);
342
+ }
343
+ if (typeof raw.modelAliases === "object" && raw.modelAliases !== null && !Array.isArray(raw.modelAliases)) {
344
+ const aliases = {};
345
+ for (const [k, v] of Object.entries(raw.modelAliases)) {
346
+ if (typeof v === "string") {
347
+ aliases[k.toLowerCase()] = v;
348
+ }
349
+ else {
350
+ warn(`[akm] Ignoring non-string value for agent.profiles."${name}".modelAliases key "${k}".`);
351
+ }
352
+ }
353
+ if (Object.keys(aliases).length > 0)
354
+ out.modelAliases = aliases;
355
+ }
356
+ else if (raw.modelAliases !== undefined) {
357
+ warn(`[akm] Ignoring agent.profiles."${name}".modelAliases: expected a string-valued object.`);
358
+ }
174
359
  return out;
175
360
  }
176
361
  /**
@@ -184,7 +369,7 @@ function parseAgentProfileConfig(name, value) {
184
369
  */
185
370
  export function resolveAgentProfile(name, overrides) {
186
371
  const builtin = getBuiltinAgentProfile(name);
187
- if (!builtin && !overrides?.bin)
372
+ if (!builtin && !overrides?.bin && overrides?.sdkMode !== true)
188
373
  return undefined;
189
374
  const base = builtin ??
190
375
  {
@@ -208,6 +393,14 @@ export function resolveAgentProfile(name, overrides) {
208
393
  : base.envPassthrough,
209
394
  timeoutMs: overrides.timeoutMs ?? base.timeoutMs,
210
395
  parseOutput: overrides.parseOutput ?? base.parseOutput,
396
+ sdkMode: overrides.sdkMode ?? base.sdkMode,
397
+ model: overrides.model ?? base.model,
398
+ endpoint: overrides.endpoint ?? base.endpoint,
399
+ apiKey: overrides.apiKey ?? base.apiKey,
400
+ commandBuilder: overrides.commandBuilder ?? base.commandBuilder,
401
+ modelAliases: overrides.modelAliases
402
+ ? { ...(base.modelAliases ?? {}), ...overrides.modelAliases }
403
+ : base.modelAliases,
211
404
  };
212
405
  return merged;
213
406
  }
@@ -274,6 +467,13 @@ export function requireAgentProfile(agent, requested) {
274
467
  if (!profile) {
275
468
  throw new ConfigError(`agent profile "${name}" is not built-in and has no \`bin\` override.`, "INVALID_CONFIG_FILE", `Define agent.profiles."${name}".bin in config.json, or pick one of: ${listAgentProfileNames(agent).join(", ")}.`);
276
469
  }
470
+ // Apply the top-level agent.timeoutMs as the effective default for this
471
+ // profile when the profile itself has no timeout override. This makes
472
+ // `agent.timeoutMs` the universal fallback without requiring every
473
+ // profile definition in config.json to repeat it.
474
+ if (profile.timeoutMs === undefined && agent?.timeoutMs !== undefined) {
475
+ return { ...profile, timeoutMs: agent.timeoutMs };
476
+ }
277
477
  return profile;
278
478
  }
279
479
  /**
@@ -7,11 +7,14 @@
7
7
  * • Types: AgentProfile, AgentConfig, AgentRunResult, AgentFailureReason.
8
8
  * • Profiles: getBuiltinAgentProfile, listBuiltinAgentProfiles, BUILTIN_AGENT_PROFILE_NAMES.
9
9
  * • Config: parseAgentConfig, resolveProfileFromConfig, requireAgentProfile, listResolvedAgentProfiles, listAgentProfileNames.
10
- * • Spawn: runAgent.
10
+ * • Spawn: runAgent. Builders: getCommandBuilder, AgentCommandBuilder, AgentDispatchRequest — platform-specific argv construction.
11
11
  * • Detection: detectAgentCliProfiles, pickDefaultAgentProfile, defaultWhich.
12
12
  */
13
+ export { getCommandBuilder } from "./builders";
13
14
  export { DEFAULT_AGENT_TIMEOUT_MS, listAgentProfileNames, listResolvedAgentProfiles, parseAgentConfig, requireAgentProfile, resolveAgentProfile, resolveDefaultProfileName, resolveProfileFromConfig, } from "./config";
14
15
  export { defaultWhich, detectAgentCliProfiles, pickDefaultAgentProfile } from "./detect";
16
+ export { listBuiltinModelAliases, resolveModel } from "./model-aliases";
15
17
  export { BUILTIN_AGENT_PROFILE_NAMES, getBuiltinAgentProfile, listBuiltinAgentProfiles, } from "./profiles";
16
- export { buildProposePrompt, buildReflectPrompt, parseAgentProposalPayload, stripJsonFences } from "./prompts";
18
+ export { buildProposePrompt, buildReflectPrompt, buildSchemaRepairPrompt, parseAgentProposalPayload, stripJsonFences, } from "./prompts";
19
+ export { runAgentSdk } from "./sdk-runner";
17
20
  export { runAgent } from "./spawn";
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Model alias registry for agent CLI dispatch (v1 spec §12.3).
3
+ *
4
+ * Translates human-friendly aliases and cross-platform model identifiers to
5
+ * the exact string each agent CLI expects for its --model flag.
6
+ *
7
+ * Resolution order (highest → lowest precedence):
8
+ * 1. Profile-level modelAliases from config.json (user-defined)
9
+ * 2. Built-in alias table
10
+ * 3. Verbatim pass-through (caller already supplied an exact model ID)
11
+ */
12
+ /**
13
+ * Built-in alias table. Alias keys are lowercase.
14
+ *
15
+ * Platform model string conventions:
16
+ * opencode — "<provider>/<model>" e.g. "opencode/claude-opus-4-7"
17
+ * claude — bare model name e.g. "claude-opus-4-7"
18
+ * (Claude Code also accepts its own built-in shorthands, but we
19
+ * always resolve to the full name for determinism)
20
+ */
21
+ const BUILTIN_ALIASES = [
22
+ {
23
+ alias: "opus",
24
+ platforms: {
25
+ claude: "claude-opus-4-7",
26
+ opencode: "opencode/claude-opus-4-7",
27
+ },
28
+ },
29
+ {
30
+ alias: "sonnet",
31
+ platforms: {
32
+ claude: "claude-sonnet-4-6",
33
+ opencode: "opencode/claude-sonnet-4-6",
34
+ },
35
+ },
36
+ {
37
+ alias: "haiku",
38
+ platforms: {
39
+ claude: "claude-haiku-4-5-20251001",
40
+ opencode: "opencode/claude-haiku-4-5",
41
+ },
42
+ },
43
+ ];
44
+ /**
45
+ * Resolve a model alias or exact model ID to the string the target platform
46
+ * CLI expects for its --model flag.
47
+ *
48
+ * @param model Raw alias ("opus") or exact model ID ("claude-opus-4-7").
49
+ * @param platform Builder platform name ("claude", "opencode", ...).
50
+ * @param custom Profile-level aliases from config.json — take priority over builtins.
51
+ * @returns Resolved model string, or `model` verbatim when no alias matches.
52
+ */
53
+ export function resolveModel(model, platform, custom) {
54
+ const key = model.toLowerCase();
55
+ if (custom?.[key])
56
+ return custom[key];
57
+ const entry = BUILTIN_ALIASES.find((a) => a.alias === key);
58
+ return entry?.platforms[platform] ?? model;
59
+ }
60
+ /** Return all built-in alias entries (for tests and documentation). */
61
+ export function listBuiltinModelAliases() {
62
+ return BUILTIN_ALIASES;
63
+ }
@@ -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
- /** Names of every built-in profile. Stable, sorted. */
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 built-in. */
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. Callers
57
- * should not assume reference equality with subsequent calls.
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 should be 1–3 short paragraphs of practical guidance.",
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`).",
@@ -47,16 +48,30 @@ function knownTypeList() {
47
48
  return Object.keys(TYPE_DIRS).sort().join(", ");
48
49
  }
49
50
  /**
50
- * Common envelope every prompt asks the agent to honour. The wrapper code
51
- * uses `JSON.parse(stdout)` to extract the payload — anything outside the
52
- * JSON object will be treated as a parse error.
51
+ * Common envelope every prompt asks the agent to honour when NO draft file
52
+ * path is available. The wrapper code uses `JSON.parse(stdout)` to extract
53
+ * the payload — anything outside the JSON object will be treated as a parse
54
+ * error.
53
55
  */
54
- const RESPONSE_CONTRACT = [
56
+ const RESPONSE_CONTRACT_JSON = [
55
57
  "Respond ONLY with a single JSON object. No prose before or after.",
56
58
  'Shape: {"ref": "<type>:<name>", "content": "<full file contents>", "frontmatter": {...}}',
57
59
  "`content` is the full file body that will be written if accepted.",
58
60
  "`frontmatter` is optional — include it if `content` starts with `---` so reviewers can sanity-check the keys.",
59
61
  ].join("\n");
62
+ /**
63
+ * Response contract used when a draft file path is available. Instructs the
64
+ * agent to write the improved asset content directly to the file using its
65
+ * native file-editing tools — no stdout JSON parsing required.
66
+ */
67
+ function fileWriteContract(draftFilePath) {
68
+ return [
69
+ `Write the complete improved asset content to: ${draftFilePath}`,
70
+ "Use your file-editing tools to create or overwrite that file.",
71
+ "Do NOT output JSON to stdout. Do NOT print the file contents. Just write the file.",
72
+ "When you are done writing the file, output a single line: DRAFT_WRITTEN",
73
+ ].join("\n");
74
+ }
60
75
  /**
61
76
  * Build the prompt for `akm reflect [ref]`. Asks the agent to review an
62
77
  * existing asset (plus any negative feedback / lint findings) and propose
@@ -66,7 +81,15 @@ const RESPONSE_CONTRACT = [
66
81
  export function buildReflectPrompt(input) {
67
82
  const sections = [];
68
83
  if (input.ref && input.type && input.name) {
69
- sections.push(`You are reviewing an akm stash asset (${input.type}) called "${input.name}" and proposing an improved version.`);
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);
70
93
  sections.push(`Target ref: ${input.ref}`);
71
94
  sections.push(`Asset-type guidance: ${hintForType(input.type)}`);
72
95
  }
@@ -78,6 +101,23 @@ export function buildReflectPrompt(input) {
78
101
  if (input.task?.trim()) {
79
102
  sections.push(`Task / focus: ${input.task.trim()}`);
80
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
+ }
81
121
  if (input.assetContent?.trim()) {
82
122
  sections.push("Current asset content (verbatim):");
83
123
  sections.push("```");
@@ -90,22 +130,28 @@ export function buildReflectPrompt(input) {
90
130
  else {
91
131
  sections.push("(No existing asset content was supplied.)");
92
132
  }
93
- if (input.feedback && input.feedback.length > 0) {
94
- sections.push("Recent feedback / signals:");
95
- for (const line of input.feedback)
96
- sections.push(`- ${line}`);
97
- }
98
- else if (!input.ref) {
99
- sections.push("Recent feedback / signals:");
100
- sections.push("- (no feedback events recorded)");
101
- }
102
133
  if (input.schemaHints && input.schemaHints.length > 0) {
103
134
  sections.push("Schema / lint hints to address:");
104
135
  for (const line of input.schemaHints)
105
136
  sections.push(`- ${line}`);
106
137
  }
107
- sections.push("Produce a single proposal that addresses the feedback and respects the asset-type contract.");
108
- sections.push(RESPONSE_CONTRACT);
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.");
154
+ sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
109
155
  return sections.join("\n\n");
110
156
  }
111
157
  /**
@@ -124,19 +170,63 @@ export function buildProposePrompt(input) {
124
170
  sections.push(`- ${line}`);
125
171
  }
126
172
  sections.push("Produce a single proposal that, if accepted, would land as the asset described above.");
127
- sections.push(RESPONSE_CONTRACT);
173
+ sections.push(input.draftFilePath ? fileWriteContract(input.draftFilePath) : RESPONSE_CONTRACT_JSON);
174
+ return sections.join("\n\n");
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);
128
201
  return sections.join("\n\n");
129
202
  }
130
203
  /**
131
204
  * Parse agent stdout into a proposal payload. The agent contract requires a
132
205
  * single JSON object; anything else is reported as a parse error so callers
133
206
  * can map to {@link AgentFailureReason} `parse_error`.
207
+ *
208
+ * Resilient to two common local-LLM failure modes:
209
+ * 1. `<think>…</think>` blocks emitted before the JSON (stripped by `stripJsonFences`).
210
+ * 2. Prose preamble / postamble around the JSON object (handled by `extractEmbeddedJson`).
134
211
  */
135
212
  export function parseAgentProposalPayload(stdout) {
136
- const trimmed = stripJsonFences(stdout).trim();
213
+ // Strip <think> blocks and fences, then attempt full parse with embedded fallback.
214
+ const trimmed = stripCodeFences(stripThinkBlocks(stdout)).trim();
137
215
  if (!trimmed)
138
216
  throw new Error("agent produced empty output");
139
- const parsed = JSON.parse(trimmed);
217
+ let parsed;
218
+ try {
219
+ parsed = JSON.parse(trimmed);
220
+ }
221
+ catch (directErr) {
222
+ // Agent output contains prose before/after the JSON object (e.g. a local
223
+ // LLM that narrates before responding). Try extracting the first balanced
224
+ // top-level `{…}` from the text rather than failing immediately.
225
+ const embedded = parseEmbeddedJsonResponse(trimmed);
226
+ if (!embedded)
227
+ throw directErr;
228
+ parsed = embedded;
229
+ }
140
230
  if (typeof parsed.ref !== "string" || !parsed.ref.trim()) {
141
231
  throw new Error('agent response missing required string field "ref"');
142
232
  }
@@ -153,15 +243,10 @@ export function parseAgentProposalPayload(stdout) {
153
243
  return out;
154
244
  }
155
245
  /**
156
- * Strip `\`\`\`json … \`\`\`` fences if the agent wrapped its JSON output.
157
- * Mirrors the same helper in `src/llm/client.ts` but kept local here so
158
- * `agent/` does not import from `llm/` (the boundary is one-way per
159
- * v1 spec §9.7 — agents are shell-out only).
246
+ * Strip `\`\`\`json … \`\`\`` fences and `<think>…</think>` reasoning blocks
247
+ * from agent output. Thin wrapper around `core/parse` helpers, kept exported
248
+ * for backward compatibility (re-exported from `integrations/agent/index.ts`).
160
249
  */
161
250
  export function stripJsonFences(text) {
162
- const trimmed = text.trim();
163
- const fenced = trimmed.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/);
164
- if (fenced)
165
- return fenced[1] ?? trimmed;
166
- return trimmed;
251
+ return stripCodeFences(stripThinkBlocks(text));
167
252
  }