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.
- package/CHANGELOG.md +711 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/profiles/default.json +9 -4
- package/dist/assets/profiles/frequent.json +1 -1
- package/dist/assets/profiles/memory-focus.json +1 -1
- package/dist/assets/profiles/quick.json +1 -1
- package/dist/assets/profiles/synthesize.json +15 -0
- package/dist/assets/profiles/thorough.json +1 -1
- package/dist/assets/prompts/consolidate-system.md +23 -0
- package/dist/assets/prompts/contradiction-judge.md +33 -0
- package/dist/assets/prompts/distill-knowledge-system.md +22 -0
- package/dist/assets/prompts/distill-lesson-system.md +36 -0
- package/dist/assets/prompts/extract-session.md +6 -2
- package/dist/assets/prompts/graph-extract-system.md +1 -0
- package/dist/assets/prompts/graph-extract-user-prompt.md +1 -1
- package/dist/assets/prompts/memory-infer-system.md +1 -0
- package/dist/assets/prompts/memory-infer-user.md +5 -0
- package/dist/assets/prompts/metadata-enhance-system.md +1 -0
- package/dist/assets/prompts/procedural-system.md +44 -0
- package/dist/assets/prompts/recombine-system.md +40 -0
- package/dist/assets/prompts/staleness-detect-system.md +6 -0
- package/dist/assets/prompts/validate-summary-judge.md +1 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/agent.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/command.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/fact.md +39 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/knowledge.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/lesson.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/memory.md +38 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/script.md +43 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/skill.md +40 -0
- package/dist/assets/stash-skeleton/facts/conventions/assets/workflow.md +43 -0
- package/dist/assets/templates/html/health.html +281 -111
- package/dist/assets/wiki/ingest-workflow-template.md +38 -10
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli/shared.js +28 -0
- package/dist/cli.js +27 -11
- package/dist/commands/agent/agent-dispatch.js +2 -2
- package/dist/commands/agent/agent-support.js +0 -7
- package/dist/commands/agent/contribute-cli.js +17 -4
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +33 -26
- package/dist/commands/env/secret-cli.js +36 -22
- package/dist/commands/feedback-cli.js +15 -6
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +76 -72
- package/dist/commands/health/checks.js +49 -1
- package/dist/commands/health/html-report.js +422 -80
- package/dist/commands/health.js +386 -9
- package/dist/commands/improve/calibration.js +161 -0
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +81 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/{lint.js → improve/consolidate/types.js} +1 -1
- package/dist/commands/improve/consolidate.js +635 -660
- package/dist/commands/improve/dedup.js +482 -0
- package/dist/commands/improve/distill.js +159 -69
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/encoding-salience.js +205 -0
- package/dist/commands/improve/extract-cli.js +124 -2
- package/dist/commands/improve/extract-prompt.js +39 -2
- package/dist/commands/improve/extract-watch.js +140 -0
- package/dist/commands/improve/extract.js +389 -40
- package/dist/commands/improve/feedback-valence.js +54 -0
- package/dist/commands/improve/homeostatic.js +467 -0
- package/dist/commands/improve/improve-auto-accept.js +138 -7
- package/dist/commands/improve/improve-cli.js +36 -61
- package/dist/commands/improve/improve-profiles.js +14 -0
- package/dist/commands/improve/improve-result-file.js +14 -25
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +485 -2498
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1083 -0
- package/dist/commands/improve/memory/memory-contradiction-detect.js +23 -28
- package/dist/commands/improve/outcome-loop.js +256 -0
- package/dist/commands/improve/preparation.js +1966 -0
- package/dist/commands/improve/proactive-maintenance.js +115 -0
- package/dist/commands/improve/procedural.js +418 -0
- package/dist/commands/improve/recombine.js +850 -0
- package/dist/commands/improve/reflect-noise.js +0 -0
- package/dist/commands/improve/reflect.js +183 -40
- package/dist/commands/improve/salience.js +438 -0
- package/dist/commands/improve/triage.js +93 -0
- package/dist/commands/lint/agent-linter.js +19 -24
- package/dist/commands/lint/base-linter.js +173 -60
- package/dist/commands/lint/command-linter.js +19 -24
- package/dist/commands/lint/env-key-rules.js +38 -1
- package/dist/commands/lint/fact-linter.js +39 -0
- package/dist/commands/lint/index.js +31 -13
- package/dist/commands/lint/memory-linter.js +1 -1
- package/dist/commands/lint/registry.js +7 -2
- package/dist/commands/lint/task-linter.js +3 -3
- package/dist/commands/lint/workflow-linter.js +26 -1
- package/dist/commands/proposal/drain-policies.js +5 -0
- package/dist/commands/proposal/drain.js +43 -50
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/proposal.js +5 -0
- package/dist/commands/proposal/propose.js +7 -2
- package/dist/commands/proposal/validators/proposal-quality-validators.js +9 -8
- package/dist/commands/proposal/validators/proposals.js +189 -63
- package/dist/commands/read/curate.js +414 -94
- package/dist/commands/read/knowledge.js +6 -3
- package/dist/commands/read/search-cli.js +9 -4
- package/dist/commands/read/search.js +10 -6
- package/dist/commands/read/show.js +86 -7
- package/dist/commands/sources/init.js +49 -17
- package/dist/commands/sources/installed-stashes.js +11 -3
- package/dist/commands/sources/schema-repair.js +43 -45
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/sources/stash-cli.js +28 -40
- package/dist/commands/sources/stash-skeleton.js +23 -8
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/tasks/tasks.js +39 -11
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/asset/asset-registry.js +3 -1
- package/dist/core/asset/asset-spec.js +18 -2
- package/dist/core/asset/frontmatter.js +166 -167
- package/dist/core/asset/markdown.js +8 -0
- package/dist/core/authoring-rules.js +92 -0
- package/dist/core/common.js +0 -5
- package/dist/core/config/config-migration.js +12 -11
- package/dist/core/config/config-schema.js +340 -56
- package/dist/core/config/config-types.js +3 -3
- package/dist/core/config/config.js +28 -7
- package/dist/core/events.js +3 -7
- package/dist/core/improve-types.js +11 -8
- package/dist/core/logs-db.js +10 -66
- package/dist/core/parse.js +36 -16
- package/dist/core/paths.js +3 -0
- package/dist/core/standards/resolve-standards-context.js +87 -0
- package/dist/core/standards/resolve-stash-standards.js +99 -0
- package/dist/core/standards/resolve-type-conventions.js +66 -0
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +525 -474
- package/dist/indexer/db/db.js +439 -247
- package/dist/indexer/db/graph-db.js +129 -86
- package/dist/indexer/ensure-index.js +152 -17
- package/dist/indexer/graph/graph-boost.js +51 -41
- package/dist/indexer/graph/graph-extraction.js +218 -4
- package/dist/indexer/index-writer-lock.js +99 -0
- package/dist/indexer/indexer.js +123 -221
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/passes/memory-inference.js +13 -5
- package/dist/indexer/passes/staleness-detect.js +2 -5
- package/dist/indexer/search/db-search.js +19 -6
- package/dist/indexer/search/ranking-contributors.js +22 -0
- package/dist/indexer/search/ranking.js +4 -0
- package/dist/indexer/search/search-source.js +17 -18
- package/dist/indexer/search/semantic-status.js +4 -0
- package/dist/indexer/walk/matchers.js +9 -0
- package/dist/integrations/agent/config.js +6 -53
- package/dist/integrations/agent/index.js +2 -18
- package/dist/integrations/agent/prompts.js +75 -9
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/integrations/harnesses/claude/session-log.js +11 -1
- package/dist/integrations/harnesses/index.js +2 -3
- package/dist/integrations/harnesses/opencode/session-log.js +173 -3
- package/dist/integrations/harnesses/opencode-sdk/index.js +2 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +0 -2
- package/dist/integrations/session-logs/index.js +16 -0
- package/dist/llm/client.js +45 -15
- package/dist/llm/embedder.js +42 -3
- package/dist/llm/embedders/deterministic.js +66 -0
- package/dist/llm/embedders/local.js +66 -2
- package/dist/llm/feature-gate.js +8 -4
- package/dist/llm/graph-extract.js +67 -44
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -127
- package/dist/llm/metadata-enhance.js +44 -31
- package/dist/llm/structured-call.js +49 -0
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/context.js +5 -5
- package/dist/output/renderers.js +74 -2
- package/dist/output/shapes/curate.js +14 -2
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/text/helpers.js +16 -1
- package/dist/registry/providers/skills-sh.js +21 -147
- package/dist/registry/providers/static-index.js +15 -157
- package/dist/registry/resolve.js +22 -9
- package/dist/runtime.js +25 -1
- package/dist/scripts/migrate-storage.js +2617 -1961
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +759 -510
- package/dist/setup/setup.js +29 -8
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +210 -0
- package/dist/sources/providers/git-provider.js +234 -0
- package/dist/sources/providers/git-stash.js +248 -0
- package/dist/sources/providers/git.js +10 -661
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/tar-utils.js +16 -8
- package/dist/sources/providers/website.js +9 -5
- package/dist/sources/website-ingest.js +187 -29
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +239 -0
- package/dist/storage/database.js +45 -10
- package/dist/storage/managed-db.js +82 -0
- package/dist/storage/repositories/registry-cache.js +92 -0
- package/dist/storage/sqlite-pragmas.js +146 -0
- package/dist/tasks/backends/cron.js +1 -1
- package/dist/tasks/backends/launchd.js +1 -1
- package/dist/tasks/backends/schtasks.js +1 -1
- package/dist/tasks/{resolveAkmBin.js → resolve-akm-bin.js} +2 -2
- package/dist/tasks/runner.js +5 -13
- package/dist/text-import-hook.mjs +0 -0
- package/dist/wiki/wiki.js +37 -0
- package/dist/workflows/db.js +3 -4
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/data-and-telemetry.md +3 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +13 -11
- package/dist/commands/db-cli.js +0 -23
- 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
|
|
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
|
+
}
|