akm-cli 0.9.0-beta.5 → 0.9.0-beta.51

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 (221) hide show
  1. package/CHANGELOG.md +711 -0
  2. package/README.md +12 -4
  3. package/dist/akm +38 -0
  4. package/dist/akm-migrate-storage +38 -0
  5. package/dist/assets/profiles/default.json +9 -4
  6. package/dist/assets/profiles/frequent.json +1 -1
  7. package/dist/assets/profiles/memory-focus.json +1 -1
  8. package/dist/assets/profiles/quick.json +1 -1
  9. package/dist/assets/profiles/synthesize.json +15 -0
  10. package/dist/assets/profiles/thorough.json +1 -1
  11. package/dist/assets/prompts/consolidate-system.md +23 -0
  12. package/dist/assets/prompts/contradiction-judge.md +33 -0
  13. package/dist/assets/prompts/distill-knowledge-system.md +22 -0
  14. package/dist/assets/prompts/distill-lesson-system.md +36 -0
  15. package/dist/assets/prompts/extract-session.md +6 -2
  16. package/dist/assets/prompts/graph-extract-system.md +1 -0
  17. package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
  18. package/dist/assets/prompts/memory-infer-system.md +1 -0
  19. package/dist/assets/prompts/memory-infer-user.md +5 -0
  20. package/dist/assets/prompts/metadata-enhance-system.md +1 -0
  21. package/dist/assets/prompts/procedural-system.md +44 -0
  22. package/dist/assets/prompts/recombine-system.md +40 -0
  23. package/dist/assets/prompts/staleness-detect-system.md +6 -0
  24. package/dist/assets/prompts/validate-summary-judge.md +1 -0
  25. package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
  26. package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
  27. package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
  28. package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
  29. package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
  30. package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
  31. package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
  32. package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
  33. package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
  34. package/dist/assets/templates/html/health.html +281 -111
  35. package/dist/assets/wiki/ingest-workflow-template.md +38 -10
  36. package/dist/cli/parse-args.js +46 -1
  37. package/dist/cli/shared.js +28 -0
  38. package/dist/cli.js +27 -11
  39. package/dist/commands/agent/agent-dispatch.js +2 -2
  40. package/dist/commands/agent/agent-support.js +0 -7
  41. package/dist/commands/agent/contribute-cli.js +17 -4
  42. package/dist/commands/config-cli.js +18 -2
  43. package/dist/commands/env/child-env.js +47 -0
  44. package/dist/commands/env/env-cli.js +33 -26
  45. package/dist/commands/env/secret-cli.js +36 -22
  46. package/dist/commands/feedback-cli.js +15 -6
  47. package/dist/commands/graph/graph-cli.js +5 -13
  48. package/dist/commands/graph/graph.js +76 -72
  49. package/dist/commands/health/checks.js +49 -1
  50. package/dist/commands/health/html-report.js +422 -80
  51. package/dist/commands/health.js +386 -9
  52. package/dist/commands/improve/calibration.js +161 -0
  53. package/dist/commands/improve/consolidate/chunking.js +141 -0
  54. package/dist/commands/improve/consolidate/eligibility.js +81 -0
  55. package/dist/commands/improve/consolidate/merge.js +145 -0
  56. package/dist/commands/improve/consolidate/sanitize.js +231 -0
  57. package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
  58. package/dist/commands/improve/consolidate.js +635 -660
  59. package/dist/commands/improve/dedup.js +482 -0
  60. package/dist/commands/improve/distill.js +159 -69
  61. package/dist/commands/improve/eligibility.js +434 -0
  62. package/dist/commands/improve/encoding-salience.js +205 -0
  63. package/dist/commands/improve/extract-cli.js +124 -2
  64. package/dist/commands/improve/extract-prompt.js +39 -2
  65. package/dist/commands/improve/extract-watch.js +140 -0
  66. package/dist/commands/improve/extract.js +389 -40
  67. package/dist/commands/improve/feedback-valence.js +54 -0
  68. package/dist/commands/improve/homeostatic.js +467 -0
  69. package/dist/commands/improve/improve-auto-accept.js +138 -7
  70. package/dist/commands/improve/improve-cli.js +36 -61
  71. package/dist/commands/improve/improve-profiles.js +14 -0
  72. package/dist/commands/improve/improve-result-file.js +14 -25
  73. package/dist/commands/improve/improve-session.js +58 -0
  74. package/dist/commands/improve/improve.js +485 -2498
  75. package/dist/commands/improve/locks.js +154 -0
  76. package/dist/commands/improve/loop-stages.js +1083 -0
  77. package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
  78. package/dist/commands/improve/outcome-loop.js +256 -0
  79. package/dist/commands/improve/preparation.js +1966 -0
  80. package/dist/commands/improve/proactive-maintenance.js +115 -0
  81. package/dist/commands/improve/procedural.js +418 -0
  82. package/dist/commands/improve/recombine.js +850 -0
  83. package/dist/commands/improve/reflect-noise.js +0 -0
  84. package/dist/commands/improve/reflect.js +183 -40
  85. package/dist/commands/improve/salience.js +438 -0
  86. package/dist/commands/improve/triage.js +93 -0
  87. package/dist/commands/lint/agent-linter.js +19 -24
  88. package/dist/commands/lint/base-linter.js +173 -60
  89. package/dist/commands/lint/command-linter.js +19 -24
  90. package/dist/commands/lint/env-key-rules.js +38 -1
  91. package/dist/commands/lint/fact-linter.js +39 -0
  92. package/dist/commands/lint/index.js +31 -13
  93. package/dist/commands/lint/memory-linter.js +1 -1
  94. package/dist/commands/lint/registry.js +7 -2
  95. package/dist/commands/lint/task-linter.js +3 -3
  96. package/dist/commands/lint/workflow-linter.js +26 -1
  97. package/dist/commands/proposal/drain-policies.js +5 -0
  98. package/dist/commands/proposal/drain.js +43 -50
  99. package/dist/commands/proposal/proposal-cli.js +21 -31
  100. package/dist/commands/proposal/proposal.js +5 -0
  101. package/dist/commands/proposal/propose.js +7 -2
  102. package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
  103. package/dist/commands/proposal/validators/proposals.js +189 -63
  104. package/dist/commands/read/curate.js +414 -94
  105. package/dist/commands/read/knowledge.js +6 -3
  106. package/dist/commands/read/search-cli.js +9 -4
  107. package/dist/commands/read/search.js +10 -6
  108. package/dist/commands/read/show.js +86 -7
  109. package/dist/commands/sources/init.js +49 -17
  110. package/dist/commands/sources/installed-stashes.js +11 -3
  111. package/dist/commands/sources/schema-repair.js +43 -45
  112. package/dist/commands/sources/self-update.js +2 -2
  113. package/dist/commands/sources/source-add.js +7 -3
  114. package/dist/commands/sources/stash-cli.js +28 -40
  115. package/dist/commands/sources/stash-skeleton.js +23 -8
  116. package/dist/commands/tasks/tasks-cli.js +19 -27
  117. package/dist/commands/tasks/tasks.js +39 -11
  118. package/dist/commands/wiki-cli.js +21 -35
  119. package/dist/core/asset/asset-registry.js +3 -1
  120. package/dist/core/asset/asset-spec.js +18 -2
  121. package/dist/core/asset/frontmatter.js +166 -167
  122. package/dist/core/asset/markdown.js +8 -0
  123. package/dist/core/authoring-rules.js +92 -0
  124. package/dist/core/common.js +0 -5
  125. package/dist/core/config/config-migration.js +12 -11
  126. package/dist/core/config/config-schema.js +340 -56
  127. package/dist/core/config/config-types.js +3 -3
  128. package/dist/core/config/config.js +28 -7
  129. package/dist/core/events.js +3 -7
  130. package/dist/core/improve-types.js +11 -8
  131. package/dist/core/logs-db.js +10 -66
  132. package/dist/core/parse.js +36 -16
  133. package/dist/core/paths.js +3 -0
  134. package/dist/core/standards/resolve-standards-context.js +87 -0
  135. package/dist/core/standards/resolve-stash-standards.js +99 -0
  136. package/dist/core/standards/resolve-type-conventions.js +66 -0
  137. package/dist/core/state/migrations.js +714 -0
  138. package/dist/core/state-db.js +525 -474
  139. package/dist/indexer/db/db.js +439 -247
  140. package/dist/indexer/db/graph-db.js +129 -86
  141. package/dist/indexer/ensure-index.js +152 -17
  142. package/dist/indexer/graph/graph-boost.js +51 -41
  143. package/dist/indexer/graph/graph-extraction.js +218 -4
  144. package/dist/indexer/index-writer-lock.js +99 -0
  145. package/dist/indexer/indexer.js +123 -221
  146. package/dist/indexer/passes/dir-staleness.js +114 -0
  147. package/dist/indexer/passes/memory-inference.js +13 -5
  148. package/dist/indexer/passes/staleness-detect.js +2 -5
  149. package/dist/indexer/search/db-search.js +19 -6
  150. package/dist/indexer/search/ranking-contributors.js +22 -0
  151. package/dist/indexer/search/ranking.js +4 -0
  152. package/dist/indexer/search/search-source.js +17 -18
  153. package/dist/indexer/search/semantic-status.js +4 -0
  154. package/dist/indexer/walk/matchers.js +9 -0
  155. package/dist/integrations/agent/config.js +6 -53
  156. package/dist/integrations/agent/index.js +2 -18
  157. package/dist/integrations/agent/prompts.js +75 -9
  158. package/dist/integrations/agent/runner-dispatch.js +59 -0
  159. package/dist/integrations/harnesses/claude/session-log.js +11 -1
  160. package/dist/integrations/harnesses/index.js +2 -3
  161. package/dist/integrations/harnesses/opencode/session-log.js +173 -3
  162. package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
  163. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
  164. package/dist/integrations/session-logs/index.js +16 -0
  165. package/dist/llm/client.js +45 -15
  166. package/dist/llm/embedder.js +42 -3
  167. package/dist/llm/embedders/deterministic.js +66 -0
  168. package/dist/llm/embedders/local.js +66 -2
  169. package/dist/llm/feature-gate.js +8 -4
  170. package/dist/llm/graph-extract.js +67 -44
  171. package/dist/llm/memory-infer-impl.js +138 -0
  172. package/dist/llm/memory-infer.js +1 -127
  173. package/dist/llm/metadata-enhance.js +44 -31
  174. package/dist/llm/structured-call.js +49 -0
  175. package/dist/migrate-storage-node.mjs +8 -0
  176. package/dist/output/context.js +5 -5
  177. package/dist/output/renderers.js +74 -2
  178. package/dist/output/shapes/curate.js +14 -2
  179. package/dist/output/shapes/passthrough.js +0 -1
  180. package/dist/output/text/helpers.js +16 -1
  181. package/dist/registry/providers/skills-sh.js +21 -147
  182. package/dist/registry/providers/static-index.js +15 -157
  183. package/dist/registry/resolve.js +22 -9
  184. package/dist/runtime.js +25 -1
  185. package/dist/scripts/migrate-storage.js +2617 -1961
  186. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
  187. package/dist/setup/setup.js +29 -8
  188. package/dist/sources/include.js +6 -2
  189. package/dist/sources/providers/filesystem.js +0 -1
  190. package/dist/sources/providers/git-install.js +210 -0
  191. package/dist/sources/providers/git-provider.js +234 -0
  192. package/dist/sources/providers/git-stash.js +248 -0
  193. package/dist/sources/providers/git.js +10 -661
  194. package/dist/sources/providers/npm.js +2 -6
  195. package/dist/sources/providers/provider-utils.js +13 -7
  196. package/dist/sources/providers/sync-from-ref.js +9 -1
  197. package/dist/sources/providers/tar-utils.js +16 -8
  198. package/dist/sources/providers/website.js +9 -5
  199. package/dist/sources/website-ingest.js +187 -29
  200. package/dist/sources/wiki-fetchers/registry.js +53 -0
  201. package/dist/sources/wiki-fetchers/youtube.js +239 -0
  202. package/dist/storage/database.js +45 -10
  203. package/dist/storage/managed-db.js +82 -0
  204. package/dist/storage/repositories/registry-cache.js +92 -0
  205. package/dist/storage/sqlite-pragmas.js +146 -0
  206. package/dist/tasks/backends/cron.js +1 -1
  207. package/dist/tasks/backends/launchd.js +1 -1
  208. package/dist/tasks/backends/schtasks.js +1 -1
  209. package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
  210. package/dist/tasks/runner.js +5 -13
  211. package/dist/text-import-hook.mjs +0 -0
  212. package/dist/wiki/wiki.js +37 -0
  213. package/dist/workflows/db.js +3 -4
  214. package/dist/workflows/runtime/runs.js +1 -117
  215. package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
  216. package/dist/workflows/validate-summary.js +2 -7
  217. package/docs/data-and-telemetry.md +3 -2
  218. package/docs/migration/release-notes/0.9.0.md +39 -0
  219. package/package.json +13 -11
  220. package/dist/commands/db-cli.js +0 -23
  221. package/dist/indexer/db/db-backup.js +0 -376
