akm-cli 0.9.0-beta.50 → 0.9.0-beta.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -0
- package/README.md +12 -4
- package/dist/akm +38 -0
- package/dist/akm-migrate-storage +38 -0
- package/dist/assets/wiki/ingest-workflow-template.md +27 -6
- package/dist/cli/parse-args.js +46 -1
- package/dist/cli.js +12 -6
- package/dist/commands/config-cli.js +18 -2
- package/dist/commands/env/child-env.js +47 -0
- package/dist/commands/env/env-cli.js +17 -2
- package/dist/commands/env/secret-cli.js +24 -2
- package/dist/commands/health/checks.js +1 -1
- package/dist/commands/improve/improve-auto-accept.js +30 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-result-file.js +9 -2
- package/dist/commands/improve/recombine.js +52 -15
- package/dist/commands/lint/env-key-rules.js +4 -0
- package/dist/commands/read/knowledge.js +5 -2
- package/dist/commands/read/search-cli.js +2 -4
- package/dist/commands/read/search.js +9 -6
- package/dist/commands/read/show.js +19 -5
- package/dist/commands/sources/init.js +13 -8
- package/dist/commands/sources/installed-stashes.js +6 -2
- package/dist/commands/sources/schema-repair.js +33 -47
- package/dist/commands/sources/source-add.js +7 -3
- package/dist/commands/tasks/tasks.js +38 -10
- package/dist/core/asset/asset-registry.js +1 -1
- package/dist/core/asset/asset-spec.js +4 -2
- package/dist/core/config/config-migration.js +12 -11
- package/dist/indexer/passes/memory-inference.js +3 -2
- package/dist/indexer/search/db-search.js +6 -4
- package/dist/indexer/search/search-source.js +15 -2
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/llm/memory-infer-impl.js +138 -0
- package/dist/llm/memory-infer.js +1 -135
- package/dist/migrate-storage-node.mjs +8 -0
- package/dist/output/renderers.js +1 -1
- package/dist/scripts/migrate-storage.js +463 -347
- package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +99 -99
- package/dist/sources/include.js +6 -2
- package/dist/sources/providers/git-install.js +10 -6
- package/dist/sources/providers/provider-utils.js +13 -7
- package/dist/sources/providers/website.js +8 -3
- package/dist/sources/website-ingest.js +136 -20
- package/dist/text-import-hook.mjs +0 -0
- package/docs/data-and-telemetry.md +2 -2
- package/docs/migration/release-notes/0.9.0.md +39 -0
- package/package.json +8 -8
|
@@ -51,7 +51,7 @@ 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";
|
|
53
53
|
import { chatCompletion } from "../../llm/client.js";
|
|
54
|
-
import { validateProposalFrontmatter } from "../proposal/validators/proposal-quality-validators.js";
|
|
54
|
+
import { isValidDescription, isValidWhenToUse, validateProposalFrontmatter, } from "../proposal/validators/proposal-quality-validators.js";
|
|
55
55
|
import { archiveProposal, createProposal, isProposalSkipped, listProposals } from "../proposal/validators/proposals.js";
|
|
56
56
|
import { isConsolidationEligibleMemoryName, isSessionCaptureMemoryName } from "./consolidate.js";
|
|
57
57
|
const RECOMBINE_SYSTEM_PROMPT = recombineSystemPrompt;
|
|
@@ -438,6 +438,20 @@ export function deriveRecombineLessonRef(cluster) {
|
|
|
438
438
|
const hash = createHash("sha256").update(memberKey, "utf8").digest("hex").slice(0, 8);
|
|
439
439
|
return `lesson:recombined/${slug || "cluster"}-${hash}`;
|
|
440
440
|
}
|
|
441
|
+
function validatePromotedLessonFrontmatter(ref, frontmatter) {
|
|
442
|
+
const descCheck = isValidDescription(frontmatter.description, ref);
|
|
443
|
+
if (!descCheck.ok)
|
|
444
|
+
return { ok: false, reason: descCheck.reason };
|
|
445
|
+
const whenToUseCheck = isValidWhenToUse(frontmatter.when_to_use, ref);
|
|
446
|
+
if (!whenToUseCheck.ok)
|
|
447
|
+
return { ok: false, reason: whenToUseCheck.reason };
|
|
448
|
+
if (typeof frontmatter.description === "string" &&
|
|
449
|
+
typeof frontmatter.when_to_use === "string" &&
|
|
450
|
+
frontmatter.description.trim().toLowerCase() === frontmatter.when_to_use.trim().toLowerCase()) {
|
|
451
|
+
return { ok: false, reason: "description and when_to_use are identical" };
|
|
452
|
+
}
|
|
453
|
+
return { ok: true };
|
|
454
|
+
}
|
|
441
455
|
/**
|
|
442
456
|
* The membership fingerprint of a cluster: its member entryKeys sorted and
|
|
443
457
|
* joined. Single source of truth shared by {@link deriveRecombineLessonRef}'s
|
|
@@ -652,9 +666,18 @@ export async function akmRecombine(opts) {
|
|
|
652
666
|
: undefined;
|
|
653
667
|
const lessonRef = matchedRow?.hypothesis_ref ?? derivedRef;
|
|
654
668
|
const sourceRefs = cluster.members.map((m) => `memory:${m.entry.name}`);
|
|
669
|
+
const priorRow = stateDb ? getRecombineHypothesis(stateDb, lessonRef) : undefined;
|
|
670
|
+
const alreadyPromoted = priorRow?.promoted_at != null;
|
|
671
|
+
const nextCount = stateDb == null
|
|
672
|
+
? 0
|
|
673
|
+
: priorRow == null
|
|
674
|
+
? 1
|
|
675
|
+
: priorRow.last_run === sourceRun
|
|
676
|
+
? priorRow.consecutive_count
|
|
677
|
+
: priorRow.consecutive_count + 1;
|
|
655
678
|
// Quality gate (always-run): the frontmatter description must be present
|
|
656
|
-
// and non-truncated.
|
|
657
|
-
//
|
|
679
|
+
// and non-truncated. Promotion adds the full lesson frontmatter check so
|
|
680
|
+
// `when_to_use` never bypasses validation on the promote=true path.
|
|
658
681
|
const fmCheck = validateProposalFrontmatter({ description: generalization.description });
|
|
659
682
|
if (!fmCheck.ok) {
|
|
660
683
|
appendEvent({
|
|
@@ -670,29 +693,43 @@ export async function akmRecombine(opts) {
|
|
|
670
693
|
}, opts.ctx);
|
|
671
694
|
continue;
|
|
672
695
|
}
|
|
696
|
+
const promote = stateDb != null && !alreadyPromoted && nextCount >= confirmThreshold;
|
|
697
|
+
if (promote) {
|
|
698
|
+
const lessonFmCheck = validatePromotedLessonFrontmatter(lessonRef, {
|
|
699
|
+
description: generalization.description,
|
|
700
|
+
when_to_use: generalization.when_to_use,
|
|
701
|
+
});
|
|
702
|
+
if (!lessonFmCheck.ok) {
|
|
703
|
+
appendEvent({
|
|
704
|
+
eventType: "recombine_invoked",
|
|
705
|
+
ref: lessonRef,
|
|
706
|
+
metadata: {
|
|
707
|
+
signal: cluster.signature,
|
|
708
|
+
memberCount: cluster.members.length,
|
|
709
|
+
outcome: "quality_rejected",
|
|
710
|
+
reason: lessonFmCheck.reason,
|
|
711
|
+
sourceRun,
|
|
712
|
+
},
|
|
713
|
+
}, opts.ctx);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
673
717
|
// A defensible generalization was produced this run — record it so it is
|
|
674
718
|
// NOT decayed by the unseen sweep below.
|
|
675
719
|
seenThisRun.add(lessonRef);
|
|
676
|
-
// #625/#633 — record the re-induction
|
|
677
|
-
//
|
|
678
|
-
// derived member-set ref (first/non-overlapping induction). The induction
|
|
679
|
-
// refreshes the row's `member_key` to the current membership so the
|
|
680
|
-
// overlap window slides with the drifting cluster.
|
|
681
|
-
const nowIso = new Date().toISOString();
|
|
682
|
-
const priorRow = stateDb ? getRecombineHypothesis(stateDb, lessonRef) : undefined;
|
|
683
|
-
const alreadyPromoted = priorRow?.promoted_at != null;
|
|
720
|
+
// #625/#633 — record the re-induction only AFTER the quality gate passed.
|
|
721
|
+
// Quality-rejected outputs must not advance the confirmation streak.
|
|
684
722
|
const count = stateDb
|
|
685
723
|
? recordRecombineInduction(stateDb, {
|
|
686
724
|
hypothesisRef: lessonRef,
|
|
687
725
|
signature: cluster.signature,
|
|
688
726
|
memberKey,
|
|
689
|
-
seenAt:
|
|
727
|
+
seenAt: new Date().toISOString(),
|
|
690
728
|
run: sourceRun,
|
|
691
729
|
})
|
|
692
730
|
: 0;
|
|
693
731
|
// Promote to a `type: lesson` proposal when the confirmation streak
|
|
694
732
|
// reaches the threshold AND the hypothesis has not already been promoted.
|
|
695
|
-
const promote = stateDb != null && !alreadyPromoted && count >= confirmThreshold;
|
|
696
733
|
const proposalType = promote ? "lesson" : "hypothesis";
|
|
697
734
|
const frontmatter = {
|
|
698
735
|
type: proposalType,
|
|
@@ -716,7 +753,7 @@ export async function akmRecombine(opts) {
|
|
|
716
753
|
ref: lessonRef,
|
|
717
754
|
source: "recombine",
|
|
718
755
|
sourceRun,
|
|
719
|
-
payload: { content, frontmatter
|
|
756
|
+
payload: { content, frontmatter },
|
|
720
757
|
eligibilitySource,
|
|
721
758
|
// The promotion is a distinct asset (lesson) for the same ref; force
|
|
722
759
|
// past the duplicate-pending guard (the stale hypothesis was just
|
|
@@ -738,7 +775,7 @@ export async function akmRecombine(opts) {
|
|
|
738
775
|
continue;
|
|
739
776
|
}
|
|
740
777
|
if (promote && stateDb) {
|
|
741
|
-
markRecombineHypothesisPromoted(stateDb, lessonRef,
|
|
778
|
+
markRecombineHypothesisPromoted(stateDb, lessonRef, new Date().toISOString());
|
|
742
779
|
lessonsPromoted += 1;
|
|
743
780
|
appendEvent({
|
|
744
781
|
eventType: "recombine_invoked",
|
|
@@ -96,6 +96,10 @@ export const DANGEROUS_VAULT_KEY_PATTERNS = [
|
|
|
96
96
|
pattern: /^BASH_FUNC_/,
|
|
97
97
|
reason: "Shellshock-class bash function injection (CVE-2014-6271)",
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
pattern: /^GIT_CONFIG_/,
|
|
101
|
+
reason: "Git config injection through environment override variables",
|
|
102
|
+
},
|
|
99
103
|
];
|
|
100
104
|
/**
|
|
101
105
|
* Returns `true` if the given key name is dangerous — either by literal match
|
|
@@ -16,7 +16,7 @@ import { isHttpUrl, isWithin, tryReadStdinText } from "../../core/common.js";
|
|
|
16
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
17
|
import { UsageError } from "../../core/errors.js";
|
|
18
18
|
import { commitWriteTargetBoundary, formatRefForMessage, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
19
|
-
import { fetchWebsiteMarkdownSnapshot } from "../../sources/website-ingest.js";
|
|
19
|
+
import { fetchWebsiteMarkdownSnapshot, shouldAllowPrivateWebsiteUrlForTests } from "../../sources/website-ingest.js";
|
|
20
20
|
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
21
21
|
// ── Asset-name normalisation ─────────────────────────────────────────────────
|
|
22
22
|
/**
|
|
@@ -104,7 +104,10 @@ export function readKnowledgeContent(source) {
|
|
|
104
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, {
|
|
108
|
+
stashDir: options?.stashDir,
|
|
109
|
+
allowPrivateHosts: options?.allowPrivateHosts ?? shouldAllowPrivateWebsiteUrlForTests(source),
|
|
110
|
+
});
|
|
108
111
|
return { content: snapshot.content, preferredName: snapshot.preferredName };
|
|
109
112
|
}
|
|
110
113
|
// ── Asset writing ────────────────────────────────────────────────────────────
|
|
@@ -85,10 +85,6 @@ export const searchCommand = defineJsonCommand({
|
|
|
85
85
|
const belief = parseBeliefFilterMode(typeof args.belief === "string" ? args.belief : undefined);
|
|
86
86
|
const noProjectContext = getHyphenatedBoolean(args, "no-project-context");
|
|
87
87
|
const includeSessions = getHyphenatedBoolean(args, "include-sessions");
|
|
88
|
-
// --no-project-context sets env so searchDatabase picks it up without
|
|
89
|
-
// threading the flag through the entire call stack.
|
|
90
|
-
if (noProjectContext)
|
|
91
|
-
process.env.AKM_DISABLE_PROJECT_CONTEXT = "1";
|
|
92
88
|
const result = await akmSearch({
|
|
93
89
|
query,
|
|
94
90
|
type,
|
|
@@ -98,6 +94,8 @@ export const searchCommand = defineJsonCommand({
|
|
|
98
94
|
includeProposed,
|
|
99
95
|
belief,
|
|
100
96
|
includeSessions,
|
|
97
|
+
disableProjectContext: noProjectContext,
|
|
98
|
+
disableScopedUtility: noProjectContext,
|
|
101
99
|
eventSource: resolveEventSource(),
|
|
102
100
|
});
|
|
103
101
|
output("search", result);
|
|
@@ -109,6 +109,8 @@ export async function akmSearch(input) {
|
|
|
109
109
|
// would leak hits from sources the caller did not request.
|
|
110
110
|
restrictToSources: namedSourceName !== undefined,
|
|
111
111
|
includeExcludedTypes: input.includeSessions === true,
|
|
112
|
+
disableProjectContext: input.disableProjectContext === true,
|
|
113
|
+
disableScopedUtility: input.disableScopedUtility === true,
|
|
112
114
|
});
|
|
113
115
|
const registryResult = source === "stash" ? undefined : await searchRegistry(query, { limit, registries: config.registries });
|
|
114
116
|
if (source === "stash") {
|
|
@@ -123,8 +125,9 @@ export async function akmSearch(input) {
|
|
|
123
125
|
warnings: localResult?.warnings?.length ? localResult.warnings : undefined,
|
|
124
126
|
timing: { totalMs: Date.now() - t0, rankMs: localResult?.rankMs, embedMs: localResult?.embedMs },
|
|
125
127
|
};
|
|
126
|
-
if (!input.skipLogging)
|
|
127
|
-
logSearchEvent(query, response, localResult?.mode ?? "keyword", input.eventSource);
|
|
128
|
+
if (!input.skipLogging) {
|
|
129
|
+
logSearchEvent(query, response, localResult?.mode ?? "keyword", input.eventSource, input.disableScopedUtility === true);
|
|
130
|
+
}
|
|
128
131
|
return response;
|
|
129
132
|
}
|
|
130
133
|
const registryHits = (registryResult?.hits ?? []).map((hit) => {
|
|
@@ -159,7 +162,7 @@ export async function akmSearch(input) {
|
|
|
159
162
|
timing: { totalMs: Date.now() - t0 },
|
|
160
163
|
};
|
|
161
164
|
if (!input.skipLogging)
|
|
162
|
-
logSearchEvent(query, response, undefined, input.eventSource);
|
|
165
|
+
logSearchEvent(query, response, undefined, input.eventSource, input.disableScopedUtility === true);
|
|
163
166
|
return response;
|
|
164
167
|
}
|
|
165
168
|
// source === "both"
|
|
@@ -177,7 +180,7 @@ export async function akmSearch(input) {
|
|
|
177
180
|
timing: { totalMs: Date.now() - t0 },
|
|
178
181
|
};
|
|
179
182
|
if (!input.skipLogging)
|
|
180
|
-
logSearchEvent(query, response, undefined, input.eventSource);
|
|
183
|
+
logSearchEvent(query, response, undefined, input.eventSource, input.disableScopedUtility === true);
|
|
181
184
|
return response;
|
|
182
185
|
}
|
|
183
186
|
/**
|
|
@@ -212,7 +215,7 @@ function resolveEntryIds(db, hits) {
|
|
|
212
215
|
* Per-entry events are recorded only for stash hits because registry hits
|
|
213
216
|
* have no local entry_id to reference.
|
|
214
217
|
*/
|
|
215
|
-
function logSearchEvent(query, response, mode = "keyword", eventSource = "user") {
|
|
218
|
+
function logSearchEvent(query, response, mode = "keyword", eventSource = "user", disableScopedUtility = false) {
|
|
216
219
|
// Emit a structured event to events.jsonl so workflow-trace consumers
|
|
217
220
|
// detect akm search invocations without relying on stdout scraping.
|
|
218
221
|
const stashHits = response.hits.filter((h) => h.type !== "registry");
|
|
@@ -242,7 +245,7 @@ function logSearchEvent(query, response, mode = "keyword", eventSource = "user")
|
|
|
242
245
|
let scopeKey;
|
|
243
246
|
try {
|
|
244
247
|
const stashPath = response.stashDir;
|
|
245
|
-
const disabled =
|
|
248
|
+
const disabled = disableScopedUtility || (stashPath && isTransientStashPath(stashPath));
|
|
246
249
|
scopeKey = disabled ? undefined : getCurrentWorkflowScopeKey();
|
|
247
250
|
}
|
|
248
251
|
catch {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
import { findCittyTopLevelCommandIndex } from "../../cli/parse-args.js";
|
|
21
22
|
import { parseAssetRef } from "../../core/asset/asset-ref.js";
|
|
22
23
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
23
24
|
import { META_DIR, parseMetaRef, resolveMetaFilePath } from "../../core/asset/stash-meta.js";
|
|
@@ -523,6 +524,14 @@ function buildSummaryResponse(full, assetPath) {
|
|
|
523
524
|
}
|
|
524
525
|
// ── argv normalisation ───────────────────────────────────────────────────────
|
|
525
526
|
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
527
|
+
const SHOW_ARGV_TOP_LEVEL_ARGS = {
|
|
528
|
+
format: { type: "string" },
|
|
529
|
+
output: { type: "string" },
|
|
530
|
+
detail: { type: "string" },
|
|
531
|
+
shape: { type: "string" },
|
|
532
|
+
quiet: { type: "boolean", alias: "q" },
|
|
533
|
+
verbose: { type: "boolean" },
|
|
534
|
+
};
|
|
526
535
|
/**
|
|
527
536
|
* Normalize argv so positional view-mode arguments after the asset ref
|
|
528
537
|
* are rewritten into internal flags that citty can parse.
|
|
@@ -536,15 +545,20 @@ const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines
|
|
|
536
545
|
* Returns a new array; the input is never modified.
|
|
537
546
|
*/
|
|
538
547
|
export function normalizeShowArgv(argv) {
|
|
539
|
-
|
|
540
|
-
|
|
548
|
+
const rawArgs = argv.slice(2);
|
|
549
|
+
const commandIndex = findCittyTopLevelCommandIndex(rawArgs, SHOW_ARGV_TOP_LEVEL_ARGS);
|
|
550
|
+
if (commandIndex < 0 || rawArgs[commandIndex] !== "show")
|
|
541
551
|
return argv;
|
|
542
|
-
|
|
552
|
+
const commandArgs = rawArgs.slice(commandIndex + 1);
|
|
553
|
+
if (commandArgs.includes("--view") ||
|
|
554
|
+
commandArgs.includes("--heading") ||
|
|
555
|
+
commandArgs.includes("--start") ||
|
|
556
|
+
commandArgs.includes("--end")) {
|
|
543
557
|
throw new UsageError('Legacy show flags are no longer supported. Use positional syntax like `akm show knowledge:guide toc` or `akm show knowledge:guide section "Auth"`.');
|
|
544
558
|
}
|
|
545
559
|
// Separate global flags from positional/show-specific args
|
|
546
|
-
const prefix = argv.slice(0,
|
|
547
|
-
const rest =
|
|
560
|
+
const prefix = [...argv.slice(0, 2), ...rawArgs.slice(0, commandIndex + 1)];
|
|
561
|
+
const rest = commandArgs;
|
|
548
562
|
const globalFlags = [];
|
|
549
563
|
const showArgs = [];
|
|
550
564
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -37,7 +37,7 @@ import { copyStashSkeleton, scaffoldStashMeta } from "./stash-skeleton.js";
|
|
|
37
37
|
function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
38
38
|
if (!dirExplicitlyProvided)
|
|
39
39
|
return; // Only guard explicit --dir, not default HOME resolution.
|
|
40
|
-
const isUnderTest =
|
|
40
|
+
const isUnderTest = isUnderTestRunner();
|
|
41
41
|
if (!isUnderTest)
|
|
42
42
|
return;
|
|
43
43
|
if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
|
|
@@ -52,6 +52,9 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
|
|
|
52
52
|
return;
|
|
53
53
|
throw new ConfigError(`refusing to persist --dir stashDir to a temporary path while under test runner; set AKM_FORCE_INIT_TMP_STASH=1 if you really mean it (stashDir=${stashDir})`, "INIT_TMP_STASH_REFUSED");
|
|
54
54
|
}
|
|
55
|
+
function isUnderTestRunner() {
|
|
56
|
+
return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
|
|
57
|
+
}
|
|
55
58
|
export async function akmInit(options) {
|
|
56
59
|
const dirExplicitlyProvided = options?.dir != null;
|
|
57
60
|
const setDefault = options?.setDefault === true;
|
|
@@ -114,13 +117,15 @@ export async function akmInit(options) {
|
|
|
114
117
|
}
|
|
115
118
|
// Ensure ripgrep is available (install to cache/bin if needed)
|
|
116
119
|
let ripgrep;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
if (!isUnderTestRunner()) {
|
|
121
|
+
try {
|
|
122
|
+
const binDir = getBinDir();
|
|
123
|
+
const rgResult = ensureRg(binDir);
|
|
124
|
+
ripgrep = rgResult;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Non-fatal: ripgrep is optional, search works without it
|
|
128
|
+
}
|
|
124
129
|
}
|
|
125
130
|
return { stashDir, created, configPath, defaultStashUpdated, previousStashDir, ripgrep };
|
|
126
131
|
}
|
|
@@ -17,7 +17,7 @@ import { removeLockEntry, upsertLockEntry } from "../../integrations/lockfile.js
|
|
|
17
17
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
18
18
|
import { parseGitRepoUrl, syncMirroredRepo } from "../../sources/providers/git.js";
|
|
19
19
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
20
|
-
import { ensureWebsiteMirror } from "../../sources/website-ingest.js";
|
|
20
|
+
import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests } from "../../sources/website-ingest.js";
|
|
21
21
|
import { listWikis, resolveWikisRoot } from "../../wiki/wiki.js";
|
|
22
22
|
import { removeInstalledRegistryEntry, upsertInstalledRegistryEntry } from "./source-add.js";
|
|
23
23
|
import { removeStash } from "./source-manage.js";
|
|
@@ -194,7 +194,11 @@ async function updateGitSource(stashDir, target, all, gitSource) {
|
|
|
194
194
|
/** Re-crawl a website source and return an UpdateResponse. */
|
|
195
195
|
async function updateWebsiteSource(stashDir, target, all, websiteSource) {
|
|
196
196
|
// TODO: full incremental re-crawl with delta tracking (#19)
|
|
197
|
-
await ensureWebsiteMirror(websiteSource, {
|
|
197
|
+
await ensureWebsiteMirror(websiteSource, {
|
|
198
|
+
requireStashDir: true,
|
|
199
|
+
force: true,
|
|
200
|
+
...(shouldAllowPrivateWebsiteUrlForTests(websiteSource.url ?? "") ? { allowPrivateHosts: true } : {}),
|
|
201
|
+
});
|
|
198
202
|
return buildUpdateResponse(stashDir, target, all, []);
|
|
199
203
|
}
|
|
200
204
|
/** Sync a single installed registry entry and return the processed record. */
|
|
@@ -20,7 +20,7 @@ import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
|
20
20
|
import { authoringRulesForType } from "../../core/authoring-rules.js";
|
|
21
21
|
import { appendEvent, readEvents } from "../../core/events.js";
|
|
22
22
|
import { resolveStandardsContext } from "../../core/standards/resolve-standards-context.js";
|
|
23
|
-
import { info
|
|
23
|
+
import { info } from "../../core/warn.js";
|
|
24
24
|
import { resolveAssetPath } from "../../indexer/walk/path-resolver.js";
|
|
25
25
|
import { chatCompletion, parseEmbeddedJsonResponse } from "../../llm/client.js";
|
|
26
26
|
import { createProposal, isProposalSkipped } from "../proposal/validators/proposals.js";
|
|
@@ -46,6 +46,9 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
46
46
|
const repairs = [];
|
|
47
47
|
const repairedRefs = new Set();
|
|
48
48
|
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, chatFn = chatCompletion, } = options;
|
|
49
|
+
if (!stashDir) {
|
|
50
|
+
throw new Error("runSchemaRepairPass requires stashDir so repairs route through the proposal queue");
|
|
51
|
+
}
|
|
49
52
|
for (const failure of failures) {
|
|
50
53
|
if (Date.now() - startMs >= budgetMs)
|
|
51
54
|
break;
|
|
@@ -97,9 +100,9 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
97
100
|
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
98
101
|
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
99
102
|
// Standards "rulebook" for this target — wiki schema (wiki page) or stash
|
|
100
|
-
// convention/meta facts (non-wiki asset)
|
|
101
|
-
//
|
|
102
|
-
const standardsContext =
|
|
103
|
+
// convention/meta facts (non-wiki asset). `resolveStandardsContext`
|
|
104
|
+
// dispatches on the ref.
|
|
105
|
+
const standardsContext = resolveStandardsContext(failure.ref, stashDir);
|
|
103
106
|
const standardsSection = standardsContext.trim()
|
|
104
107
|
? `\n\nStandards to follow (the rulebook for this target):\n${standardsContext.trim()}`
|
|
105
108
|
: "";
|
|
@@ -139,50 +142,33 @@ export async function runSchemaRepairPass(failures, options) {
|
|
|
139
142
|
// them human-reviewable before they affect search ranking and curate hints.
|
|
140
143
|
// mem0 open gaps (arXiv:2504.19413) — any LLM write to a memory field
|
|
141
144
|
// should be human-reviewable.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
|
|
157
|
-
appendEvent({
|
|
158
|
-
eventType: "schema_repair_invoked",
|
|
159
|
-
ref: failure.ref,
|
|
160
|
-
metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
|
|
161
|
-
});
|
|
162
|
-
repairs.push({
|
|
163
|
-
ref: failure.ref,
|
|
164
|
-
reason: failure.reason,
|
|
165
|
-
outcome: "queued",
|
|
166
|
-
proposalId: proposalResult.id,
|
|
167
|
-
});
|
|
168
|
-
// Mark as repaired so the caller removes it from the validation-failure set.
|
|
169
|
-
repairedRefs.add(failure.ref);
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
// Fallback: no stash dir available — write directly (legacy path).
|
|
173
|
-
// This should not occur in production; stashDir is always provided by
|
|
174
|
-
// `runSchemaRepairPass` callers in improve.ts.
|
|
175
|
-
warn(`[improve] schema-repair: no stashDir available for ${failure.ref}, falling back to direct write`);
|
|
176
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
177
|
-
info(`[improve] schema-repair written: ${failure.ref}`);
|
|
178
|
-
appendEvent({
|
|
179
|
-
eventType: "schema_repair_invoked",
|
|
180
|
-
ref: failure.ref,
|
|
181
|
-
metadata: { outcome: "written", reason: failure.reason },
|
|
182
|
-
});
|
|
183
|
-
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
|
|
184
|
-
repairedRefs.add(failure.ref);
|
|
145
|
+
const proposalResult = createProposal(stashDir, {
|
|
146
|
+
ref: failure.ref,
|
|
147
|
+
source: "schema-repair",
|
|
148
|
+
payload: {
|
|
149
|
+
content: newContent,
|
|
150
|
+
...(Object.keys(newFm).length > 0 ? { frontmatter: newFm } : {}),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (isProposalSkipped(proposalResult)) {
|
|
154
|
+
info(`[improve] schema-repair proposal skipped for ${failure.ref}: ${proposalResult.message}`);
|
|
155
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
156
|
+
continue;
|
|
185
157
|
}
|
|
158
|
+
info(`[improve] schema-repair queued: ${failure.ref} (proposal id: ${proposalResult.id})`);
|
|
159
|
+
appendEvent({
|
|
160
|
+
eventType: "schema_repair_invoked",
|
|
161
|
+
ref: failure.ref,
|
|
162
|
+
metadata: { outcome: "queued", reason: failure.reason, proposalId: proposalResult.id },
|
|
163
|
+
});
|
|
164
|
+
repairs.push({
|
|
165
|
+
ref: failure.ref,
|
|
166
|
+
reason: failure.reason,
|
|
167
|
+
outcome: "queued",
|
|
168
|
+
proposalId: proposalResult.id,
|
|
169
|
+
});
|
|
170
|
+
// Mark as repaired so the caller removes it from the validation-failure set.
|
|
171
|
+
repairedRefs.add(failure.ref);
|
|
186
172
|
}
|
|
187
173
|
catch (e) {
|
|
188
174
|
appendEvent({
|
|
@@ -11,7 +11,7 @@ import { upsertLockEntry } from "../../integrations/lockfile.js";
|
|
|
11
11
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
12
12
|
import { detectStashRoot } from "../../sources/providers/provider-utils.js";
|
|
13
13
|
import { syncFromRef } from "../../sources/providers/sync-from-ref.js";
|
|
14
|
-
import { ensureWebsiteMirror, validateWebsiteInputUrl } from "../../sources/website-ingest.js";
|
|
14
|
+
import { ensureWebsiteMirror, shouldAllowPrivateWebsiteUrlForTests, validateWebsiteInputUrl, } from "../../sources/website-ingest.js";
|
|
15
15
|
import { ensureWikiNameAvailable, validateWikiName } from "../../wiki/wiki.js";
|
|
16
16
|
const VALID_OVERRIDE_TYPES = new Set(["wiki"]);
|
|
17
17
|
export async function akmAdd(input) {
|
|
@@ -126,7 +126,8 @@ async function addLocalSource(ref, sourcePath, stashDir, wikiName, explicitName)
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
129
|
-
const
|
|
129
|
+
const allowPrivateHosts = shouldAllowPrivateWebsiteUrlForTests(ref);
|
|
130
|
+
const normalizedUrl = validateWebsiteInputUrl(ref, { allowPrivateHosts });
|
|
130
131
|
const config = loadUserConfig();
|
|
131
132
|
const sources = [...getSources(config)];
|
|
132
133
|
let entry = sources.find((stash) => stash.type === "website" && stash.url === normalizedUrl);
|
|
@@ -154,7 +155,10 @@ async function addWebsiteSource(ref, stashDir, name, options, wikiName) {
|
|
|
154
155
|
if (changed)
|
|
155
156
|
saveConfig({ ...config, sources });
|
|
156
157
|
}
|
|
157
|
-
const cachePaths = await ensureWebsiteMirror(entry, {
|
|
158
|
+
const cachePaths = await ensureWebsiteMirror(entry, {
|
|
159
|
+
requireStashDir: true,
|
|
160
|
+
...(allowPrivateHosts ? { allowPrivateHosts: true } : {}),
|
|
161
|
+
});
|
|
158
162
|
const index = await akmIndex({ stashDir });
|
|
159
163
|
const updatedConfig = loadConfig();
|
|
160
164
|
return {
|
|
@@ -16,6 +16,7 @@ import { isWithin, resolveStashDir } from "../../core/common.js";
|
|
|
16
16
|
import { loadConfig } from "../../core/config/config.js";
|
|
17
17
|
import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
|
|
18
18
|
import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
|
|
19
|
+
import { commitWriteTargetBoundary, deleteAssetFromSource, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
|
|
19
20
|
import { listAgentProfileNames } from "../../integrations/agent/index.js";
|
|
20
21
|
import { resolveAssetPath } from "../../sources/resolve.js";
|
|
21
22
|
import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
|
|
@@ -38,7 +39,8 @@ export async function akmTasksAdd(input) {
|
|
|
38
39
|
// Validate the schedule for the active backend before writing anything.
|
|
39
40
|
const backend = backendNameForPlatform();
|
|
40
41
|
parseSchedule(input.schedule, backend);
|
|
41
|
-
const
|
|
42
|
+
const target = resolveTaskWriteTarget();
|
|
43
|
+
const stashDir = target.source.path;
|
|
42
44
|
const typeRoot = path.join(stashDir, "tasks");
|
|
43
45
|
fs.mkdirSync(typeRoot, { recursive: true });
|
|
44
46
|
const assetPath = resolveAssetPathFromName("task", typeRoot, id);
|
|
@@ -64,7 +66,8 @@ export async function akmTasksAdd(input) {
|
|
|
64
66
|
});
|
|
65
67
|
const task = parseTaskDocument({ yaml, filePath: assetPath, id });
|
|
66
68
|
await validateTaskDocument(task, { backend, stashDir });
|
|
67
|
-
|
|
69
|
+
const ref = taskAssetRef(id);
|
|
70
|
+
await writeAssetToSource(target.source, target.config, ref, yaml);
|
|
68
71
|
// Install in the OS scheduler. If install fails after the file was written,
|
|
69
72
|
// delete the file so the on-disk state never claims a task is registered
|
|
70
73
|
// when it isn't.
|
|
@@ -74,13 +77,14 @@ export async function akmTasksAdd(input) {
|
|
|
74
77
|
}
|
|
75
78
|
catch (err) {
|
|
76
79
|
try {
|
|
77
|
-
|
|
80
|
+
await deleteAssetFromSource(target.source, target.config, ref);
|
|
78
81
|
}
|
|
79
82
|
catch {
|
|
80
83
|
/* ignore */
|
|
81
84
|
}
|
|
82
85
|
throw err;
|
|
83
86
|
}
|
|
87
|
+
commitWriteTargetBoundary(target, `Update task:${id}`);
|
|
84
88
|
return {
|
|
85
89
|
id,
|
|
86
90
|
ref: `task:${id}`,
|
|
@@ -192,30 +196,47 @@ export async function akmTasksShow(id) {
|
|
|
192
196
|
}
|
|
193
197
|
export async function akmTasksRemove(id) {
|
|
194
198
|
const normalised = normaliseTaskId(id);
|
|
195
|
-
const
|
|
199
|
+
const target = resolveTaskWriteTarget();
|
|
200
|
+
const stashDir = target.source.path;
|
|
196
201
|
const typeRoot = path.join(stashDir, "tasks");
|
|
197
202
|
if (fs.existsSync(typeRoot))
|
|
198
203
|
warnLegacyMdTaskFiles(typeRoot);
|
|
199
|
-
|
|
204
|
+
await resolveAssetPath(stashDir, "task", normalised);
|
|
205
|
+
const ref = taskAssetRef(normalised);
|
|
200
206
|
const sched = selectBackend();
|
|
207
|
+
let uninstallError;
|
|
208
|
+
let deleteError;
|
|
201
209
|
try {
|
|
202
210
|
await sched.uninstall(normalised);
|
|
203
211
|
}
|
|
204
|
-
|
|
205
|
-
|
|
212
|
+
catch (err) {
|
|
213
|
+
uninstallError = err;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await deleteAssetFromSource(target.source, target.config, ref);
|
|
206
217
|
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
deleteError = err;
|
|
220
|
+
}
|
|
221
|
+
if (uninstallError !== undefined)
|
|
222
|
+
throw uninstallError;
|
|
223
|
+
if (deleteError !== undefined)
|
|
224
|
+
throw deleteError;
|
|
225
|
+
commitWriteTargetBoundary(target, `Remove task:${normalised}`);
|
|
207
226
|
return { id: normalised, removed: true, backend: sched.name };
|
|
208
227
|
}
|
|
209
228
|
export async function akmTasksSetEnabled(id, enabled) {
|
|
210
229
|
const normalised = normaliseTaskId(id);
|
|
211
|
-
const
|
|
230
|
+
const target = resolveTaskWriteTarget();
|
|
231
|
+
const stashDir = target.source.path;
|
|
212
232
|
const typeRoot = path.join(stashDir, "tasks");
|
|
213
233
|
if (fs.existsSync(typeRoot))
|
|
214
234
|
warnLegacyMdTaskFiles(typeRoot);
|
|
215
235
|
const filePath = await resolveAssetPath(stashDir, "task", normalised);
|
|
216
236
|
const yaml = fs.readFileSync(filePath, "utf8");
|
|
217
237
|
const updated = setEnabledInYaml(yaml, enabled);
|
|
218
|
-
|
|
238
|
+
const ref = taskAssetRef(normalised);
|
|
239
|
+
await writeAssetToSource(target.source, target.config, ref, updated);
|
|
219
240
|
const sched = selectBackend();
|
|
220
241
|
try {
|
|
221
242
|
// Reinstall from the (just-updated) definition rather than only toggling
|
|
@@ -229,9 +250,10 @@ export async function akmTasksSetEnabled(id, enabled) {
|
|
|
229
250
|
catch (err) {
|
|
230
251
|
// Roll the file back so the YAML source-of-truth and the OS
|
|
231
252
|
// scheduler don't diverge silently when the backend call fails.
|
|
232
|
-
|
|
253
|
+
await writeAssetToSource(target.source, target.config, ref, yaml);
|
|
233
254
|
throw err;
|
|
234
255
|
}
|
|
256
|
+
commitWriteTargetBoundary(target, `Update task:${normalised}`);
|
|
235
257
|
return { id: normalised, enabled, backend: sched.name };
|
|
236
258
|
}
|
|
237
259
|
export async function akmTasksRun(id) {
|
|
@@ -392,6 +414,12 @@ function normaliseTaskId(raw) {
|
|
|
392
414
|
}
|
|
393
415
|
return id;
|
|
394
416
|
}
|
|
417
|
+
function taskAssetRef(id) {
|
|
418
|
+
return { type: "task", name: id };
|
|
419
|
+
}
|
|
420
|
+
function resolveTaskWriteTarget() {
|
|
421
|
+
return resolveWriteTarget(loadConfig());
|
|
422
|
+
}
|
|
395
423
|
function renderTaskYaml(input) {
|
|
396
424
|
const obj = { schedule: input.schedule };
|
|
397
425
|
if (input.workflow) {
|
|
@@ -41,7 +41,7 @@ export const ACTION_BUILDERS = {
|
|
|
41
41
|
lesson: (ref) => `akm show ${ref} -> read the lesson and apply when_to_use`,
|
|
42
42
|
memory: (ref) => `akm show ${ref} -> recall context`,
|
|
43
43
|
workflow: (ref) => buildWorkflowAction(ref),
|
|
44
|
-
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
44
|
+
env: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted). akm env export ${ref} --out <file> writes a sourceable script (values to a file, not stdout).`,
|
|
45
45
|
secret: (ref) => `akm show ${ref} -> name only (value never shown); akm secret path ${ref} -> file path; akm secret run ${ref} <VAR> -- <command> -> run with value injected into $VAR`,
|
|
46
46
|
wiki: (ref) => `akm show ${ref} -> read the wiki page`,
|
|
47
47
|
task: (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`,
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { buildWorkflowAction } from "../../output/renderers.js";
|
|
6
|
-
import { toPosix } from "../common.js";
|
|
7
6
|
import { registerActionBuilder, registerTypeRenderer } from "./asset-registry.js";
|
|
7
|
+
function toPosix(input) {
|
|
8
|
+
return input.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
8
10
|
const buildTaskAction = (ref) => `akm tasks show ${ref.replace(/^task:/, "")} -> inspect; akm tasks run <id> -> run now; akm tasks remove <id> -> unschedule`;
|
|
9
11
|
const markdownSpec = {
|
|
10
12
|
isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
|
|
@@ -89,7 +91,7 @@ const ASSET_SPECS_INTERNAL = {
|
|
|
89
91
|
return path.join(typeRoot, name.endsWith(".env") ? name : `${name}.env`);
|
|
90
92
|
},
|
|
91
93
|
rendererName: "env-file",
|
|
92
|
-
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (
|
|
94
|
+
actionBuilder: (ref) => `akm show ${ref} -> inspect key names; akm env run ${ref} -- <command> -> run with the whole .env injected (prefer --clean to minimize inherited parent env; child stdout is not redacted); akm env export ${ref} --out <file> -> write a sourceable script to a file`,
|
|
93
95
|
},
|
|
94
96
|
// Secrets — a single sensitive value used on its own for authentication (a
|
|
95
97
|
// PEM key, API token, TLS cert). Unlike `env` (a group of related .env
|