akm-cli 0.8.1 → 0.9.0-beta.0

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 (318) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/dist/assets/help/help-proposals.md +1 -2
  3. package/dist/assets/hints/cli-hints-full.md +34 -19
  4. package/dist/assets/hints/cli-hints-short.md +1 -1
  5. package/dist/assets/profiles/catchup.json +13 -0
  6. package/dist/assets/profiles/consolidate.json +13 -0
  7. package/dist/assets/profiles/frequent.json +13 -0
  8. package/dist/assets/stash-skeleton/README.md +76 -0
  9. package/dist/assets/tasks/core/backup.yml +4 -0
  10. package/dist/assets/tasks/core/extract.yml +4 -0
  11. package/dist/assets/tasks/core/improve.yml +4 -0
  12. package/dist/assets/tasks/core/index-refresh.yml +4 -0
  13. package/dist/assets/tasks/core/sync.yml +4 -0
  14. package/dist/assets/tasks/core/update-stashes.yml +4 -0
  15. package/dist/assets/tasks/core/version-check.yml +4 -0
  16. package/dist/cli/config-migrate.js +6 -6
  17. package/dist/cli/config-validate.js +4 -4
  18. package/dist/cli/confirm.js +3 -3
  19. package/dist/cli/parse-args.js +1 -1
  20. package/dist/cli/shared.js +51 -14
  21. package/dist/cli-node.mjs +26 -0
  22. package/dist/cli.js +171 -3857
  23. package/dist/commands/{agent-dispatch.js → agent/agent-dispatch.js} +6 -6
  24. package/dist/commands/{agent-support.js → agent/agent-support.js} +2 -2
  25. package/dist/commands/agent/contribute-cli.js +200 -0
  26. package/dist/commands/completions.js +1 -1
  27. package/dist/commands/config-cli.js +240 -3
  28. package/dist/commands/config-edit.js +344 -0
  29. package/dist/commands/db-cli.js +2 -2
  30. package/dist/commands/env/env-cli.js +529 -0
  31. package/dist/commands/env/env.js +410 -0
  32. package/dist/commands/env/secret-cli.js +259 -0
  33. package/dist/commands/{secret.js → env/secret.js} +6 -47
  34. package/dist/commands/events.js +4 -4
  35. package/dist/commands/feedback-cli.js +18 -34
  36. package/dist/commands/graph/graph-cli.js +132 -0
  37. package/dist/commands/{graph.js → graph/graph.js} +22 -16
  38. package/dist/commands/health/checks.js +279 -0
  39. package/dist/commands/health.js +101 -249
  40. package/dist/commands/{consolidate.js → improve/consolidate.js} +52 -40
  41. package/dist/commands/{distill-promotion-policy.js → improve/distill-promotion-policy.js} +3 -3
  42. package/dist/commands/{distill.js → improve/distill.js} +39 -18
  43. package/dist/commands/{eval-cases.js → improve/eval-cases.js} +1 -1
  44. package/dist/commands/{extract-cli.js → improve/extract-cli.js} +4 -4
  45. package/dist/commands/{extract-prompt.js → improve/extract-prompt.js} +2 -2
  46. package/dist/commands/{extract.js → improve/extract.js} +185 -26
  47. package/dist/commands/{improve-auto-accept.js → improve/improve-auto-accept.js} +4 -4
  48. package/dist/commands/{improve-cli.js → improve/improve-cli.js} +45 -23
  49. package/dist/commands/{improve-profiles.js → improve/improve-profiles.js} +13 -7
  50. package/dist/commands/{improve-result-file.js → improve/improve-result-file.js} +10 -5
  51. package/dist/commands/{improve.js → improve/improve.js} +536 -248
  52. package/dist/{core → commands/improve/memory}/memory-belief.js +2 -2
  53. package/dist/{core → commands/improve/memory}/memory-contradiction-detect.js +5 -5
  54. package/dist/{core → commands/improve/memory}/memory-improve.js +4 -4
  55. package/dist/commands/{reflect.js → improve/reflect.js} +33 -28
  56. package/dist/commands/improve/session-asset.js +248 -0
  57. package/dist/commands/lint/agent-linter.js +1 -1
  58. package/dist/commands/lint/base-linter.js +55 -37
  59. package/dist/commands/lint/command-linter.js +1 -1
  60. package/dist/commands/lint/default-linter.js +1 -1
  61. package/dist/commands/lint/env-key-rules.js +1 -1
  62. package/dist/commands/lint/index.js +19 -25
  63. package/dist/commands/lint/knowledge-linter.js +1 -1
  64. package/dist/commands/lint/memory-linter.js +1 -1
  65. package/dist/commands/lint/registry.js +8 -8
  66. package/dist/commands/lint/skill-linter.js +1 -1
  67. package/dist/commands/lint/task-linter.js +1 -1
  68. package/dist/commands/lint/workflow-linter.js +1 -1
  69. package/dist/commands/lint.js +1 -1
  70. package/dist/commands/observability-cli.js +244 -0
  71. package/dist/commands/{proposal-drain-policies.js → proposal/drain-policies.js} +3 -3
  72. package/dist/commands/{proposal-drain.js → proposal/drain.js} +15 -10
  73. package/dist/commands/proposal/proposal-cli.js +478 -0
  74. package/dist/commands/{proposal.js → proposal/proposal.js} +5 -5
  75. package/dist/commands/{propose.js → proposal/propose.js} +11 -11
  76. package/dist/{core → commands/proposal/validators}/proposal-quality-validators.js +8 -3
  77. package/dist/{core → commands/proposal/validators}/proposal-validators.js +5 -5
  78. package/dist/{core → commands/proposal/validators}/proposals.js +13 -7
  79. package/dist/commands/{curate.js → read/curate.js} +7 -7
  80. package/dist/commands/{knowledge.js → read/knowledge.js} +22 -9
  81. package/dist/commands/{registry-search.js → read/registry-search.js} +5 -5
  82. package/dist/commands/{remember-cli.js → read/remember-cli.js} +15 -7
  83. package/dist/commands/read/search-cli.js +207 -0
  84. package/dist/commands/{search.js → read/search.js} +22 -27
  85. package/dist/commands/{show.js → read/show.js} +77 -44
  86. package/dist/commands/registry-cli.js +8 -8
  87. package/dist/commands/remember.js +8 -8
  88. package/dist/commands/sources/add-cli.js +293 -0
  89. package/dist/commands/{history.js → sources/history.js} +27 -25
  90. package/dist/commands/{info.js → sources/info.js} +6 -6
  91. package/dist/commands/{init.js → sources/init.js} +10 -5
  92. package/dist/commands/{installed-stashes.js → sources/installed-stashes.js} +12 -12
  93. package/dist/commands/{migration-help.js → sources/migration-help.js} +3 -2
  94. package/dist/commands/{schema-repair.js → sources/schema-repair.js} +8 -8
  95. package/dist/commands/{self-update.js → sources/self-update.js} +10 -9
  96. package/dist/commands/{source-add.js → sources/source-add.js} +10 -10
  97. package/dist/commands/{source-clone.js → sources/source-clone.js} +7 -7
  98. package/dist/commands/{source-manage.js → sources/source-manage.js} +4 -4
  99. package/dist/commands/sources/sources-cli.js +305 -0
  100. package/dist/commands/sources/stash-cli.js +219 -0
  101. package/dist/commands/sources/stash-skeleton.js +79 -0
  102. package/dist/commands/tasks/default-tasks.js +173 -0
  103. package/dist/commands/tasks/tasks-cli.js +210 -0
  104. package/dist/commands/{tasks.js → tasks/tasks.js} +14 -14
  105. package/dist/commands/wiki-cli.js +307 -0
  106. package/dist/commands/workflow-cli.js +329 -0
  107. package/dist/core/action-contributors.js +1 -1
  108. package/dist/core/assert.js +40 -0
  109. package/dist/core/asset/asset-create.js +54 -0
  110. package/dist/core/{asset-ref.js → asset/asset-ref.js} +21 -4
  111. package/dist/core/{asset-registry.js → asset/asset-registry.js} +3 -3
  112. package/dist/core/{asset-spec.js → asset/asset-spec.js} +17 -31
  113. package/dist/core/{markdown.js → asset/markdown.js} +1 -1
  114. package/dist/core/asset/stash-meta.js +110 -0
  115. package/dist/core/best-effort.js +64 -0
  116. package/dist/core/common.js +32 -18
  117. package/dist/core/{config-io.js → config/config-io.js} +29 -19
  118. package/dist/core/{config-migration.js → config/config-migration.js} +11 -9
  119. package/dist/core/{config-schema.js → config/config-schema.js} +45 -1
  120. package/dist/core/config/config-types.js +16 -0
  121. package/dist/core/{config-walker.js → config/config-walker.js} +2 -2
  122. package/dist/core/{config.js → config/config.js} +10 -8
  123. package/dist/core/env-secret-ref.js +90 -0
  124. package/dist/core/errors.js +13 -3
  125. package/dist/core/events.js +27 -4
  126. package/dist/core/file-lock.js +1 -1
  127. package/dist/core/improve-types.js +48 -0
  128. package/dist/core/lesson-lint.js +2 -2
  129. package/dist/core/paths.js +2 -2
  130. package/dist/{setup/ripgrep-install.js → core/ripgrep/install.js} +2 -2
  131. package/dist/{setup/ripgrep-resolve.js → core/ripgrep/resolve.js} +2 -2
  132. package/dist/core/state-db.js +88 -46
  133. package/dist/core/text-truncation.js +148 -0
  134. package/dist/core/time.js +1 -1
  135. package/dist/core/write-source.js +98 -85
  136. package/dist/indexer/{db-backup.js → db/db-backup.js} +9 -24
  137. package/dist/indexer/{db.js → db/db.js} +126 -116
  138. package/dist/indexer/{graph-db.js → db/graph-db.js} +9 -4
  139. package/dist/indexer/{llm-cache.js → db/llm-cache.js} +15 -12
  140. package/dist/indexer/ensure-index.js +4 -4
  141. package/dist/indexer/{graph-boost.js → graph/graph-boost.js} +1 -1
  142. package/dist/indexer/{graph-extraction.js → graph/graph-extraction.js} +55 -13
  143. package/dist/indexer/indexer.js +37 -30
  144. package/dist/indexer/init.js +54 -0
  145. package/dist/indexer/manifest.js +10 -10
  146. package/dist/indexer/{memory-inference.js → passes/memory-inference.js} +92 -23
  147. package/dist/indexer/{metadata-contributors.js → passes/metadata-contributors.js} +10 -8
  148. package/dist/indexer/{metadata.js → passes/metadata.js} +15 -19
  149. package/dist/indexer/{staleness-detect.js → passes/staleness-detect.js} +53 -12
  150. package/dist/indexer/{db-search.js → search/db-search.js} +28 -16
  151. package/dist/indexer/{ranking-contributors.js → search/ranking-contributors.js} +1 -1
  152. package/dist/indexer/{ranking.js → search/ranking.js} +2 -2
  153. package/dist/indexer/{search-hit-enrichers.js → search/search-hit-enrichers.js} +3 -3
  154. package/dist/indexer/{search-source.js → search/search-source.js} +8 -8
  155. package/dist/indexer/{semantic-status.js → search/semantic-status.js} +3 -3
  156. package/dist/indexer/usage/unmigrated-vaults-guard.js +94 -0
  157. package/dist/indexer/{usage-events.js → usage/usage-events.js} +32 -0
  158. package/dist/indexer/{file-context.js → walk/file-context.js} +10 -15
  159. package/dist/indexer/{matchers.js → walk/matchers.js} +13 -9
  160. package/dist/indexer/{path-resolver.js → walk/path-resolver.js} +6 -6
  161. package/dist/indexer/{project-context.js → walk/project-context.js} +1 -1
  162. package/dist/indexer/{walker.js → walk/walker.js} +4 -3
  163. package/dist/integrations/agent/builder-shared.js +39 -0
  164. package/dist/integrations/agent/builders.js +14 -81
  165. package/dist/integrations/agent/config.js +6 -4
  166. package/dist/integrations/agent/detect.js +1 -1
  167. package/dist/integrations/agent/index.js +23 -8
  168. package/dist/integrations/agent/prompts.js +2 -3
  169. package/dist/integrations/agent/runner.js +22 -3
  170. package/dist/integrations/agent/spawn.js +9 -10
  171. package/dist/integrations/harnesses/claude/agent-builder.js +48 -0
  172. package/dist/integrations/harnesses/claude/config-import.js +70 -0
  173. package/dist/integrations/harnesses/claude/index.js +64 -0
  174. package/dist/integrations/{session-logs/providers/claude-code.js → harnesses/claude/session-log.js} +16 -1
  175. package/dist/integrations/harnesses/index.js +144 -0
  176. package/dist/integrations/harnesses/opencode/agent-builder.js +43 -0
  177. package/dist/integrations/harnesses/opencode/config-import.js +82 -0
  178. package/dist/integrations/harnesses/opencode/index.js +59 -0
  179. package/dist/integrations/{session-logs/providers/opencode.js → harnesses/opencode/session-log.js} +1 -1
  180. package/dist/integrations/harnesses/opencode-sdk/index.js +49 -0
  181. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +234 -0
  182. package/dist/integrations/harnesses/types.js +43 -0
  183. package/dist/integrations/lockfile.js +7 -16
  184. package/dist/integrations/session-logs/index.js +82 -9
  185. package/dist/llm/call-ai.js +4 -4
  186. package/dist/llm/client.js +131 -6
  187. package/dist/llm/embedder.js +6 -6
  188. package/dist/llm/embedders/local.js +9 -22
  189. package/dist/llm/embedders/remote.js +2 -2
  190. package/dist/llm/embedders/types.js +1 -1
  191. package/dist/llm/graph-extract.js +31 -12
  192. package/dist/llm/index-passes.js +1 -1
  193. package/dist/llm/memory-infer.js +12 -5
  194. package/dist/llm/metadata-enhance.js +2 -2
  195. package/dist/output/context.js +6 -44
  196. package/dist/output/renderers.js +88 -58
  197. package/dist/output/shapes/curate.js +7 -3
  198. package/dist/output/shapes/distill.js +7 -3
  199. package/dist/output/shapes/env-list.js +18 -16
  200. package/dist/output/shapes/events.js +5 -4
  201. package/dist/output/shapes/helpers.js +2 -4
  202. package/dist/output/shapes/history.js +7 -3
  203. package/dist/output/shapes/passthrough.js +8 -11
  204. package/dist/output/shapes/{proposal-accept.js → proposal/accept.js} +7 -3
  205. package/dist/output/shapes/{proposal-diff.js → proposal/diff.js} +7 -3
  206. package/dist/output/shapes/{proposal-list.js → proposal/list.js} +7 -3
  207. package/dist/output/shapes/{proposal-producer.js → proposal/producer.js} +5 -4
  208. package/dist/output/shapes/{proposal-reject.js → proposal/reject.js} +7 -3
  209. package/dist/output/shapes/{proposal-show.js → proposal/show.js} +7 -3
  210. package/dist/output/shapes/registry-search.js +7 -3
  211. package/dist/output/shapes/registry.js +12 -0
  212. package/dist/output/shapes/search.js +7 -3
  213. package/dist/output/shapes/secret-list.js +18 -16
  214. package/dist/output/shapes/show.js +7 -3
  215. package/dist/output/shapes.js +55 -30
  216. package/dist/output/text/add.js +2 -3
  217. package/dist/output/text/clone.js +2 -3
  218. package/dist/output/text/config.js +2 -3
  219. package/dist/output/text/curate.js +4 -3
  220. package/dist/output/text/distill.js +2 -3
  221. package/dist/output/text/enable-disable.js +5 -4
  222. package/dist/output/text/env.js +13 -0
  223. package/dist/output/text/events.js +5 -4
  224. package/dist/output/text/feedback.js +4 -3
  225. package/dist/output/text/helpers.js +54 -39
  226. package/dist/output/text/history.js +2 -3
  227. package/dist/output/text/import.js +2 -3
  228. package/dist/output/text/index.js +2 -3
  229. package/dist/output/text/info.js +2 -3
  230. package/dist/output/text/init.js +2 -3
  231. package/dist/output/text/list.js +2 -3
  232. package/dist/output/text/proposal/producer.js +9 -0
  233. package/dist/output/text/proposal/proposal.js +13 -0
  234. package/dist/output/text/registry-commands.js +8 -7
  235. package/dist/output/text/registry.js +12 -0
  236. package/dist/output/text/remember.js +4 -3
  237. package/dist/output/text/remove.js +2 -3
  238. package/dist/output/text/save.js +2 -3
  239. package/dist/output/text/search.js +4 -3
  240. package/dist/output/text/show.js +4 -3
  241. package/dist/output/text/update.js +2 -3
  242. package/dist/output/text/upgrade.js +2 -3
  243. package/dist/output/text/wiki.js +12 -11
  244. package/dist/output/text/workflow.js +12 -10
  245. package/dist/output/text.js +66 -32
  246. package/dist/registry/build-index.js +11 -10
  247. package/dist/registry/factory.js +1 -1
  248. package/dist/registry/origin-resolve.js +1 -1
  249. package/dist/registry/providers/index.js +2 -2
  250. package/dist/registry/providers/skills-sh.js +91 -72
  251. package/dist/registry/providers/static-index.js +75 -52
  252. package/dist/registry/resolve.js +3 -3
  253. package/dist/runtime.js +242 -0
  254. package/dist/scripts/migrate-storage.js +1594 -673
  255. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +240 -166
  256. package/dist/setup/detect.js +338 -9
  257. package/dist/setup/harness-config-import.js +56 -0
  258. package/dist/setup/registry-stash-loader.js +99 -0
  259. package/dist/setup/setup.js +664 -96
  260. package/dist/sources/include.js +1 -1
  261. package/dist/sources/provider-factory.js +2 -2
  262. package/dist/sources/providers/filesystem.js +3 -3
  263. package/dist/sources/providers/git.js +9 -9
  264. package/dist/sources/providers/index.js +4 -4
  265. package/dist/sources/providers/npm.js +6 -6
  266. package/dist/sources/providers/provider-utils.js +13 -20
  267. package/dist/sources/providers/sync-from-ref.js +5 -5
  268. package/dist/sources/providers/tar-utils.js +2 -2
  269. package/dist/sources/providers/website.js +2 -2
  270. package/dist/sources/resolve.js +5 -5
  271. package/dist/sources/website-ingest.js +5 -5
  272. package/dist/storage/database.js +102 -0
  273. package/dist/storage/engines/sqlite-migrations.js +42 -0
  274. package/dist/storage/locations.js +25 -0
  275. package/dist/storage/repositories/index-db.js +43 -0
  276. package/dist/storage/repositories/workflow-runs-repository.js +141 -0
  277. package/dist/tasks/backends/cron.js +4 -4
  278. package/dist/tasks/backends/exec-utils.js +32 -0
  279. package/dist/tasks/backends/index.js +3 -3
  280. package/dist/tasks/backends/launchd.js +7 -14
  281. package/dist/tasks/backends/schtasks.js +7 -16
  282. package/dist/tasks/embedded.js +71 -0
  283. package/dist/tasks/parser.js +2 -2
  284. package/dist/tasks/resolveAkmBin.js +1 -1
  285. package/dist/tasks/runner.js +28 -15
  286. package/dist/tasks/schedule.js +1 -1
  287. package/dist/tasks/validator.js +7 -7
  288. package/dist/text-import-hook.mjs +51 -0
  289. package/dist/version.js +2 -1
  290. package/dist/wiki/wiki.js +7 -7
  291. package/dist/workflows/{authoring.js → authoring/authoring.js} +6 -6
  292. package/dist/workflows/{scope-key.js → authoring/scope-key.js} +1 -1
  293. package/dist/workflows/cli.js +1 -1
  294. package/dist/workflows/db.js +50 -32
  295. package/dist/workflows/parser.js +4 -4
  296. package/dist/workflows/renderer.js +5 -5
  297. package/dist/workflows/runtime/agent-identity.js +56 -0
  298. package/dist/workflows/runtime/checkin.js +57 -0
  299. package/dist/workflows/{runs.js → runtime/runs.js} +197 -101
  300. package/dist/workflows/validate-summary.js +82 -0
  301. package/docs/README.md +1 -1
  302. package/docs/data-and-telemetry.md +6 -6
  303. package/package.json +16 -8
  304. package/dist/commands/add-cli.js +0 -279
  305. package/dist/commands/env.js +0 -213
  306. package/dist/integrations/agent/sdk-runner.js +0 -126
  307. package/dist/output/shapes/vault-list.js +0 -19
  308. package/dist/output/text/proposal-producer.js +0 -8
  309. package/dist/output/text/proposal.js +0 -12
  310. package/dist/output/text/vault.js +0 -16
  311. /package/dist/core/{asset-serialize.js → asset/asset-serialize.js} +0 -0
  312. /package/dist/core/{frontmatter.js → asset/frontmatter.js} +0 -0
  313. /package/dist/core/{config-sources.js → config/config-sources.js} +0 -0
  314. /package/dist/indexer/{graph-dedup.js → graph/graph-dedup.js} +0 -0
  315. /package/dist/{core/config-types.js → indexer/passes/pass-context.js} +0 -0
  316. /package/dist/indexer/{search-fields.js → search/search-fields.js} +0 -0
  317. /package/dist/indexer/{index-context.js → walk/index-context.js} +0 -0
  318. /package/dist/workflows/{document-cache.js → runtime/document-cache.js} +0 -0
