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
@@ -143,21 +143,93 @@ function buildChildEnv(profile, options) {
143
143
  }
144
144
  return env;
145
145
  }
146
+ const STREAM_READ_TIMEOUT = Symbol("stream-read-timeout");
146
147
  async function readStream(stream, opts) {
147
148
  if (!stream)
148
- return "";
149
- const readPromise = new Response(stream).text().catch(() => "");
150
- if (!opts?.timeoutMs)
151
- return readPromise;
149
+ return { text: "", timedOut: false };
150
+ const reader = stream.getReader();
151
+ const decoder = new TextDecoder();
152
+ let text = "";
153
+ if (!opts?.timeoutMs) {
154
+ try {
155
+ while (true) {
156
+ const chunk = await reader.read();
157
+ if (chunk.done)
158
+ break;
159
+ text += decoder.decode(chunk.value, { stream: true });
160
+ }
161
+ text += decoder.decode();
162
+ return { text, timedOut: false };
163
+ }
164
+ catch (error) {
165
+ return { text, timedOut: false, error };
166
+ }
167
+ finally {
168
+ try {
169
+ reader.releaseLock();
170
+ }
171
+ catch {
172
+ /* ignore */
173
+ }
174
+ }
175
+ }
152
176
  // Race the stream read against a timeout so a process that is killed via
153
177
  // SIGTERM/SIGKILL but whose pipe endpoints stay open (e.g. background
154
178
  // threads still holding the fd) cannot block the caller indefinitely.
155
- // On timeout we return whatever we received so far (empty string here since
156
- // `readPromise` is all-or-nothing with `Response.text()`).
179
+ // On timeout we return whatever was decoded before the pipe stopped draining.
180
+ const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
181
+ const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
182
+ let timer;
157
183
  const timeoutPromise = new Promise((resolve) => {
158
- setTimeout(() => resolve(""), opts.timeoutMs);
184
+ timer = setTimeoutImpl(() => {
185
+ timer = undefined;
186
+ resolve(STREAM_READ_TIMEOUT);
187
+ }, opts.timeoutMs);
188
+ if (typeof timer !== "number")
189
+ timer.unref?.();
159
190
  });
160
- return Promise.race([readPromise, timeoutPromise]);
191
+ try {
192
+ while (true) {
193
+ const chunk = await Promise.race([reader.read(), timeoutPromise]);
194
+ if (chunk === STREAM_READ_TIMEOUT) {
195
+ void reader.cancel().catch(() => { });
196
+ return { text, timedOut: true };
197
+ }
198
+ if (chunk.done)
199
+ break;
200
+ text += decoder.decode(chunk.value, { stream: true });
201
+ }
202
+ text += decoder.decode();
203
+ return { text, timedOut: false };
204
+ }
205
+ catch (error) {
206
+ return { text, timedOut: false, error };
207
+ }
208
+ finally {
209
+ if (timer !== undefined) {
210
+ clearTimeoutImpl(timer);
211
+ }
212
+ try {
213
+ reader.releaseLock();
214
+ }
215
+ catch {
216
+ /* ignore */
217
+ }
218
+ }
219
+ }
220
+ function streamFailureMessage(profileName, stdout, stderr) {
221
+ const failures = [];
222
+ if (stdout.error)
223
+ failures.push(`stdout read failed: ${stdout.error instanceof Error ? stdout.error.message : String(stdout.error)}`);
224
+ if (stderr.error)
225
+ failures.push(`stderr read failed: ${stderr.error instanceof Error ? stderr.error.message : String(stderr.error)}`);
226
+ if (stdout.timedOut)
227
+ failures.push("stdout drain timed out");
228
+ if (stderr.timedOut)
229
+ failures.push("stderr drain timed out");
230
+ if (failures.length === 0)
231
+ return undefined;
232
+ return `agent CLI "${profileName}" output capture failed: ${failures.join("; ")}`;
161
233
  }
162
234
  /**
163
235
  * Spawn the agent CLI described by `profile` with `prompt` (forwarded as
@@ -179,17 +251,28 @@ async function readStream(stream, opts) {
179
251
  */
