akm-cli 0.8.0-rc2 → 0.8.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.
- package/{.github/CHANGELOG.md → CHANGELOG.md} +191 -3
- package/README.md +22 -6
- package/SECURITY.md +93 -0
- package/dist/cli/config-migrate.js +144 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/confirm.js +73 -0
- package/dist/cli/parse-args.js +93 -3
- package/dist/cli/shared.js +129 -0
- package/dist/cli.js +2141 -1268
- package/dist/commands/add-cli.js +279 -0
- package/dist/commands/agent-dispatch.js +20 -12
- package/dist/commands/agent-support.js +11 -5
- package/dist/commands/completions.js +3 -0
- package/dist/commands/config-cli.js +129 -517
- package/dist/commands/consolidate.js +1533 -144
- package/dist/commands/curate.js +44 -3
- package/dist/commands/db-cli.js +23 -0
- package/dist/commands/distill-promotion-policy.js +5 -3
- package/dist/commands/distill.js +906 -100
- package/dist/commands/env.js +213 -0
- package/dist/commands/eval-cases.js +3 -0
- package/dist/commands/events.js +3 -0
- package/dist/commands/extract-cli.js +127 -0
- package/dist/commands/extract-prompt.js +204 -0
- package/dist/commands/extract.js +477 -0
- package/dist/commands/feedback-cli.js +331 -0
- package/dist/commands/graph.js +260 -5
- package/dist/commands/health.js +977 -51
- package/dist/commands/help/help-accept.md +6 -3
- package/dist/commands/help/help-improve.md +36 -8
- package/dist/commands/help/help-proposals.md +7 -4
- package/dist/commands/help/help-reject.md +5 -2
- package/dist/commands/history.js +51 -16
- package/dist/commands/improve-auto-accept.js +97 -0
- package/dist/commands/improve-cli.js +236 -0
- package/dist/commands/improve-profiles.js +184 -0
- package/dist/commands/improve-result-file.js +167 -0
- package/dist/commands/improve.js +1725 -332
- package/dist/commands/info.js +3 -0
- package/dist/commands/init.js +49 -1
- package/dist/commands/installed-stashes.js +6 -23
- package/dist/commands/knowledge.js +3 -0
- package/dist/commands/lint/agent-linter.js +3 -0
- package/dist/commands/lint/base-linter.js +199 -5
- package/dist/commands/lint/command-linter.js +3 -0
- package/dist/commands/lint/default-linter.js +3 -0
- package/dist/commands/lint/env-key-rules.js +154 -0
- package/dist/commands/lint/index.js +92 -3
- package/dist/commands/lint/knowledge-linter.js +3 -0
- package/dist/commands/lint/markdown-insertion.js +343 -0
- package/dist/commands/lint/memory-linter.js +3 -0
- package/dist/commands/lint/registry.js +3 -0
- package/dist/commands/lint/skill-linter.js +3 -0
- package/dist/commands/lint/task-linter.js +15 -12
- package/dist/commands/lint/types.js +3 -0
- package/dist/commands/lint/workflow-linter.js +3 -0
- package/dist/commands/lint.js +3 -0
- package/dist/commands/migration-help.js +5 -2
- package/dist/commands/proposal-drain-policies.js +128 -0
- package/dist/commands/proposal-drain.js +477 -0
- package/dist/commands/proposal.js +60 -6
- package/dist/commands/propose.js +24 -19
- package/dist/commands/reflect.js +1004 -94
- package/dist/commands/registry-cli.js +150 -0
- package/dist/commands/registry-search.js +3 -0
- package/dist/commands/remember-cli.js +257 -0
- package/dist/commands/remember.js +15 -6
- package/dist/commands/schema-repair.js +88 -15
- package/dist/commands/search.js +99 -14
- package/dist/commands/secret.js +173 -0
- package/dist/commands/self-update.js +3 -0
- package/dist/commands/show.js +32 -13
- package/dist/commands/source-add.js +7 -35
- package/dist/commands/source-clone.js +3 -0
- package/dist/commands/source-manage.js +3 -0
- package/dist/commands/tasks.js +161 -95
- package/dist/commands/url-checker.js +3 -0
- package/dist/core/action-contributors.js +3 -0
- package/dist/core/asset-ref.js +13 -2
- package/dist/core/asset-registry.js +9 -2
- package/dist/core/asset-serialize.js +88 -0
- package/dist/core/asset-spec.js +61 -5
- package/dist/core/common.js +93 -5
- package/dist/core/concurrent.js +3 -0
- package/dist/core/config-io.js +347 -0
- package/dist/core/config-migration.js +622 -0
- package/dist/core/config-schema.js +558 -0
- package/dist/core/config-sources.js +108 -0
- package/dist/core/config-types.js +4 -0
- package/dist/core/config-walker.js +337 -0
- package/dist/core/config.js +366 -1077
- package/dist/core/errors.js +42 -20
- package/dist/core/events.js +31 -25
- package/dist/core/file-lock.js +104 -0
- package/dist/core/frontmatter.js +75 -10
- package/dist/core/lesson-lint.js +3 -0
- package/dist/core/markdown.js +3 -0
- package/dist/core/memory-belief.js +62 -0
- package/dist/core/memory-contradiction-detect.js +274 -0
- package/dist/core/memory-improve.js +142 -14
- package/dist/core/parse.js +3 -0
- package/dist/core/paths.js +218 -50
- package/dist/core/proposal-quality-validators.js +380 -0
- package/dist/core/proposal-validators.js +11 -3
- package/dist/core/proposals.js +464 -5
- package/dist/core/state-db.js +349 -56
- package/dist/core/text-truncation.js +107 -0
- package/dist/core/time.js +3 -0
- package/dist/core/tty.js +59 -0
- package/dist/core/warn.js +7 -2
- package/dist/core/write-source.js +12 -0
- package/dist/indexer/db-backup.js +391 -0
- package/dist/indexer/db-search.js +136 -28
- package/dist/indexer/db.js +661 -166
- package/dist/indexer/ensure-index.js +3 -0
- package/dist/indexer/file-context.js +3 -0
- package/dist/indexer/graph-boost.js +162 -40
- package/dist/indexer/graph-db.js +241 -51
- package/dist/indexer/graph-dedup.js +3 -7
- package/dist/indexer/graph-extraction.js +242 -149
- package/dist/indexer/index-context.js +3 -9
- package/dist/indexer/indexer.js +84 -14
- package/dist/indexer/llm-cache.js +24 -19
- package/dist/indexer/manifest.js +3 -0
- package/dist/indexer/matchers.js +184 -11
- package/dist/indexer/memory-inference.js +94 -50
- package/dist/indexer/metadata-contributors.js +3 -0
- package/dist/indexer/metadata.js +110 -50
- package/dist/indexer/path-resolver.js +3 -0
- package/dist/indexer/project-context.js +192 -0
- package/dist/indexer/ranking-contributors.js +134 -7
- package/dist/indexer/ranking.js +8 -1
- package/dist/indexer/search-fields.js +5 -9
- package/dist/indexer/search-hit-enrichers.js +91 -2
- package/dist/indexer/search-source.js +20 -1
- package/dist/indexer/semantic-status.js +4 -1
- package/dist/indexer/staleness-detect.js +447 -0
- package/dist/indexer/usage-events.js +12 -9
- package/dist/indexer/walker.js +3 -0
- package/dist/integrations/agent/builders.js +135 -0
- package/dist/integrations/agent/config.js +121 -401
- package/dist/integrations/agent/detect.js +3 -0
- package/dist/integrations/agent/index.js +6 -14
- package/dist/integrations/agent/model-aliases.js +55 -0
- package/dist/integrations/agent/profiles.js +3 -0
- package/dist/integrations/agent/prompts.js +137 -8
- package/dist/integrations/agent/runner.js +208 -0
- package/dist/integrations/agent/sdk-runner.js +8 -2
- package/dist/integrations/agent/spawn.js +54 -14
- package/dist/integrations/github.js +3 -0
- package/dist/integrations/lockfile.js +22 -51
- package/dist/integrations/session-logs/index.js +4 -0
- package/dist/integrations/session-logs/inline-refs.js +35 -0
- package/dist/integrations/session-logs/pre-filter.js +152 -0
- package/dist/integrations/session-logs/providers/claude-code.js +226 -0
- package/dist/integrations/session-logs/providers/opencode.js +231 -25
- package/dist/integrations/session-logs/types.js +3 -0
- package/dist/llm/call-ai.js +14 -26
- package/dist/llm/client.js +16 -2
- package/dist/llm/embedder.js +20 -29
- package/dist/llm/embedders/cache.js +3 -7
- package/dist/llm/embedders/local.js +42 -1
- package/dist/llm/embedders/remote.js +20 -8
- package/dist/llm/embedders/types.js +3 -7
- package/dist/llm/feature-gate.js +92 -56
- package/dist/llm/graph-extract.js +401 -30
- package/dist/llm/index-passes.js +44 -29
- package/dist/llm/memory-infer.js +30 -2
- package/dist/llm/metadata-enhance.js +3 -7
- package/dist/llm/prompts/extract-session.md +80 -0
- package/dist/llm/prompts/graph-extract-user-prompt.md +24 -1
- package/dist/output/cli-hints-full.md +60 -32
- package/dist/output/cli-hints-short.md +10 -7
- package/dist/output/cli-hints.js +5 -2
- package/dist/output/context.js +60 -8
- package/dist/output/renderers.js +170 -194
- package/dist/output/shapes/curate.js +56 -0
- package/dist/output/shapes/distill.js +10 -0
- package/dist/output/shapes/env-list.js +19 -0
- package/dist/output/shapes/events.js +11 -0
- package/dist/output/shapes/helpers.js +424 -0
- package/dist/output/shapes/history.js +7 -0
- package/dist/output/shapes/passthrough.js +105 -0
- package/dist/output/shapes/proposal-accept.js +7 -0
- package/dist/output/shapes/proposal-diff.js +7 -0
- package/dist/output/shapes/proposal-list.js +7 -0
- package/dist/output/shapes/proposal-producer.js +11 -0
- package/dist/output/shapes/proposal-reject.js +7 -0
- package/dist/output/shapes/proposal-show.js +7 -0
- package/dist/output/shapes/registry-search.js +6 -0
- package/dist/output/shapes/registry.js +30 -0
- package/dist/output/shapes/search.js +6 -0
- package/dist/output/shapes/secret-list.js +19 -0
- package/dist/output/shapes/show.js +6 -0
- package/dist/output/shapes/vault-list.js +19 -0
- package/dist/output/shapes.js +51 -549
- package/dist/output/text/add.js +6 -0
- package/dist/output/text/clone.js +6 -0
- package/dist/output/text/config.js +6 -0
- package/dist/output/text/curate.js +6 -0
- package/dist/output/text/distill.js +7 -0
- package/dist/output/text/enable-disable.js +7 -0
- package/dist/output/text/events.js +10 -0
- package/dist/output/text/feedback.js +6 -0
- package/dist/output/text/helpers.js +1059 -0
- package/dist/output/text/history.js +7 -0
- package/dist/output/text/import.js +6 -0
- package/dist/output/text/index.js +6 -0
- package/dist/output/text/info.js +6 -0
- package/dist/output/text/init.js +6 -0
- package/dist/output/text/list.js +6 -0
- package/dist/output/text/proposal-producer.js +8 -0
- package/dist/output/text/proposal.js +12 -0
- package/dist/output/text/registry-commands.js +11 -0
- package/dist/output/text/registry.js +30 -0
- package/dist/output/text/remember.js +6 -0
- package/dist/output/text/remove.js +6 -0
- package/dist/output/text/save.js +6 -0
- package/dist/output/text/search.js +6 -0
- package/dist/output/text/show.js +6 -0
- package/dist/output/text/update.js +6 -0
- package/dist/output/text/upgrade.js +6 -0
- package/dist/output/text/vault.js +16 -0
- package/dist/output/text/wiki.js +15 -0
- package/dist/output/text/workflow.js +14 -0
- package/dist/output/text.js +44 -1329
- package/dist/registry/build-index.js +3 -0
- package/dist/registry/create-provider-registry.js +3 -0
- package/dist/registry/factory.js +4 -1
- package/dist/registry/origin-resolve.js +3 -0
- package/dist/registry/providers/index.js +3 -0
- package/dist/registry/providers/skills-sh.js +11 -2
- package/dist/registry/providers/static-index.js +10 -1
- package/dist/registry/providers/types.js +3 -24
- package/dist/registry/resolve.js +11 -16
- package/dist/registry/types.js +3 -0
- package/dist/scripts/migrate-storage.js +17767 -0
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +9031 -0
- package/dist/scripts/migrations/v16-to-v17.js +141 -0
- package/dist/setup/detect.js +3 -0
- package/dist/setup/ripgrep-install.js +3 -0
- package/dist/setup/ripgrep-resolve.js +3 -0
- package/dist/setup/setup.js +306 -67
- package/dist/setup/steps.js +3 -15
- package/dist/sources/include.js +3 -0
- package/dist/sources/provider-factory.js +3 -11
- package/dist/sources/provider.js +3 -20
- package/dist/sources/providers/filesystem.js +19 -23
- package/dist/sources/providers/git.js +171 -21
- package/dist/sources/providers/index.js +3 -0
- package/dist/sources/providers/install-types.js +3 -13
- package/dist/sources/providers/npm.js +3 -4
- package/dist/sources/providers/provider-utils.js +3 -0
- package/dist/sources/providers/sync-from-ref.js +3 -11
- package/dist/sources/providers/tar-utils.js +3 -0
- package/dist/sources/providers/website.js +18 -22
- package/dist/sources/resolve.js +3 -0
- package/dist/sources/types.js +3 -0
- package/dist/sources/website-ingest.js +3 -0
- package/dist/tasks/backends/cron.js +3 -0
- package/dist/tasks/backends/exec-utils.js +3 -0
- package/dist/tasks/backends/index.js +3 -11
- package/dist/tasks/backends/launchd.js +3 -0
- package/dist/tasks/backends/schtasks.js +3 -0
- package/dist/tasks/parser.js +51 -38
- package/dist/tasks/resolveAkmBin.js +3 -0
- package/dist/tasks/runner.js +35 -9
- package/dist/tasks/schedule.js +20 -1
- package/dist/tasks/schema.js +5 -3
- package/dist/tasks/validator.js +6 -3
- package/dist/version.js +3 -0
- package/dist/wiki/wiki-templates.js +3 -0
- package/dist/wiki/wiki.js +3 -0
- package/dist/workflows/authoring.js +3 -0
- package/dist/workflows/cli.js +3 -0
- package/dist/workflows/db.js +140 -10
- package/dist/workflows/document-cache.js +3 -10
- package/dist/workflows/parser.js +3 -0
- package/dist/workflows/renderer.js +3 -0
- package/dist/workflows/runs.js +18 -1
- package/dist/workflows/schema.js +3 -0
- package/dist/workflows/scope-key.js +3 -0
- package/dist/workflows/validator.js +5 -9
- package/docs/README.md +7 -2
- package/docs/data-and-telemetry.md +225 -0
- package/docs/migration/release-notes/0.7.5.md +2 -2
- package/docs/migration/release-notes/0.8.0.md +57 -5
- package/docs/migration/v0.7-to-v0.8.md +1378 -0
- package/package.json +28 -11
- package/.github/LICENSE +0 -374
- package/dist/commands/install-audit.js +0 -385
- package/dist/commands/vault.js +0 -310
- package/dist/indexer/match-contributors.js +0 -141
- package/dist/integrations/agent/pipeline.js +0 -39
- package/dist/integrations/agent/runners.js +0 -31
|
@@ -0,0 +1,477 @@
|
|
|
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 { stringify as yamlStringify } from "yaml";
|
|
5
|
+
import { assembleAssetFromString } from "../core/asset-serialize";
|
|
6
|
+
import { resolveStashDir, timestampForFilename } from "../core/common";
|
|
7
|
+
import { getDefaultLlmConfig, loadConfig } from "../core/config";
|
|
8
|
+
import { ConfigError, UsageError } from "../core/errors";
|
|
9
|
+
import { appendEvent } from "../core/events";
|
|
10
|
+
import { createProposal, isProposalSkipped } from "../core/proposals";
|
|
11
|
+
import { getExtractedSessionsMap, openStateDatabase, shouldSkipAlreadyExtractedSession, upsertExtractedSession, } from "../core/state-db";
|
|
12
|
+
import { warn } from "../core/warn";
|
|
13
|
+
import { resolveImproveProcessRunnerFromProfile } from "../integrations/agent/runner";
|
|
14
|
+
import { getAvailableHarnesses } from "../integrations/session-logs";
|
|
15
|
+
import { preFilterSession } from "../integrations/session-logs/pre-filter";
|
|
16
|
+
import { chatCompletion } from "../llm/client";
|
|
17
|
+
import { isLlmFeatureEnabled, tryLlmFeature } from "../llm/feature-gate";
|
|
18
|
+
import { buildExtractPrompt, EXTRACT_JSON_SCHEMA, parseExtractPayload } from "./extract-prompt";
|
|
19
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Parse a since-string into an absolute ms-epoch cutoff. Accepts:
|
|
22
|
+
* - ISO timestamps (parsed via Date.parse)
|
|
23
|
+
* - Relative durations: `<n>m`, `<n>h`, `<n>d` (minutes / hours / days)
|
|
24
|
+
*
|
|
25
|
+
* Throws UsageError on unparseable input so the CLI surfaces a clear error
|
|
26
|
+
* rather than silently defaulting.
|
|
27
|
+
*/
|
|
28
|
+
export function parseSinceArg(value, now = Date.now()) {
|
|
29
|
+
if (!value || value.trim() === "") {
|
|
30
|
+
return now - 24 * 60 * 60 * 1000; // default: 24h
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
const relMatch = trimmed.match(/^(\d+)\s*([mhd])$/i);
|
|
34
|
+
if (relMatch) {
|
|
35
|
+
const n = Number.parseInt(relMatch[1] ?? "0", 10);
|
|
36
|
+
const unit = (relMatch[2] ?? "h").toLowerCase();
|
|
37
|
+
const ms = unit === "m" ? n * 60_000 : unit === "h" ? n * 3_600_000 : n * 86_400_000;
|
|
38
|
+
return now - ms;
|
|
39
|
+
}
|
|
40
|
+
const iso = Date.parse(trimmed);
|
|
41
|
+
if (!Number.isNaN(iso))
|
|
42
|
+
return iso;
|
|
43
|
+
throw new UsageError(`--since value "${value}" could not be parsed (expected ISO timestamp or duration like 24h / 7d / 30m)`, "INVALID_FLAG_VALUE");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a harness instance for the given type, either from the explicit
|
|
47
|
+
* `harnesses` seam or the {@link getAvailableHarnesses} registry. Returns
|
|
48
|
+
* `undefined` when no harness matches (the caller surfaces that as a warning).
|
|
49
|
+
*/
|
|
50
|
+
function resolveHarness(type, harnesses) {
|
|
51
|
+
const pool = harnesses ?? getAvailableHarnesses();
|
|
52
|
+
return pool.find((h) => h.name === type);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build the ref + content for a candidate. The body must contain a
|
|
56
|
+
* frontmatter block carrying `description` (and `when_to_use` for lessons)
|
|
57
|
+
* so the accept-time descriptionQualityValidator passes — same pattern as
|
|
58
|
+
* the consolidate-writer fix at consolidate.ts.
|
|
59
|
+
*/
|
|
60
|
+
function buildCandidateProposal(candidate, sourceRef) {
|
|
61
|
+
const ref = `${candidate.type}:${candidate.name}`;
|
|
62
|
+
const fm = {
|
|
63
|
+
description: candidate.description,
|
|
64
|
+
sources: [`session:${sourceRef.harness}:${sourceRef.sessionId}`],
|
|
65
|
+
};
|
|
66
|
+
if (candidate.type === "lesson" && candidate.when_to_use) {
|
|
67
|
+
fm.when_to_use = candidate.when_to_use;
|
|
68
|
+
}
|
|
69
|
+
const serialized = yamlStringify(fm).trimEnd();
|
|
70
|
+
const content = assembleAssetFromString(serialized, candidate.body);
|
|
71
|
+
return { ref, content };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Process one session through the full pipeline: read → pre-filter → LLM →
|
|
75
|
+
* parse → createProposal-per-candidate. Returns the per-session result.
|
|
76
|
+
*
|
|
77
|
+
* On any non-fatal failure (LLM error, unparseable response, individual
|
|
78
|
+
* proposal validation failure) the session result records a warning and
|
|
79
|
+
* keeps going — one session's bad luck never aborts a multi-session run.
|
|
80
|
+
*/
|
|
81
|
+
async function processSession(harness, sessionRef, stashDir, config, llmConfig, chat, ctx, sourceRun, dryRun, timeoutMs, maxTotalChars) {
|
|
82
|
+
const warnings = [];
|
|
83
|
+
let data;
|
|
84
|
+
try {
|
|
85
|
+
data = harness.readSession(sessionRef);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
sessionId: sessionRef.sessionId,
|
|
90
|
+
harness: harness.name,
|
|
91
|
+
candidateCount: 0,
|
|
92
|
+
proposalIds: [],
|
|
93
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
94
|
+
warnings: [`readSession failed: ${err instanceof Error ? err.message : String(err)}`],
|
|
95
|
+
skipped: true,
|
|
96
|
+
skipReason: "read_failed",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const filtered = preFilterSession(data, {
|
|
100
|
+
...(typeof maxTotalChars === "number" ? { maxTotalChars } : {}),
|
|
101
|
+
});
|
|
102
|
+
const prompt = buildExtractPrompt({ data, events: filtered.events, inlineRefs: data.inlineRefs });
|
|
103
|
+
let llmRaw = "";
|
|
104
|
+
const llmResult = await tryLlmFeature("session_extraction", config, async () => {
|
|
105
|
+
llmRaw = await chat(llmConfig, [{ role: "user", content: prompt }], {
|
|
106
|
+
timeoutMs,
|
|
107
|
+
responseSchema: EXTRACT_JSON_SCHEMA,
|
|
108
|
+
});
|
|
109
|
+
return llmRaw;
|
|
110
|
+
}, "", { timeoutMs });
|
|
111
|
+
if (llmResult === "" && !llmRaw) {
|
|
112
|
+
// tryLlmFeature took the fallback path (disabled / timeout / error). Return skipped.
|
|
113
|
+
return {
|
|
114
|
+
sessionId: sessionRef.sessionId,
|
|
115
|
+
harness: harness.name,
|
|
116
|
+
candidateCount: 0,
|
|
117
|
+
proposalIds: [],
|
|
118
|
+
preFilter: {
|
|
119
|
+
inputCount: filtered.stats.inputCount,
|
|
120
|
+
outputCount: filtered.stats.outputCount,
|
|
121
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
122
|
+
},
|
|
123
|
+
warnings: ["session_extraction feature returned empty (disabled / timeout / error)"],
|
|
124
|
+
skipped: true,
|
|
125
|
+
skipReason: "llm_unavailable",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const payload = parseExtractPayload(llmRaw);
|
|
129
|
+
const proposalIds = [];
|
|
130
|
+
if (payload.candidates.length === 0) {
|
|
131
|
+
appendEvent({
|
|
132
|
+
eventType: "extract_invoked",
|
|
133
|
+
ref: `session:${harness.name}:${sessionRef.sessionId}`,
|
|
134
|
+
metadata: {
|
|
135
|
+
outcome: "no_candidates",
|
|
136
|
+
sessionId: sessionRef.sessionId,
|
|
137
|
+
harness: harness.name,
|
|
138
|
+
sourceRun,
|
|
139
|
+
rationale: payload.rationale_if_empty,
|
|
140
|
+
preFilterInput: filtered.stats.inputCount,
|
|
141
|
+
preFilterOutput: filtered.stats.outputCount,
|
|
142
|
+
},
|
|
143
|
+
}, ctx);
|
|
144
|
+
return {
|
|
145
|
+
sessionId: sessionRef.sessionId,
|
|
146
|
+
harness: harness.name,
|
|
147
|
+
candidateCount: 0,
|
|
148
|
+
proposalIds: [],
|
|
149
|
+
...(payload.rationale_if_empty ? { rationaleIfEmpty: payload.rationale_if_empty } : {}),
|
|
150
|
+
preFilter: {
|
|
151
|
+
inputCount: filtered.stats.inputCount,
|
|
152
|
+
outputCount: filtered.stats.outputCount,
|
|
153
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
154
|
+
},
|
|
155
|
+
warnings,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
for (const candidate of payload.candidates) {
|
|
159
|
+
if (dryRun) {
|
|
160
|
+
proposalIds.push(`dry-run:${candidate.type}:${candidate.name}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const { ref, content } = buildCandidateProposal(candidate, sessionRef);
|
|
165
|
+
const result = createProposal(stashDir, {
|
|
166
|
+
ref,
|
|
167
|
+
source: "extract",
|
|
168
|
+
sourceRun,
|
|
169
|
+
payload: {
|
|
170
|
+
content,
|
|
171
|
+
frontmatter: {
|
|
172
|
+
description: candidate.description,
|
|
173
|
+
...(candidate.when_to_use ? { when_to_use: candidate.when_to_use } : {}),
|
|
174
|
+
...(typeof candidate.confidence === "number" ? { confidence: candidate.confidence } : {}),
|
|
175
|
+
sources: [`session:${sessionRef.harness}:${sessionRef.sessionId}`],
|
|
176
|
+
evidence: candidate.evidence,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
}, ctx);
|
|
180
|
+
if (isProposalSkipped(result)) {
|
|
181
|
+
warnings.push(`candidate ${candidate.type}:${candidate.name} skipped: ${result.reason}: ${result.message}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
proposalIds.push(result.id);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
warnings.push(`candidate ${candidate.type}:${candidate.name} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
appendEvent({
|
|
192
|
+
eventType: "extract_invoked",
|
|
193
|
+
ref: `session:${harness.name}:${sessionRef.sessionId}`,
|
|
194
|
+
metadata: {
|
|
195
|
+
outcome: "candidates_queued",
|
|
196
|
+
sessionId: sessionRef.sessionId,
|
|
197
|
+
harness: harness.name,
|
|
198
|
+
sourceRun,
|
|
199
|
+
candidateCount: payload.candidates.length,
|
|
200
|
+
proposalCount: proposalIds.length,
|
|
201
|
+
preFilterInput: filtered.stats.inputCount,
|
|
202
|
+
preFilterOutput: filtered.stats.outputCount,
|
|
203
|
+
},
|
|
204
|
+
}, ctx);
|
|
205
|
+
return {
|
|
206
|
+
sessionId: sessionRef.sessionId,
|
|
207
|
+
harness: harness.name,
|
|
208
|
+
candidateCount: payload.candidates.length,
|
|
209
|
+
proposalIds,
|
|
210
|
+
preFilter: {
|
|
211
|
+
inputCount: filtered.stats.inputCount,
|
|
212
|
+
outputCount: filtered.stats.outputCount,
|
|
213
|
+
truncatedCount: filtered.stats.truncatedCount,
|
|
214
|
+
},
|
|
215
|
+
warnings,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ── Public entrypoint ────────────────────────────────────────────────────────
|
|
219
|
+
export async function akmExtract(options) {
|
|
220
|
+
const startMs = Date.now();
|
|
221
|
+
if (!options.type || options.type.trim() === "") {
|
|
222
|
+
throw new UsageError("--type is required. Pass a harness name (e.g. --type claude-code).", "MISSING_REQUIRED_ARGUMENT");
|
|
223
|
+
}
|
|
224
|
+
const config = options.config ?? loadConfig();
|
|
225
|
+
const stashDir = options.stashDir ?? resolveStashDir();
|
|
226
|
+
const dryRun = options.dryRun ?? false;
|
|
227
|
+
const sourceRun = options.sourceRun ?? `extract-${timestampForFilename()}`;
|
|
228
|
+
// Read the per-process extract config from the active improve profile. Matches
|
|
229
|
+
// the pattern reflect/distill/consolidate use: `profiles.improve.<active>.processes.extract`.
|
|
230
|
+
// Only the `default` improve profile is consulted here — extract isn't invoked
|
|
231
|
+
// with a profile flag yet (parity item for a future change).
|
|
232
|
+
const extractProcess = config.profiles?.improve?.default?.processes?.extract;
|
|
233
|
+
// Feature-gate early so we get a clean "skipped because disabled" envelope.
|
|
234
|
+
if (!isLlmFeatureEnabled(config, "session_extraction")) {
|
|
235
|
+
return {
|
|
236
|
+
schemaVersion: 1,
|
|
237
|
+
ok: true,
|
|
238
|
+
shape: "extract-result",
|
|
239
|
+
dryRun,
|
|
240
|
+
type: options.type,
|
|
241
|
+
sessionsProcessed: 0,
|
|
242
|
+
sessionsSkipped: 0,
|
|
243
|
+
candidatesCreated: 0,
|
|
244
|
+
proposals: [],
|
|
245
|
+
sessions: [],
|
|
246
|
+
warnings: [
|
|
247
|
+
"session_extraction feature disabled — set profiles.improve.default.processes.extract.enabled: true to use",
|
|
248
|
+
],
|
|
249
|
+
durationMs: Date.now() - startMs,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Resolve the LLM connection. Priority order:
|
|
253
|
+
// 1. Options.config.profiles.improve.default.processes.extract.profile
|
|
254
|
+
// (per-process override, matches reflect/distill/consolidate)
|
|
255
|
+
// 2. config.defaults.llm (the default LLM profile)
|
|
256
|
+
// 3. throw — extract requires an LLM.
|
|
257
|
+
let llmConfig;
|
|
258
|
+
const runnerSpec = resolveImproveProcessRunnerFromProfile(extractProcess, config);
|
|
259
|
+
if (runnerSpec) {
|
|
260
|
+
if (runnerSpec.kind !== "llm") {
|
|
261
|
+
throw new ConfigError(`Extract only supports mode: "llm" (in-tree LLM call). Got mode: "${runnerSpec.kind}" from profiles.improve.default.processes.extract — change it to "llm" or remove the override.`, "INVALID_CONFIG_FILE");
|
|
262
|
+
}
|
|
263
|
+
llmConfig = runnerSpec.connection;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
llmConfig = getDefaultLlmConfig(config) ?? undefined;
|
|
267
|
+
}
|
|
268
|
+
if (!llmConfig) {
|
|
269
|
+
throw new ConfigError("No LLM connection configured for extract. Set profiles.llm + defaults.llm, or set profiles.improve.default.processes.extract.profile to a configured LLM profile.");
|
|
270
|
+
}
|
|
271
|
+
// Honor per-process timeoutMs override; fall back to options.timeoutMs; then 60s.
|
|
272
|
+
const timeoutMs = options.timeoutMs ??
|
|
273
|
+
(typeof extractProcess?.timeoutMs === "number" ? extractProcess.timeoutMs : undefined) ??
|
|
274
|
+
60_000;
|
|
275
|
+
// Pre-filter budget — process config can raise it for large-context models.
|
|
276
|
+
const maxTotalChars = typeof extractProcess?.maxTotalChars === "number" ? extractProcess.maxTotalChars : undefined;
|
|
277
|
+
// Default discovery window — process config can override the built-in 24h.
|
|
278
|
+
const effectiveSince = options.since ?? extractProcess?.defaultSince;
|
|
279
|
+
const harness = resolveHarness(options.type, options.harnesses);
|
|
280
|
+
if (!harness) {
|
|
281
|
+
return {
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
ok: false,
|
|
284
|
+
shape: "extract-result",
|
|
285
|
+
dryRun,
|
|
286
|
+
type: options.type,
|
|
287
|
+
sessionsProcessed: 0,
|
|
288
|
+
sessionsSkipped: 0,
|
|
289
|
+
candidatesCreated: 0,
|
|
290
|
+
proposals: [],
|
|
291
|
+
sessions: [],
|
|
292
|
+
warnings: [`no available harness matches type "${options.type}" (check that the platform is installed)`],
|
|
293
|
+
durationMs: Date.now() - startMs,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (!harness.isAvailable()) {
|
|
297
|
+
return {
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
ok: false,
|
|
300
|
+
shape: "extract-result",
|
|
301
|
+
dryRun,
|
|
302
|
+
type: options.type,
|
|
303
|
+
sessionsProcessed: 0,
|
|
304
|
+
sessionsSkipped: 0,
|
|
305
|
+
candidatesCreated: 0,
|
|
306
|
+
proposals: [],
|
|
307
|
+
sessions: [],
|
|
308
|
+
warnings: [`harness ${options.type} is registered but reports not-available (no session data on this machine)`],
|
|
309
|
+
durationMs: Date.now() - startMs,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Decide which sessions to process: explicit sessionId OR discovery via since.
|
|
313
|
+
let candidates;
|
|
314
|
+
if (options.sessionId) {
|
|
315
|
+
const all = harness.listSessions({
|
|
316
|
+
...(options.location ? { location: options.location } : {}),
|
|
317
|
+
});
|
|
318
|
+
const target = all.find((s) => s.sessionId === options.sessionId);
|
|
319
|
+
if (!target) {
|
|
320
|
+
return {
|
|
321
|
+
schemaVersion: 1,
|
|
322
|
+
ok: false,
|
|
323
|
+
shape: "extract-result",
|
|
324
|
+
dryRun,
|
|
325
|
+
type: options.type,
|
|
326
|
+
sessionsProcessed: 0,
|
|
327
|
+
sessionsSkipped: 0,
|
|
328
|
+
candidatesCreated: 0,
|
|
329
|
+
proposals: [],
|
|
330
|
+
sessions: [],
|
|
331
|
+
warnings: [`session ${options.sessionId} not found for harness ${options.type}`],
|
|
332
|
+
durationMs: Date.now() - startMs,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
candidates = [target];
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const sinceMs = parseSinceArg(effectiveSince);
|
|
339
|
+
candidates = harness.listSessions({
|
|
340
|
+
sinceMs,
|
|
341
|
+
...(options.location ? { location: options.location } : {}),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const sessions = [];
|
|
345
|
+
let processedCount = 0;
|
|
346
|
+
let skippedCount = 0;
|
|
347
|
+
const allProposalIds = [];
|
|
348
|
+
const topLevelWarnings = [];
|
|
349
|
+
const chat = options.chat ?? chatCompletion;
|
|
350
|
+
// Open state.db once for the run and bulk-load seen-rows for the candidate
|
|
351
|
+
// set so we can decide skip/process in O(1) per session. Tracking is opt-out
|
|
352
|
+
// via options.skipTracking (used by tests + one-shot debug calls).
|
|
353
|
+
const trackingEnabled = options.skipTracking !== true;
|
|
354
|
+
let stateDb;
|
|
355
|
+
let seenMap = new Map();
|
|
356
|
+
if (trackingEnabled && candidates.length > 0) {
|
|
357
|
+
try {
|
|
358
|
+
stateDb = options.stateDb ?? openStateDatabase();
|
|
359
|
+
seenMap = getExtractedSessionsMap(stateDb, harness.name, candidates.map((c) => c.sessionId));
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
// state.db open is best-effort — log and proceed without skip-tracking
|
|
363
|
+
// so a transient sqlite error never blocks the actual extraction.
|
|
364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
365
|
+
warn(`[extract] state.db unavailable, processing without skip-tracking: ${msg}`);
|
|
366
|
+
topLevelWarnings.push(`state.db unavailable: ${msg}`);
|
|
367
|
+
stateDb = undefined;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const summary of candidates) {
|
|
371
|
+
// Skip-tracking: if this session was already processed AND no new events
|
|
372
|
+
// have arrived since (live endedAt <= recorded endedAt), don't burn an LLM
|
|
373
|
+
// call. --force or single-session mode (explicit sessionId) bypasses.
|
|
374
|
+
const prior = seenMap.get(summary.sessionId);
|
|
375
|
+
if (!options.force && !options.sessionId && shouldSkipAlreadyExtractedSession(prior, summary.endedAt)) {
|
|
376
|
+
sessions.push({
|
|
377
|
+
sessionId: summary.sessionId,
|
|
378
|
+
harness: harness.name,
|
|
379
|
+
candidateCount: 0,
|
|
380
|
+
proposalIds: [],
|
|
381
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
382
|
+
warnings: [
|
|
383
|
+
`already extracted at ${prior?.processed_at}; pass --force to re-process or wait until the session has new content`,
|
|
384
|
+
],
|
|
385
|
+
skipped: true,
|
|
386
|
+
skipReason: "already_extracted",
|
|
387
|
+
});
|
|
388
|
+
skippedCount += 1;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const result = await processSession(harness, summary, stashDir, config, llmConfig, chat, options.ctx, sourceRun, dryRun, timeoutMs, maxTotalChars);
|
|
393
|
+
sessions.push(result);
|
|
394
|
+
if (result.skipped)
|
|
395
|
+
skippedCount += 1;
|
|
396
|
+
else
|
|
397
|
+
processedCount += 1;
|
|
398
|
+
allProposalIds.push(...result.proposalIds);
|
|
399
|
+
// Persist outcome so the next run skips this session unless new events
|
|
400
|
+
// arrive. We only track non-dry-run paths — dry-run is for inspection
|
|
401
|
+
// and should never poison the seen-table.
|
|
402
|
+
if (trackingEnabled && stateDb && !dryRun) {
|
|
403
|
+
try {
|
|
404
|
+
const outcome = result.skipped
|
|
405
|
+
? result.skipReason === "read_failed" || result.skipReason === "exception"
|
|
406
|
+
? "failed"
|
|
407
|
+
: "skipped"
|
|
408
|
+
: result.candidateCount === 0
|
|
409
|
+
? "no_candidates"
|
|
410
|
+
: "candidates_queued";
|
|
411
|
+
upsertExtractedSession(stateDb, {
|
|
412
|
+
harness: harness.name,
|
|
413
|
+
sessionId: summary.sessionId,
|
|
414
|
+
processedAt: new Date().toISOString(),
|
|
415
|
+
sessionEndedAt: summary.endedAt ?? null,
|
|
416
|
+
outcome,
|
|
417
|
+
candidateCount: result.candidateCount,
|
|
418
|
+
proposalCount: result.proposalIds.length,
|
|
419
|
+
rationale: result.rationaleIfEmpty ?? null,
|
|
420
|
+
sourceRun,
|
|
421
|
+
metadata: {
|
|
422
|
+
preFilterInputCount: result.preFilter.inputCount,
|
|
423
|
+
preFilterOutputCount: result.preFilter.outputCount,
|
|
424
|
+
preFilterTruncatedCount: result.preFilter.truncatedCount,
|
|
425
|
+
...(result.skipReason ? { skipReason: result.skipReason } : {}),
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
// Tracking failure must not abort the run — log + continue.
|
|
431
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
432
|
+
warn(`[extract] failed to record session ${summary.sessionId} in state.db: ${msg}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
438
|
+
warn(`[extract] session ${summary.sessionId} threw: ${msg}`);
|
|
439
|
+
topLevelWarnings.push(`session ${summary.sessionId} threw: ${msg}`);
|
|
440
|
+
sessions.push({
|
|
441
|
+
sessionId: summary.sessionId,
|
|
442
|
+
harness: harness.name,
|
|
443
|
+
candidateCount: 0,
|
|
444
|
+
proposalIds: [],
|
|
445
|
+
preFilter: { inputCount: 0, outputCount: 0, truncatedCount: 0 },
|
|
446
|
+
warnings: [msg],
|
|
447
|
+
skipped: true,
|
|
448
|
+
skipReason: "exception",
|
|
449
|
+
});
|
|
450
|
+
skippedCount += 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// Close the state.db connection we opened. Callers that injected stateDb
|
|
454
|
+
// via the test seam own its lifecycle.
|
|
455
|
+
if (stateDb && !options.stateDb) {
|
|
456
|
+
try {
|
|
457
|
+
stateDb.close();
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
// best-effort close
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
schemaVersion: 1,
|
|
465
|
+
ok: true,
|
|
466
|
+
shape: "extract-result",
|
|
467
|
+
dryRun,
|
|
468
|
+
type: options.type,
|
|
469
|
+
sessionsProcessed: processedCount,
|
|
470
|
+
sessionsSkipped: skippedCount,
|
|
471
|
+
candidatesCreated: allProposalIds.length,
|
|
472
|
+
proposals: allProposalIds,
|
|
473
|
+
sessions,
|
|
474
|
+
warnings: topLevelWarnings,
|
|
475
|
+
durationMs: Date.now() - startMs,
|
|
476
|
+
};
|
|
477
|
+
}
|