akm-cli 0.9.0-rc.0 → 0.9.0-rc.2

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 (262) hide show
  1. package/CHANGELOG.md +339 -2
  2. package/SECURITY.md +23 -24
  3. package/dist/assets/help/help-improve.md +10 -10
  4. package/dist/assets/hints/cli-hints-full.md +44 -10
  5. package/dist/assets/hints/cli-hints-short.md +6 -2
  6. package/dist/assets/{profiles → improve-strategies}/default.json +1 -0
  7. package/dist/assets/{profiles → improve-strategies}/graph-refresh.json +1 -1
  8. package/dist/assets/{profiles → improve-strategies}/proactive-maintenance.json +2 -3
  9. package/dist/assets/{profiles → improve-strategies}/reflect-distill.json +3 -4
  10. package/dist/assets/prompts/workflow-unit-preamble.md +26 -0
  11. package/dist/assets/stash-skeleton/README.md +28 -0
  12. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +6 -0
  13. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +6 -0
  14. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +12 -1
  15. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +11 -1
  16. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +6 -0
  17. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +9 -0
  18. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +7 -0
  19. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +7 -0
  20. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +6 -0
  21. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +98 -0
  22. package/dist/assets/stash-skeleton/facts/conventions/domains.md +63 -0
  23. package/dist/assets/stash-skeleton/facts/conventions/organization.md +127 -0
  24. package/dist/assets/tasks/core/backup.yml +1 -0
  25. package/dist/assets/tasks/core/extract.yml +1 -0
  26. package/dist/assets/tasks/core/improve.yml +1 -0
  27. package/dist/assets/tasks/core/index-refresh.yml +1 -0
  28. package/dist/assets/tasks/core/sync.yml +1 -0
  29. package/dist/assets/tasks/core/version-check.yml +1 -0
  30. package/dist/assets/tasks/graph-refresh-weekly.yml +4 -4
  31. package/dist/assets/templates/html/health.html +5 -1
  32. package/dist/cli/config-migrate.js +31 -138
  33. package/dist/cli/config-validate.js +10 -8
  34. package/dist/cli.js +48 -14
  35. package/dist/commands/agent/agent-dispatch.js +17 -16
  36. package/dist/commands/agent/agent-support.js +0 -24
  37. package/dist/commands/agent/contribute-cli.js +5 -15
  38. package/dist/commands/backup-cli.js +54 -0
  39. package/dist/commands/config-cli.js +45 -159
  40. package/dist/commands/env/env-binding.js +95 -0
  41. package/dist/commands/env/env-cli.js +8 -65
  42. package/dist/commands/env/secret.js +8 -5
  43. package/dist/commands/health/checks.js +130 -83
  44. package/dist/commands/health/html-report.js +4 -0
  45. package/dist/commands/health/improve-metrics.js +30 -32
  46. package/dist/commands/health/llm-usage.js +19 -19
  47. package/dist/commands/health/md-report.js +4 -0
  48. package/dist/commands/health/metrics.js +2 -1
  49. package/dist/commands/health/surfaces.js +5 -4
  50. package/dist/commands/health.js +1 -1
  51. package/dist/commands/improve/consolidate/chunking.js +2 -2
  52. package/dist/commands/improve/consolidate.js +28 -25
  53. package/dist/commands/improve/distill/promote-memory.js +5 -12
  54. package/dist/commands/improve/distill/quality-gate.js +5 -7
  55. package/dist/commands/improve/distill.js +16 -5
  56. package/dist/commands/improve/eligibility.js +22 -12
  57. package/dist/commands/improve/extract-cli.js +47 -19
  58. package/dist/commands/improve/extract.js +110 -62
  59. package/dist/commands/improve/improve-cli.js +38 -16
  60. package/dist/commands/improve/improve-result-file.js +30 -24
  61. package/dist/commands/improve/improve-strategies.js +137 -0
  62. package/dist/commands/improve/improve.js +60 -30
  63. package/dist/commands/improve/locks.js +66 -45
  64. package/dist/commands/improve/loop-stages.js +75 -33
  65. package/dist/commands/improve/memory/memory-belief.js +79 -7
  66. package/dist/commands/improve/memory/memory-contradiction-detect.js +12 -4
  67. package/dist/commands/improve/preparation.js +71 -73
  68. package/dist/commands/improve/procedural.js +3 -2
  69. package/dist/commands/improve/recombine.js +2 -1
  70. package/dist/commands/improve/reflect.js +119 -214
  71. package/dist/commands/improve/shared.js +11 -5
  72. package/dist/commands/lint/base-linter.js +152 -42
  73. package/dist/commands/mv-cli.js +809 -0
  74. package/dist/commands/proposal/proposal-cli.js +18 -8
  75. package/dist/commands/proposal/propose.js +64 -69
  76. package/dist/commands/read/knowledge.js +436 -4
  77. package/dist/commands/read/remember-cli.js +39 -2
  78. package/dist/commands/read/search-cli.js +6 -1
  79. package/dist/commands/registry-cli.js +29 -14
  80. package/dist/commands/remember.js +2 -0
  81. package/dist/commands/sources/init.js +13 -14
  82. package/dist/commands/sources/migration-help.js +7 -4
  83. package/dist/commands/sources/schema-repair.js +2 -4
  84. package/dist/commands/sources/source-add.js +62 -73
  85. package/dist/commands/sources/source-manage.js +50 -46
  86. package/dist/commands/sources/stash-cli.js +41 -4
  87. package/dist/commands/tasks/default-tasks.js +12 -12
  88. package/dist/commands/tasks/tasks-cli.js +7 -3
  89. package/dist/commands/tasks/tasks.js +113 -18
  90. package/dist/commands/wiki-cli.js +9 -10
  91. package/dist/commands/workflow-cli.js +276 -12
  92. package/dist/core/asset/asset-spec.js +58 -1
  93. package/dist/core/asset/frontmatter.js +12 -2
  94. package/dist/core/common.js +5 -3
  95. package/dist/core/config/config-io.js +28 -17
  96. package/dist/core/config/config-schema.js +379 -66
  97. package/dist/core/config/config-types.js +3 -3
  98. package/dist/core/config/config-version.js +29 -0
  99. package/dist/core/config/config-walker.js +98 -27
  100. package/dist/core/config/config.js +132 -266
  101. package/dist/core/config/deep-merge.js +41 -0
  102. package/dist/core/config/engine-semantics.js +32 -0
  103. package/dist/core/errors.js +2 -2
  104. package/dist/core/extra-params.js +61 -0
  105. package/dist/core/file-lock.js +201 -56
  106. package/dist/core/improve-result.js +178 -0
  107. package/dist/core/json-schema.js +142 -0
  108. package/dist/core/maintenance-barrier.js +119 -0
  109. package/dist/core/migration-backup.js +416 -0
  110. package/dist/core/paths.js +3 -0
  111. package/dist/core/redaction.js +358 -0
  112. package/dist/core/state/migrations.js +17 -2
  113. package/dist/core/state-db.js +44 -1
  114. package/dist/indexer/db/db.js +118 -2
  115. package/dist/indexer/graph/graph-extraction.js +28 -16
  116. package/dist/indexer/index-writer-lock.js +31 -24
  117. package/dist/indexer/index-written-assets.js +15 -6
  118. package/dist/indexer/indexer.js +47 -2
  119. package/dist/indexer/passes/memory-inference.js +10 -6
  120. package/dist/indexer/passes/metadata.js +250 -0
  121. package/dist/indexer/search/db-search.js +111 -44
  122. package/dist/indexer/search/fts-query.js +41 -0
  123. package/dist/indexer/search/ranking-contributors.js +48 -0
  124. package/dist/indexer/search/ranking.js +36 -23
  125. package/dist/indexer/search/search-fields.js +11 -1
  126. package/dist/indexer/walk/matchers.js +39 -0
  127. package/dist/integrations/agent/builder-shared.js +7 -0
  128. package/dist/integrations/agent/builders.js +5 -50
  129. package/dist/integrations/agent/config.js +3 -143
  130. package/dist/integrations/agent/detect.js +17 -2
  131. package/dist/integrations/agent/engine-resolution.js +202 -0
  132. package/dist/integrations/agent/index.js +1 -2
  133. package/dist/integrations/agent/model-aliases.js +16 -2
  134. package/dist/integrations/agent/profiles.js +36 -62
  135. package/dist/integrations/agent/runner-dispatch.js +91 -4
  136. package/dist/integrations/agent/runner.js +76 -207
  137. package/dist/integrations/agent/spawn.js +141 -20
  138. package/dist/integrations/harnesses/aider/agent-builder.js +112 -0
  139. package/dist/integrations/harnesses/aider/index.js +57 -0
  140. package/dist/integrations/harnesses/aider/result-extractor.js +53 -0
  141. package/dist/integrations/harnesses/amazonq/agent-builder.js +152 -0
  142. package/dist/integrations/harnesses/amazonq/index.js +58 -0
  143. package/dist/integrations/harnesses/amazonq/result-extractor.js +48 -0
  144. package/dist/integrations/harnesses/claude/agent-builder.js +46 -8
  145. package/dist/integrations/harnesses/claude/index.js +25 -25
  146. package/dist/integrations/harnesses/claude/result-extractor.js +52 -0
  147. package/dist/integrations/harnesses/codex/agent-builder.js +136 -0
  148. package/dist/integrations/harnesses/codex/index.js +62 -0
  149. package/dist/integrations/harnesses/codex/result-extractor.js +73 -0
  150. package/dist/integrations/harnesses/copilot/agent-builder.js +121 -0
  151. package/dist/integrations/harnesses/copilot/index.js +59 -0
  152. package/dist/integrations/harnesses/copilot/result-extractor.js +151 -0
  153. package/dist/integrations/harnesses/gemini/agent-builder.js +120 -0
  154. package/dist/integrations/harnesses/gemini/index.js +59 -0
  155. package/dist/integrations/harnesses/gemini/result-extractor.js +121 -0
  156. package/dist/integrations/harnesses/index.js +27 -28
  157. package/dist/integrations/harnesses/opencode/agent-builder.js +2 -3
  158. package/dist/integrations/harnesses/opencode/index.js +15 -22
  159. package/dist/integrations/harnesses/opencode-sdk/harness.js +60 -0
  160. package/dist/integrations/harnesses/opencode-sdk/index.js +8 -32
  161. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +681 -108
  162. package/dist/integrations/harnesses/openhands/agent-builder.js +125 -0
  163. package/dist/integrations/harnesses/openhands/index.js +57 -0
  164. package/dist/integrations/harnesses/openhands/result-extractor.js +103 -0
  165. package/dist/integrations/harnesses/pi/agent-builder.js +103 -0
  166. package/dist/integrations/harnesses/pi/index.js +57 -0
  167. package/dist/integrations/harnesses/pi/result-extractor.js +135 -0
  168. package/dist/integrations/harnesses/types.js +8 -32
  169. package/dist/integrations/lockfile.js +32 -21
  170. package/dist/integrations/session-logs/index.js +24 -11
  171. package/dist/llm/client.js +48 -14
  172. package/dist/llm/feature-gate.js +15 -47
  173. package/dist/llm/graph-extract.js +1 -1
  174. package/dist/llm/index-passes.js +8 -42
  175. package/dist/llm/memory-infer-impl.js +1 -1
  176. package/dist/llm/usage-persist.js +4 -0
  177. package/dist/llm/usage-telemetry.js +35 -5
  178. package/dist/output/renderers.js +3 -2
  179. package/dist/output/shapes/helpers.js +2 -1
  180. package/dist/output/shapes/passthrough.js +6 -0
  181. package/dist/output/text/helpers.js +215 -2
  182. package/dist/output/text/workflow.js +3 -1
  183. package/dist/schemas/akm-config.json +16638 -0
  184. package/dist/schemas/akm-task.json +87 -0
  185. package/dist/schemas/akm-workflow.json +372 -0
  186. package/dist/scripts/migrate-storage.js +10944 -8801
  187. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9247 -350
  188. package/dist/setup/detected-engines.js +142 -0
  189. package/dist/setup/engine-config.js +89 -0
  190. package/dist/setup/setup.js +236 -132
  191. package/dist/setup/steps/connection.js +61 -32
  192. package/dist/setup/steps/platforms.js +4 -4
  193. package/dist/setup/steps.js +3 -2
  194. package/dist/storage/database.js +13 -1
  195. package/dist/storage/engines/sqlite-migrations.js +1 -0
  196. package/dist/storage/repositories/improve-runs-repository.js +5 -5
  197. package/dist/storage/repositories/task-history-repository.js +78 -0
  198. package/dist/storage/repositories/workflow-runs-repository.js +190 -1
  199. package/dist/tasks/parser.js +138 -52
  200. package/dist/tasks/runner.js +71 -75
  201. package/dist/tasks/schema.js +1 -1
  202. package/dist/tasks/validator.js +11 -6
  203. package/dist/text-import-hook.mjs +1 -1
  204. package/dist/wiki/wiki.js +9 -8
  205. package/dist/workflows/authoring/authoring.js +123 -10
  206. package/dist/workflows/authoring/workflow-program-template.yaml +31 -0
  207. package/dist/workflows/cli.js +4 -0
  208. package/dist/workflows/concurrency-policy.js +15 -0
  209. package/dist/workflows/db.js +200 -13
  210. package/dist/workflows/exec/brief.js +478 -0
  211. package/dist/workflows/exec/frozen-judge.js +47 -0
  212. package/dist/workflows/exec/native-executor.js +1034 -0
  213. package/dist/workflows/exec/param-secrets.js +115 -0
  214. package/dist/workflows/exec/report.js +1355 -0
  215. package/dist/workflows/exec/run-workflow.js +609 -0
  216. package/dist/workflows/exec/scheduler.js +71 -0
  217. package/dist/workflows/exec/step-work.js +1212 -0
  218. package/dist/workflows/exec/unit-writer.js +23 -0
  219. package/dist/workflows/exec/watch.js +116 -0
  220. package/dist/workflows/exec/worktree.js +171 -0
  221. package/dist/workflows/ir/compile.js +375 -0
  222. package/dist/workflows/ir/freeze.js +243 -0
  223. package/dist/workflows/ir/params.js +54 -0
  224. package/dist/workflows/ir/plan-hash.js +68 -0
  225. package/dist/workflows/ir/schema.js +545 -0
  226. package/dist/workflows/parser.js +10 -1
  227. package/dist/workflows/program/expressions.js +369 -0
  228. package/dist/workflows/program/parser.js +869 -0
  229. package/dist/workflows/program/project.js +104 -0
  230. package/dist/workflows/program/schema.js +54 -0
  231. package/dist/workflows/renderer.js +82 -5
  232. package/dist/workflows/resource-limits.js +20 -0
  233. package/dist/workflows/runtime/agent-identity.js +59 -14
  234. package/dist/workflows/runtime/plan-classifier.js +187 -0
  235. package/dist/workflows/runtime/runs.js +246 -69
  236. package/dist/workflows/runtime/unit-checkin.js +45 -0
  237. package/dist/workflows/runtime/workflow-asset-loader.js +42 -1
  238. package/dist/workflows/validate-summary.js +24 -3
  239. package/dist/workflows/validator.js +26 -1
  240. package/docs/data-and-telemetry.md +4 -3
  241. package/docs/migration/release-notes/0.6.0.md +1 -1
  242. package/docs/migration/release-notes/0.7.0.md +5 -4
  243. package/docs/migration/release-notes/0.9.0-beta.60.md +19 -0
  244. package/docs/migration/v0.8-to-v0.9.md +401 -0
  245. package/package.json +4 -2
  246. package/schemas/akm-config.json +16638 -0
  247. package/schemas/akm-task.json +87 -0
  248. package/schemas/akm-workflow.json +372 -0
  249. package/dist/commands/improve/improve-profiles.js +0 -168
  250. package/dist/core/config/config-migration.js +0 -602
  251. package/dist/core/deep-merge.js +0 -38
  252. package/dist/llm/call-ai.js +0 -62
  253. package/dist/setup/legacy-config.js +0 -106
  254. package/docs/README.md +0 -104
  255. /package/dist/assets/{profiles → improve-strategies}/catchup.json +0 -0
  256. /package/dist/assets/{profiles → improve-strategies}/consolidate.json +0 -0
  257. /package/dist/assets/{profiles → improve-strategies}/frequent.json +0 -0
  258. /package/dist/assets/{profiles → improve-strategies}/memory-focus.json +0 -0
  259. /package/dist/assets/{profiles → improve-strategies}/quick.json +0 -0
  260. /package/dist/assets/{profiles → improve-strategies}/recombine-only.json +0 -0
  261. /package/dist/assets/{profiles → improve-strategies}/synthesize.json +0 -0
  262. /package/dist/assets/{profiles → improve-strategies}/thorough.json +0 -0
