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,150 @@
|
|
|
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 { defineCommand } from "citty";
|
|
5
|
+
import { parsePositiveIntFlag } from "../cli/parse-args";
|
|
6
|
+
import { output, runWithJsonErrors } from "../cli/shared";
|
|
7
|
+
import { DEFAULT_CONFIG, loadUserConfig, saveConfig } from "../core/config";
|
|
8
|
+
import { UsageError } from "../core/errors";
|
|
9
|
+
import { warn } from "../core/warn";
|
|
10
|
+
import { getHyphenatedArg, getHyphenatedBoolean } from "../output/context";
|
|
11
|
+
import { buildRegistryIndex, writeRegistryIndex } from "../registry/build-index";
|
|
12
|
+
import { searchRegistry } from "./registry-search";
|
|
13
|
+
export const registryCommand = defineCommand({
|
|
14
|
+
meta: { name: "registry", description: "Manage stash registries" },
|
|
15
|
+
subCommands: {
|
|
16
|
+
list: defineCommand({
|
|
17
|
+
meta: { name: "list", description: "List configured registries" },
|
|
18
|
+
run() {
|
|
19
|
+
return runWithJsonErrors(() => {
|
|
20
|
+
const config = loadUserConfig();
|
|
21
|
+
const registries = config.registries ?? DEFAULT_CONFIG.registries;
|
|
22
|
+
output("registry-list", { registries });
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
add: defineCommand({
|
|
27
|
+
meta: { name: "add", description: "Add a registry by URL" },
|
|
28
|
+
args: {
|
|
29
|
+
url: { type: "positional", description: "Registry index URL", required: true },
|
|
30
|
+
name: { type: "string", description: "Human-friendly name for the registry" },
|
|
31
|
+
provider: { type: "string", description: "Provider type (e.g. static-index, skills-sh)" },
|
|
32
|
+
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
33
|
+
"allow-insecure": {
|
|
34
|
+
type: "boolean",
|
|
35
|
+
description: "Allow a plain HTTP registry URL (otherwise rejected)",
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
run({ args }) {
|
|
40
|
+
return runWithJsonErrors(() => {
|
|
41
|
+
if (!args.url.startsWith("http")) {
|
|
42
|
+
throw new UsageError("Registry URL must start with http:// or https://");
|
|
43
|
+
}
|
|
44
|
+
if (args.url.startsWith("http://")) {
|
|
45
|
+
const allowInsecure = getHyphenatedBoolean(args, "allow-insecure");
|
|
46
|
+
if (!allowInsecure) {
|
|
47
|
+
throw new UsageError("Registry URL uses plain HTTP (not HTTPS). An on-path attacker could substitute a malicious index. " +
|
|
48
|
+
"Use https:// or pass --allow-insecure if you have explicitly accepted the risk.");
|
|
49
|
+
}
|
|
50
|
+
warn("Warning: registry URL uses plain HTTP (not HTTPS). --allow-insecure was set; an on-path attacker could substitute a malicious index.");
|
|
51
|
+
}
|
|
52
|
+
const config = loadUserConfig();
|
|
53
|
+
const registries = [...(config.registries ?? [])];
|
|
54
|
+
// Deduplicate by URL
|
|
55
|
+
if (registries.some((r) => r.url === args.url)) {
|
|
56
|
+
output("registry-add", { registries, added: false, message: "Registry URL already configured" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const entry = { url: args.url };
|
|
60
|
+
if (args.name)
|
|
61
|
+
entry.name = args.name;
|
|
62
|
+
if (args.provider)
|
|
63
|
+
entry.provider = args.provider;
|
|
64
|
+
if (args.options) {
|
|
65
|
+
try {
|
|
66
|
+
entry.options = JSON.parse(args.options);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new UsageError("--options must be valid JSON");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
registries.push(entry);
|
|
73
|
+
saveConfig({ ...config, registries });
|
|
74
|
+
output("registry-add", { registries, added: true });
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
remove: defineCommand({
|
|
79
|
+
meta: { name: "remove", description: "Remove a registry by URL or name" },
|
|
80
|
+
args: {
|
|
81
|
+
target: { type: "positional", description: "Registry URL or name to remove", required: true },
|
|
82
|
+
yes: { type: "boolean", alias: "y", description: "Skip confirmation prompt", default: false },
|
|
83
|
+
},
|
|
84
|
+
run({ args }) {
|
|
85
|
+
return runWithJsonErrors(async () => {
|
|
86
|
+
const config = loadUserConfig();
|
|
87
|
+
const registries = [...(config.registries ?? [])];
|
|
88
|
+
const idx = registries.findIndex((r) => r.url === args.target || r.name === args.target);
|
|
89
|
+
if (idx === -1) {
|
|
90
|
+
output("registry-remove", { registries, removed: false, message: "No matching registry found" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const { confirmDestructive } = await import("../cli/confirm.js");
|
|
94
|
+
const confirmed = await confirmDestructive(`Remove registry "${args.target}"? This cannot be undone.`, {
|
|
95
|
+
yes: args.yes === true,
|
|
96
|
+
});
|
|
97
|
+
if (!confirmed) {
|
|
98
|
+
process.stderr.write("Aborted.\n");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const removed = registries.splice(idx, 1)[0];
|
|
102
|
+
saveConfig({ ...config, registries });
|
|
103
|
+
output("registry-remove", { registries, removed: true, entry: removed });
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
search: defineCommand({
|
|
108
|
+
meta: { name: "search", description: "Search enabled registries for stashes" },
|
|
109
|
+
args: {
|
|
110
|
+
query: { type: "positional", description: "Search query", required: true },
|
|
111
|
+
limit: { type: "string", description: "Maximum number of results" },
|
|
112
|
+
assets: { type: "boolean", description: "Include asset-level search results", default: false },
|
|
113
|
+
},
|
|
114
|
+
async run({ args }) {
|
|
115
|
+
await runWithJsonErrors(async () => {
|
|
116
|
+
const limitRaw = parsePositiveIntFlag(args.limit ?? undefined);
|
|
117
|
+
const result = await searchRegistry(args.query, { limit: limitRaw, includeAssets: args.assets });
|
|
118
|
+
output("registry-search", result);
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
"build-index": defineCommand({
|
|
123
|
+
meta: { name: "build-index", description: "Build a v2 registry index from discovery and manual entries" },
|
|
124
|
+
args: {
|
|
125
|
+
out: { type: "string", description: "Output path for the generated index" },
|
|
126
|
+
manual: { type: "string", description: "Manual entries JSON file" },
|
|
127
|
+
"npm-registry": { type: "string", description: "Override npm registry base URL" },
|
|
128
|
+
"github-api": { type: "string", description: "Override GitHub API base URL" },
|
|
129
|
+
},
|
|
130
|
+
async run({ args }) {
|
|
131
|
+
await runWithJsonErrors(async () => {
|
|
132
|
+
const result = await buildRegistryIndex({
|
|
133
|
+
manualEntriesPath: args.manual,
|
|
134
|
+
npmRegistryBase: getHyphenatedArg(args, "npm-registry"),
|
|
135
|
+
githubApiBase: getHyphenatedArg(args, "github-api"),
|
|
136
|
+
});
|
|
137
|
+
const outPath = writeRegistryIndex(result.index, args.out);
|
|
138
|
+
output("registry-build-index", {
|
|
139
|
+
outPath,
|
|
140
|
+
version: result.index.version,
|
|
141
|
+
updatedAt: result.index.updatedAt,
|
|
142
|
+
totalKits: result.counts.total,
|
|
143
|
+
counts: result.counts,
|
|
144
|
+
manualEntriesPath: result.paths.manualEntriesPath,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
import { toErrorMessage } from "../core/common";
|
|
2
5
|
import { DEFAULT_CONFIG } from "../core/config";
|
|
3
6
|
import { warn } from "../core/warn";
|
|
@@ -0,0 +1,257 @@
|
|
|
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 { defineCommand } from "citty";
|
|
5
|
+
import { output, parseAllFlagValues, runWithJsonErrors } from "../cli/shared";
|
|
6
|
+
import { UsageError } from "../core/errors";
|
|
7
|
+
import { appendEvent } from "../core/events";
|
|
8
|
+
import { inferAssetName, writeMarkdownAsset } from "./knowledge";
|
|
9
|
+
import { buildMemoryFrontmatter, parseDuration, readMemoryContent, resolveRememberContentArg, runAutoHeuristics, runLlmEnrich, } from "./remember";
|
|
10
|
+
import { akmSearch } from "./search";
|
|
11
|
+
// ── Helper: similar memory search ────────────────────────────────────────────
|
|
12
|
+
/**
|
|
13
|
+
* Best-effort top-3 similar memory search for `--show-similar`.
|
|
14
|
+
* Scoped to memory: type; excludes the just-written ref.
|
|
15
|
+
*/
|
|
16
|
+
async function fetchSimilarMemories(query, excludeRef) {
|
|
17
|
+
try {
|
|
18
|
+
const result = await akmSearch({ query, type: "memory", limit: 4 });
|
|
19
|
+
return (result.hits ?? [])
|
|
20
|
+
.filter((h) => "ref" in h && h.ref !== excludeRef)
|
|
21
|
+
.slice(0, 3)
|
|
22
|
+
.map((h) => ({ ref: h.ref, ...(h.name ? { title: h.name } : {}) }));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// ── Command definition ────────────────────────────────────────────────────────
|
|
29
|
+
export const rememberCommand = defineCommand({
|
|
30
|
+
meta: {
|
|
31
|
+
name: "remember",
|
|
32
|
+
description: "Record a memory in the default stash",
|
|
33
|
+
},
|
|
34
|
+
args: {
|
|
35
|
+
content: {
|
|
36
|
+
type: "positional",
|
|
37
|
+
description: "Memory content. Omit to read markdown from stdin.",
|
|
38
|
+
required: false,
|
|
39
|
+
},
|
|
40
|
+
name: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Memory name (defaults to a slug from the content)",
|
|
43
|
+
},
|
|
44
|
+
force: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Overwrite an existing memory with the same name",
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
description: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Short description written to frontmatter (persisted as the memory's description field)",
|
|
52
|
+
},
|
|
53
|
+
tag: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Tag to add to the memory (repeatable: --tag foo --tag bar)",
|
|
56
|
+
},
|
|
57
|
+
expires: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Expiry duration shorthand (e.g. 30d, 12h, 6m). Resolved to an ISO date.",
|
|
60
|
+
},
|
|
61
|
+
source: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Source reference (URL, asset ref, file path, or any free-form string)",
|
|
64
|
+
},
|
|
65
|
+
auto: {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
description: "Apply heuristic tagging (code, subjective, source, observed_at) from the body",
|
|
68
|
+
default: false,
|
|
69
|
+
},
|
|
70
|
+
enrich: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Call the configured LLM to propose tags and description (requires LLM config)",
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
75
|
+
target: {
|
|
76
|
+
type: "string",
|
|
77
|
+
description: "Override the write destination. Accepts a source name from your config; falls back to defaultWriteTarget then the working stash.",
|
|
78
|
+
},
|
|
79
|
+
user: {
|
|
80
|
+
type: "string",
|
|
81
|
+
description: "Scope this memory to a user id (persisted as `scope_user` frontmatter)",
|
|
82
|
+
},
|
|
83
|
+
agent: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Scope this memory to an agent id (persisted as `scope_agent` frontmatter)",
|
|
86
|
+
},
|
|
87
|
+
run: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Scope this memory to a run id (persisted as `scope_run` frontmatter)",
|
|
90
|
+
},
|
|
91
|
+
channel: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Scope this memory to a channel name (persisted as `scope_channel` frontmatter)",
|
|
94
|
+
},
|
|
95
|
+
showSimilar: {
|
|
96
|
+
type: "boolean",
|
|
97
|
+
description: "Return top-3 similar existing memories in output (opt-in)",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async run({ args }) {
|
|
101
|
+
return runWithJsonErrors(async () => {
|
|
102
|
+
const body = readMemoryContent(resolveRememberContentArg(args.content));
|
|
103
|
+
// Determine if the user has requested any structured metadata mode.
|
|
104
|
+
// Collect all --tag occurrences directly from process.argv because citty
|
|
105
|
+
// only exposes the last value for repeated string flags.
|
|
106
|
+
const rawTags = parseAllFlagValues("--tag");
|
|
107
|
+
// Collect scope flags. Scope alone counts as structured metadata so we
|
|
108
|
+
// emit frontmatter, but it does NOT trigger the "tags required" check —
|
|
109
|
+
// memory + scope (no tags) is a valid combination for multi-tenant use.
|
|
110
|
+
const scopeFields = {};
|
|
111
|
+
if (typeof args.user === "string" && args.user.trim())
|
|
112
|
+
scopeFields.user = args.user.trim();
|
|
113
|
+
if (typeof args.agent === "string" && args.agent.trim())
|
|
114
|
+
scopeFields.agent = args.agent.trim();
|
|
115
|
+
if (typeof args.run === "string" && args.run.trim())
|
|
116
|
+
scopeFields.run = args.run.trim();
|
|
117
|
+
if (typeof args.channel === "string" && args.channel.trim())
|
|
118
|
+
scopeFields.channel = args.channel.trim();
|
|
119
|
+
const hasScope = Object.keys(scopeFields).length > 0;
|
|
120
|
+
const hasTagRequiringArgs = rawTags.length > 0 || !!args.expires || !!args.source || !!args.description;
|
|
121
|
+
const hasStructuredArgs = hasTagRequiringArgs || hasScope || args.auto;
|
|
122
|
+
if (!hasStructuredArgs) {
|
|
123
|
+
// Phase 1B / Rec 7: even the zero-flag hot-path emits
|
|
124
|
+
// `captureMode: hot` + `beliefState: asserted` so user-supplied
|
|
125
|
+
// memories outrank background-derived ones during ranking.
|
|
126
|
+
const frontmatterBlock = buildMemoryFrontmatter({
|
|
127
|
+
captureMode: "hot",
|
|
128
|
+
beliefState: "asserted",
|
|
129
|
+
});
|
|
130
|
+
const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
|
|
131
|
+
// Derive the asset slug from the body (not the frontmatter block);
|
|
132
|
+
// otherwise inferAssetName would key off the leading `---` delimiter.
|
|
133
|
+
const result = await writeMarkdownAsset({
|
|
134
|
+
type: "memory",
|
|
135
|
+
content: contentWithFrontmatter,
|
|
136
|
+
name: args.name,
|
|
137
|
+
fallbackPrefix: "memory",
|
|
138
|
+
preferredName: inferAssetName(body, "memory"),
|
|
139
|
+
force: args.force,
|
|
140
|
+
target: args.target,
|
|
141
|
+
});
|
|
142
|
+
appendEvent({
|
|
143
|
+
eventType: "remember",
|
|
144
|
+
ref: result.ref,
|
|
145
|
+
metadata: { path: result.path, force: args.force === true },
|
|
146
|
+
});
|
|
147
|
+
if (args.showSimilar) {
|
|
148
|
+
const similar = await fetchSimilarMemories(body.slice(0, 500), result.ref);
|
|
149
|
+
output("remember", { ok: true, ...result, similar });
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
output("remember", { ok: true, ...result });
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// ── Accumulate metadata from all three modes ──────────────────────────
|
|
157
|
+
// Start with CLI args (Mode 1: always)
|
|
158
|
+
const tags = [...rawTags];
|
|
159
|
+
// --description is persisted as-is; LLM enrichment may fill it if absent.
|
|
160
|
+
let description = args.description || undefined;
|
|
161
|
+
let source = args.source;
|
|
162
|
+
let observed_at;
|
|
163
|
+
let expires;
|
|
164
|
+
let subjective;
|
|
165
|
+
// Resolve --expires to an ISO date string
|
|
166
|
+
if (args.expires) {
|
|
167
|
+
const durationMs = parseDuration(args.expires);
|
|
168
|
+
const expiresDate = new Date(Date.now() + durationMs);
|
|
169
|
+
expires = expiresDate.toISOString().slice(0, 10);
|
|
170
|
+
}
|
|
171
|
+
// Mode 2: --auto heuristics
|
|
172
|
+
if (args.auto) {
|
|
173
|
+
const auto = runAutoHeuristics(body);
|
|
174
|
+
for (const t of auto.tags) {
|
|
175
|
+
if (!tags.includes(t))
|
|
176
|
+
tags.push(t);
|
|
177
|
+
}
|
|
178
|
+
if (!source && auto.source)
|
|
179
|
+
source = auto.source;
|
|
180
|
+
if (!observed_at && auto.observed_at)
|
|
181
|
+
observed_at = auto.observed_at;
|
|
182
|
+
if (!subjective && auto.subjective)
|
|
183
|
+
subjective = auto.subjective;
|
|
184
|
+
}
|
|
185
|
+
// Mode 3: --enrich LLM (fail-soft)
|
|
186
|
+
if (args.enrich) {
|
|
187
|
+
const enriched = await runLlmEnrich(body);
|
|
188
|
+
for (const t of enriched.tags) {
|
|
189
|
+
if (!tags.includes(t))
|
|
190
|
+
tags.push(t);
|
|
191
|
+
}
|
|
192
|
+
if (!description && enriched.description)
|
|
193
|
+
description = enriched.description;
|
|
194
|
+
if (!observed_at && enriched.observed_at)
|
|
195
|
+
observed_at = enriched.observed_at;
|
|
196
|
+
}
|
|
197
|
+
// ── Required-field check (before any write) ───────────────────────────
|
|
198
|
+
// Tags remain required when the user explicitly asked for tag-bearing
|
|
199
|
+
// metadata (--tag / --enrich / --description / --source / --expires).
|
|
200
|
+
// `--auto` alone is allowed even when its heuristics derive zero tags.
|
|
201
|
+
// Scope-only writes (`akm remember "..." --user u1`) also skip this
|
|
202
|
+
// check — scope is independent metadata and a memory with only scope is
|
|
203
|
+
// valid.
|
|
204
|
+
const missing = [];
|
|
205
|
+
if (hasTagRequiringArgs && tags.length === 0)
|
|
206
|
+
missing.push("tags");
|
|
207
|
+
if (missing.length > 0) {
|
|
208
|
+
throw new UsageError(`Memory is missing required frontmatter field(s): ${missing.join(", ")}. ` +
|
|
209
|
+
"Provide them via --tag <value>, --auto (heuristics), or --enrich (LLM).");
|
|
210
|
+
}
|
|
211
|
+
// ── Build frontmatter and write ───────────────────────────────────────
|
|
212
|
+
// Phase 1B / Rec 7: the hot-path CLI write always marks the memory as
|
|
213
|
+
// `captureMode: hot` and `beliefState: asserted`. Ranking applies a
|
|
214
|
+
// hot-capture boost so user-supplied memories outrank otherwise-equal
|
|
215
|
+
// background-derived ones.
|
|
216
|
+
const frontmatterBlock = buildMemoryFrontmatter({
|
|
217
|
+
description,
|
|
218
|
+
tags,
|
|
219
|
+
source,
|
|
220
|
+
observed_at,
|
|
221
|
+
expires,
|
|
222
|
+
subjective,
|
|
223
|
+
captureMode: "hot",
|
|
224
|
+
beliefState: "asserted",
|
|
225
|
+
...(hasScope ? { scope: scopeFields } : {}),
|
|
226
|
+
});
|
|
227
|
+
const contentWithFrontmatter = `${frontmatterBlock}\n${body}`;
|
|
228
|
+
const result = await writeMarkdownAsset({
|
|
229
|
+
type: "memory",
|
|
230
|
+
content: contentWithFrontmatter,
|
|
231
|
+
name: args.name,
|
|
232
|
+
fallbackPrefix: "memory",
|
|
233
|
+
force: args.force,
|
|
234
|
+
target: args.target,
|
|
235
|
+
});
|
|
236
|
+
appendEvent({
|
|
237
|
+
eventType: "remember",
|
|
238
|
+
ref: result.ref,
|
|
239
|
+
metadata: {
|
|
240
|
+
path: result.path,
|
|
241
|
+
force: args.force === true,
|
|
242
|
+
tagCount: tags.length,
|
|
243
|
+
enriched: args.enrich === true,
|
|
244
|
+
auto: args.auto === true,
|
|
245
|
+
...(hasScope ? { scope: scopeFields } : {}),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
if (args.showSimilar) {
|
|
249
|
+
const similar = await fetchSimilarMemories((body ?? args.content ?? "").slice(0, 500), result.ref);
|
|
250
|
+
output("remember", { ok: true, ...result, similar });
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
output("remember", { ok: true, ...result });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
});
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Memory-specific helpers for `akm remember`.
|
|
3
6
|
*
|
|
@@ -5,9 +8,9 @@
|
|
|
5
8
|
* heuristic derivation, LLM enrichment) is testable in isolation and the
|
|
6
9
|
* CLI entry point stays focused on argument parsing + output routing.
|
|
7
10
|
*/
|
|
8
|
-
import {
|
|
11
|
+
import { serializeFrontmatter } from "../core/asset-serialize";
|
|
9
12
|
import { toErrorMessage, tryReadStdinText } from "../core/common";
|
|
10
|
-
import { loadConfig } from "../core/config";
|
|
13
|
+
import { getDefaultLlmConfig, loadConfig } from "../core/config";
|
|
11
14
|
import { UsageError } from "../core/errors";
|
|
12
15
|
import { warn } from "../core/warn";
|
|
13
16
|
import { SCOPE_KEYS } from "../indexer/metadata";
|
|
@@ -54,6 +57,12 @@ export function buildMemoryFrontmatter(fields) {
|
|
|
54
57
|
obj.expires = fields.expires;
|
|
55
58
|
if (fields.subjective)
|
|
56
59
|
obj.subjective = true;
|
|
60
|
+
if (fields.captureMode === "hot" || fields.captureMode === "background") {
|
|
61
|
+
obj.captureMode = fields.captureMode;
|
|
62
|
+
}
|
|
63
|
+
if (typeof fields.beliefState === "string" && fields.beliefState.trim()) {
|
|
64
|
+
obj.beliefState = fields.beliefState.trim();
|
|
65
|
+
}
|
|
57
66
|
// Scope keys are emitted as flat top-level keys (`scope_user`, …) so the
|
|
58
67
|
// existing one-level frontmatter parser can read them without nesting.
|
|
59
68
|
// A scope object with no populated values is dropped.
|
|
@@ -69,7 +78,7 @@ export function buildMemoryFrontmatter(fields) {
|
|
|
69
78
|
// produce `---\n{}\n---` (the YAML serializer's empty-object form).
|
|
70
79
|
if (Object.keys(obj).length === 0)
|
|
71
80
|
return "---\n---";
|
|
72
|
-
const serialized =
|
|
81
|
+
const serialized = serializeFrontmatter(obj);
|
|
73
82
|
return `---\n${serialized}\n---`;
|
|
74
83
|
}
|
|
75
84
|
/**
|
|
@@ -134,11 +143,11 @@ const LLM_ENRICH_TIMEOUT_MS = 10_000;
|
|
|
134
143
|
*/
|
|
135
144
|
export async function runLlmEnrich(body) {
|
|
136
145
|
const config = loadConfig();
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
const llmConfig = getDefaultLlmConfig(config);
|
|
147
|
+
if (!llmConfig) {
|
|
148
|
+
warn("Warning: --enrich requires an LLM to be configured. Run `akm setup` to configure one.");
|
|
139
149
|
return { tags: [] };
|
|
140
150
|
}
|
|
141
|
-
const llmConfig = config.llm;
|
|
142
151
|
const { chatCompletion, parseEmbeddedJsonResponse: parseJsonResponse } = await import("../llm/client");
|
|
143
152
|
const prompt = `You are a memory tagger for a developer knowledge base.
|
|
144
153
|
Given the memory text below, return ONLY a JSON object with these fields:
|
|
@@ -1,3 +1,6 @@
|
|
|
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/.
|
|
1
4
|
/**
|
|
2
5
|
* Schema-repair pass for `akm improve`.
|
|
3
6
|
*
|
|
@@ -10,16 +13,27 @@
|
|
|
10
13
|
* `chatCompletion`.
|
|
11
14
|
*/
|
|
12
15
|
import fs from "node:fs";
|
|
13
|
-
import
|
|
16
|
+
import path from "node:path";
|
|
14
17
|
import { parseAssetRef } from "../core/asset-ref";
|
|
18
|
+
import { assembleAsset } from "../core/asset-serialize";
|
|
15
19
|
import { appendEvent, readEvents } from "../core/events";
|
|
16
20
|
import { parseFrontmatter } from "../core/frontmatter";
|
|
17
|
-
import {
|
|
21
|
+
import { createProposal, isProposalSkipped } from "../core/proposals";
|
|
22
|
+
import { info, warn } from "../core/warn";
|
|
18
23
|
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
19
24
|
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
20
25
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
26
|
/** Minimum gap between schema-repair attempts on the same asset. */
|
|
22
27
|
const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
28
|
+
/**
|
|
29
|
+
* Per-ref attempt cap (O-6 / #379): maximum number of schema-repair attempts
|
|
30
|
+
* allowed within SCHEMA_REPAIR_WINDOW_MS. Prevents indefinite nightly re-repair
|
|
31
|
+
* of assets whose source content is genuinely ambiguous or inconsistently
|
|
32
|
+
* structured. After cap, the asset is skipped until the window rolls over.
|
|
33
|
+
* Self-Refine arXiv:2303.17651 — iteration must be bounded.
|
|
34
|
+
*/
|
|
35
|
+
const SCHEMA_REPAIR_MAX_ATTEMPTS = 3;
|
|
36
|
+
const SCHEMA_REPAIR_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
23
37
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
24
38
|
/**
|
|
25
39
|
* Run the schema-repair loop for a batch of validation failures.
|
|
@@ -29,7 +43,7 @@ const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
|
29
43
|
export async function runSchemaRepairPass(failures, options) {
|
|
30
44
|
const repairs = [];
|
|
31
45
|
const repairedRefs = new Set();
|
|
32
|
-
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, } = options;
|
|
46
|
+
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn = chatCompletion, } = options;
|
|
33
47
|
for (const failure of failures) {
|
|
34
48
|
if (Date.now() - startMs >= budgetMs)
|
|
35
49
|
break;
|
|
@@ -42,11 +56,29 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
42
56
|
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
43
57
|
continue;
|
|
44
58
|
}
|
|
59
|
+
// O-6 / #379: Cap total attempts at SCHEMA_REPAIR_MAX_ATTEMPTS per SCHEMA_REPAIR_WINDOW_MS.
|
|
60
|
+
// Prevents indefinite nightly re-repair of assets whose source is genuinely ambiguous.
|
|
61
|
+
// After the cap is reached, the asset is skipped until the window rolls over.
|
|
62
|
+
const windowStart = Date.now() - SCHEMA_REPAIR_WINDOW_MS;
|
|
63
|
+
const attemptsInWindow = recentRepairs.events.filter((e) => e.ts !== undefined && new Date(e.ts).getTime() >= windowStart).length;
|
|
64
|
+
if (attemptsInWindow >= SCHEMA_REPAIR_MAX_ATTEMPTS) {
|
|
65
|
+
repairs.push({
|
|
66
|
+
ref: failure.ref,
|
|
67
|
+
reason: failure.reason,
|
|
68
|
+
outcome: "skipped",
|
|
69
|
+
error: `schema-repair attempt cap reached (${attemptsInWindow}/${SCHEMA_REPAIR_MAX_ATTEMPTS} in 30d window)`,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
45
73
|
const filePath = await findFilePath(failure.ref, stashDir);
|
|
46
74
|
if (!filePath) {
|
|
47
75
|
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
48
76
|
continue;
|
|
49
77
|
}
|
|
78
|
+
if (path.extname(filePath).toLowerCase() !== ".md") {
|
|
79
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
50
82
|
try {
|
|
51
83
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
84
|
const fm = parseFrontmatter(raw);
|
|
@@ -62,7 +94,7 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
62
94
|
const fieldList = missingFields.join(" and ");
|
|
63
95
|
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
64
96
|
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
65
|
-
const llmResponse = await
|
|
97
|
+
const llmResponse = await chatFn(llmConfig, [
|
|
66
98
|
{
|
|
67
99
|
role: "system",
|
|
68
100
|
content: `You generate concise asset frontmatter fields. Respond with a JSON object containing only the missing fields. No prose, no markdown fences.`,
|
|
@@ -87,17 +119,58 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
87
119
|
newFm.description = parsed.description;
|
|
88
120
|
if (parsed.when_to_use)
|
|
89
121
|
newFm.when_to_use = parsed.when_to_use;
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
const newContent = assembleAsset(newFm, fm.content);
|
|
123
|
+
// M-3 / #387: Route through proposal queue instead of writing directly to
|
|
124
|
+
// disk. This restores akm's safety invariant — the proposal queue is the
|
|
125
|
+
// only path to a committed asset write. LLM-generated `description` /
|
|
126
|
+
// `when_to_use` fields can be incorrect; routing through the queue makes
|
|
127
|
+
// them human-reviewable before they affect search ranking and curate hints.
|
|
128
|
+
// mem0 open gaps (arXiv:2504.19413) — any LLM write to a memory field
|
|
129
|
+
// should be human-reviewable.
|
|
130
|
+
if (stashDir) {
|
|
131
|
+
const proposalResult = createProposal(stashDir, {
|
|
132
|
+
ref: failure.ref,
|
|
133
|
+
source: "schema-repair",
|
|
134
|
+
payload: {
|
|
135
|
+
content: newContent,
|
|
136
|
+
...(Object.keys(newFm).length > 0 ? { frontmatter: newFm } : {}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
if (isProposalSkipped(proposalResult)) {
|
|
140
|
+
info(`[improve] schema-repair proposal skipped for ${failure.ref}: ${proposalResult.message}`);
|
|
141
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
|
|
145
|
+
appendEvent({
|
|
146
|
+
eventType: "schema_repair_invoked",
|
|
147
|
+
ref: failure.ref,
|
|
148
|
+
metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
|
|
149
|
+
});
|
|
150
|
+
repairs.push({
|
|
151
|
+
ref: failure.ref,
|
|
152
|
+
reason: failure.reason,
|
|
153
|
+
outcome: "queued",
|
|
154
|
+
proposalId: proposalResult.id,
|
|
155
|
+
});
|
|
156
|
+
// Mark as repaired so the caller removes it from the validation-failure set.
|
|
157
|
+
repairedRefs.add(failure.ref);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Fallback: no stash dir available — write directly (legacy path).
|
|
161
|
+
// This should not occur in production; stashDir is always provided by
|
|
162
|
+
// `runSchemaRepairPass` callers in improve.ts.
|
|
163
|
+
warn(`[improve] schema-repair: no stashDir available for ${failure.ref}, falling back to direct write`);
|
|
164
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
165
|
+
info(`[improve] schema-repair written: ${failure.ref}`);
|
|
166
|
+
appendEvent({
|
|
167
|
+
eventType: "schema_repair_invoked",
|
|
168
|
+
ref: failure.ref,
|
|
169
|
+
metadata: { outcome: "written", reason: failure.reason },
|
|
170
|
+
});
|
|
171
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
|
|
172
|
+
repairedRefs.add(failure.ref);
|
|
173
|
+
}
|
|
101
174
|
}
|
|
102
175
|
catch (e) {
|
|
103
176
|
appendEvent({
|