akm-cli 0.9.0-beta.45 → 0.9.0-beta.46
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/dist/cli/shared.js +28 -0
- package/dist/cli.js +1 -2
- package/dist/commands/env/env-cli.js +16 -24
- package/dist/commands/env/secret-cli.js +12 -20
- package/dist/commands/graph/graph-cli.js +5 -13
- package/dist/commands/graph/graph.js +3 -3
- package/dist/commands/improve/consolidate/chunking.js +141 -0
- package/dist/commands/improve/consolidate/eligibility.js +64 -0
- package/dist/commands/improve/consolidate/merge.js +145 -0
- package/dist/commands/improve/consolidate/sanitize.js +231 -0
- package/dist/commands/improve/consolidate/types.js +4 -0
- package/dist/commands/improve/consolidate.js +20 -571
- package/dist/commands/improve/distill.js +5 -9
- package/dist/commands/improve/eligibility.js +434 -0
- package/dist/commands/improve/extract-cli.js +9 -1
- package/dist/commands/improve/extract.js +5 -19
- package/dist/commands/improve/improve-auto-accept.js +4 -8
- package/dist/commands/improve/improve-cli.js +35 -60
- package/dist/commands/improve/improve-result-file.js +5 -23
- package/dist/commands/improve/improve-session.js +58 -0
- package/dist/commands/improve/improve.js +107 -3606
- package/dist/commands/improve/locks.js +154 -0
- package/dist/commands/improve/loop-stages.js +1079 -0
- package/dist/commands/improve/preparation.js +1963 -0
- package/dist/commands/improve/recombine.js +6 -12
- package/dist/commands/improve/reflect.js +29 -34
- package/dist/commands/proposal/drain.js +25 -48
- package/dist/commands/proposal/proposal-cli.js +21 -31
- package/dist/commands/proposal/validators/proposals.js +3 -7
- package/dist/commands/read/curate.js +70 -14
- package/dist/commands/read/knowledge.js +2 -2
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +9 -37
- package/dist/commands/tasks/tasks-cli.js +19 -27
- package/dist/commands/wiki-cli.js +21 -35
- package/dist/core/config/config.js +18 -2
- package/dist/core/events.js +3 -7
- package/dist/core/logs-db.js +6 -63
- package/dist/core/state/migrations.js +714 -0
- package/dist/core/state-db.js +28 -779
- package/dist/indexer/db/db.js +82 -216
- package/dist/indexer/indexer.js +11 -112
- package/dist/indexer/passes/dir-staleness.js +114 -0
- package/dist/indexer/search/search-source.js +10 -24
- package/dist/integrations/agent/runner-dispatch.js +59 -0
- package/dist/llm/client.js +22 -11
- package/dist/llm/graph-extract.js +28 -39
- package/dist/llm/memory-infer.js +34 -22
- package/dist/llm/metadata-enhance.js +35 -30
- package/dist/llm/structured-call.js +49 -0
- package/dist/output/shapes/passthrough.js +0 -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/scripts/migrate-storage.js +892 -1186
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +214 -179
- package/dist/setup/setup.js +26 -5
- package/dist/sources/providers/filesystem.js +0 -1
- package/dist/sources/providers/git-install.js +206 -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 -671
- package/dist/sources/providers/npm.js +2 -6
- package/dist/sources/providers/sync-from-ref.js +9 -1
- package/dist/sources/providers/website.js +2 -3
- package/dist/sources/website-ingest.js +51 -9
- package/dist/sources/wiki-fetchers/registry.js +53 -0
- package/dist/sources/wiki-fetchers/youtube.js +181 -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/tasks/runner.js +5 -13
- package/dist/workflows/runtime/runs.js +1 -117
- package/dist/workflows/runtime/workflow-asset-loader.js +125 -0
- package/package.json +5 -5
- package/dist/commands/db-cli.js +0 -23
- package/dist/indexer/db/db-backup.js +0 -376
|
@@ -46,7 +46,7 @@ import { getDefaultLlmConfig, loadConfig } from "../../core/config/config.js";
|
|
|
46
46
|
import { appendEvent } from "../../core/events.js";
|
|
47
47
|
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
48
48
|
import { resolveStashStandards } from "../../core/standards/resolve-stash-standards.js";
|
|
49
|
-
import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis,
|
|
49
|
+
import { decayUnseenRecombineHypotheses, findMatchingRecombineHypothesis, getRecombineHypothesis, markRecombineHypothesisPromoted, recordRecombineInduction, withStateDbAsync, } from "../../core/state-db.js";
|
|
50
50
|
import { warn } from "../../core/warn.js";
|
|
51
51
|
import { closeDatabase, getAllEntries, getEntitiesByEntryIds, openExistingDatabase, } from "../../indexer/db/db.js";
|
|
52
52
|
import { resolveImproveProcessRunnerFromProfile, runnerIsLlm } from "../../integrations/agent/runner.js";
|
|
@@ -586,18 +586,16 @@ export async function akmRecombine(opts) {
|
|
|
586
586
|
warnings.push("recombine: no LLM configured — skipping");
|
|
587
587
|
return finish({ clustersFormed: 0 });
|
|
588
588
|
}
|
|
589
|
-
// #625 — open the confirmation-count store once per run via the ctx seam,
|
|
590
|
-
// reusing a long-lived ctx.db handle when the caller provided one (mirrors
|
|
591
|
-
// proposals.ts). Only handles WE opened are closed in the finally below.
|
|
592
|
-
const ownStateDb = opts.ctx?.db ? undefined : openStateDatabase(opts.ctx?.dbPath ?? getStateDbPath());
|
|
593
|
-
const stateDb = opts.ctx?.db ?? ownStateDb;
|
|
594
589
|
// Refs re-induced (defensible generalization passed the quality gate) THIS
|
|
595
590
|
// run — everything else is decayed after the loop.
|
|
596
591
|
const seenThisRun = new Set();
|
|
597
592
|
// Recombine output is knowledge/lesson (non-wiki) → stash authoring
|
|
598
593
|
// standards. Resolved ONCE per run and passed to each cluster prompt.
|
|
599
594
|
const standardsContext = resolveStashStandards(stashDir);
|
|
600
|
-
|
|
595
|
+
// #625 — open the confirmation-count store once per run via the ctx seam,
|
|
596
|
+
// reusing a long-lived ctx.db handle when the caller provided one (mirrors
|
|
597
|
+
// proposals.ts). Only handles WE opened are closed by the seam.
|
|
598
|
+
await withStateDbAsync(async (stateDb) => {
|
|
601
599
|
for (const cluster of clusters) {
|
|
602
600
|
if (opts.signal?.aborted) {
|
|
603
601
|
warnings.push("aborted-mid-run");
|
|
@@ -782,11 +780,7 @@ export async function akmRecombine(opts) {
|
|
|
782
780
|
}, opts.ctx);
|
|
783
781
|
}
|
|
784
782
|
}
|
|
785
|
-
}
|
|
786
|
-
finally {
|
|
787
|
-
if (ownStateDb)
|
|
788
|
-
ownStateDb.close();
|
|
789
|
-
}
|
|
783
|
+
}, { path: opts.ctx?.dbPath, borrowed: opts.ctx?.db });
|
|
790
784
|
return finish({ clustersFormed, proposalsEmitted, lessonsPromoted, nullsReturned });
|
|
791
785
|
}
|
|
792
786
|
/** Serialize frontmatter + body into a markdown asset string. */
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
|
-
import { assertNever } from "../../core/assert.js";
|
|
28
27
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
29
28
|
import { assembleAssetFromString, serializeFrontmatter } from "../../core/asset/asset-serialize.js";
|
|
30
29
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
@@ -41,6 +40,7 @@ import { runAgent, } from "../../integrations/agent/index.js";
|
|
|
41
40
|
import { resolveProcessAgentProfile } from "../../integrations/agent/config.js";
|
|
42
41
|
import { buildReflectPrompt, extractDraftConfidence, parseAgentProposalPayload, } from "../../integrations/agent/prompts.js";
|
|
43
42
|
import { resolveImproveProcessRunnerFromProfile, runnerIsLlm, runnerSupportsFileWrite, } from "../../integrations/agent/runner.js";
|
|
43
|
+
import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
|
|
44
44
|
import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
|
|
45
45
|
import { chatCompletion } from "../../llm/client.js";
|
|
46
46
|
import { isLlmFeatureEnabled } from "../../llm/feature-gate.js";
|
|
@@ -954,44 +954,39 @@ export async function akmReflect(options = {}) {
|
|
|
954
954
|
iterResult = await runAgent(resolvedProfile, prompt, runOptions);
|
|
955
955
|
}
|
|
956
956
|
else if (runnerSpec) {
|
|
957
|
-
// v2: dispatch through unified RunnerSpec
|
|
957
|
+
// v2: dispatch through the unified RunnerSpec seam (X3). The `agent` /
|
|
958
|
+
// `sdk` arms route to the default profile runners; the `llm` arm is
|
|
959
|
+
// reflect-specific (wraps `runReflectViaLlm` — its bespoke iteration
|
|
960
|
+
// shape) so it is supplied as the `llm` handler.
|
|
958
961
|
const runOptions = {
|
|
959
962
|
stdio: "captured",
|
|
960
963
|
parseOutput: "text",
|
|
961
964
|
...(Object.keys(agentEnv).length > 0 ? { env: agentEnv } : {}),
|
|
962
965
|
};
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
|
|
988
|
-
});
|
|
989
|
-
break;
|
|
990
|
-
default:
|
|
991
|
-
// Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile
|
|
992
|
-
// error here instead of leaving `iterResult` unassigned at runtime.
|
|
993
|
-
assertNever(runnerSpec);
|
|
994
|
-
}
|
|
966
|
+
iterResult = await executeRunner(runnerSpec, prompt ?? "", runOptions, {
|
|
967
|
+
llm: async (spec) =>
|
|
968
|
+
// LLM HTTP path — `draftFilePath` is accepted for type symmetry
|
|
969
|
+
// (see `RunReflectViaLlmOptions.draftFilePath` docstring) but is
|
|
970
|
+
// intentionally a no-op. The prompt builder above also did not
|
|
971
|
+
// include the file-write contract for this kind, so the LLM is
|
|
972
|
+
// still asked for JSON via stdout.
|
|
973
|
+
runReflectViaLlm({
|
|
974
|
+
prompt,
|
|
975
|
+
connection: spec.connection,
|
|
976
|
+
timeoutMs: spec.timeoutMs ?? (typeof resolvedTimeoutMs === "number" ? resolvedTimeoutMs : undefined),
|
|
977
|
+
priorDraft,
|
|
978
|
+
iteration: iter,
|
|
979
|
+
responseSchema: REFLECT_JSON_SCHEMA,
|
|
980
|
+
chat: options.chat,
|
|
981
|
+
...(maxTokensForLlm !== undefined ? { maxTokens: maxTokensForLlm } : {}),
|
|
982
|
+
}),
|
|
983
|
+
// The `agent` arm (and only the agent arm — preserving prior behavior)
|
|
984
|
+
// overlays `spec.timeoutMs` onto the base run options.
|
|
985
|
+
runAgent: (profile, p, opts) => runAgent(profile, p, {
|
|
986
|
+
...opts,
|
|
987
|
+
...(runnerSpec.timeoutMs !== undefined ? { timeoutMs: runnerSpec.timeoutMs } : {}),
|
|
988
|
+
}),
|
|
989
|
+
});
|
|
995
990
|
}
|
|
996
991
|
else {
|
|
997
992
|
// Production path (v1): dispatch directly to the appropriate runner.
|
|
@@ -36,14 +36,12 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import fs from "node:fs";
|
|
38
38
|
import path from "node:path";
|
|
39
|
-
import { assertNever } from "../../core/assert.js";
|
|
40
39
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
41
40
|
import { resolveAssetPathFromName, TYPE_DIRS } from "../../core/asset/asset-spec.js";
|
|
42
41
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
43
42
|
import { appendEvent } from "../../core/events.js";
|
|
44
43
|
import { info, warn } from "../../core/warn.js";
|
|
45
|
-
import {
|
|
46
|
-
import { runOpencodeSdk } from "../../integrations/harnesses/opencode-sdk/index.js";
|
|
44
|
+
import { executeRunner } from "../../integrations/agent/runner-dispatch.js";
|
|
47
45
|
import { chatCompletion, stripJsonFences } from "../../llm/client.js";
|
|
48
46
|
import { akmProposalAccept, akmProposalReject } from "./proposal.js";
|
|
49
47
|
import { listProposals, recordGateDecision } from "./validators/proposals.js";
|
|
@@ -209,59 +207,38 @@ export function parseJudgmentVerdict(raw) {
|
|
|
209
207
|
return { decision, reason: typeof reason === "string" ? reason : "" };
|
|
210
208
|
}
|
|
211
209
|
/**
|
|
212
|
-
* Dispatch a single judgment prompt to the resolved runner
|
|
213
|
-
*
|
|
214
|
-
*
|
|
210
|
+
* Dispatch a single judgment prompt to the resolved runner via the unified
|
|
211
|
+
* {@link executeRunner} seam (X3). The `llm` arm is drain-specific (wraps
|
|
212
|
+
* `chatCompletion` — no filesystem) so it is supplied as the `llm` handler; the
|
|
213
|
+
* byte-identical `agent` / `sdk` arms route to the default profile runners (or
|
|
214
|
+
* the injected {@link JudgmentSeams} test fakes). A failed spawn warns and
|
|
215
|
+
* yields `null`, matching the prior per-arm behavior.
|
|
215
216
|
*/
|
|
216
217
|
async function dispatchJudgment(runner, prompt, seams) {
|
|
217
|
-
|
|
218
|
-
switch (runner.kind) {
|
|
219
|
-
case "llm": {
|
|
220
|
-
const messages = [{ role: "user", content: prompt }];
|
|
221
|
-
raw = seams.chat
|
|
222
|
-
? await seams.chat(runner, messages)
|
|
223
|
-
: await chatCompletion(runner.connection, messages, {
|
|
224
|
-
...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
|
|
225
|
-
});
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
case "agent": {
|
|
229
|
-
const stdout = await runProfileJudgment(seams.runAgentFn ?? runAgent, runner.profile, prompt, "agent", runner.timeoutMs);
|
|
230
|
-
if (stdout === null)
|
|
231
|
-
return null;
|
|
232
|
-
raw = stdout;
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
case "sdk": {
|
|
236
|
-
const stdout = await runProfileJudgment(seams.runSdkFn ?? runOpencodeSdk, runner.profile, prompt, "sdk", runner.timeoutMs);
|
|
237
|
-
if (stdout === null)
|
|
238
|
-
return null;
|
|
239
|
-
raw = stdout;
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
default:
|
|
243
|
-
// Exhaustiveness arm (H1): a 4th RunnerSpec kind becomes a compile error
|
|
244
|
-
// here instead of leaving `raw` undefined at runtime.
|
|
245
|
-
return assertNever(runner);
|
|
246
|
-
}
|
|
247
|
-
return parseJudgmentVerdict(raw);
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Shared spawn-runner dispatch for the byte-identical `agent` / `sdk` arms: run
|
|
251
|
-
* the profile-based runner, log a labelled warning on failure, and return the
|
|
252
|
-
* captured stdout (or `null` on failure). `label` only changes the warn prefix.
|
|
253
|
-
*/
|
|
254
|
-
async function runProfileJudgment(run, profile, prompt, label, timeoutMs) {
|
|
255
|
-
const result = await run(profile, prompt, {
|
|
218
|
+
const runOptions = {
|
|
256
219
|
stdio: "captured",
|
|
257
220
|
parseOutput: "text",
|
|
258
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
221
|
+
...(runner.timeoutMs !== undefined ? { timeoutMs: runner.timeoutMs } : {}),
|
|
222
|
+
};
|
|
223
|
+
const result = await executeRunner(runner, prompt, runOptions, {
|
|
224
|
+
llm: async (spec, p) => {
|
|
225
|
+
const messages = [{ role: "user", content: p }];
|
|
226
|
+
const raw = seams.chat
|
|
227
|
+
? await seams.chat(spec, messages)
|
|
228
|
+
: await chatCompletion(spec.connection, messages, {
|
|
229
|
+
...(spec.timeoutMs !== undefined ? { timeoutMs: spec.timeoutMs } : {}),
|
|
230
|
+
});
|
|
231
|
+
// chatCompletion has no failure envelope — a returned string is success.
|
|
232
|
+
return { ok: true, exitCode: 0, stdout: raw, stderr: "", durationMs: 0 };
|
|
233
|
+
},
|
|
234
|
+
...(seams.runAgentFn ? { runAgent: seams.runAgentFn } : {}),
|
|
235
|
+
...(seams.runSdkFn ? { runSdk: seams.runSdkFn } : {}),
|
|
259
236
|
});
|
|
260
237
|
if (!result.ok) {
|
|
261
|
-
warn(`[triage] judgment ${
|
|
238
|
+
warn(`[triage] judgment ${runner.kind} failed: ${result.error ?? result.reason ?? "unknown error"}`);
|
|
262
239
|
return null;
|
|
263
240
|
}
|
|
264
|
-
return result.stdout;
|
|
241
|
+
return parseJudgmentVerdict(result.stdout);
|
|
265
242
|
}
|
|
266
243
|
/**
|
|
267
244
|
* Run the judgment tier over the deferred items. The runner only *judges*; the
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
* and emits the same JSON envelope (stdout/stderr/exit-code) as the inline
|
|
10
10
|
* `runWithJsonErrors` form it replaces.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
12
|
+
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
13
|
+
import { defineGroupCommand, defineJsonCommand, output } from "../../cli/shared.js";
|
|
15
14
|
import { resolveStashDir } from "../../core/common.js";
|
|
16
15
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
16
|
import { UsageError } from "../../core/errors.js";
|
|
@@ -448,19 +447,7 @@ const proposalDrainCommand = defineJsonCommand({
|
|
|
448
447
|
// (`proposals`/`accept`/`reject`/`diff`/`revert`) remain as deprecated aliases
|
|
449
448
|
// that warn to stderr and delegate to the same command bodies; they are removed
|
|
450
449
|
// in 0.9.0. Bare `akm proposal` behaves as `proposal list` (mirrors `akm env`).
|
|
451
|
-
|
|
452
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
453
|
-
const proposalSubCommands = {
|
|
454
|
-
list: proposalListCommand,
|
|
455
|
-
show: proposalShowCommand,
|
|
456
|
-
diff: proposalDiffCommand,
|
|
457
|
-
accept: proposalAcceptCommand,
|
|
458
|
-
reject: proposalRejectCommand,
|
|
459
|
-
revert: proposalRevertCommand,
|
|
460
|
-
drain: proposalDrainCommand,
|
|
461
|
-
};
|
|
462
|
-
const PROPOSAL_SUBCOMMAND_SET = new Set(Object.keys(proposalSubCommands));
|
|
463
|
-
export const proposalCommand = defineCommand({
|
|
450
|
+
export const proposalCommand = defineGroupCommand({
|
|
464
451
|
meta: { name: "proposal", description: "Manage the proposal queue: list, show, diff, accept, reject, revert" },
|
|
465
452
|
args: {
|
|
466
453
|
status: {
|
|
@@ -470,21 +457,24 @@ export const proposalCommand = defineCommand({
|
|
|
470
457
|
ref: { type: "string", description: "Filter by asset ref (type:name)" },
|
|
471
458
|
type: { type: "string", description: "Filter by asset type" },
|
|
472
459
|
},
|
|
473
|
-
subCommands:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
460
|
+
subCommands: {
|
|
461
|
+
list: proposalListCommand,
|
|
462
|
+
show: proposalShowCommand,
|
|
463
|
+
diff: proposalDiffCommand,
|
|
464
|
+
accept: proposalAcceptCommand,
|
|
465
|
+
reject: proposalRejectCommand,
|
|
466
|
+
revert: proposalRevertCommand,
|
|
467
|
+
drain: proposalDrainCommand,
|
|
468
|
+
},
|
|
469
|
+
// Default body fires only for bare `akm proposal [--status …]`.
|
|
470
|
+
defaultRun({ args }) {
|
|
471
|
+
const status = parseProposalStatus(args.status);
|
|
472
|
+
const result = akmProposalList({
|
|
473
|
+
status,
|
|
474
|
+
ref: args.ref,
|
|
475
|
+
type: args.type,
|
|
476
|
+
includeArchive: status === "accepted" || status === "rejected" || status === "reverted",
|
|
488
477
|
});
|
|
478
|
+
output("proposal-list", result);
|
|
489
479
|
},
|
|
490
480
|
});
|
|
@@ -50,7 +50,7 @@ import { makeAssetRef, parseAssetRef } from "../../../core/asset/asset-ref.js";
|
|
|
50
50
|
import { resolveAssetPathFromName, TYPE_DIRS } from "../../../core/asset/asset-spec.js";
|
|
51
51
|
import { NotFoundError, UsageError } from "../../../core/errors.js";
|
|
52
52
|
import { appendEvent } from "../../../core/events.js";
|
|
53
|
-
import {
|
|
53
|
+
import { getStateProposal, hasImportedFsProposals, insertProposalIfAbsent, listStateProposalIdsByPrefix, listStateProposals, recordFsProposalsImport, upsertProposal, withImmediateTransaction, withStateDb, } from "../../../core/state-db.js";
|
|
54
54
|
import { repairTruncatedDescription } from "../../../core/text-truncation.js";
|
|
55
55
|
import { warn } from "../../../core/warn.js";
|
|
56
56
|
import { commitWriteTargetBoundary, formatRefForMessage, resolveWriteTarget, writeAssetToSource, } from "../../../core/write-source.js";
|
|
@@ -161,14 +161,10 @@ function newId(ctx) {
|
|
|
161
161
|
* guaranteed to have run before any read or write.
|
|
162
162
|
*/
|
|
163
163
|
function withProposalsDb(stashDir, ctx, fn) {
|
|
164
|
-
|
|
165
|
-
try {
|
|
164
|
+
return withStateDb((db) => {
|
|
166
165
|
importLegacyProposalFiles(db, stashDir);
|
|
167
166
|
return fn(db);
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
db.close();
|
|
171
|
-
}
|
|
167
|
+
}, { path: ctx?.dbPath });
|
|
172
168
|
}
|
|
173
169
|
// ── Legacy filesystem import (#578) ─────────────────────────────────────────
|
|
174
170
|
/** Legacy (pre-0.9.0) proposal directory: `<stashDir>/.akm/proposals[/archive]`. */
|
|
@@ -278,33 +278,89 @@ export function deriveCurateFallbackQueries(query) {
|
|
|
278
278
|
return tokens;
|
|
279
279
|
}
|
|
280
280
|
export function mergeCurateSearchResponses(base, extras) {
|
|
281
|
-
|
|
281
|
+
// The base (full-query) ranking is the relevance signal — keyword fallback
|
|
282
|
+
// searches exist only to ADD recall when that ranking is thin, never to
|
|
283
|
+
// re-rank it. So we PRESERVE base order and APPEND fallback-only hits below
|
|
284
|
+
// it. A single-token fallback match on an exact title/path normalizes to a
|
|
285
|
+
// high FTS score, but those scores are not comparable to the full-query
|
|
286
|
+
// (hybrid) scores; re-sorting the union by raw score (the prior behaviour)
|
|
287
|
+
// let that keyword junk leapfrog the contextually-relevant full-query hits.
|
|
288
|
+
// Dup refs (present in both base and a fallback) keep their base POSITION but
|
|
289
|
+
// take the MAX score, since matching both the full query and a key term is a
|
|
290
|
+
// stronger relevance signal for the downstream score floor.
|
|
291
|
+
const bestExtraStashScore = new Map();
|
|
292
|
+
for (const result of extras) {
|
|
293
|
+
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
294
|
+
const prev = bestExtraStashScore.get(hit.ref);
|
|
295
|
+
if (prev === undefined || (hit.score ?? 0) > prev)
|
|
296
|
+
bestExtraStashScore.set(hit.ref, hit.score ?? 0);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const baseRefs = new Set();
|
|
300
|
+
const baseStash = [];
|
|
282
301
|
for (const hit of base.hits.filter((entry) => entry.type !== "registry")) {
|
|
283
|
-
|
|
302
|
+
baseRefs.add(hit.ref);
|
|
303
|
+
const extraScore = bestExtraStashScore.get(hit.ref);
|
|
304
|
+
baseStash.push(extraScore !== undefined && extraScore > (hit.score ?? 0) ? { ...hit, score: extraScore } : hit);
|
|
284
305
|
}
|
|
306
|
+
const extraOnly = new Map();
|
|
285
307
|
for (const result of extras) {
|
|
286
308
|
for (const hit of result.hits.filter((entry) => entry.type !== "registry")) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
309
|
+
if (baseRefs.has(hit.ref))
|
|
310
|
+
continue;
|
|
311
|
+
const existing = extraOnly.get(hit.ref);
|
|
312
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0))
|
|
313
|
+
extraOnly.set(hit.ref, hit);
|
|
291
314
|
}
|
|
292
315
|
}
|
|
293
|
-
|
|
316
|
+
// Fallback-only hits must rank BELOW every full-query hit through the rest of
|
|
317
|
+
// the pipeline. The downstream selector (`selectCuratedStashHits`) RE-SORTS by
|
|
318
|
+
// score and derives its relevance floor from the top score, so preserving
|
|
319
|
+
// order here is not enough — a single-token FTS match (normalized ~0.9) would
|
|
320
|
+
// otherwise become the leader and evict the contextual full-query memories.
|
|
321
|
+
// We therefore restamp fallback-only scores into a band strictly below the
|
|
322
|
+
// minimum base score (keeping their own relative order). When there are no
|
|
323
|
+
// base hits, fallback IS the result, so scores are kept as-is.
|
|
324
|
+
const sortedExtra = [...extraOnly.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
325
|
+
const minBaseScore = baseStash.length
|
|
326
|
+
? Math.min(...baseStash.map((hit) => hit.score ?? 0))
|
|
327
|
+
: Number.POSITIVE_INFINITY;
|
|
328
|
+
const cappedExtra = baseStash.length
|
|
329
|
+
? sortedExtra.map((hit, i) => ({ ...hit, score: minBaseScore - 1e-6 * (i + 1) }))
|
|
330
|
+
: sortedExtra;
|
|
331
|
+
const mergedHits = [...baseStash, ...cappedExtra];
|
|
332
|
+
// Registry hits are supplemental fill — same rule: base first (max score on
|
|
333
|
+
// dups), then fallback-only registry hits appended by score.
|
|
334
|
+
const bestExtraRegScore = new Map();
|
|
335
|
+
for (const result of extras) {
|
|
336
|
+
for (const hit of result.registryHits ?? []) {
|
|
337
|
+
const prev = bestExtraRegScore.get(hit.id);
|
|
338
|
+
if (prev === undefined || (hit.score ?? 0) > prev)
|
|
339
|
+
bestExtraRegScore.set(hit.id, hit.score ?? 0);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const baseRegIds = new Set();
|
|
343
|
+
const baseReg = [];
|
|
294
344
|
for (const hit of base.registryHits ?? []) {
|
|
295
|
-
|
|
345
|
+
baseRegIds.add(hit.id);
|
|
346
|
+
const extraScore = bestExtraRegScore.get(hit.id);
|
|
347
|
+
baseReg.push(extraScore !== undefined && extraScore > (hit.score ?? 0) ? { ...hit, score: extraScore } : hit);
|
|
296
348
|
}
|
|
349
|
+
const extraRegOnly = new Map();
|
|
297
350
|
for (const result of extras) {
|
|
298
351
|
for (const hit of result.registryHits ?? []) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
352
|
+
if (baseRegIds.has(hit.id))
|
|
353
|
+
continue;
|
|
354
|
+
const existing = extraRegOnly.get(hit.id);
|
|
355
|
+
if (!existing || (hit.score ?? 0) > (existing.score ?? 0))
|
|
356
|
+
extraRegOnly.set(hit.id, hit);
|
|
303
357
|
}
|
|
304
358
|
}
|
|
305
359
|
const warnings = Array.from(new Set([...(base.warnings ?? []), ...extras.flatMap((result) => result.warnings ?? [])]));
|
|
306
|
-
const
|
|
307
|
-
|
|
360
|
+
const mergedRegistryHits = [
|
|
361
|
+
...baseReg,
|
|
362
|
+
...[...extraRegOnly.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)),
|
|
363
|
+
];
|
|
308
364
|
return {
|
|
309
365
|
...base,
|
|
310
366
|
hits: mergedHits,
|
|
@@ -101,10 +101,10 @@ export function readKnowledgeContent(source) {
|
|
|
101
101
|
* URLs are fetched via `fetchWebsiteMarkdownSnapshot`; local sources delegate
|
|
102
102
|
* to `readKnowledgeContent`.
|
|
103
103
|
*/
|
|
104
|
-
export async function readKnowledgeInput(source) {
|
|
104
|
+
export async function readKnowledgeInput(source, options) {
|
|
105
105
|
if (!isHttpUrl(source))
|
|
106
106
|
return readKnowledgeContent(source);
|
|
107
|
-
const snapshot = await fetchWebsiteMarkdownSnapshot(source);
|
|
107
|
+
const snapshot = await fetchWebsiteMarkdownSnapshot(source, { stashDir: options?.stashDir });
|
|
108
108
|
return { content: snapshot.content, preferredName: snapshot.preferredName };
|
|
109
109
|
}
|
|
110
110
|
// ── Asset writing ────────────────────────────────────────────────────────────
|
|
@@ -306,8 +306,8 @@ export async function performUpgrade(check, opts) {
|
|
|
306
306
|
* The new binary's `akm index` does the work for us:
|
|
307
307
|
* 1. loadConfig() runs at startup — auto-migrates legacy `stashes` →
|
|
308
308
|
* `sources` if the on-disk config still uses the old key.
|
|
309
|
-
* 2. ensureSchema()
|
|
310
|
-
*
|
|
309
|
+
* 2. ensureSchema() converges index.db forward via its idempotent baseline
|
|
310
|
+
* schema + additive migrations (no destructive rebuild).
|
|
311
311
|
* 3. The full reindex repopulates entries + workflow_documents + FTS.
|
|
312
312
|
*/
|
|
313
313
|
function runPostUpgradeTasks(akmBin, opts) {
|
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* Stash-lifecycle command cluster — the create/index/ingest/inspect verbs for
|
|
6
6
|
* the working stash and its index database: `akm init` (create the stash +
|
|
7
7
|
* persist stashDir), `akm index` (build/refresh the search index), `akm import`
|
|
8
|
-
* (ingest a knowledge doc/URL), `akm
|
|
9
|
-
*
|
|
8
|
+
* (ingest a knowledge doc/URL), and `akm info` (system capabilities + index
|
|
9
|
+
* stats).
|
|
10
10
|
* Extracted verbatim from src/cli.ts (WS6) so the God Module shrinks; the
|
|
11
|
-
* `main.subCommands.{init,index,import,
|
|
11
|
+
* `main.subCommands.{init,index,import,info}` keys and every subcommand's
|
|
12
12
|
* args/output shape stay byte-identical.
|
|
13
13
|
*
|
|
14
14
|
* These share no private helper with any command still inline in cli.ts — every
|
|
15
15
|
* dependency is already exported from a shared module (core/paths, core/warn,
|
|
16
16
|
* core/errors, core/events, output/context, cli/shared, cli/parse-args, plus the
|
|
17
|
-
* per-command implementations in ./init, ./indexer, ./info, ./
|
|
17
|
+
* per-command implementations in ./init, ./indexer, ./info, ./knowledge,
|
|
18
18
|
* ./core/asset-create, ./core/common), so the cluster moves with zero hoisting.
|
|
19
19
|
*
|
|
20
20
|
* The leaf handlers whose body is a plain `runWithJsonErrors(...) + output(...)`
|
|
21
|
-
* (`init`, `import`, `info
|
|
21
|
+
* (`init`, `import`, `info`) are migrated onto
|
|
22
22
|
* `defineJsonCommand`, which emits the same JSON envelope (stdout/stderr/
|
|
23
23
|
* exit-code) as the inline form. `index` keeps a plain `defineCommand` wrapping
|
|
24
24
|
* `runWithJsonErrors` because its body owns a spinner, an AbortController, and
|
|
@@ -27,17 +27,17 @@
|
|
|
27
27
|
import path from "node:path";
|
|
28
28
|
import * as p from "@clack/prompts";
|
|
29
29
|
import { defineCommand } from "citty";
|
|
30
|
-
import { hasSubcommand } from "../../cli/parse-args.js";
|
|
31
30
|
import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
32
31
|
import { assertFlatAssetName } from "../../core/asset/asset-create.js";
|
|
33
32
|
import { isHttpUrl } from "../../core/common.js";
|
|
33
|
+
import { loadConfig } from "../../core/config/config.js";
|
|
34
34
|
import { UsageError } from "../../core/errors.js";
|
|
35
35
|
import { appendEvent } from "../../core/events.js";
|
|
36
36
|
import { getCacheDir } from "../../core/paths.js";
|
|
37
37
|
import { clearLogFile, info, isVerbose, setLogFile } from "../../core/warn.js";
|
|
38
|
+
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
38
39
|
import { akmIndex } from "../../indexer/indexer.js";
|
|
39
40
|
import { getHyphenatedBoolean, getOutputMode, parseFlagValue } from "../../output/context.js";
|
|
40
|
-
import { akmDbBackups } from "../db-cli.js";
|
|
41
41
|
import { readKnowledgeInput, writeMarkdownAsset } from "../read/knowledge.js";
|
|
42
42
|
import { assembleInfo } from "./info.js";
|
|
43
43
|
import { akmInit } from "./init.js";
|
|
@@ -153,35 +153,6 @@ export const infoCommand = defineJsonCommand({
|
|
|
153
153
|
output("info", result);
|
|
154
154
|
},
|
|
155
155
|
});
|
|
156
|
-
// MVP DB administration. Currently only `akm db backups`; restore is manual —
|
|
157
|
-
// stop akm and run `scripts/migrations/restore-data-dir.sh <backup>`.
|
|
158
|
-
// Single source of truth: the routing set is derived from the subCommands keys
|
|
159
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
160
|
-
const dbSubCommands = {
|
|
161
|
-
backups: defineJsonCommand({
|
|
162
|
-
meta: {
|
|
163
|
-
name: "backups",
|
|
164
|
-
description: "List pre-upgrade snapshots of the data directory (newest first). Backups are created automatically before destructive DB version upgrades unless AKM_DB_BACKUP=0.",
|
|
165
|
-
},
|
|
166
|
-
run() {
|
|
167
|
-
output("db-backups", akmDbBackups());
|
|
168
|
-
},
|
|
169
|
-
}),
|
|
170
|
-
};
|
|
171
|
-
const DB_SUBCOMMAND_SET = new Set(Object.keys(dbSubCommands));
|
|
172
|
-
export const dbCommand = defineJsonCommand({
|
|
173
|
-
meta: {
|
|
174
|
-
name: "db",
|
|
175
|
-
description: "Inspect the AKM SQLite data directory. Currently exposes `backups`; to restore from a snapshot, stop akm and run scripts/migrations/restore-data-dir.sh against the chosen backup.",
|
|
176
|
-
},
|
|
177
|
-
subCommands: dbSubCommands,
|
|
178
|
-
run({ args }) {
|
|
179
|
-
if (hasSubcommand(args, DB_SUBCOMMAND_SET))
|
|
180
|
-
return;
|
|
181
|
-
// Default action: list backups.
|
|
182
|
-
output("db-backups", akmDbBackups());
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
156
|
export const importKnowledgeCommand = defineJsonCommand({
|
|
186
157
|
meta: {
|
|
187
158
|
name: "import",
|
|
@@ -214,7 +185,8 @@ export const importKnowledgeCommand = defineJsonCommand({
|
|
|
214
185
|
async run({ args }) {
|
|
215
186
|
// `--name` is a flat name; subdirectory placement is `--path`'s job.
|
|
216
187
|
assertFlatAssetName(args.name);
|
|
217
|
-
const
|
|
188
|
+
const stashDir = resolveWriteTarget(loadConfig(), args.target).source.path;
|
|
189
|
+
const { content, preferredName } = await readKnowledgeInput(args.source, { stashDir });
|
|
218
190
|
const result = await writeMarkdownAsset({
|
|
219
191
|
type: "knowledge",
|
|
220
192
|
content,
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* with the family.
|
|
14
14
|
*/
|
|
15
15
|
import { defineCommand } from "citty";
|
|
16
|
-
import {
|
|
17
|
-
import { defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
16
|
+
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
17
|
+
import { defineGroupCommand, defineJsonCommand, output, runWithJsonErrors } from "../../cli/shared.js";
|
|
18
18
|
import { getHyphenatedArg } from "../../output/context.js";
|
|
19
19
|
import { detectServerDefault, registerDefaultTasks } from "./default-tasks.js";
|
|
20
20
|
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksList, akmTasksRemove, akmTasksRun, akmTasksSetEnabled, akmTasksShow, akmTasksSync, parseTaskRef, } from "./tasks.js";
|
|
@@ -176,35 +176,27 @@ const tasksDoctorCommand = defineJsonCommand({
|
|
|
176
176
|
output("tasks-doctor", result);
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
|
-
|
|
180
|
-
// (M10) so adding a subcommand can never silently desync from `hasSubcommand`.
|
|
181
|
-
const tasksSubCommands = {
|
|
182
|
-
add: tasksAddCommand,
|
|
183
|
-
init: tasksInitCommand,
|
|
184
|
-
list: tasksListCommand,
|
|
185
|
-
show: tasksShowCommand,
|
|
186
|
-
remove: tasksRemoveCommand,
|
|
187
|
-
enable: tasksEnableCommand,
|
|
188
|
-
disable: tasksDisableCommand,
|
|
189
|
-
run: tasksRunCommand,
|
|
190
|
-
history: tasksHistoryCommand,
|
|
191
|
-
sync: tasksSyncCommand,
|
|
192
|
-
doctor: tasksDoctorCommand,
|
|
193
|
-
};
|
|
194
|
-
const TASKS_SUBCOMMAND_SET = new Set(Object.keys(tasksSubCommands));
|
|
195
|
-
export const tasksCommand = defineCommand({
|
|
179
|
+
export const tasksCommand = defineGroupCommand({
|
|
196
180
|
meta: {
|
|
197
181
|
name: "tasks",
|
|
198
182
|
alias: "task",
|
|
199
183
|
description: "Schedule workflows or prompts via the OS-native scheduler (cron / launchd / schtasks)",
|
|
200
184
|
},
|
|
201
|
-
subCommands:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
185
|
+
subCommands: {
|
|
186
|
+
add: tasksAddCommand,
|
|
187
|
+
init: tasksInitCommand,
|
|
188
|
+
list: tasksListCommand,
|
|
189
|
+
show: tasksShowCommand,
|
|
190
|
+
remove: tasksRemoveCommand,
|
|
191
|
+
enable: tasksEnableCommand,
|
|
192
|
+
disable: tasksDisableCommand,
|
|
193
|
+
run: tasksRunCommand,
|
|
194
|
+
history: tasksHistoryCommand,
|
|
195
|
+
sync: tasksSyncCommand,
|
|
196
|
+
doctor: tasksDoctorCommand,
|
|
197
|
+
},
|
|
198
|
+
async defaultRun() {
|
|
199
|
+
const result = await akmTasksList();
|
|
200
|
+
output("tasks-list", result);
|
|
209
201
|
},
|
|
210
202
|
});
|