@@ -0,0 +1,125 @@
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
+ * OpenHands CLI agent command builder (P2, plan §"The adapter contract"
6
+ * step 2 / §"Capability matrix").
7
+ *
8
+ * Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
9
+ * headless argv the `openhands` CLI expects. Per the capability matrix the
10
+ * headless invocation is:
11
+ *
12
+ * openhands --headless -t "<p>" --json
13
+ *
14
+ * with `--json` producing the documented JSONL event stream (structured tier
15
+ * "via prompt+validate"), workspace-state resume (no session-id flag), and
16
+ * native MCP support.
17
+ *
18
+ * Platform-specific mapping decisions (all localized here, per the adapter
19
+ * contract):
20
+ *
21
+ * - **prompt** — `--task` is the long form of the matrix's `-t` (used for
22
+ * self-documenting argv, mirroring the aider builder's `--message`). Like
23
+ * aider — and unlike claude/codex/pi — there is NO positional prompt and NO
24
+ * `--` end-of-options separator to hide behind: the task is a *flag value*.
25
+ * The glued `--task=<payload>` form is therefore used so a payload that
26
+ * begins with `-`/`--` binds to the flag lexically and can never be parsed
27
+ * as a separate option by the CLI's argument parser.
28
+ * - **--headless / --json** — always emitted: `--headless` is what makes a
29
+ * captured, unattended run possible, and `--json` switches stdout to the
30
+ * JSONL event stream that `./result-extractor.ts` normalizes (the same
31
+ * "dispatch is the captured path" reasoning as the Claude builder's
32
+ * unconditional `--print`; both flags are the matrix's documented headless
33
+ * shape).
34
+ * - **systemPrompt** — OpenHands headless mode has no documented
35
+ * system-prompt flag (agent behaviour is shaped by its own agent configs
36
+ * and microagents). Folded into the task payload — system text first, blank
37
+ * line, then the task — mirroring the aider/codex treatment.
38
+ * - **model** — OpenHands does not take the model on the headless command
39
+ * line; its documented configuration channel is the `LLM_MODEL` environment
40
+ * variable (config.toml `[llm] model` equivalent). The alias is resolved via
41
+ * `resolveModel` with this platform id and returned on `BuiltCommand.env`
42
+ * (the seam exists precisely for platform-specific extras), NOT as an
43
+ * invented `--model` flag that would produce a silently broken command.
44
+ * - **schema** — the matrix places OpenHands in the "via prompt+validate"
45
+ * tier (plan §"Structured-output normalization", tier "native-json"): no
46
+ * Codex-style `--output-schema` flag exists, so NO temp schema file is
47
+ * written; the JSON Schema is injected into the task payload using the
48
+ * exact directive wording of the engine's prompt assembly
49
+ * (`step-work.ts` `buildUnitPrompt`) and the pi/aider builders, so
50
+ * all dispatch paths speak one dialect. Downstream, the extractor pulls the
51
+ * final message out of the JSONL stream and the engine's shared
52
+ * retry-until-valid loop performs the actual validation.
53
+ * - **tools** — deliberately unconsumed. OpenHands has no per-tool allowlist
54
+ * flag; tool access is governed by its own runtime/sandbox configuration
55
+ * and (natively supported) MCP config. A restrictive policy is therefore
56
+ * dropped rather than approximated — never silently widened.
57
+ * - **resume/session** — NOT expressible: per the matrix OpenHands resumes
58
+ * from *workspace state*, not a session-id flag, so there is no flag-shaped
59
+ * `HarnessResumeSupport` to export. The extractor still captures a
60
+ * conversation/session id opportunistically when the stream reveals one;
61
+ * akm's `workflow_run_units` remains the durable source of truth either way
62
+ * (plan §"Session, MCP, and identity across harnesses").
63
+ * - **effort** — stays unconsumed (reserved; the shared request contract's
64
+ * "no builder consumes it yet" note stays true).
65
+ *
66
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
67
+ * follow-up integration task (as is the registry entry declaring
68
+ * `pattern: "local-runner"`, `structuredOutput: "native-json"`, and no
69
+ * `resume`). Exported standalone so that task only adds a registry entry.
70
+ */
71
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
72
+ /** Canonical harness/platform id used for model-alias resolution. */
73
+ export const OPENHANDS_PLATFORM = "openhands";
74
+ /**
75
+ * Env var OpenHands reads its LLM model from — the platform's documented
76
+ * model-selection channel for headless runs (there is no headless model
77
+ * flag). Exported so the follow-up integration task and tests share the one
78
+ * constant.
79
+ */
80
+ export const OPENHANDS_MODEL_ENV = "LLM_MODEL";
81
+ /**
82
+ * Assemble the `--task` payload: optional system text, the task prompt, and —
83
+ * when a schema is requested — the same schema directive the workflow
84
+ * engine's prompt assembly uses (OpenHands has no native schema flag, so the
85
+ * prompt is the schema's only channel; plan §"Structured-output
86
+ * normalization").
87
+ */
88
+ function buildTaskPayload(req) {
89
+ const sections = [];
90
+ if (req.systemPrompt)
91
+ sections.push(req.systemPrompt);
92
+ sections.push(req.prompt);
93
+ if (req.schema) {
94
+ sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
95
+ }
96
+ return sections.join("\n\n");
97
+ }
98
+ /**
99
+ * OpenHands builder.
100
+ * Command shape:
101
+ * openhands --headless --json --task=<[system\n\n]prompt[\n\nschema directive]>
102
+ * with the resolved model (if any) carried on env as LLM_MODEL.
103
+ */
104
+ export const openhandsBuilder = {
105
+ platform: OPENHANDS_PLATFORM,
106
+ build(profile, req) {
107
+ assertNotFlag(req.systemPrompt, "systemPrompt");
108
+ assertNotFlag(req.model, "model");
109
+ const args = [...profile.args];
110
+ // Headless essentials (matrix shape): non-interactive run, JSONL stdout
111
+ // for the extractor.
112
+ args.push("--headless");
113
+ args.push("--json");
114
+ // Glued form: the payload is a flag VALUE (no positional prompt exists),
115
+ // so `=` binding keeps a dash-leading payload from parsing as an option.
116
+ args.push(`--task=${buildTaskPayload(req)}`);
117
+ let env;
118
+ if (req.model) {
119
+ const resolved = resolveDispatchModel(req, profile, OPENHANDS_PLATFORM);
120
+ // Model travels via env, not argv — OpenHands' documented channel.
121
+ env = { [OPENHANDS_MODEL_ENV]: resolved };
122
+ }
123
+ return { argv: [profile.bin, ...args], ...(env ? { env } : {}) };
124
+ },
125
+ };
@@ -0,0 +1,57 @@
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
+ * OpenHands CLI harness (P2 integration, plan §"The adapter contract").
6
+ *
7
+ * Per-harness barrel gathering the OpenHands integration surfaces:
8
+ * - agent command builder → ./agent-builder.ts (openhandsBuilder)
9
+ * - result extractor → ./result-extractor.ts (openhandsResultExtractor)
10
+ *
11
+ * It also defines {@link OpenhandsHarness}, the {@link AkmHarness} descriptor
12
+ * that `HARNESS_REGISTRY` registers. Dispatch-only: no native session-log
13
+ * reader or config importer yet.
14
+ */
15
+ import { BaseHarness } from "../types.js";
16
+ import { openhandsBuilder } from "./agent-builder.js";
17
+ import { openhandsResultExtractor } from "./result-extractor.js";
18
+ export { OPENHANDS_MODEL_ENV, OPENHANDS_PLATFORM, openhandsBuilder } from "./agent-builder.js";
19
+ export { openhandsResultExtractor } 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
+ ...c,
28
+ };
29
+ }
30
+ /**
31
+ * OpenHands CLI.
32
+ *
33
+ * Canonical id is `'openhands'`; no alias or distinct runtime identity.
34
+ */
35
+ export class OpenhandsHarness extends BaseHarness {
36
+ id = "openhands";
37
+ displayName = "OpenHands";
38
+ aliases = [];
39
+ agentBuilder = openhandsBuilder;
40
+ resultExtractor = openhandsResultExtractor;
41
+ // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
42
+ // akm spawns `openhands --headless` locally per unit ⇒ local-runner.
43
+ pattern = "local-runner";
44
+ // `--json` emits a documented JSONL event stream akm parses, then validates
45
+ // against the node schema ⇒ native-json tier.
46
+ structuredOutput = "native-json";
47
+ // No `resume`: per the matrix OpenHands resumes from workspace state, not a
48
+ // session-id flag. The extractor still captures a conversation id
49
+ // opportunistically; akm's `workflow_run_units` remains the durable source
50
+ // of truth.
51
+ // No `identityEnv`: the matrix lists OpenHands' identity markers as
52
+ // uncertain, and it stamps no session var onto child processes.
53
+ capabilities = caps({
54
+ agentDispatch: true,
55
+ detection: true,
56
+ });
57
+ }
@@ -0,0 +1,103 @@
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
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ /** Keys that may carry the harness-native session id, in precedence order. */
8
+ const SESSION_KEYS = ["session_id", "sessionId", "sid", "conversation_id"];
9
+ /** Return a non-empty string, else undefined. */
10
+ function nonEmptyString(value) {
11
+ return typeof value === "string" && value.length > 0 ? value : undefined;
12
+ }
13
+ /**
14
+ * Extract the agent's answer text from one parsed OpenHands event, if the
15
+ * event is one of the two message-bearing kinds (see module doc). Everything
16
+ * else — user echoes, observations, tool actions — yields undefined.
17
+ */
18
+ function extractAgentText(event) {
19
+ // Only agent-authored events count; a missing `source` is tolerated for
20
+ // finish/message actions from older serializations, but an explicit
21
+ // non-agent source (user echo, environment) never contributes.
22
+ if (event.source !== undefined && event.source !== "agent")
23
+ return undefined;
24
+ if (typeof event.action !== "string")
25
+ return undefined;
26
+ const args = isRecord(event.args) ? event.args : undefined;
27
+ if (event.action === "message") {
28
+ return nonEmptyString(args?.content) ?? nonEmptyString(event.message);
29
+ }
30
+ if (event.action === "finish") {
31
+ return nonEmptyString(args?.final_thought) ?? nonEmptyString(args?.thought) ?? nonEmptyString(event.message);
32
+ }
33
+ return undefined;
34
+ }
35
+ /** Extract a harness-native session id from one parsed JSON value, if any. */
36
+ function extractSessionId(event) {
37
+ for (const key of SESSION_KEYS) {
38
+ const candidate = nonEmptyString(event[key]);
39
+ if (candidate)
40
+ return candidate;
41
+ }
42
+ return undefined;
43
+ }
44
+ /** JSON.parse that returns undefined instead of throwing. */
45
+ function tryParseJson(raw) {
46
+ try {
47
+ return JSON.parse(raw);
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ /**
54
+ * Normalize a raw openhands run result into `{ text, sessionId? }`.
55
+ * See the module doc for the stdout shapes handled.
56
+ */
57
+ export const openhandsResultExtractor = (result) => {
58
+ // The raw result's own sessionId (SDK-style paths) is the fallback; an id
59
+ // found in the output stream wins because it is fresher.
60
+ const fallbackSessionId = result.sessionId;
61
+ const raw = result.stdout.trim();
62
+ // Shape 1 — whole stdout is one JSON document (spawn may have pre-parsed it
63
+ // when the profile requested parseOutput: "json"). A multi-event JSONL
64
+ // stream never parses as a single document, so this cannot shadow shape 2.
65
+ const whole = result.parsed !== undefined ? result.parsed : raw.length > 0 ? tryParseJson(raw) : undefined;
66
+ if (whole !== undefined) {
67
+ if (typeof whole === "string") {
68
+ return { text: whole, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
69
+ }
70
+ if (isRecord(whole)) {
71
+ const sessionId = extractSessionId(whole) ?? fallbackSessionId;
72
+ // Unknown envelope (no agent message): fall back to raw stdout so
73
+ // downstream embedded-JSON parsing still has material to work with.
74
+ const text = extractAgentText(whole) ?? raw;
75
+ return { text, ...(sessionId ? { sessionId } : {}) };
76
+ }
77
+ }
78
+ // Shape 2 — JSONL event stream (`--json`): last message-bearing agent event
79
+ // wins; first id-bearing event supplies the session id. Non-JSON banner
80
+ // lines are skipped.
81
+ let lastText;
82
+ let streamSessionId;
83
+ let sawJsonLine = false;
84
+ for (const line of raw.split("\n")) {
85
+ const trimmed = line.trim();
86
+ if (trimmed.length === 0)
87
+ continue;
88
+ const event = tryParseJson(trimmed);
89
+ if (event === undefined || !isRecord(event))
90
+ continue;
91
+ sawJsonLine = true;
92
+ streamSessionId ??= extractSessionId(event);
93
+ const text = extractAgentText(event);
94
+ if (text !== undefined)
95
+ lastText = text;
96
+ }
97
+ if (sawJsonLine) {
98
+ const sessionId = streamSessionId ?? fallbackSessionId;
99
+ return { text: lastText ?? raw, ...(sessionId ? { sessionId } : {}) };
100
+ }
101
+ // Shape 3 — plain text passthrough.
102
+ return { text: raw, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
103
+ };
@@ -0,0 +1,103 @@
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
+ * Pi coding-agent CLI command builder (P2, plan §"The adapter contract"
6
+ * step 2 / §"Capability matrix").
7
+ *
8
+ * Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
9
+ * headless argv the `pi` CLI expects. Per the capability matrix the headless
10
+ * invocation is:
11
+ *
12
+ * pi -p "<prompt>"
13
+ *
14
+ * with `--mode json` for structured (JSONL) output, `--model <m>` for model
15
+ * selection, and `-c`/`-r`/`--session <id>` for resume. Resume is
16
+ * registry-side (`AkmHarness.resume`, flag-shaped: {@link PI_RESUME_FLAG}) —
17
+ * not built here because `AgentDispatchRequest` carries no session id.
18
+ *
19
+ * Platform-specific mapping decisions (all localized here, per the adapter
20
+ * contract):
21
+ *
22
+ * - **prompt** — `-p` is Pi's non-interactive print mode (same convention as
23
+ * Claude Code's `--print`); the prompt is the trailing positional message.
24
+ * The `--` end-of-options separator precedes it, mirroring the claude/codex
25
+ * builders, so a prompt whose text begins with `-`/`--` can never be parsed
26
+ * as flags.
27
+ * - **systemPrompt** — passed via `--system-prompt` (Pi follows the Claude
28
+ * Code flag conventions), guarded by `assertNotFlag`.
29
+ * - **schema** — the matrix places Pi in the "via prompt+validate" tier (no
30
+ * native `--output-schema` equivalent, unlike Codex), so the JSON Schema is
31
+ * passed through the prompt: a directive matching the engine's wording
32
+ * (`step-work.ts` `buildUnitPrompt`) is appended to the prompt
33
+ * payload, and `--mode json` is emitted so stdout is the documented JSONL
34
+ * event stream that `./result-extractor.ts` normalizes. The engine's shared
35
+ * retry-until-valid loop performs the actual validation. Without a schema
36
+ * the argv matches the matrix's bare headless shape (`pi -p "<p>"`) and the
37
+ * extractor's plain-text path applies.
38
+ * - **tools** — deliberately unconsumed. Pi manages tool access through its
39
+ * own config/extension system (the matrix lists MCP as "extensions only");
40
+ * there is no documented per-tool allowlist flag, and inventing one would
41
+ * produce a silently broken command. A restrictive policy is therefore
42
+ * dropped rather than approximated — never silently widened.
43
+ * - **effort** — stays unconsumed (reserved; the shared request contract's
44
+ * "no builder consumes it yet" note stays true).
45
+ *
46
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
47
+ * follow-up integration task (as are the `PI_*` identity-env markers, which
48
+ * are registry-side). Exported standalone so that task only adds a registry
49
+ * entry.
50
+ */
51
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
52
+ /** Canonical harness/platform id used for model-alias resolution. */
53
+ export const PI_PLATFORM = "pi";
54
+ /**
55
+ * Flag-shaped resume support per the capability matrix (`--session <id>`).
56
+ * Exported for the integration task's `AkmHarness.resume` registry entry; the
57
+ * harness-native session id comes from the unit row (stored opportunistically
58
+ * by the result extractor) — akm never depends on it (plan §"Session, MCP,
59
+ * and identity across harnesses").
60
+ */
61
+ export const PI_RESUME_FLAG = "--session";
62
+ /**
63
+ * Assemble the positional prompt payload: the task prompt and — when a schema
64
+ * is requested — the same schema directive the workflow engine's prompt
65
+ * assembly uses, so both dispatch paths speak one dialect.
66
+ */
67
+ function buildPromptPayload(req) {
68
+ const sections = [req.prompt];
69
+ if (req.schema) {
70
+ sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
71
+ }
72
+ return sections.join("\n\n");
73
+ }
74
+ /**
75
+ * Pi builder.
76
+ * Command shape:
77
+ * pi [--system-prompt "..."] [--model <m>] [--mode json] -p -- "<prompt[\n\nschema directive]>"
78
+ */
79
+ export const piBuilder = {
80
+ platform: PI_PLATFORM,
81
+ build(profile, req) {
82
+ assertNotFlag(req.systemPrompt, "systemPrompt");
83
+ assertNotFlag(req.model, "model");
84
+ const args = [...profile.args];
85
+ if (req.systemPrompt) {
86
+ args.push("--system-prompt", req.systemPrompt);
87
+ }
88
+ if (req.model) {
89
+ const resolved = resolveDispatchModel(req, profile, PI_PLATFORM);
90
+ args.push("--model", resolved);
91
+ }
92
+ if (req.schema) {
93
+ // Structured unit: JSONL event stream on stdout — the pi result
94
+ // extractor's documented input (prompt+validate tier).
95
+ args.push("--mode", "json");
96
+ }
97
+ // -p = non-interactive print mode; prompt is the trailing positional.
98
+ args.push("-p");
99
+ args.push("--");
100
+ args.push(buildPromptPayload(req));
101
+ return { argv: [profile.bin, ...args] };
102
+ },
103
+ };
@@ -0,0 +1,57 @@
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
+ * Pi coding-agent CLI harness (P2 integration, plan §"The adapter contract").
6
+ *
7
+ * Per-harness barrel gathering the Pi integration surfaces:
8
+ * - agent command builder → ./agent-builder.ts (piBuilder)
9
+ * - result extractor → ./result-extractor.ts (piResultExtractor)
10
+ *
11
+ * It also defines {@link PiHarness}, 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 { PI_RESUME_FLAG, piBuilder } from "./agent-builder.js";
17
+ import { piResultExtractor } from "./result-extractor.js";
18
+ export { PI_PLATFORM, PI_RESUME_FLAG, piBuilder } from "./agent-builder.js";
19
+ export { piResultExtractor } 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
+ ...c,
28
+ };
29
+ }
30
+ /**
31
+ * Pi coding-agent CLI.
32
+ *
33
+ * Canonical id is `'pi'`; no alias or distinct runtime identity.
34
+ */
35
+ export class PiHarness extends BaseHarness {
36
+ id = "pi";
37
+ displayName = "Pi";
38
+ aliases = [];
39
+ agentBuilder = piBuilder;
40
+ resultExtractor = piResultExtractor;
41
+ // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
42
+ // akm spawns the `pi` CLI locally per unit ⇒ local-runner.
43
+ pattern = "local-runner";
44
+ // `--mode json` emits a documented JSONL event stream akm parses, then
45
+ // validates against the node schema ⇒ native-json tier.
46
+ structuredOutput = "native-json";
47
+ // `pi --session <id>` replays a previous session (the matrix's -c/-r/
48
+ // --session family; the id-taking long form is the registered flag).
49
+ resume = { flag: PI_RESUME_FLAG, takesSessionId: true };
50
+ // Session-id env marker only — the matrix's bare PI_* presence vars must
51
+ // not stamp identity onto manual runs (see `AkmHarness.identityEnv`).
52
+ identityEnv = ["PI_SESSION_ID"];
53
+ capabilities = caps({
54
+ agentDispatch: true,
55
+ detection: true,
56
+ });
57
+ }
@@ -0,0 +1,135 @@
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
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ /** Keys that may carry the harness-native session id, in precedence order. */
8
+ const SESSION_KEYS = ["session_id", "sessionId", "session"];
9
+ /**
10
+ * Coerce a candidate value into text. Handles the shapes Pi nests assistant
11
+ * answers in: bare strings, arrays of content blocks, and `{content|text}`
12
+ * wrappers. Blocks without a `content`/`text` string (thinking, tool-use)
13
+ * contribute nothing. Depth-bounded: JSON.parse output is acyclic, but
14
+ * nesting is capped anyway so a pathological envelope cannot recurse
15
+ * unboundedly.
16
+ */
17
+ function textOf(value, depth = 0) {
18
+ if (depth > 4)
19
+ return undefined;
20
+ if (typeof value === "string")
21
+ return value;
22
+ if (Array.isArray(value)) {
23
+ const parts = [];
24
+ for (const block of value) {
25
+ const t = textOf(block, depth + 1);
26
+ if (t)
27
+ parts.push(t);
28
+ }
29
+ return parts.length > 0 ? parts.join("\n") : undefined;
30
+ }
31
+ if (isRecord(value)) {
32
+ return textOf(value.content ?? value.text, depth + 1);
33
+ }
34
+ return undefined;
35
+ }
36
+ /** Extract the text of one assistant-authored message record, if it is one. */
37
+ function assistantMessageText(message) {
38
+ if (!isRecord(message) || message.role !== "assistant")
39
+ return undefined;
40
+ return textOf(message.content ?? message.text);
41
+ }
42
+ /**
43
+ * Extract assistant text from one parsed JSON event/value:
44
+ * `message` envelopes (`message_start`/`message_end`), `messages` transcripts
45
+ * (`agent_end` — last assistant entry wins), and tolerant flat
46
+ * `role:"assistant"` events.
47
+ */
48
+ function extractAssistantText(event) {
49
+ const fromEnvelope = assistantMessageText(event.message);
50
+ if (fromEnvelope !== undefined)
51
+ return fromEnvelope;
52
+ const transcript = event.messages;
53
+ if (Array.isArray(transcript)) {
54
+ for (let i = transcript.length - 1; i >= 0; i--) {
55
+ const text = assistantMessageText(transcript[i]);
56
+ if (text !== undefined)
57
+ return text;
58
+ }
59
+ return undefined;
60
+ }
61
+ return assistantMessageText(event);
62
+ }
63
+ /** Extract a harness-native session id from one parsed JSON value, if any. */
64
+ function extractSessionId(event) {
65
+ for (const key of SESSION_KEYS) {
66
+ const candidate = event[key];
67
+ if (typeof candidate === "string" && candidate.length > 0)
68
+ return candidate;
69
+ }
70
+ // Session-lifecycle events may carry the id under a bare `id` key.
71
+ if (typeof event.type === "string" && event.type.startsWith("session") && typeof event.id === "string" && event.id) {
72
+ return event.id;
73
+ }
74
+ return undefined;
75
+ }
76
+ /** JSON.parse that returns undefined instead of throwing. */
77
+ function tryParseJson(raw) {
78
+ try {
79
+ return JSON.parse(raw);
80
+ }
81
+ catch {
82
+ return undefined;
83
+ }
84
+ }
85
+ /**
86
+ * Normalize a raw pi run result into `{ text, sessionId? }`.
87
+ * See the module doc for the stdout shapes handled.
88
+ */
89
+ export const piResultExtractor = (result) => {
90
+ // The raw result's own sessionId (SDK-style paths) is the fallback; an id
91
+ // found in the output stream wins because it is fresher.
92
+ const fallbackSessionId = result.sessionId;
93
+ const raw = result.stdout.trim();
94
+ // Shape 1 — whole stdout is one JSON document (spawn may have pre-parsed it
95
+ // when the profile requested parseOutput: "json"). A multi-event JSONL
96
+ // stream never parses as a single document, so this cannot shadow shape 2.
97
+ const whole = result.parsed !== undefined ? result.parsed : raw.length > 0 ? tryParseJson(raw) : undefined;
98
+ if (whole !== undefined) {
99
+ if (typeof whole === "string") {
100
+ return { text: whole, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
101
+ }
102
+ if (isRecord(whole)) {
103
+ const sessionId = extractSessionId(whole) ?? fallbackSessionId;
104
+ // Unknown envelope (no assistant text): fall back to raw stdout so
105
+ // downstream embedded-JSON parsing still has material to work with.
106
+ const text = extractAssistantText(whole) ?? textOf(whole) ?? raw;
107
+ return { text, ...(sessionId ? { sessionId } : {}) };
108
+ }
109
+ }
110
+ // Shape 2 — JSONL event stream (`--mode json`): last assistant text-bearing
111
+ // event wins; first session-id-bearing event supplies the id. Non-JSON
112
+ // banner lines are skipped.
113
+ let lastText;
114
+ let streamSessionId;
115
+ let sawJsonLine = false;
116
+ for (const line of raw.split("\n")) {
117
+ const trimmed = line.trim();
118
+ if (trimmed.length === 0)
119
+ continue;
120
+ const event = tryParseJson(trimmed);
121
+ if (event === undefined || !isRecord(event))
122
+ continue;
123
+ sawJsonLine = true;
124
+ streamSessionId ??= extractSessionId(event);
125
+ const text = extractAssistantText(event);
126
+ if (text !== undefined && text.length > 0)
127
+ lastText = text;
128
+ }
129
+ if (sawJsonLine) {
130
+ const sessionId = streamSessionId ?? fallbackSessionId;
131
+ return { text: lastText ?? raw, ...(sessionId ? { sessionId } : {}) };
132
+ }
133
+ // Shape 3 — plain text passthrough.
134
+ return { text: raw, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
135
+ };
@@ -4,41 +4,17 @@
4
4
  /**
5
5
  * Shared base for harness descriptors (#566).
6
6
  *
7
- * Provides the default v1-profile-name matcher so the inference logic lives in
8
- * ONE place instead of being duplicated across the old `guessAgentPlatform()`
9
- * (config-migration) and the `name.includes("claude")` heuristic (setup). A
10
- * concrete harness sets `id`/`aliases`/`capabilities` and, when its v1 profile
11
- * names could be decorated (e.g. `"opencode-sdk-fast"`), `v1ProfilePrefixes`.
7
+ * Provides shared optional descriptor fields for concrete harnesses.
12
8
  */
13
9
  export class BaseHarness {
14
10
  runtimeId;
15
11
  setupDetectionDir;
16
12
  agentBuilder;
17
- /**
18
- * Lowercase prefixes that a decorated v1 profile name may start with and
19
- * still belong to this harness (e.g. `["opencode-sdk"]`). The canonical id
20
- * and aliases are always matched in addition to these; an empty list means
21
- * only exact id/alias matches.
22
- */
23
- v1ProfilePrefixes = [];
24
- matchesV1ProfileName(name) {
25
- // Only harnesses with a v1→v2 mapping participate; others never claim a
26
- // legacy profile name (so unknown names are dropped, not misclassified).
27
- if (!this.capabilities.v1Migration)
28
- return false;
29
- const lower = name.trim().toLowerCase();
30
- if (!lower)
31
- return false;
32
- if (lower === this.id.toLowerCase())
33
- return true;
34
- for (const alias of this.aliases) {
35
- if (lower === alias.toLowerCase())
36
- return true;
37
- }
38
- for (const prefix of this.v1ProfilePrefixes) {
39
- if (lower.startsWith(prefix.toLowerCase()))
40
- return true;
41
- }
42
- return false;
43
- }
13
+ pattern;
14
+ structuredOutput;
15
+ resume;
16
+ identityEnv;
17
+ presenceEnv;
18
+ resultExtractor;
19
+ sessionLogProvider;
44
20
  }