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,121 @@
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
+ * GitHub Copilot 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 `copilot` CLI expects. Per the capability matrix the
10
+ * headless invocation is:
11
+ *
12
+ * copilot -p "<prompt>" --allow-all-tools
13
+ *
14
+ * with `--output-format json` for structured output, `--model <m>` for model
15
+ * selection, and `--resume <id>` for resume (resume is registry-side —
16
+ * `AkmHarness.resume` — not built here because `AgentDispatchRequest` carries
17
+ * no session id).
18
+ *
19
+ * Platform-specific mapping decisions (all localized here, per the adapter
20
+ * contract):
21
+ *
22
+ * - **systemPrompt** — Copilot CLI has no system-prompt flag (it reads
23
+ * repo/user custom-instructions files instead), so the system prompt is
24
+ * folded into the `-p` payload ahead of the task prompt, separated by a
25
+ * blank line. `assertNotFlag` still guards it so a `--`-prefixed system
26
+ * prompt cannot turn the front of the `-p` value into a flag.
27
+ * - **schema** — the matrix places Copilot in the "via prompt+validate" tier
28
+ * (no native `--output-schema` equivalent, unlike Codex), so the JSON
29
+ * Schema is passed through the prompt: a directive matching the engine's
30
+ * wording (`step-work.ts` `buildUnitPrompt`) is appended to the `-p`
31
+ * payload, and `--output-format json` is emitted so stdout is the
32
+ * documented JSON envelope the copilot result extractor normalizes. The
33
+ * engine's shared retry-until-valid loop performs the actual validation.
34
+ * - **tools** — a string/array tool policy maps to repeated
35
+ * `--allow-tool <t>` flags (Copilot's per-tool approval flag). With no
36
+ * policy at all, headless runs need autonomy, so `--allow-all-tools` is
37
+ * emitted per the matrix. A *structured* policy object is NOT expressible
38
+ * as Copilot flags; it is deliberately dropped without falling back to
39
+ * `--allow-all-tools` (never silently widen a restriction) — in
40
+ * programmatic mode Copilot then denies unapproved tool calls, which is the
41
+ * conservative failure mode.
42
+ *
43
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
44
+ * follow-up integration task. Exported standalone so that task only adds a
45
+ * registry entry.
46
+ */
47
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
48
+ /** Canonical harness/platform id used for model-alias resolution. */
49
+ export const COPILOT_PLATFORM = "copilot";
50
+ /**
51
+ * Split a tool policy into individual tool names for `--allow-tool`.
52
+ * Strings are comma-separated lists; arrays are taken as-is. Structured
53
+ * policy objects return `undefined` (not expressible as Copilot flags — see
54
+ * module doc).
55
+ */
56
+ function toolPolicyEntries(tools) {
57
+ if (typeof tools === "string") {
58
+ return tools
59
+ .split(",")
60
+ .map((t) => t.trim())
61
+ .filter(Boolean);
62
+ }
63
+ if (Array.isArray(tools)) {
64
+ return tools.map((t) => t.trim()).filter(Boolean);
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Assemble the `-p` payload: optional system prompt, the task prompt, and —
70
+ * when a schema is requested — the same schema directive the workflow
71
+ * engine's prompt assembly uses, so both dispatch paths speak one dialect.
72
+ */
73
+ function buildPromptPayload(req) {
74
+ const sections = [];
75
+ if (req.systemPrompt)
76
+ sections.push(req.systemPrompt);
77
+ sections.push(req.prompt);
78
+ if (req.schema) {
79
+ sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
80
+ }
81
+ return sections.join("\n\n");
82
+ }
83
+ /**
84
+ * GitHub Copilot CLI builder.
85
+ * Command shape:
86
+ * copilot [--model <m>] (--allow-all-tools | --allow-tool <t> ...)
87
+ * [--output-format json] -p "<systemPrompt?\n\nprompt\n\nschema?>"
88
+ */
89
+ export const copilotBuilder = {
90
+ platform: COPILOT_PLATFORM,
91
+ build(profile, req) {
92
+ assertNotFlag(req.systemPrompt, "systemPrompt");
93
+ assertNotFlag(req.model, "model");
94
+ const args = [...profile.args];
95
+ if (req.model) {
96
+ const resolved = resolveDispatchModel(req, profile, COPILOT_PLATFORM);
97
+ args.push("--model", resolved);
98
+ }
99
+ if (req.tools) {
100
+ const entries = toolPolicyEntries(req.tools);
101
+ // Structured policy objects (entries === undefined) emit NO allow flags:
102
+ // dropping a restriction must never widen to --allow-all-tools.
103
+ for (const tool of entries ?? []) {
104
+ assertNotFlag(tool, "tools entry");
105
+ args.push("--allow-tool", tool);
106
+ }
107
+ }
108
+ else {
109
+ // Headless default per the capability matrix: units must run without
110
+ // interactive tool-approval prompts.
111
+ args.push("--allow-all-tools");
112
+ }
113
+ if (req.schema) {
114
+ // Structured unit: ask for the documented JSON envelope so the result
115
+ // extractor can pull the final message + session id deterministically.
116
+ args.push("--output-format", "json");
117
+ }
118
+ args.push("-p", buildPromptPayload(req));
119
+ return { argv: [profile.bin, ...args] };
120
+ },
121
+ };
@@ -0,0 +1,59 @@
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
+ * GitHub Copilot CLI harness (P2 integration, plan §"The adapter contract").
6
+ *
7
+ * Per-harness barrel gathering the Copilot CLI integration surfaces:
8
+ * - agent command builder → ./agent-builder.ts (copilotBuilder)
9
+ * - result extractor → ./result-extractor.ts (copilotResultExtractor)
10
+ *
11
+ * It also defines {@link CopilotHarness}, the {@link AkmHarness} descriptor
12
+ * that `HARNESS_REGISTRY` registers. This is the LOCAL Copilot CLI
13
+ * (`copilot -p …`); the cloud "Copilot coding agent" is the plan's
14
+ * cloud-delegate pattern and is a separate, future descriptor.
15
+ */
16
+ import { BaseHarness } from "../types.js";
17
+ import { copilotBuilder } from "./agent-builder.js";
18
+ import { copilotResultExtractor } from "./result-extractor.js";
19
+ export { COPILOT_PLATFORM, copilotBuilder } from "./agent-builder.js";
20
+ export { copilotResultExtractor } from "./result-extractor.js";
21
+ function caps(c) {
22
+ return {
23
+ sessionLogs: false,
24
+ agentDispatch: false,
25
+ detection: false,
26
+ configImport: false,
27
+ runtimeIdentity: false,
28
+ ...c,
29
+ };
30
+ }
31
+ /**
32
+ * GitHub Copilot CLI (local headless CLI, not the cloud coding agent).
33
+ *
34
+ * Canonical id is `'copilot'`; no alias or distinct runtime identity.
35
+ */
36
+ export class CopilotHarness extends BaseHarness {
37
+ id = "copilot";
38
+ displayName = "GitHub Copilot CLI";
39
+ aliases = [];
40
+ agentBuilder = copilotBuilder;
41
+ resultExtractor = copilotResultExtractor;
42
+ // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
43
+ // akm spawns the `copilot` CLI locally per unit ⇒ local-runner.
44
+ pattern = "local-runner";
45
+ // `--output-format json` emits a documented JSON envelope akm parses, then
46
+ // validates against the node schema ⇒ native-json tier.
47
+ structuredOutput = "native-json";
48
+ // `copilot --resume <sessionId>` replays a previous session.
49
+ resume = { flag: "--resume", takesSessionId: true };
50
+ // Session-id env marker only. The matrix's other candidates (GH_TOKEN,
51
+ // bare COPILOT_* presence vars) are credential/presence flags that would
52
+ // stamp identity onto manual runs, so they are deliberately NOT registered
53
+ // (see `AkmHarness.identityEnv`).
54
+ identityEnv = ["COPILOT_SESSION_ID"];
55
+ capabilities = caps({
56
+ agentDispatch: true,
57
+ detection: true,
58
+ });
59
+ }
@@ -0,0 +1,151 @@
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 final answer, in precedence order. */
8
+ const TEXT_KEYS = ["result", "response", "text", "output", "content", "message"];
9
+ /** Keys that may carry the harness-native session id, in precedence order. */
10
+ const SESSION_KEYS = ["session_id", "sessionId", "conversation_id", "conversationId", "thread_id", "threadId"];
11
+ /**
12
+ * Coerce a candidate value into text. Handles the shapes Copilot's envelope
13
+ * nests answers in: bare strings, `{content|text}` wrappers (assistant
14
+ * message objects), and arrays of `{type:"text",text}` content blocks.
15
+ * Depth-bounded: JSON.parse output is acyclic, but nesting is capped anyway
16
+ * so a pathological envelope cannot recurse unboundedly.
17
+ */
18
+ function textOf(value, depth = 0) {
19
+ if (depth > 4)
20
+ return undefined;
21
+ if (typeof value === "string")
22
+ return value;
23
+ if (Array.isArray(value)) {
24
+ const parts = [];
25
+ for (const block of value) {
26
+ const t = textOf(block, depth + 1);
27
+ if (t)
28
+ parts.push(t);
29
+ }
30
+ return parts.length > 0 ? parts.join("\n") : undefined;
31
+ }
32
+ if (isRecord(value)) {
33
+ return textOf(value.content ?? value.text, depth + 1);
34
+ }
35
+ return undefined;
36
+ }
37
+ /** Extract the final-answer text from one parsed JSON value, if any. */
38
+ function extractText(value) {
39
+ if (typeof value === "string")
40
+ return value;
41
+ if (!isRecord(value))
42
+ return undefined;
43
+ for (const key of TEXT_KEYS) {
44
+ if (!(key in value))
45
+ continue;
46
+ const text = textOf(value[key]);
47
+ if (text !== undefined)
48
+ return text;
49
+ }
50
+ return undefined;
51
+ }
52
+ /** Extract a harness-native session id from one parsed JSON value, if any. */
53
+ function extractSessionId(value) {
54
+ if (!isRecord(value))
55
+ return undefined;
56
+ for (const key of SESSION_KEYS) {
57
+ const candidate = value[key];
58
+ if (typeof candidate === "string" && candidate.length > 0)
59
+ return candidate;
60
+ }
61
+ return undefined;
62
+ }
63
+ /**
64
+ * `type` discriminator values Copilot's `--output-format json` envelopes are
65
+ * documented to use (plus the `session.*` event family). Deliberately a
66
+ * CLOSED set: a schema unit's own answer may legitimately be a discriminated
67
+ * union with a `type` field (`{"type":"success","output":"data"}`), and
68
+ * treating ANY string `type` as a transport marker would unwrap — and
69
+ * corrupt — such answers. An unrecognized type degrades to raw pass-through,
70
+ * which the schema validator handles (whole object) and free-text tolerates.
71
+ */
72
+ const ENVELOPE_TYPES = new Set(["result", "response", "message", "assistant", "error"]);
73
+ /**
74
+ * Does this parsed object look like one of Copilot's transport ENVELOPES (a
75
+ * `result`/event document) rather than a bare JSON answer the model produced?
76
+ *
77
+ * Copilot's `--output-format json` documents always carry a transport marker:
78
+ * a known `type` discriminator (`"result"`, `"session.start"`, `"message"`, …)
79
+ * and, in every documented result envelope, a session-id field. A bare
80
+ * structured answer a schema unit was asked for — e.g. `{"result":"ok"}` or
81
+ * `{"type":"success","output":"data"}` — carries neither a known discriminator
82
+ * nor (normally) a session-id key. Unwrapping such an answer would hand the
83
+ * schema validator only the field value (`"ok"`) and reject the run despite
84
+ * valid JSON (PR #714 review), so we only unwrap objects that are genuinely
85
+ * envelopes and pass everything else through raw.
86
+ */
87
+ function isTransportEnvelope(value) {
88
+ const type = value.type;
89
+ if (typeof type === "string" && (ENVELOPE_TYPES.has(type) || type.startsWith("session.")))
90
+ return true;
91
+ return extractSessionId(value) !== undefined;
92
+ }
93
+ /** JSON.parse that returns undefined instead of throwing. */
94
+ function tryParseJson(raw) {
95
+ try {
96
+ return JSON.parse(raw);
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ /**
103
+ * Normalize a raw copilot run result into `{ text, sessionId? }`.
104
+ * See the module doc for the three stdout shapes handled.
105
+ */
106
+ export const copilotResultExtractor = (result) => {
107
+ // The raw result's own sessionId (SDK-style paths) is the fallback; an id
108
+ // found in the output stream wins because it is fresher.
109
+ const fallbackSessionId = result.sessionId;
110
+ const raw = result.stdout.trim();
111
+ // Shape 1 — whole stdout is one JSON document (spawn may have pre-parsed
112
+ // it when the profile requested parseOutput: "json").
113
+ const whole = result.parsed !== undefined ? result.parsed : raw.length > 0 ? tryParseJson(raw) : undefined;
114
+ if (whole !== undefined) {
115
+ const sessionId = extractSessionId(whole) ?? fallbackSessionId;
116
+ // A bare JSON OBJECT with no transport marker is a legitimate structured
117
+ // answer (a schema unit's `{"result":"ok"}`), NOT an envelope — pass its
118
+ // raw JSON through so the engine's schema validator receives the whole
119
+ // object instead of an unwrapped field value (PR #714 review). Only
120
+ // genuine Copilot envelopes are unwrapped; non-object JSON (a top-level
121
+ // string/array/primitive) keeps the prior extraction. Either way, an
122
+ // unknown envelope with no recognized text key falls back to raw stdout.
123
+ const text = isRecord(whole) && !isTransportEnvelope(whole) ? raw : (extractText(whole) ?? raw);
124
+ return { text, ...(sessionId ? { sessionId } : {}) };
125
+ }
126
+ // Shape 2 — JSONL event stream: last text-bearing event wins; first
127
+ // session-id-bearing event supplies the id. Non-JSON lines are banner
128
+ // noise and are skipped.
129
+ let lastText;
130
+ let streamSessionId;
131
+ let sawJsonLine = false;
132
+ for (const line of raw.split("\n")) {
133
+ const trimmed = line.trim();
134
+ if (trimmed.length === 0)
135
+ continue;
136
+ const event = tryParseJson(trimmed);
137
+ if (event === undefined)
138
+ continue;
139
+ sawJsonLine = true;
140
+ streamSessionId ??= extractSessionId(event);
141
+ const text = extractText(event);
142
+ if (text !== undefined)
143
+ lastText = text;
144
+ }
145
+ if (sawJsonLine) {
146
+ const sessionId = streamSessionId ?? fallbackSessionId;
147
+ return { text: lastText ?? raw, ...(sessionId ? { sessionId } : {}) };
148
+ }
149
+ // Shape 3 — plain text passthrough.
150
+ return { text: raw, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
151
+ };
@@ -0,0 +1,120 @@
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
+ * Gemini CLI agent command builder (P2, plan §"The adapter contract" step 2 /
6
+ * §"Capability matrix").
7
+ *
8
+ * Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
9
+ * headless argv the `gemini` CLI expects. Per the capability matrix the
10
+ * headless invocation is:
11
+ *
12
+ * gemini -p "<prompt>"
13
+ *
14
+ * with `--output-format json` for structured output, `--model <m>` for model
15
+ * selection, and `--resume <id>` for resume. Resume is registry-side
16
+ * (`AkmHarness.resume` — Gemini's resume IS flag-shaped, so the shared
17
+ * `HarnessResumeSupport` seam covers it); it is not built here because
18
+ * `AgentDispatchRequest` carries no session id. The `GEMINI_CLI=1` identity
19
+ * env marker is likewise registry-side (`AkmHarness.identityEnv` /
20
+ * `agent-identity.ts`), not a builder concern.
21
+ *
22
+ * Platform-specific mapping decisions (all localized here, per the adapter
23
+ * contract):
24
+ *
25
+ * - **systemPrompt** — Gemini CLI has no system-prompt flag in headless mode
26
+ * (system text comes from `GEMINI.md` context files / `GEMINI_SYSTEM_MD`),
27
+ * so the system prompt is folded into the `-p` payload ahead of the task
28
+ * prompt, separated by a blank line. `assertNotFlag` still guards it so a
29
+ * `--`-prefixed system prompt cannot turn the front of the `-p` value into
30
+ * a flag.
31
+ * - **schema** — the matrix places Gemini in the "via prompt+validate" tier
32
+ * (no native `--output-schema` equivalent, unlike Codex — so no temp-file
33
+ * plumbing here), so the JSON Schema is passed through the prompt: a
34
+ * directive matching the engine's wording (`step-work.ts`
35
+ * `buildUnitPrompt`) is appended to the `-p` payload, and
36
+ * `--output-format json` is emitted so stdout is the documented JSON
37
+ * envelope the gemini result extractor normalizes. The engine's shared
38
+ * retry-until-valid loop performs the actual validation.
39
+ * - **tools** — a string/array tool policy maps to repeated
40
+ * `--allowed-tools <t>` flags (Gemini's run-without-confirmation
41
+ * allowlist). A *structured* policy object is NOT expressible as Gemini
42
+ * flags; it is deliberately dropped without widening to an auto-approve
43
+ * flag (never silently widen a restriction). With no policy at all,
44
+ * NOTHING is emitted — the matrix's headless shape is the bare
45
+ * `gemini -p "<p>"`; autonomy flags (`--yolo`, `--approval-mode`) belong in
46
+ * `profile.args` where the operator opts in explicitly.
47
+ *
48
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
49
+ * follow-up integration task. Exported standalone so that task only adds a
50
+ * registry entry.
51
+ */
52
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
53
+ /** Canonical harness/platform id used for model-alias resolution. */
54
+ export const GEMINI_PLATFORM = "gemini";
55
+ /**
56
+ * Split a tool policy into individual tool names for `--allowed-tools`.
57
+ * Strings are comma-separated lists; arrays are taken as-is. Structured
58
+ * policy objects return `undefined` (not expressible as Gemini flags — see
59
+ * module doc).
60
+ */
61
+ function toolPolicyEntries(tools) {
62
+ if (typeof tools === "string") {
63
+ return tools
64
+ .split(",")
65
+ .map((t) => t.trim())
66
+ .filter(Boolean);
67
+ }
68
+ if (Array.isArray(tools)) {
69
+ return tools.map((t) => t.trim()).filter(Boolean);
70
+ }
71
+ return undefined;
72
+ }
73
+ /**
74
+ * Assemble the `-p` payload: optional system prompt, the task prompt, and —
75
+ * when a schema is requested — the same schema directive the workflow
76
+ * engine's prompt assembly uses, so both dispatch paths speak one dialect.
77
+ */
78
+ function buildPromptPayload(req) {
79
+ const sections = [];
80
+ if (req.systemPrompt)
81
+ sections.push(req.systemPrompt);
82
+ sections.push(req.prompt);
83
+ if (req.schema) {
84
+ sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
85
+ }
86
+ return sections.join("\n\n");
87
+ }
88
+ /**
89
+ * Gemini CLI builder.
90
+ * Command shape:
91
+ * gemini [--model <m>] [--allowed-tools <t> ...]
92
+ * [--output-format json] -p "<systemPrompt?\n\nprompt\n\nschema?>"
93
+ */
94
+ export const geminiBuilder = {
95
+ platform: GEMINI_PLATFORM,
96
+ build(profile, req) {
97
+ assertNotFlag(req.systemPrompt, "systemPrompt");
98
+ assertNotFlag(req.model, "model");
99
+ const args = [...profile.args];
100
+ if (req.model) {
101
+ const resolved = resolveDispatchModel(req, profile, GEMINI_PLATFORM);
102
+ args.push("--model", resolved);
103
+ }
104
+ if (req.tools) {
105
+ // Structured policy objects (entries === undefined) emit NO flags:
106
+ // dropping a restriction must never widen to auto-approval.
107
+ for (const tool of toolPolicyEntries(req.tools) ?? []) {
108
+ assertNotFlag(tool, "tools entry");
109
+ args.push("--allowed-tools", tool);
110
+ }
111
+ }
112
+ if (req.schema) {
113
+ // Structured unit: ask for the documented JSON envelope so the result
114
+ // extractor can pull the final message + session id deterministically.
115
+ args.push("--output-format", "json");
116
+ }
117
+ args.push("-p", buildPromptPayload(req));
118
+ return { argv: [profile.bin, ...args] };
119
+ },
120
+ };
@@ -0,0 +1,59 @@
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
+ * Gemini CLI harness (P2 integration, plan §"The adapter contract").
6
+ *
7
+ * Per-harness barrel gathering the Gemini CLI integration surfaces:
8
+ * - agent command builder → ./agent-builder.ts (geminiBuilder)
9
+ * - result extractor → ./result-extractor.ts (geminiResultExtractor)
10
+ *
11
+ * It also defines {@link GeminiHarness}, 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 { geminiBuilder } from "./agent-builder.js";
17
+ import { geminiResultExtractor } from "./result-extractor.js";
18
+ export { GEMINI_PLATFORM, geminiBuilder } from "./agent-builder.js";
19
+ export { geminiResultExtractor } 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
+ * Gemini CLI.
32
+ *
33
+ * Canonical id is `'gemini'`; no alias or distinct runtime identity.
34
+ */
35
+ export class GeminiHarness extends BaseHarness {
36
+ id = "gemini";
37
+ displayName = "Gemini CLI";
38
+ aliases = [];
39
+ agentBuilder = geminiBuilder;
40
+ resultExtractor = geminiResultExtractor;
41
+ // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
42
+ // akm spawns the `gemini` CLI locally per unit ⇒ local-runner.
43
+ pattern = "local-runner";
44
+ // `--output-format json` emits a documented JSON envelope akm parses, then
45
+ // validates against the node schema ⇒ native-json tier.
46
+ structuredOutput = "native-json";
47
+ // `gemini --resume <id>` replays a previous session.
48
+ resume = { flag: "--resume", takesSessionId: true };
49
+ // The matrix's identity marker: Gemini CLI stamps GEMINI_CLI=1 only on
50
+ // processes it spawns, so it genuinely means "running under gemini" (it is
51
+ // not a user-profile config var) — but its VALUE ("1") is a bare flag, not
52
+ // a session id, so it is registered as `presenceEnv` (harness inference
53
+ // only), never `identityEnv` (whose values persist as agent_session_id).
54
+ presenceEnv = ["GEMINI_CLI"];
55
+ capabilities = caps({
56
+ agentDispatch: true,
57
+ detection: true,
58
+ });
59
+ }
@@ -0,0 +1,121 @@
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
+ /**
8
+ * Keys that may carry the final answer, in precedence order. `response` is
9
+ * Gemini's documented `--output-format json` envelope key.
10
+ */
11
+ const TEXT_KEYS = ["response", "result", "text", "output", "content", "message"];
12
+ /** Keys that may carry the harness-native session id, in precedence order. */
13
+ const SESSION_KEYS = ["session_id", "sessionId", "conversation_id", "conversationId", "chat_id", "chatId"];
14
+ /**
15
+ * Coerce a candidate value into text. Handles the shapes Gemini nests
16
+ * answers in: bare strings, `{content|text}` wrappers (assistant message
17
+ * objects), and arrays of `{type:"text",text}` content blocks (Gemini API
18
+ * `parts`-style lists). Depth-bounded: JSON.parse output is acyclic, but
19
+ * nesting is capped anyway so a pathological envelope cannot recurse
20
+ * unboundedly.
21
+ */
22
+ function textOf(value, depth = 0) {
23
+ if (depth > 4)
24
+ return undefined;
25
+ if (typeof value === "string")
26
+ return value;
27
+ if (Array.isArray(value)) {
28
+ const parts = [];
29
+ for (const block of value) {
30
+ const t = textOf(block, depth + 1);
31
+ if (t)
32
+ parts.push(t);
33
+ }
34
+ return parts.length > 0 ? parts.join("\n") : undefined;
35
+ }
36
+ if (isRecord(value)) {
37
+ return textOf(value.content ?? value.text, depth + 1);
38
+ }
39
+ return undefined;
40
+ }
41
+ /** Extract the final-answer text from one parsed JSON value, if any. */
42
+ function extractText(value) {
43
+ if (typeof value === "string")
44
+ return value;
45
+ if (!isRecord(value))
46
+ return undefined;
47
+ for (const key of TEXT_KEYS) {
48
+ if (!(key in value))
49
+ continue;
50
+ const text = textOf(value[key]);
51
+ if (text !== undefined)
52
+ return text;
53
+ }
54
+ return undefined;
55
+ }
56
+ /** Extract a harness-native session id from one parsed JSON value, if any. */
57
+ function extractSessionId(value) {
58
+ if (!isRecord(value))
59
+ return undefined;
60
+ for (const key of SESSION_KEYS) {
61
+ const candidate = value[key];
62
+ if (typeof candidate === "string" && candidate.length > 0)
63
+ return candidate;
64
+ }
65
+ return undefined;
66
+ }
67
+ /** JSON.parse that returns undefined instead of throwing. */
68
+ function tryParseJson(raw) {
69
+ try {
70
+ return JSON.parse(raw);
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ /**
77
+ * Normalize a raw gemini run result into `{ text, sessionId? }`.
78
+ * See the module doc for the three stdout shapes handled.
79
+ */
80
+ export const geminiResultExtractor = (result) => {
81
+ // The raw result's own sessionId (SDK-style paths) is the fallback; an id
82
+ // found in the output stream wins because it is fresher.
83
+ const fallbackSessionId = result.sessionId;
84
+ const raw = result.stdout.trim();
85
+ // Shape 1 — whole stdout is one JSON document (spawn may have pre-parsed
86
+ // it when the profile requested parseOutput: "json").
87
+ const whole = result.parsed !== undefined ? result.parsed : raw.length > 0 ? tryParseJson(raw) : undefined;
88
+ if (whole !== undefined) {
89
+ const sessionId = extractSessionId(whole) ?? fallbackSessionId;
90
+ // Unknown envelope (no recognized text key — includes the error-only
91
+ // variant): fall back to raw stdout so downstream embedded-JSON parsing
92
+ // and failure handling still have the full material to work with.
93
+ const text = extractText(whole) ?? raw;
94
+ return { text, ...(sessionId ? { sessionId } : {}) };
95
+ }
96
+ // Shape 2 — JSONL event stream: last text-bearing event wins; first
97
+ // session-id-bearing event supplies the id. Non-JSON lines are banner
98
+ // noise and are skipped.
99
+ let lastText;
100
+ let streamSessionId;
101
+ let sawJsonLine = false;
102
+ for (const line of raw.split("\n")) {
103
+ const trimmed = line.trim();
104
+ if (trimmed.length === 0)
105
+ continue;
106
+ const event = tryParseJson(trimmed);
107
+ if (event === undefined)
108
+ continue;
109
+ sawJsonLine = true;
110
+ streamSessionId ??= extractSessionId(event);
111
+ const text = extractText(event);
112
+ if (text !== undefined)
113
+ lastText = text;
114
+ }
115
+ if (sawJsonLine) {
116
+ const sessionId = streamSessionId ?? fallbackSessionId;
117
+ return { text: lastText ?? raw, ...(sessionId ? { sessionId } : {}) };
118
+ }
119
+ // Shape 3 — plain text passthrough.
120
+ return { text: raw, ...(fallbackSessionId ? { sessionId: fallbackSessionId } : {}) };
121
+ };