@@ -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
+ * OpenCode harness (#564).
6
+ *
7
+ * Per-harness barrel that gathers the OpenCode integration surfaces previously
8
+ * scattered across the codebase:
9
+ * - session-log reader → ./session-log.ts (OpenCodeProvider)
10
+ * - agent command builder → ./agent-builder.ts (opencodeBuilder)
11
+ * - config importer → ./config-import.ts (openCodeImporter)
12
+ *
13
+ * It also defines {@link OpencodeHarness}, the {@link AkmHarness} descriptor
14
+ * that `HARNESS_REGISTRY` registers.
15
+ *
16
+ * id normalization: OpenCode's canonical id (`'opencode'`) is also its runtime
17
+ * identity and session-log provider name — there is no historical split (unlike
18
+ * Claude Code's 'claude' vs 'claude-code'), so no alias bridge is needed.
19
+ */
20
+ import { BaseHarness } from "../types.js";
21
+ export { opencodeBuilder } from "./agent-builder.js";
22
+ export { openCodeImporter } from "./config-import.js";
23
+ export { OpenCodeProvider } from "./session-log.js";
24
+ function caps(c) {
25
+ return {
26
+ sessionLogs: false,
27
+ agentDispatch: false,
28
+ detection: false,
29
+ configImport: false,
30
+ runtimeIdentity: false,
31
+ v1Migration: false,
32
+ ...c,
33
+ };
34
+ }
35
+ /**
36
+ * OpenCode.
37
+ *
38
+ * Canonical id is `'opencode'`; it has no distinct runtime identity or alias.
39
+ */
40
+ export class OpencodeHarness extends BaseHarness {
41
+ id = "opencode";
42
+ displayName = "OpenCode";
43
+ aliases = [];
44
+ // Home-relative config dir scanned by `akm setup` (#567). OpenCode has a
45
+ // session-log provider, so offering it as a stash source is functional.
46
+ setupDetectionDir = ".config/opencode";
47
+ // Decorated v1 profile names like "opencode-fast" still belong to OpenCode.
48
+ // `v1ProfilePlatform()` resolves most-specific-id-first, so "opencode-sdk-*"
49
+ // is claimed by OpencodeSdkHarness before this prefix can over-match it.
50
+ v1ProfilePrefixes = ["opencode"];
51
+ capabilities = caps({
52
+ sessionLogs: true,
53
+ agentDispatch: true,
54
+ detection: true,
55
+ configImport: true,
56
+ runtimeIdentity: true,
57
+ v1Migration: true,
58
+ });
59
+ }
@@ -4,7 +4,7 @@
4
4
  import fs from "node:fs";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
- import { extractInlineRefMentions } from "../inline-refs";
7
+ import { extractInlineRefMentions } from "../../session-logs/inline-refs.js";
8
8
  function getOpenCodeBaseDir() {
9
9
  if (process.platform === "darwin") {
10
10
  return path.join(os.homedir(), "Library", "Application Support", "opencode");
@@ -0,0 +1,49 @@
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
+ * OpenCode SDK harness (#564).
6
+ *
7
+ * Per-harness barrel for the SDK-mode dispatch path:
8
+ * - agent runner → ./sdk-runner.ts (runOpencodeSdk / runAgentSdk)
9
+ *
10
+ * It also defines {@link OpencodeSdkHarness}, the {@link AkmHarness} descriptor
11
+ * that `HARNESS_REGISTRY` registers.
12
+ *
13
+ * Unlike the CLI harnesses, the SDK path has no native session logs of its own
14
+ * (`capabilities.sessionLogs = false`): it dispatches via the embedded
15
+ * `@opencode-ai/sdk` and surfaces output directly rather than writing platform
16
+ * session files. It is still detected at setup and migrated from v1 profile
17
+ * names. Canonical id is `'opencode-sdk'` with no alias.
18
+ */
19
+ import { BaseHarness } from "../types.js";
20
+ export { closeServer, runAgentSdk, runOpencodeSdk } from "./sdk-runner.js";
21
+ function caps(c) {
22
+ return {
23
+ sessionLogs: false,
24
+ agentDispatch: false,
25
+ detection: false,
26
+ configImport: false,
27
+ runtimeIdentity: false,
28
+ v1Migration: false,
29
+ ...c,
30
+ };
31
+ }
32
+ /**
33
+ * OpenCode SDK (embedded-SDK dispatch path).
34
+ *
35
+ * Dispatch-only: no native session logs, but detected at setup and migrated
36
+ * from v1 profile names.
37
+ */
38
+ export class OpencodeSdkHarness extends BaseHarness {
39
+ id = "opencode-sdk";
40
+ displayName = "OpenCode SDK";
41
+ aliases = [];
42
+ // Decorated v1 profile names like "opencode-sdk-fast" belong to the SDK path.
43
+ v1ProfilePrefixes = ["opencode-sdk"];
44
+ capabilities = caps({
45
+ agentDispatch: true,
46
+ detection: true,
47
+ v1Migration: true,
48
+ });
49
+ }
@@ -0,0 +1,234 @@
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
+ * OpenCode SDK agent runner (migrated from `agent/sdk-runner.ts`, #564).
6
+ *
7
+ * Uses the embedded `@opencode-ai/sdk` instead of `Bun.spawn`. Requires no
8
+ * agent CLI binary to be installed. The user provides an OpenAI-compatible
9
+ * endpoint (or inherits from config.llm) for the SDK.
10
+ *
11
+ * This is the runtime surface of the {@link OpencodeSdkHarness} (`id =
12
+ * 'opencode-sdk'`). It is the dispatch path for `sdkMode` profiles; it exposes
13
+ * no native session logs of its own (`capabilities.sessionLogs = false`).
14
+ */
15
+ import { resolveSecret } from "../../../core/config/config.js";
16
+ import { DEFAULT_AGENT_TIMEOUT_MS } from "../../agent/config.js";
17
+ // Singleton server — started once per process, reused across calls
18
+ let _server = null;
19
+ /**
20
+ * Test-only seam: inject a fake {@link SdkServer} so `runOpencodeSdk` can be
21
+ * exercised without the real `@opencode-ai/sdk` (which would spin up a server).
22
+ * Pass `null` to clear. NOT part of the public runtime API — used only to
23
+ * assert the #564 bug fixes (systemPrompt/tools forwarding + timeout). The
24
+ * leading underscores mark it as internal.
25
+ */
26
+ export function __setTestServer(server) {
27
+ _server = server;
28
+ }
29
+ /**
30
+ * Close the singleton OpenCode SDK server and reset the handle.
31
+ * Primarily for use in tests to ensure clean teardown between test runs.
32
+ */
33
+ export function closeServer() {
34
+ try {
35
+ _server?.server.close();
36
+ }
37
+ catch {
38
+ /* ignore */
39
+ }
40
+ _server = null;
41
+ }
42
+ /**
43
+ * Convert an `AgentDispatchRequest.tools` policy into the SDK's tool-allowlist
44
+ * shape (`{ [toolName]: boolean }`).
45
+ *
46
+ * #564 bug fix (2): the tool list was previously dropped entirely. The CLI
47
+ * builder passes tools as a comma-separated `--allowedTools` flag; the SDK
48
+ * instead wants a per-tool boolean map. A list/comma-string of tool names is
49
+ * treated as an allowlist (each name → true). A structured policy object whose
50
+ * values are already booleans is forwarded as-is.
51
+ *
52
+ * Returns `undefined` when there is nothing to forward, so an absent policy
53
+ * leaves the SDK's own defaults untouched (behaviour-preserving for callers
54
+ * that pass no tools).
55
+ */
56
+ function toolsToSdkAllowlist(tools) {
57
+ if (tools === undefined || tools === null)
58
+ return undefined;
59
+ const names = [];
60
+ if (typeof tools === "string") {
61
+ names.push(...tools
62
+ .split(",")
63
+ .map((t) => t.trim())
64
+ .filter(Boolean));
65
+ }
66
+ else if (Array.isArray(tools)) {
67
+ for (const t of tools) {
68
+ if (typeof t === "string" && t.trim())
69
+ names.push(t.trim());
70
+ }
71
+ }
72
+ else if (typeof tools === "object") {
73
+ // Structured policy: forward boolean entries directly.
74
+ const out = {};
75
+ for (const [k, v] of Object.entries(tools)) {
76
+ if (typeof v === "boolean")
77
+ out[k] = v;
78
+ }
79
+ return Object.keys(out).length > 0 ? out : undefined;
80
+ }
81
+ if (names.length === 0)
82
+ return undefined;
83
+ const out = {};
84
+ for (const n of names)
85
+ out[n] = true;
86
+ return out;
87
+ }
88
+ async function getOrStartServer(profile, llmConfig) {
89
+ if (_server)
90
+ return _server;
91
+ const { createOpencode } = await import("@opencode-ai/sdk").catch(() => {
92
+ throw new Error("OpenCode SDK not available. Install @opencode-ai/sdk or configure a CLI agent instead.");
93
+ });
94
+ // Resolve endpoint and model: profile fields take precedence over config.llm
95
+ const endpoint = profile.endpoint ?? llmConfig?.endpoint;
96
+ const apiKey = resolveSecret(profile.apiKey ?? llmConfig?.apiKey);
97
+ const model = profile.model;
98
+ const sdkConfig = {};
99
+ if (model)
100
+ sdkConfig.model = model;
101
+ if (endpoint || apiKey) {
102
+ // Configure a custom OpenAI-compatible provider
103
+ sdkConfig.provider = {
104
+ "akm-custom": {
105
+ npm: "@ai-sdk/openai-compatible",
106
+ options: {
107
+ baseURL: endpoint?.replace(/\/chat\/completions$/, "").replace(/\/$/, ""),
108
+ ...(apiKey ? { apiKey } : {}),
109
+ },
110
+ },
111
+ };
112
+ // Use the custom provider's model if not already qualified
113
+ if (model && !model.includes("/")) {
114
+ sdkConfig.model = `akm-custom/${model}`;
115
+ }
116
+ }
117
+ _server = (await createOpencode(Object.keys(sdkConfig).length > 0 ? { config: sdkConfig } : {}));
118
+ process.once("exit", () => {
119
+ closeServer();
120
+ });
121
+ if (!_server)
122
+ throw new Error("Failed to initialise OpenCode SDK server.");
123
+ return _server;
124
+ }
125
+ export async function runOpencodeSdk(profile, prompt, opts = {}, llmConfig) {
126
+ const start = Date.now();
127
+ let client;
128
+ try {
129
+ ({ client } = await getOrStartServer(profile, llmConfig));
130
+ }
131
+ catch (e) {
132
+ return {
133
+ ok: false,
134
+ stdout: "",
135
+ stderr: String(e),
136
+ durationMs: Date.now() - start,
137
+ exitCode: 1,
138
+ reason: "spawn_failed",
139
+ error: String(e),
140
+ };
141
+ }
142
+ // One session per call — do NOT reuse (history accumulates, token costs grow)
143
+ const sessionRes = await client.session.create({ body: { title: "akm" } });
144
+ const sessionId = sessionRes.data?.id;
145
+ if (!sessionId) {
146
+ return {
147
+ ok: false,
148
+ stdout: "",
149
+ stderr: "Failed to create session",
150
+ durationMs: Date.now() - start,
151
+ exitCode: 1,
152
+ reason: "spawn_failed",
153
+ error: "Failed to create OpenCode session",
154
+ };
155
+ }
156
+ // #564 bug fixes (1) + (2): forward systemPrompt and tools from the abstract
157
+ // dispatch request. Both were previously accepted on AgentDispatchRequest but
158
+ // silently dropped on the SDK path, so SDK-mode dispatch ignored agent-asset
159
+ // system prompts and tool policies entirely (the CLI path honours both).
160
+ const dispatch = opts.dispatch;
161
+ const system = dispatch?.systemPrompt;
162
+ const tools = toolsToSdkAllowlist(dispatch?.tools);
163
+ const body = { parts: [{ type: "text", text: prompt }] };
164
+ if (system)
165
+ body.system = system;
166
+ if (tools)
167
+ body.tools = tools;
168
+ // #564 bug fix (3): enforce a hard timeout like the CLI path (runAgent).
169
+ // Previously runOpencodeSdk() awaited session.prompt() with no timeout, so a
170
+ // hung SDK call (e.g. a stalled local-model endpoint) blocked the caller
171
+ // indefinitely while the CLI path would have killed the process. We resolve
172
+ // the same budget runAgent uses (opts.timeoutMs override → profile.timeoutMs
173
+ // → DEFAULT_AGENT_TIMEOUT_MS) and race the prompt against it. null disables
174
+ // the timer (parity with runAgent's "no timeout" contract). There is no
175
+ // OS process to SIGTERM/SIGKILL here, so on timeout we best-effort delete the
176
+ // session (the SDK's equivalent of reaping the in-flight work) and return a
177
+ // structured `timeout` failure with the same reason vocabulary as the CLI.
178
+ const timeoutMs = opts.timeoutMs !== undefined ? opts.timeoutMs : (profile.timeoutMs ?? DEFAULT_AGENT_TIMEOUT_MS);
179
+ const setTimeoutImpl = opts.setTimeoutFn ?? setTimeout;
180
+ const clearTimeoutImpl = opts.clearTimeoutFn ?? clearTimeout;
181
+ let timer;
182
+ const TIMED_OUT = Symbol("opencode-sdk-timeout");
183
+ try {
184
+ const promptPromise = client.session.prompt({ path: { id: sessionId }, body });
185
+ const result = timeoutMs === null
186
+ ? await promptPromise
187
+ : await Promise.race([
188
+ promptPromise,
189
+ new Promise((resolve) => {
190
+ timer = setTimeoutImpl(() => resolve(TIMED_OUT), timeoutMs);
191
+ }),
192
+ ]);
193
+ if (result === TIMED_OUT) {
194
+ return {
195
+ ok: false,
196
+ stdout: "",
197
+ stderr: "",
198
+ durationMs: Date.now() - start,
199
+ exitCode: null,
200
+ reason: "timeout",
201
+ error: `opencode-sdk agent "${profile.name}" timed out after ${timeoutMs}ms`,
202
+ };
203
+ }
204
+ const parts = result.data?.parts ?? [];
205
+ const textPart = parts.find((p) => p.type === "text");
206
+ const stdout = textPart?.text ?? "";
207
+ return {
208
+ ok: true,
209
+ stdout,
210
+ stderr: "",
211
+ durationMs: Date.now() - start,
212
+ exitCode: 0,
213
+ };
214
+ }
215
+ catch (e) {
216
+ return {
217
+ ok: false,
218
+ stdout: "",
219
+ stderr: String(e),
220
+ durationMs: Date.now() - start,
221
+ exitCode: 1,
222
+ reason: "non_zero_exit",
223
+ error: String(e),
224
+ };
225
+ }
226
+ finally {
227
+ if (timer !== undefined)
228
+ clearTimeoutImpl(timer);
229
+ // Clean up session to prevent disk accumulation in ~/.local/share/opencode/
230
+ await client.session.delete({ path: { id: sessionId } }).catch(() => { });
231
+ }
232
+ }
233
+ /** @deprecated Use {@link runOpencodeSdk} instead. */
234
+ export const runAgentSdk = runOpencodeSdk;
@@ -0,0 +1,43 @@
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
+ * Shared base for harness descriptors (#566).
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`.
12
+ */
13
+ export class BaseHarness {
14
+ runtimeId;
15
+ setupDetectionDir;
16
+ /**
17
+ * Lowercase prefixes that a decorated v1 profile name may start with and
18
+ * still belong to this harness (e.g. `["opencode-sdk"]`). The canonical id
19
+ * and aliases are always matched in addition to these; an empty list means
20
+ * only exact id/alias matches.
21
+ */
22
+ v1ProfilePrefixes = [];
23
+ matchesV1ProfileName(name) {
24
+ // Only harnesses with a v1→v2 mapping participate; others never claim a
25
+ // legacy profile name (so unknown names are dropped, not misclassified).
26
+ if (!this.capabilities.v1Migration)
27
+ return false;
28
+ const lower = name.trim().toLowerCase();
29
+ if (!lower)
30
+ return false;
31
+ if (lower === this.id.toLowerCase())
32
+ return true;
33
+ for (const alias of this.aliases) {
34
+ if (lower === alias.toLowerCase())
35
+ return true;
36
+ }
37
+ for (const prefix of this.v1ProfilePrefixes) {
38
+ if (lower.startsWith(prefix.toLowerCase()))
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ }
@@ -3,24 +3,15 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
- import { writeFileAtomic } from "../core/common";
7
- import { rethrowIfTestIsolationError } from "../core/errors";
8
- import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock";
9
- import { getDataDir } from "../core/paths";
10
- // ── Paths ───────────────────────────────────────────────────────────────────
11
- const LOCKFILE_NAME = "akm.lock";
12
- function getLockfilePath() {
13
- return path.join(getDataDir(), LOCKFILE_NAME);
14
- }
6
+ import { writeFileAtomic } from "../core/common.js";
7
+ import { rethrowIfTestIsolationError } from "../core/errors.js";
8
+ import { probeLock, releaseLock, tryAcquireLockSync } from "../core/file-lock.js";
9
+ import { getDataDir, getLockfileLockPath, getLockfilePath } from "../core/paths.js";
15
10
  // ── Lock sentinel ────────────────────────────────────────────────────────────
16
11
  const LOCK_MAX_RETRIES = 3;
17
12
  const LOCK_RETRY_DELAY_MS = 100;
18
- function getLockSentinelPath() {
19
- // The sentinel always lives next to the lock file it guards.
20
- return `${path.join(getDataDir(), LOCKFILE_NAME)}.lck`;
21
- }
22
13
  async function acquireLockSentinel() {
23
- const sentinelPath = getLockSentinelPath();
14
+ const sentinelPath = getLockfileLockPath();
24
15
  // Ensure the directory exists before attempting to create the sentinel.
25
16
  fs.mkdirSync(path.dirname(sentinelPath), { recursive: true });
26
17
  for (let attempt = 0; attempt < LOCK_MAX_RETRIES; attempt++) {
@@ -40,7 +31,7 @@ async function acquireLockSentinel() {
40
31
  return false;
41
32
  }
42
33
  function releaseLockSentinel() {
43
- releaseLock(getLockSentinelPath());
34
+ releaseLock(getLockfileLockPath());
44
35
  }
45
36
  // ── Read / Write ────────────────────────────────────────────────────────────
46
37
  export function readLockfile() {
@@ -61,7 +52,7 @@ export function readLockfile() {
61
52
  }
62
53
  export function writeLockfile(entries) {
63
54
  // Always write to $DATA — never to the legacy $CONFIG location.
64
- const lockfilePath = path.join(getDataDir(), LOCKFILE_NAME);
55
+ const lockfilePath = getLockfilePath();
65
56
  const dir = path.dirname(lockfilePath);
66
57
  fs.mkdirSync(dir, { recursive: true });
67
58
  writeFileAtomic(lockfilePath, `${JSON.stringify(entries, null, 2)}\n`);
@@ -1,10 +1,25 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { ClaudeCodeProvider } from "./providers/claude-code";
5
- import { OpenCodeProvider } from "./providers/opencode";
6
- export { extractInlineRefMentions } from "./inline-refs";
4
+ import { getHarness, SESSION_LOG_HARNESSES } from "../harnesses/index.js";
5
+ import { ClaudeCodeProvider } from "../harnesses/claude/session-log.js";
6
+ import { OpenCodeProvider } from "../harnesses/opencode/session-log.js";
7
+ export { extractInlineRefMentions } from "./inline-refs.js";
7
8
  const HARNESSES = [new ClaudeCodeProvider(), new OpenCodeProvider()];
9
+ // #562: the unified HARNESS_REGISTRY is the single source of truth for which
10
+ // harnesses expose session logs. Validate (behaviour-preserving) that every
11
+ // session-log provider instantiated above resolves to a registry harness whose
12
+ // `sessionLogs` capability is set — and via the id-normalization bridge, so a
13
+ // provider named "claude-code" still maps to the canonical "claude" harness.
14
+ // This turns a silently-drifting third registry into a startup invariant.
15
+ for (const provider of HARNESSES) {
16
+ const harness = getHarness(provider.name);
17
+ if (!harness?.capabilities.sessionLogs) {
18
+ throw new Error(`[akm] session-log provider "${provider.name}" is not registered as a sessionLogs harness in HARNESS_REGISTRY (src/integrations/harnesses). Add it there.`);
19
+ }
20
+ }
21
+ // Touch the derived list so the dependency is explicit and tree-shake-safe.
22
+ void SESSION_LOG_HARNESSES;
8
23
  const ERROR_PATTERNS = /error|failed|exception|cannot|undefined|null pointer|ENOENT|timeout/i;
9
24
  /**
10
25
  * Returns all available session log harnesses for the current machine.
@@ -51,19 +66,77 @@ export function aggregateSessionEvents(events) {
51
66
  }));
52
67
  }
53
68
  /**
54
- * Scan recent session logs from all available harnesses and return
55
- * repeated failure patterns that might warrant new AKM assets.
69
+ * Collect normalized session events from a set of harnesses for the health
70
+ * candidate scan (#568).
71
+ *
72
+ * Pipeline selection per harness (capability-gated):
73
+ * - readSession-capable harness (`supportsReadSession !== false`): drive the
74
+ * richer `listSessions()` + `readSession()` pipeline. `readSession` flattens
75
+ * structured content — tool calls, assistant content blocks, thinking,
76
+ * tool_result — into event text (e.g. ClaudeCodeProvider's `parseClaudeEvent`
77
+ * surfaces `[tool:*]` / `[tool_result]` blocks that the legacy flat
78
+ * `readEvents` scan drops entirely). This is what lets health advisories see
79
+ * repeated tool failures / long runs that the flat scan hid.
80
+ * - legacy-only harness (`supportsReadSession === false`): fall back to the
81
+ * legacy flat `readEvents()` scan (behaviour-preserving).
82
+ *
83
+ * Extracted as a pure function (harnesses injected) so it is unit-testable
84
+ * without touching the real on-disk session-log locations.
85
+ *
86
+ * `maxSessionsPerHarness` bounds the rich path: `readSession()` reads each
87
+ * session file IN FULL (unlike the legacy flat scan, which only touched files
88
+ * with mtime ≥ sinceMs and skipped non-string content). On a machine with a
89
+ * deep `~/.claude/projects` history a 30-day window can hold hundreds of
90
+ * multi-MB session files, and reading+parsing every one in full made the
91
+ * health command (`akm health`, which calls this synchronously) blow past its
92
+ * latency budget. `listSessions()` returns summaries sorted newest-first, so
93
+ * capping to the most-recent N sessions per harness keeps the richer signal
94
+ * for what actually matters (recent activity) while bounding cost. The legacy
95
+ * flat-scan path is naturally cheaper and is left uncapped.
56
96
  */
57
- export function getExecutionLogCandidates(sinceDays = 7) {
58
- const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
97
+ const DEFAULT_MAX_SESSIONS_PER_HARNESS = 50;
98
+ export function collectSessionEvents(harnesses, sinceMs, maxSessionsPerHarness = DEFAULT_MAX_SESSIONS_PER_HARNESS) {
59
99
  const events = [];
60
- for (const harness of getAvailableHarnesses()) {
100
+ for (const harness of harnesses) {
61
101
  try {
62
- events.push(...harness.readEvents({ sinceMs }));
102
+ if (harness.supportsReadSession === false) {
103
+ // Legacy-only harness: only the flat event scan is available.
104
+ events.push(...harness.readEvents({ sinceMs }));
105
+ continue;
106
+ }
107
+ // Rich path: enumerate sessions cheaply, then read each one's full
108
+ // structured event stream. Falls back to readEvents if listSessions
109
+ // surfaces nothing (e.g. a harness that wired readSession but whose
110
+ // listSessions returns empty on this machine) so we never regress
111
+ // coverage relative to the legacy scan.
112
+ const summaries = harness.listSessions({ sinceMs });
113
+ if (summaries.length === 0) {
114
+ events.push(...harness.readEvents({ sinceMs }));
115
+ continue;
116
+ }
117
+ // summaries are newest-first; bound the full-file reads (see doc above).
118
+ for (const summary of summaries.slice(0, maxSessionsPerHarness)) {
119
+ try {
120
+ const session = harness.readSession(summary);
121
+ events.push(...session.events);
122
+ }
123
+ catch {
124
+ // a single unreadable session is non-fatal
125
+ }
126
+ }
63
127
  }
64
128
  catch {
65
129
  // individual harness failures are non-fatal
66
130
  }
67
131
  }
132
+ return events;
133
+ }
134
+ /**
135
+ * Scan recent session logs from all available harnesses and return
136
+ * repeated failure patterns that might warrant new AKM assets.
137
+ */
138
+ export function getExecutionLogCandidates(sinceDays = 7) {
139
+ const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
140
+ const events = collectSessionEvents(getAvailableHarnesses(), sinceMs);
68
141
  return aggregateSessionEvents(events);
69
142
  }
@@ -1,10 +1,10 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { getDefaultLlmConfig } from "../core/config";
5
- import { warn } from "../core/warn";
6
- import { resolveAgentProfile, runAgent } from "../integrations/agent";
7
- import { chatCompletion } from "./client";
4
+ import { getDefaultLlmConfig } from "../core/config/config.js";
5
+ import { warn } from "../core/warn.js";
6
+ import { resolveAgentProfile, runAgent } from "../integrations/agent/index.js";
7
+ import { chatCompletion } from "./client.js";
8
8
  /**
9
9
  * Unified AI call: prefers the default agent profile, falls back to the
10
10
  * default LLM profile. When neither is configured, returns a structured