180
252
  export async function runAgent(profile, prompt, options = {}) {
181
253
  const stdioMode = options.stdio ?? profile.stdio;
182
- // null = explicitly disabled (no kill timer). undefined = inherit from profile/default.
183
- const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : (profile.timeoutMs ?? DEFAULT_TIMEOUT_MS);
254
+ // null = explicitly disabled (no kill timer). undefined = runtime default.
255
+ const timeoutMs = options.timeoutMs !== undefined ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
184
256
  const parseOutput = options.parseOutput ?? profile.parseOutput;
185
257
  const setTimeoutImpl = options.setTimeoutFn ?? setTimeout;
186
258
  const clearTimeoutImpl = options.clearTimeoutFn ?? clearTimeout;
259
+ // Observability seam — ids/status only, best-effort (see RunAgentOptions.onEvent).
260
+ const emitSpawnEvent = (type, data) => {
261
+ if (!options.onEvent)
262
+ return;
263
+ try {
264
+ options.onEvent({ type, data });
265
+ }
266
+ catch {
267
+ // Observability must never break the dispatch.
268
+ }
269
+ };
187
270
  // Build argv via the platform-specific builder when dispatch params are
188
271
  // provided; fall back to the legacy positional-prompt form otherwise.
189
272
  let builtArgv;
190
273
  let builtEnv;
191
274
  if (options.dispatch !== undefined) {
192
- const builder = getCommandBuilder(profile.commandBuilder ?? profile.name, options.builderRegistry);
275
+ const builder = getCommandBuilder(profile.platform ?? profile.name, options.builderRegistry);
193
276
  const built = builder.build(profile, options.dispatch);
194
277
  builtArgv = built.argv;
195
278
  builtEnv = built.env;
@@ -225,9 +308,7 @@ export async function runAgent(profile, prompt, options = {}) {
225
308
  stdout: stdioMode === "captured" ? "pipe" : "inherit",
226
309
  stderr: stdioMode === "captured" ? "pipe" : "inherit",
227
310
  env,
228
- // options.cwd wins; dispatch.cwd is the request-level fallback (it was
229
- // declared on AgentDispatchRequest but consumed by nothing — P0.5 fix).
230
- ...((options.cwd ?? options.dispatch?.cwd) ? { cwd: options.cwd ?? options.dispatch?.cwd } : {}),
311
+ ...(options.cwd ? { cwd: options.cwd } : {}),
231
312
  // Spawn in its own process group so killGroup(-pid, signal) reaches all
232
313
  // descendants (e.g. the .opencode binary that opencode's node wrapper forks).
233
314
  // Only applied in captured mode — interactive mode inherits the parent
@@ -247,6 +328,10 @@ export async function runAgent(profile, prompt, options = {}) {
247
328
  error: err instanceof Error ? err.message : String(err),
248
329
  };
249
330
  }
331
+ emitSpawnEvent("spawn_start", {
332
+ profile: profile.name,
333
+ ...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
334
+ });
250
335
  // Hard timeout. We prefer SIGTERM, then SIGKILL if SIGTERM is ignored,
251
336
  // but the subprocess only exposes a single .kill() — one signal is enough
252
337
  // for the structured-failure contract.
@@ -267,11 +352,13 @@ export async function runAgent(profile, prompt, options = {}) {
267
352
  timedOut = true;
268
353
  killGroup(proc, "SIGTERM");
269
354
  // Follow up with SIGKILL after 5 s in case the process ignores SIGTERM.
270
- setTimeoutImpl(() => {
355
+ const sigkillTimer = setTimeoutImpl(() => {
271
356
  if (!proc || proc.exitCode !== null)
272
357
  return;
273
358
  killGroup(proc, "SIGKILL");
274
359
  }, 5000);
360
+ if (typeof sigkillTimer !== "number")
361
+ sigkillTimer.unref?.();
275
362
  }, timeoutMs);
276
363
  }
277
364
  // Cooperative cancel: same SIGTERM→SIGKILL discipline as the timeout, but
@@ -308,11 +395,19 @@ export async function runAgent(profile, prompt, options = {}) {
308
395
  // When there is no kill timer, allow up to 30 s for streams to drain.
309
396
  const streamDrainTimeoutMs = timeoutMs !== null ? timeoutMs + 2_000 : 30_000;
310
397
  const stdoutPromise = stdioMode === "captured"
311
- ? readStream(proc.stdout ?? null, { timeoutMs: streamDrainTimeoutMs })
312
- : Promise.resolve("");
398
+ ? readStream(proc.stdout ?? null, {
399
+ timeoutMs: streamDrainTimeoutMs,
400
+ setTimeoutFn: setTimeoutImpl,
401
+ clearTimeoutFn: clearTimeoutImpl,
402
+ })
403
+ : Promise.resolve({ text: "", timedOut: false });
313
404
  const stderrPromise = stdioMode === "captured"
314
- ? readStream(proc.stderr ?? null, { timeoutMs: streamDrainTimeoutMs })
315
- : Promise.resolve("");
405
+ ? readStream(proc.stderr ?? null, {
406
+ timeoutMs: streamDrainTimeoutMs,
407
+ setTimeoutFn: setTimeoutImpl,
408
+ clearTimeoutFn: clearTimeoutImpl,
409
+ })
410
+ : Promise.resolve({ text: "", timedOut: false });
316
411
  // Optional stdin payload (captured mode only).
317
412
  //
318
413
  // BUG-H1: race the stdin write/close against `proc.exited` and the
@@ -352,6 +447,12 @@ export async function runAgent(profile, prompt, options = {}) {
352
447
  // will not block indefinitely.
353
448
  await Promise.allSettled([stdoutPromise, stderrPromise]);
354
449
  const durationMs = Date.now() - start;
450
+ emitSpawnEvent("spawn_exit", {
451
+ profile: profile.name,
452
+ ...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
453
+ exitCode: null,
454
+ status: "spawn_failed",
455
+ });
355
456
  return {
356
457
  ok: false,
357
458
  exitCode: null,
@@ -364,7 +465,15 @@ export async function runAgent(profile, prompt, options = {}) {
364
465
  }
365
466
  clearTimeoutImpl(timer);
366
467
  abortSignal?.removeEventListener("abort", onAbort);
367
- const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
468
+ emitSpawnEvent("spawn_exit", {
469
+ profile: profile.name,
470
+ ...(typeof proc.pid === "number" ? { pid: proc.pid } : {}),
471
+ exitCode,
472
+ status: aborted ? "aborted" : timedOut ? "timeout" : exitCode !== 0 ? "non_zero_exit" : "ok",
473
+ });
474
+ const [stdoutRead, stderrRead] = await Promise.all([stdoutPromise, stderrPromise]);
475
+ const stdout = stdoutRead.text;
476
+ const stderr = stderrRead.text;
368
477
  const durationMs = Date.now() - start;
369
478
  if (aborted) {
370
479
  return {
@@ -388,6 +497,18 @@ export async function runAgent(profile, prompt, options = {}) {
388
497
  error: `agent CLI "${profile.name}" timed out after ${timeoutMs ?? 0}ms`,
389
498
  };
390
499
  }
500
+ const captureFailure = streamFailureMessage(profile.name, stdoutRead, stderrRead);
501
+ if (captureFailure) {
502
+ return {
503
+ ok: false,
504
+ exitCode,
505
+ stdout,
506
+ stderr,
507
+ durationMs,
508
+ reason: "spawn_failed",
509
+ error: captureFailure,
510
+ };
511
+ }
391
512
  if (exitCode !== 0) {
392
513
  return {
393
514
  ok: false,
@@ -0,0 +1,112 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /**
5
+ * Aider CLI agent command builder (P2, plan §"The adapter contract" step 2 /
6
+ * §"Capability matrix").
7
+ *
8
+ * Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
9
+ * headless argv the `aider` CLI expects. Per the capability matrix the
10
+ * headless invocation is:
11
+ *
12
+ * aider -m "<p>" --yes-always
13
+ *
14
+ * with `--model <m>` for model selection, NO structured-output mode (Aider is
15
+ * the "none" tier — prompt-injected schema + embedded-JSON extraction), and
16
+ * NO session flag (resume is chat-history files, not an id).
17
+ *
18
+ * Platform-specific mapping decisions (all localized here, per the adapter
19
+ * contract):
20
+ *
21
+ * - **prompt** — `--message` is Aider's one-shot non-interactive mode (the
22
+ * matrix's `-m`; the long form is used for self-documenting argv). Unlike
23
+ * claude/codex/pi there is NO positional prompt and NO `--` end-of-options
24
+ * separator to hide behind: the prompt is a *flag value*. The glued
25
+ * `--message=<payload>` form is therefore used so a payload that begins
26
+ * with `-`/`--` binds to the flag lexically and can never be parsed as a
27
+ * separate option by Aider's argument parser.
28
+ * - **--yes-always** — always emitted: auto-confirms every interactive
29
+ * prompt (create file? run command? …), which is what makes a captured,
30
+ * unattended run possible. This is the matrix's documented headless shape.
31
+ * - **--no-pretty** — always emitted: disables colored/pretty terminal
32
+ * rendering so captured stdout is clean text for `./result-extractor.ts`
33
+ * (the same "dispatch is the captured path" reasoning as the Claude
34
+ * builder's unconditional `--print` and Codex's unconditional `--json`).
35
+ * - **systemPrompt** — Aider has no system-prompt flag (its nearest concept
36
+ * is conventions files via `--read`, which take a path, not text). Folded
37
+ * into the message payload — system text first, blank line, then the task —
38
+ * mirroring the codex builder's treatment.
39
+ * - **schema** — the matrix places Aider in the "via prompt+validate" tier
40
+ * with *no* structured output mode at all (plan §"Structured-output
41
+ * normalization", tier "none"): there is no schema flag and no JSON output
42
+ * flag, so the JSON Schema is injected into the message payload using the
43
+ * exact directive wording of the engine's prompt assembly
44
+ * (`step-work.ts` `buildUnitPrompt`) and the pi builder, so all
45
+ * dispatch paths speak one dialect. Downstream, embedded-JSON extraction +
46
+ * the engine's shared retry-until-valid loop supply the validation Aider
47
+ * lacks. No temp schema file is written — that is Codex's native-schema
48
+ * mechanism (`--output-schema`), which Aider does not have.
49
+ * - **tools** — deliberately unconsumed. Aider has no per-tool allowlist
50
+ * flag; tool-ish behaviour is governed by its own switches (`--yes-always`,
51
+ * git integration, shell-command confirmation). A restrictive policy is
52
+ * therefore dropped rather than approximated — never silently widened.
53
+ * - **resume/session** — NOT expressible: Aider persists context in
54
+ * chat-history files (`.aider.chat.history.md`), not session ids, so there
55
+ * is no flag-shaped `HarnessResumeSupport` to export and the extractor
56
+ * never yields a `sessionId`. akm's `workflow_run_units` remains the
57
+ * durable source of truth; resume works even against a harness with no
58
+ * session model (plan §"Session, MCP, and identity across harnesses" —
59
+ * Aider is the plan's named example).
60
+ * - **effort** — stays unconsumed (reserved; the shared request contract's
61
+ * "no builder consumes it yet" note stays true).
62
+ *
63
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
64
+ * follow-up integration task (as is the registry entry declaring
65
+ * `structuredOutput: "none"` and no `resume`). Exported standalone so that
66
+ * task only adds a registry entry.
67
+ */
68
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
69
+ /** Canonical harness/platform id used for model-alias resolution. */
70
+ export const AIDER_PLATFORM = "aider";
71
+ /**
72
+ * Assemble the `--message` payload: optional system text, the task prompt,
73
+ * and — when a schema is requested — the same schema directive the workflow
74
+ * engine's prompt assembly uses (Aider has no native structured output, so
75
+ * the prompt is the only channel; plan §"Structured-output normalization",
76
+ * tier "none").
77
+ */
78
+ function buildMessagePayload(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
+ * Aider builder.
90
+ * Command shape:
91
+ * aider [--model <m>] --yes-always --no-pretty --message=<[system\n\n]prompt[\n\nschema directive]>
92
+ */
93
+ export const aiderBuilder = {
94
+ platform: AIDER_PLATFORM,
95
+ build(profile, req) {
96
+ assertNotFlag(req.systemPrompt, "systemPrompt");
97
+ assertNotFlag(req.model, "model");
98
+ const args = [...profile.args];
99
+ if (req.model) {
100
+ const resolved = resolveDispatchModel(req, profile, AIDER_PLATFORM);
101
+ args.push("--model", resolved);
102
+ }
103
+ // Headless essentials (matrix shape): auto-confirm everything, and keep
104
+ // captured stdout free of pretty/ANSI rendering for the extractor.
105
+ args.push("--yes-always");
106
+ args.push("--no-pretty");
107
+ // Glued form: the payload is a flag VALUE (no positional prompt exists),
108
+ // so `=` binding keeps a dash-leading payload from parsing as an option.
109
+ args.push(`--message=${buildMessagePayload(req)}`);
110
+ return { argv: [profile.bin, ...args] };
111
+ },
112
+ };
@@ -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
+ * Aider CLI harness (P2 integration, plan §"The adapter contract").
6
+ *
7
+ * Per-harness barrel gathering the Aider integration surfaces:
8
+ * - agent command builder → ./agent-builder.ts (aiderBuilder)
9
+ * - result extractor → ./result-extractor.ts (aiderResultExtractor)
10
+ *
11
+ * It also defines {@link AiderHarness}, the {@link AkmHarness} descriptor that
12
+ * `HARNESS_REGISTRY` registers. Dispatch-only: no native session-log reader or
13
+ * config importer yet.
14
+ */
15
+ import { BaseHarness } from "../types.js";
16
+ import { aiderBuilder } from "./agent-builder.js";
17
+ import { aiderResultExtractor } from "./result-extractor.js";
18
+ export { AIDER_PLATFORM, aiderBuilder } from "./agent-builder.js";
19
+ export { aiderResultExtractor } from "./result-extractor.js";
20
+ function caps(c) {
21
+ return {
22
+ sessionLogs: false,
23
+ agentDispatch: false,
24
+ detection: false,
25
+ configImport: false,
26
+ runtimeIdentity: false,
27
+ ...c,
28
+ };
29
+ }
30
+ /**
31
+ * Aider.
32
+ *
33
+ * Canonical id is `'aider'`; no alias or distinct runtime identity.
34
+ */
35
+ export class AiderHarness extends BaseHarness {
36
+ id = "aider";
37
+ displayName = "Aider";
38
+ aliases = [];
39
+ agentBuilder = aiderBuilder;
40
+ resultExtractor = aiderResultExtractor;
41
+ // ── Workflow-engine descriptor (plan §"Capability matrix", P2) ────────────
42
+ // akm spawns the `aider` CLI locally per unit ⇒ local-runner.
43
+ pattern = "local-runner";
44
+ // No structured-output mode at all (the matrix's "none — parse output"):
45
+ // akm injects the schema into the prompt and extracts embedded JSON.
46
+ structuredOutput = "none";
47
+ // No `resume`: Aider persists context in chat-history files
48
+ // (`.aider.chat.history.md`), not session ids — the plan's named example of
49
+ // a harness with no session model. akm's `workflow_run_units` remains the
50
+ // durable resume source of truth.
51
+ // No `identityEnv`: the matrix lists Aider's identity markers as uncertain,
52
+ // and Aider stamps no session var onto child processes.
53
+ capabilities = caps({
54
+ agentDispatch: true,
55
+ detection: true,
56
+ });
57
+ }
@@ -0,0 +1,53 @@
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
+ * ANSI escape sequences (CSI color/cursor codes). Defensive only: the builder
6
+ * always passes `--no-pretty`, but a user profile may override that.
7
+ * Constructed from a char code so no literal control character appears in
8
+ * source (lint-friendly, same bytes).
9
+ */
10
+ const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g");
11
+ /**
12
+ * Aider's own announcement lines — banner, model/repo configuration echo,
13
+ * chat-history notices, edit/commit/usage reports. All patterns are anchored
14
+ * at line start and match documented phrasings only; anything else is treated
15
+ * as reply content.
16
+ */
17
+ const NOISE_LINE_PATTERNS = [
18
+ /^aider v\d/, // version banner
19
+ /^(Main model|Weak model|Editor model|Model):\s/, // model announcements
20
+ /^Git repo:\s/, // repo detection
21
+ /^Repo-map:\s/, // repo-map status
22
+ /^Added .+ to the chat\.?$/, // file add notices
23
+ /^Restored previous conversation history\.?$/, // chat-history reload notice
24
+ /^Use \/help\b/, // onboarding hint line
25
+ /^Tokens: .+/, // usage/cost footer
26
+ /^Cost: .+/, // standalone cost footer
27
+ /^Applied edit to .+/, // edit application notices
28
+ /^Commit [0-9a-f]{6,}\b/, // auto-commit notices
29
+ /^You can use \/undo\b/, // undo hint after auto-commit
30
+ /^[─━-]{4,}$/, // ── / ━━ / ---- separator rules
31
+ ];
32
+ function isNoiseLine(line) {
33
+ return NOISE_LINE_PATTERNS.some((pattern) => pattern.test(line));
34
+ }
35
+ /**
36
+ * Normalize a raw aider run result into `{ text, sessionId? }`.
37
+ * See the module doc for the stdout shape handled.
38
+ */
39
+ export const aiderResultExtractor = (result) => {
40
+ const raw = result.stdout.replace(ANSI_ESCAPE_RE, "").trim();
41
+ const kept = raw
42
+ .split("\n")
43
+ .filter((line) => !isNoiseLine(line.trim()))
44
+ .join("\n")
45
+ .trim();
46
+ // Never return empty text when stdout had content: an all-noise filter
47
+ // outcome means the patterns over-matched for this capture, so fall back to
48
+ // the full trimmed stdout (downstream parsing still gets material).
49
+ const text = kept.length > 0 ? kept : raw;
50
+ // No session model (chat-history files, not ids): only a spawn-layer id
51
+ // passes through; output never supplies one.
52
+ return result.sessionId === undefined ? { text } : { text, sessionId: result.sessionId };
53
+ };
@@ -0,0 +1,152 @@
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
+ * Amazon Q Developer CLI agent command builder (P2, plan §"The adapter
6
+ * contract" step 2 / §"Capability matrix").
7
+ *
8
+ * Translates a platform-agnostic {@link AgentDispatchRequest} into the exact
9
+ * headless argv the `q` CLI expects. Per the capability matrix the headless
10
+ * invocation is:
11
+ *
12
+ * q chat --no-interactive --trust-all-tools "<prompt>"
13
+ *
14
+ * with `--model <m>` for model selection and `--resume` for resume. Resume is
15
+ * registry-side (`AkmHarness.resume`, {@link AMAZONQ_RESUME_FLAG}) — and NOTE:
16
+ * unlike every other harness's resume, Q's `--resume` is a bare flag that
17
+ * replays the previous conversation *of the working directory*; it takes no
18
+ * session id. There is nothing to thread from `workflow_run_units` — akm's own
19
+ * unit rows remain the durable resume source of truth regardless (plan
20
+ * §"Session, MCP, and identity across harnesses").
21
+ *
22
+ * Platform-specific mapping decisions (all localized here, per the adapter
23
+ * contract):
24
+ *
25
+ * - **subcommand** — headless dispatch is the `chat` subcommand. The builder
26
+ * prepends `chat` itself (mirroring the codex builder's `exec` handling); a
27
+ * user profile that already pins `chat` as its first arg is not doubled.
28
+ * - **prompt** — the trailing positional `[INPUT]` argument of `q chat`,
29
+ * preceded by the `--` end-of-options separator (mirroring the
30
+ * claude/codex/pi builders) so a prompt whose text begins with `-`/`--` can
31
+ * never be parsed as flags. `--no-interactive` makes Q print the response
32
+ * and exit instead of opening the REPL.
33
+ * - **systemPrompt** — `q chat` has no system-prompt flag (persona/context
34
+ * comes from Q's own agent config files), so the system prompt is folded
35
+ * into the positional payload ahead of the task prompt, separated by a
36
+ * blank line. `assertNotFlag` still guards it.
37
+ * - **schema** — the matrix places Q in the NO-structured-output tier
38
+ * ("via prompt+validate": *(none documented)* — there is no `--json` or
39
+ * `--output-format` to ask for). The JSON Schema is therefore passed
40
+ * through the prompt: a directive matching the engine's wording
41
+ * (`step-work.ts` `buildUnitPrompt`) is appended to the payload.
42
+ * Stdout stays plain text; `./result-extractor.ts` strips terminal framing
43
+ * and the engine's shared embedded-JSON parse + retry-until-valid loop does
44
+ * the rest. No schema temp file is written — that seam is codex-only
45
+ * (`--output-schema`); inventing a flag here would produce a silently
46
+ * broken command.
47
+ * - **tools** — a string/array tool policy maps to Q's documented
48
+ * `--trust-tools=<t1,t2>` allowlist flag (equals-joined, per `q chat
49
+ * --help`). With no policy at all, headless runs need autonomy, so
50
+ * `--trust-all-tools` is emitted per the matrix. A *structured* policy
51
+ * object is NOT expressible as Q flags; it is deliberately dropped without
52
+ * falling back to `--trust-all-tools` (never silently widen a restriction)
53
+ * — Q then refuses untrusted tool actions in non-interactive mode, which is
54
+ * the conservative failure mode.
55
+ * - **effort** — stays unconsumed (reserved; the shared request contract's
56
+ * "no builder consumes it yet" note stays true).
57
+ *
58
+ * NOT registered anywhere: `builders.ts` / `harnesses/index.ts` wiring is a
59
+ * follow-up integration task (as is the registry-side capability entry —
60
+ * pattern `local-runner`, structuredOutput `none`). Exported standalone so
61
+ * that task only adds a registry entry.
62
+ */
63
+ import { assertNotFlag, resolveDispatchModel, } from "../../agent/builder-shared.js";
64
+ /** Canonical harness/platform id used for model-alias resolution. */
65
+ export const AMAZONQ_PLATFORM = "amazonq";
66
+ /**
67
+ * Resume flag per the capability matrix (`--resume`). Exported for the
68
+ * integration task's `AkmHarness.resume` registry entry. Bare flag: Q resumes
69
+ * the previous conversation of the current working directory and takes NO
70
+ * session id value — do not append one after it.
71
+ */
72
+ export const AMAZONQ_RESUME_FLAG = "--resume";
73
+ /**
74
+ * Split a tool policy into individual tool names for `--trust-tools`.
75
+ * Strings are comma-separated lists; arrays are taken as-is. Structured
76
+ * policy objects return `undefined` (not expressible as Q flags — see
77
+ * module doc).
78
+ */
79
+ function toolPolicyEntries(tools) {
80
+ if (typeof tools === "string") {
81
+ return tools
82
+ .split(",")
83
+ .map((t) => t.trim())
84
+ .filter(Boolean);
85
+ }
86
+ if (Array.isArray(tools)) {
87
+ return tools.map((t) => t.trim()).filter(Boolean);
88
+ }
89
+ return undefined;
90
+ }
91
+ /**
92
+ * Assemble the positional prompt payload: optional system prompt, the task
93
+ * prompt, and — when a schema is requested — the same schema directive the
94
+ * workflow engine's prompt assembly uses, so both dispatch paths speak one
95
+ * dialect.
96
+ */
97
+ function buildPromptPayload(req) {
98
+ const sections = [];
99
+ if (req.systemPrompt)
100
+ sections.push(req.systemPrompt);
101
+ sections.push(req.prompt);
102
+ if (req.schema) {
103
+ sections.push(`Respond with ONLY a JSON value matching this JSON Schema (no prose, no code fences):\n${JSON.stringify(req.schema)}`);
104
+ }
105
+ return sections.join("\n\n");
106
+ }
107
+ /**
108
+ * Amazon Q Developer CLI builder.
109
+ * Command shape:
110
+ * q chat --no-interactive (--trust-all-tools | --trust-tools=<t1,t2>)
111
+ * [--model <m>] -- "<systemPrompt?\n\nprompt\n\nschema directive?>"
112
+ */
113
+ export const amazonqBuilder = {
114
+ platform: AMAZONQ_PLATFORM,
115
+ build(profile, req) {
116
+ assertNotFlag(req.systemPrompt, "systemPrompt");
117
+ assertNotFlag(req.model, "model");
118
+ // Built-in q profiles would ship `args: []`; headless dispatch is the
119
+ // `chat` subcommand. Don't double it when a user profile already pins it.
120
+ const extra = profile.args[0] === "chat" ? profile.args.slice(1) : [...profile.args];
121
+ const args = ["chat", ...extra];
122
+ // Print the response and exit — required for captured dispatch.
123
+ args.push("--no-interactive");
124
+ if (req.tools) {
125
+ // Structured policy objects (entries === undefined) emit NO trust
126
+ // flags: dropping a restriction must never widen to --trust-all-tools.
127
+ const entries = toolPolicyEntries(req.tools);
128
+ if (entries !== undefined) {
129
+ for (const tool of entries) {
130
+ assertNotFlag(tool, "tools entry");
131
+ }
132
+ // Q's documented allowlist form is equals-joined and comma-separated
133
+ // (`--trust-tools=fs_read,fs_write`); an empty list trusts no tools.
134
+ args.push(`--trust-tools=${entries.join(",")}`);
135
+ }
136
+ }
137
+ else {
138
+ // Headless default per the capability matrix: units must run without
139
+ // interactive tool-approval prompts.
140
+ args.push("--trust-all-tools");
141
+ }
142
+ if (req.model) {
143
+ const resolved = resolveDispatchModel(req, profile, AMAZONQ_PLATFORM);
144
+ args.push("--model", resolved);
145
+ }
146
+ // No system-prompt / schema flags exist on `q chat` — both travel in the
147
+ // positional payload, after the end-of-options separator.
148
+ args.push("--");
149
+ args.push(buildPromptPayload(req));
150
+ return { argv: [profile.bin, ...args] };
151
+ },
152
+ };