@@ -14,11 +14,14 @@
14
14
  * Output is the AkmExtractResult JSON envelope (or an aggregated one when
15
15
  * `--auto` runs multiple harnesses).
16
16
  */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
17
19
  import { defineCommand } from "citty";
18
- import { output, runWithJsonErrors } from "../../cli/shared.js";
20
+ import { EXIT_CODES, output, runWithJsonErrors } from "../../cli/shared.js";
19
21
  import { UsageError } from "../../core/errors.js";
20
- import { getAvailableHarnesses } from "../../integrations/session-logs/index.js";
22
+ import { getAvailableHarnesses, getWatchTargets } from "../../integrations/session-logs/index.js";
21
23
  import { akmExtract } from "./extract.js";
24
+ import { akmExtractWatch } from "./extract-watch.js";
22
25
  export const extractCommand = defineCommand({
23
26
  meta: {
24
27
  name: "extract",
@@ -60,6 +63,15 @@ export const extractCommand = defineCommand({
60
63
  type: "string",
61
64
  description: "Per-session LLM timeout in ms (default 60000).",
62
65
  },
66
+ watch: {
67
+ type: "boolean",
68
+ description: "Watch harness session-log directories and run extract on change (debounced). Stays alive until SIGINT/SIGTERM. Falls back to the */30 cron when off.",
69
+ default: false,
70
+ },
71
+ "debounce-ms": {
72
+ type: "string",
73
+ description: "Debounce window in ms for --watch (default 2000).",
74
+ },
63
75
  },
64
76
  async run({ args }) {
65
77
  await runWithJsonErrors(async () => {
@@ -76,6 +88,17 @@ export const extractCommand = defineCommand({
76
88
  if (timeoutMs !== undefined && (!Number.isFinite(timeoutMs) || timeoutMs <= 0)) {
77
89
  throw new UsageError(`--timeout-ms must be a positive integer (got "${args["timeout-ms"]}").`, "INVALID_FLAG_VALUE");
78
90
  }
91
+ const watch = args.watch === true;
92
+ const debounceMs = typeof args["debounce-ms"] === "string" && args["debounce-ms"] !== ""
93
+ ? Number.parseInt(args["debounce-ms"], 10)
94
+ : 2000;
95
+ if (watch && (!Number.isFinite(debounceMs) || debounceMs <= 0)) {
96
+ throw new UsageError(`--debounce-ms must be a positive integer (got "${args["debounce-ms"]}").`, "INVALID_FLAG_VALUE");
97
+ }
98
+ if (watch) {
99
+ await runWatchMode({ debounceMs, dryRun, force, ...(since ? { since } : {}) });
100
+ return;
101
+ }
79
102
  if (auto && type) {
80
103
  throw new UsageError("--auto and --type are mutually exclusive. Pick one.", "INVALID_FLAG_VALUE");
81
104
  }
@@ -118,10 +141,109 @@ export const extractCommand = defineCommand({
118
141
  totalProposals,
119
142
  results,
120
143
  });
144
+ // Signal failure to callers/cron when every harness failed. output()
145
+ // only renders; without this the */30 cron exits 0 on a total failure
146
+ // and the breakage is invisible to exit-code monitoring. process.exitCode
147
+ // (not process.exit) lets stdout flush and the watcher/timers settle.
148
+ if (!ok)
149
+ process.exitCode = EXIT_CODES.GENERAL;
121
150
  return;
122
151
  }
123
152
  const result = await akmExtract({ type, ...commonOptions });
124
153
  output("extract", result);
154
+ if (!result.ok)
155
+ process.exitCode = EXIT_CODES.GENERAL;
125
156
  });
126
157
  },
127
158
  });
159
+ /**
160
+ * A thin {@link WatchEventSource} over `fs.watch` for each configured root.
161
+ * This adapter is the ONLY place a real `fs.watch` is created (the core stays
162
+ * injectable + fully unit-tested); it is intentionally not unit-covered.
163
+ *
164
+ * `fs.watch(dir, { recursive: true })` is unreliable for recursive mode on some
165
+ * Node/Bun/Linux combinations — that is acceptable here: missed events degrade
166
+ * to the unchanged scheduled cron fallback, never worse than today.
167
+ */
168
+ function createFsWatchEventSource(roots) {
169
+ return {
170
+ subscribe(listener) {
171
+ const watchers = [];
172
+ for (const root of roots) {
173
+ try {
174
+ const watcher = fs.watch(root, { recursive: true }, (_event, filename) => {
175
+ if (!filename)
176
+ return;
177
+ const filePath = path.isAbsolute(filename.toString())
178
+ ? filename.toString()
179
+ : path.join(root, filename.toString());
180
+ listener({ path: filePath });
181
+ });
182
+ watchers.push(watcher);
183
+ }
184
+ catch {
185
+ // A root that can't be watched is skipped; the cron fallback covers it.
186
+ }
187
+ }
188
+ return () => {
189
+ for (const w of watchers) {
190
+ try {
191
+ w.close();
192
+ }
193
+ catch {
194
+ // best-effort teardown
195
+ }
196
+ }
197
+ };
198
+ },
199
+ };
200
+ }
201
+ /**
202
+ * Run `akm extract --watch`: watch every available harness's session-log
203
+ * roots and run extract (debounced, per-harness) on change. Stays alive until
204
+ * SIGINT/SIGTERM, then stops cleanly. PROCESS-HYGIENE: stop() removes every
205
+ * watcher + pending timer before the process exits.
206
+ */
207
+ async function runWatchMode(opts) {
208
+ const targets = getWatchTargets();
209
+ if (targets.length === 0) {
210
+ output("extract", {
211
+ schemaVersion: 1,
212
+ ok: false,
213
+ shape: "extract-watch-started",
214
+ warnings: ["no watchable harness session-log directories found on this machine"],
215
+ watching: [],
216
+ });
217
+ return;
218
+ }
219
+ const allRoots = targets.flatMap((t) => t.roots);
220
+ const eventSource = createFsWatchEventSource(allRoots);
221
+ const handle = akmExtractWatch({
222
+ roots: targets,
223
+ eventSource,
224
+ debounceMs: opts.debounceMs,
225
+ onTrigger: async (harnessName) => {
226
+ await akmExtract({
227
+ type: harnessName,
228
+ dryRun: opts.dryRun,
229
+ force: opts.force,
230
+ ...(opts.since ? { since: opts.since } : {}),
231
+ });
232
+ },
233
+ });
234
+ output("extract", {
235
+ schemaVersion: 1,
236
+ ok: true,
237
+ shape: "extract-watch-started",
238
+ debounceMs: opts.debounceMs,
239
+ watching: allRoots,
240
+ });
241
+ await new Promise((resolve) => {
242
+ const shutdown = () => {
243
+ handle.stop();
244
+ resolve();
245
+ };
246
+ process.once("SIGINT", shutdown);
247
+ process.once("SIGTERM", shutdown);
248
+ });
249
+ }
@@ -21,12 +21,18 @@ import promptTemplate from "../../assets/prompts/extract-session.md" with { type
21
21
  *
22
22
  * Shape:
23
23
  * {
24
- * "candidates": [{type, name, description, when_to_use?, body, confidence, evidence}, ...],
24
+ * "candidates": [{type, name, description, when_to_use?, body, confidence, evidence,
25
+ * orderedActions?, outcomeData?}, ...],
25
26
  * "rationale_if_empty"?: string
26
27
  * }
27
28
  *
28
29
  * `additionalProperties: false` at each level so any hallucinated keys are
29
30
  * dropped before parsing.
31
+ *
32
+ * `orderedActions` and `outcomeData` are additive fields for #615 procedural
33
+ * compilation (WS-0 data-capture hook). Source transcripts are external logs
34
+ * not guaranteed re-extractable; we capture the ordered-action sequence and
35
+ * outcome now even though the detection/compilation feature is deferred to 0.10+.
30
36
  */
31
37
  export const EXTRACT_JSON_SCHEMA = {
32
38
  type: "object",
@@ -55,7 +61,7 @@ export const EXTRACT_JSON_SCHEMA = {
55
61
  type: "string",
56
62
  minLength: 20,
57
63
  maxLength: 400,
58
- description: "One-sentence summary of the candidate. Must be a complete sentence; do not end mid-clause.",
64
+ description: "One-sentence summary of the candidate. Must be a complete sentence in active voice. Do NOT start with 'When', 'If', 'How', 'Use', or 'Avoid'. Do NOT end with ':', ';', or ','. Do NOT use heading-fragment text ('Summary', 'Overview', 'Key finding:'). Minimum 20 characters, maximum 400 characters.",
59
65
  },
60
66
  when_to_use: {
61
67
  type: "string",
@@ -79,6 +85,17 @@ export const EXTRACT_JSON_SCHEMA = {
79
85
  minLength: 5,
80
86
  description: "One-line pointer to the moment in the session that supports this candidate.",
81
87
  },
88
+ orderedActions: {
89
+ type: "array",
90
+ description: "OPTIONAL. Ordered list of discrete actions taken during this episode, as brief imperative phrases (e.g. 'run deploy.sh', 'check VPN status', 'retry with --force'). Capture only when the candidate represents a recurring action sequence that could become a skill or workflow. Omit when the candidate is a standalone fact or observation.",
91
+ items: { type: "string", minLength: 3 },
92
+ maxItems: 20,
93
+ },
94
+ outcomeData: {
95
+ type: "string",
96
+ description: "OPTIONAL. One-sentence description of the outcome when the ordered action sequence completed (e.g. 'deploy succeeded after VPN reconnect', 'build failed with error X'). Required when orderedActions is present; omit otherwise.",
97
+ maxLength: 400,
98
+ },
82
99
  },
83
100
  },
84
101
  },
@@ -132,6 +149,11 @@ export function buildExtractPrompt(input) {
132
149
  const ref = input.data.ref;
133
150
  const startedAt = ref.startedAt ? new Date(ref.startedAt).toISOString() : "unknown";
134
151
  const endedAt = ref.endedAt ? new Date(ref.endedAt).toISOString() : "unknown";
152
+ // Optional standards block — rendered to the lead-in + body when present,
153
+ // or an empty string (no section) when absent. Gated on non-empty.
154
+ const standards = input.standardsContext?.trim()
155
+ ? `\n## Standards to follow (the rulebook for this target)\n\n${input.standardsContext.trim()}\n`
156
+ : "";
135
157
  return promptTemplate
136
158
  .replace("{{HARNESS}}", ref.harness)
137
159
  .replace("{{TITLE}}", ref.title ?? "(no title)")
@@ -139,6 +161,7 @@ export function buildExtractPrompt(input) {
139
161
  .replace("{{ENDED_AT}}", endedAt)
140
162
  .replace("{{PROJECT_HINT}}", ref.projectHint ?? "(no project hint)")
141
163
  .replace("{{ALREADY_PRESERVED}}", formatAlreadyPreserved(input.inlineRefs))
164
+ .replace("{{STANDARDS}}", standards)
142
165
  .replace("{{TRANSCRIPT}}", formatTranscript(input.events));
143
166
  }
144
167
  /**
@@ -207,6 +230,20 @@ export function parseExtractPayload(stdout) {
207
230
  };
208
231
  if (typeof c.when_to_use === "string")
209
232
  candidate.when_to_use = c.when_to_use.trim();
233
+ // #615 WS-0: parse optional ordered-action + outcome fields.
234
+ // Defensive: silently drop malformed entries rather than rejecting the whole candidate.
235
+ if (Array.isArray(c.orderedActions) && c.orderedActions.length > 0) {
236
+ const actions = c.orderedActions
237
+ .filter((a) => typeof a === "string" && a.trim().length >= 3)
238
+ .map((a) => a.trim())
239
+ .slice(0, 20);
240
+ if (actions.length > 0) {
241
+ candidate.orderedActions = actions;
242
+ if (typeof c.outcomeData === "string" && c.outcomeData.trim().length > 0) {
243
+ candidate.outcomeData = c.outcomeData.trim().slice(0, 400);
244
+ }
245
+ }
246
+ }
210
247
  candidates.push(candidate);
211
248
  }
212
249
  const result = { candidates };
@@ -0,0 +1,140 @@
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
+ import { akmExtract } from "./extract.js";
5
+ const DEFAULT_DEBOUNCE_MS = 2000;
6
+ /** Small look-back window for the default trigger's `akmExtract` call. */
7
+ const DEFAULT_TRIGGER_SINCE = "10m";
8
+ /**
9
+ * Does `filePath` look like a real session file under one of the configured
10
+ * roots? True only when the path is under a root AND matches that harness's
11
+ * on-disk session-file shape:
12
+ * - claude-code: `<root>/<project>/<id>.jsonl`
13
+ * - opencode: `<root>/<project>/<id>.json`
14
+ * Anything out-of-root, or in-root with the wrong extension, returns false.
15
+ */
16
+ export function isSessionFile(filePath, roots) {
17
+ return matchHarness(filePath, roots) !== undefined;
18
+ }
19
+ /** Normalize a path for prefix comparison (ensure a trailing separator). */
20
+ function withSep(dir) {
21
+ return dir.endsWith("/") ? dir : `${dir}/`;
22
+ }
23
+ /**
24
+ * Resolve which harness a path belongs to by longest-prefix match against the
25
+ * configured roots, then validate the file shape for that harness. Returns the
26
+ * harness name, or `undefined` when the path is out-of-root or not a session
27
+ * file. Longest-prefix match guards against the (in practice impossible) case
28
+ * of nested roots.
29
+ */
30
+ export function matchHarness(filePath, roots) {
31
+ let best;
32
+ let bestLen = -1;
33
+ for (const target of roots) {
34
+ for (const root of target.roots) {
35
+ const prefix = withSep(root);
36
+ if (filePath.startsWith(prefix) && prefix.length > bestLen) {
37
+ best = { harnessName: target.harnessName };
38
+ bestLen = prefix.length;
39
+ }
40
+ }
41
+ }
42
+ if (!best)
43
+ return undefined;
44
+ // Validate the file shape for the resolved harness.
45
+ if (best.harnessName === "claude-code") {
46
+ return filePath.endsWith(".jsonl") ? best.harnessName : undefined;
47
+ }
48
+ if (best.harnessName === "opencode") {
49
+ return filePath.endsWith(".json") ? best.harnessName : undefined;
50
+ }
51
+ // Unknown harness: accept any file under its root.
52
+ return best.harnessName;
53
+ }
54
+ /**
55
+ * Start the watch-mode core. Returns a handle whose `stop()` tears everything
56
+ * down. The event source is injected, so this never touches the real
57
+ * filesystem itself.
58
+ */
59
+ export function akmExtractWatch(options) {
60
+ const { roots, eventSource, debounceMs = DEFAULT_DEBOUNCE_MS, setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout, isSessionFile: isSessionFileFn = isSessionFile, } = options;
61
+ const onTrigger = options.onTrigger ??
62
+ (async (harnessName) => {
63
+ await akmExtract({ type: harnessName, since: DEFAULT_TRIGGER_SINCE, force: false });
64
+ });
65
+ let stopped = false;
66
+ // Pending debounce timers, keyed by harness name.
67
+ const pendingTimers = new Map();
68
+ // Harnesses with an in-flight trigger, and whether a re-run is queued.
69
+ const inFlight = new Set();
70
+ const rerunPending = new Set();
71
+ function runTrigger(harnessName) {
72
+ if (stopped)
73
+ return;
74
+ if (inFlight.has(harnessName)) {
75
+ // A trigger is already running for this harness; coalesce into a single
76
+ // re-run after it resolves rather than launching a concurrent extract.
77
+ rerunPending.add(harnessName);
78
+ return;
79
+ }
80
+ inFlight.add(harnessName);
81
+ // Invoke synchronously so a synchronous `onTrigger` observes its effect
82
+ // before control returns (tests advance the clock then assert inline).
83
+ // Wrap the (possibly async) result in a promise to drive the in-flight
84
+ // guard's release without forcing the trigger itself into a microtask.
85
+ let result;
86
+ try {
87
+ result = onTrigger(harnessName);
88
+ }
89
+ catch (err) {
90
+ // One failed extract must never kill the watcher (mirrors the non-fatal
91
+ // handling in collectSessionEvents).
92
+ console.error(`[akm] extract --watch trigger for "${harnessName}" failed:`, err);
93
+ inFlight.delete(harnessName);
94
+ if (rerunPending.delete(harnessName) && !stopped)
95
+ runTrigger(harnessName);
96
+ return;
97
+ }
98
+ Promise.resolve(result)
99
+ .catch((err) => {
100
+ console.error(`[akm] extract --watch trigger for "${harnessName}" failed:`, err);
101
+ })
102
+ .finally(() => {
103
+ inFlight.delete(harnessName);
104
+ if (rerunPending.delete(harnessName) && !stopped) {
105
+ runTrigger(harnessName);
106
+ }
107
+ });
108
+ }
109
+ function onEvent(e) {
110
+ if (stopped)
111
+ return;
112
+ if (!isSessionFileFn(e.path, roots))
113
+ return;
114
+ const harnessName = matchHarness(e.path, roots);
115
+ if (!harnessName)
116
+ return;
117
+ const existing = pendingTimers.get(harnessName);
118
+ if (existing !== undefined)
119
+ clearTimeoutFn(existing);
120
+ const timer = setTimeoutFn(() => {
121
+ pendingTimers.delete(harnessName);
122
+ if (!stopped)
123
+ runTrigger(harnessName);
124
+ }, debounceMs);
125
+ pendingTimers.set(harnessName, timer);
126
+ }
127
+ const unsubscribe = eventSource.subscribe(onEvent);
128
+ return {
129
+ stop() {
130
+ if (stopped)
131
+ return;
132
+ stopped = true;
133
+ unsubscribe();
134
+ for (const timer of pendingTimers.values())
135
+ clearTimeoutFn(timer);
136
+ pendingTimers.clear();
137
+ rerunPending.clear();
138
+ },
139
+ };
140
+ }