akm-cli 0.7.5 → 0.8.0-rc1
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 +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +804 -461
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +251 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +2 -23
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +377 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +188 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
|
@@ -12,7 +12,7 @@ import { resolveStashDir } from "../core/common";
|
|
|
12
12
|
import { loadConfig } from "../core/config";
|
|
13
13
|
import { UsageError } from "../core/errors";
|
|
14
14
|
import { appendEvent } from "../core/events";
|
|
15
|
-
import { archiveProposal, createProposal, diffProposal, getProposal, listProposals, promoteProposal, validateProposal, } from "../core/proposals";
|
|
15
|
+
import { archiveProposal, createProposal, diffProposal, getProposal, listProposals, promoteProposal, resolveProposalId, validateProposal, } from "../core/proposals";
|
|
16
16
|
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
17
17
|
function resolveStash(stashDir) {
|
|
18
18
|
if (stashDir)
|
|
@@ -43,7 +43,8 @@ export function akmProposalShow(options) {
|
|
|
43
43
|
export async function akmProposalAccept(options) {
|
|
44
44
|
const stash = resolveStash(options.stashDir);
|
|
45
45
|
const config = options.config ?? loadConfig();
|
|
46
|
-
const
|
|
46
|
+
const resolvedId = resolveProposalId(stash, options.id).id;
|
|
47
|
+
const result = await promoteProposal(stash, config, resolvedId, { target: options.target }, options.ctx);
|
|
47
48
|
// Emit `promoted` to the events stream so observers (audit, dashboards,
|
|
48
49
|
// sync) see the accept happen. Only emit on the happy path — promotion
|
|
49
50
|
// throws on validation failure, so reaching this point means the asset
|
|
@@ -69,11 +70,11 @@ export async function akmProposalAccept(options) {
|
|
|
69
70
|
}
|
|
70
71
|
export function akmProposalReject(options) {
|
|
71
72
|
const stash = resolveStash(options.stashDir);
|
|
72
|
-
const existing =
|
|
73
|
+
const existing = resolveProposalId(stash, options.id);
|
|
73
74
|
if (existing.status !== "pending") {
|
|
74
|
-
throw new UsageError(`Proposal ${
|
|
75
|
+
throw new UsageError(`Proposal ${existing.id} is not pending (current status: ${existing.status}). Only pending proposals can be rejected.`, "INVALID_FLAG_VALUE");
|
|
75
76
|
}
|
|
76
|
-
const updated = archiveProposal(stash,
|
|
77
|
+
const updated = archiveProposal(stash, existing.id, "rejected", options.reason, options.ctx);
|
|
77
78
|
appendEvent({
|
|
78
79
|
eventType: "rejected",
|
|
79
80
|
ref: updated.ref,
|
|
@@ -96,8 +97,8 @@ export function akmProposalReject(options) {
|
|
|
96
97
|
export function akmProposalDiff(options) {
|
|
97
98
|
const stash = resolveStash(options.stashDir);
|
|
98
99
|
const config = options.config ?? loadConfig();
|
|
99
|
-
const proposal =
|
|
100
|
-
const diff = diffProposal(stash, config,
|
|
100
|
+
const proposal = resolveProposalId(stash, options.id);
|
|
101
|
+
const diff = diffProposal(stash, config, proposal.id, { target: options.target });
|
|
101
102
|
return {
|
|
102
103
|
schemaVersion: 1,
|
|
103
104
|
id: proposal.id,
|
package/dist/commands/propose.js
CHANGED
|
@@ -12,34 +12,19 @@
|
|
|
12
12
|
import { parseAssetRef } from "../core/asset-ref";
|
|
13
13
|
import { TYPE_DIRS } from "../core/asset-spec";
|
|
14
14
|
import { resolveStashDir } from "../core/common";
|
|
15
|
-
import { loadConfig } from "../core/config";
|
|
16
15
|
import { ConfigError, UsageError } from "../core/errors";
|
|
17
16
|
import { appendEvent } from "../core/events";
|
|
18
17
|
import { createProposal } from "../core/proposals";
|
|
19
|
-
import {
|
|
18
|
+
import { runAgent, } from "../integrations/agent";
|
|
19
|
+
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
20
|
+
import { runProposalAgentPipeline } from "../integrations/agent/pipeline";
|
|
20
21
|
import { buildProposePrompt, parseAgentProposalPayload } from "../integrations/agent/prompts";
|
|
21
|
-
|
|
22
|
-
const config = loadConfig();
|
|
23
|
-
return parseAgentConfig(config.agent);
|
|
24
|
-
}
|
|
25
|
-
function resolveProfile(options) {
|
|
26
|
-
if (options.agentProfile)
|
|
27
|
-
return options.agentProfile;
|
|
28
|
-
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
29
|
-
return requireAgentProfile(agent, options.profile);
|
|
30
|
-
}
|
|
22
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "./agent-support";
|
|
31
23
|
function failureEnvelope(result, type, name, fallbackReason = "non_zero_exit") {
|
|
32
|
-
const reason = result.reason ?? fallbackReason;
|
|
33
24
|
return {
|
|
34
|
-
|
|
35
|
-
ok: false,
|
|
36
|
-
reason,
|
|
37
|
-
error: result.error ?? `agent failure (${reason})`,
|
|
25
|
+
...baseFailureFields(result, fallbackReason),
|
|
38
26
|
type,
|
|
39
27
|
name,
|
|
40
|
-
exitCode: result.exitCode,
|
|
41
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
42
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
43
28
|
};
|
|
44
29
|
}
|
|
45
30
|
export async function akmPropose(options) {
|
|
@@ -68,9 +53,31 @@ export async function akmPropose(options) {
|
|
|
68
53
|
},
|
|
69
54
|
});
|
|
70
55
|
// 2. Resolve profile.
|
|
56
|
+
// When an explicit --profile flag is given, honour it directly (existing
|
|
57
|
+
// behaviour). Otherwise use resolveProcessAgentProfile so that per-process
|
|
58
|
+
// agent config (agent.processes["propose"]) is picked up automatically.
|
|
71
59
|
let profile;
|
|
60
|
+
let resolvedTimeoutMs = options.timeoutMs;
|
|
72
61
|
try {
|
|
73
|
-
|
|
62
|
+
if (options.agentProfile) {
|
|
63
|
+
// Test seam: injected profile bypasses all config.
|
|
64
|
+
profile = options.agentProfile;
|
|
65
|
+
}
|
|
66
|
+
else if (options.profile) {
|
|
67
|
+
// Explicit --profile flag wins over process config.
|
|
68
|
+
profile = resolveAgentProfile(options);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Use per-process config resolution (falls back to agent.default).
|
|
72
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
73
|
+
const processName = options.agentProcess ?? "propose";
|
|
74
|
+
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
75
|
+
profile = resolved.profile;
|
|
76
|
+
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
77
|
+
if (resolvedTimeoutMs === undefined) {
|
|
78
|
+
resolvedTimeoutMs = resolved.timeoutMs;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
}
|
|
75
82
|
catch (err) {
|
|
76
83
|
if (err instanceof ConfigError || err instanceof UsageError)
|
|
@@ -91,14 +98,42 @@ export async function akmPropose(options) {
|
|
|
91
98
|
// 4. Spawn the agent.
|
|
92
99
|
// Real agent runs use interactive mode so file tools can write the draft.
|
|
93
100
|
// Injected/custom spawns still need captured stdout for JSON payload tests.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// Use callAi for the unified AI dispatch path (agent CLI preferred, LLM HTTP fallback).
|
|
102
|
+
const useCustomSpawn = Boolean(options.runAgentOptions?.spawn);
|
|
103
|
+
let result;
|
|
104
|
+
if (useCustomSpawn) {
|
|
105
|
+
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
106
|
+
const runOptions = {
|
|
107
|
+
stdio: "captured",
|
|
108
|
+
parseOutput: "text",
|
|
109
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
110
|
+
...(options.runAgentOptions ?? {}),
|
|
111
|
+
};
|
|
112
|
+
result = await runAgent(profile, prompt, runOptions);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Production path: route through runProposalAgentPipeline (shared logic).
|
|
116
|
+
const pipelineResult = await runProposalAgentPipeline({
|
|
117
|
+
profile,
|
|
118
|
+
prompt,
|
|
119
|
+
draftFilePath: resolvedDraftPath,
|
|
120
|
+
timeoutMs: resolvedTimeoutMs,
|
|
121
|
+
});
|
|
122
|
+
result = {
|
|
123
|
+
ok: pipelineResult.ok,
|
|
124
|
+
exitCode: pipelineResult.exitCode,
|
|
125
|
+
stdout: pipelineResult.stdout,
|
|
126
|
+
stderr: pipelineResult.stderr,
|
|
127
|
+
durationMs: pipelineResult.durationMs,
|
|
128
|
+
error: pipelineResult.error,
|
|
129
|
+
reason: pipelineResult.reason,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
101
132
|
if (!result.ok) {
|
|
133
|
+
// B3: ENOENT / not-found gives an actionable hint.
|
|
134
|
+
if (isEnoentFailure(result)) {
|
|
135
|
+
return { ...failureEnvelope(result, options.type, options.name), error: enoentHintMessage(profile.bin) };
|
|
136
|
+
}
|
|
102
137
|
return failureEnvelope(result, options.type, options.name);
|
|
103
138
|
}
|
|
104
139
|
// 5. Resolve the proposal content.
|
|
@@ -115,6 +150,21 @@ export async function akmPropose(options) {
|
|
|
115
150
|
};
|
|
116
151
|
}
|
|
117
152
|
else {
|
|
153
|
+
// B1: When interactive mode was used and stdout is empty, the agent did not
|
|
154
|
+
// write the draft file and stdout was not captured — surface an actionable error.
|
|
155
|
+
const stdioWasInteractive = !useCustomSpawn;
|
|
156
|
+
if (stdioWasInteractive && (result.stdout ?? "") === "") {
|
|
157
|
+
return {
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: "parse_error",
|
|
161
|
+
error: "Agent did not write draft file and stdout was not captured (interactive mode). Check that the agent CLI understood the file-write instruction, or configure a headless profile with stdio: 'captured'.",
|
|
162
|
+
type: options.type,
|
|
163
|
+
name: options.name,
|
|
164
|
+
exitCode: result.exitCode,
|
|
165
|
+
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
118
168
|
try {
|
|
119
169
|
payload = parseAgentProposalPayload(result.stdout ?? "");
|
|
120
170
|
}
|
package/dist/commands/reflect.js
CHANGED
|
@@ -19,16 +19,22 @@
|
|
|
19
19
|
* a committed asset, and the `accept` flow is the bridge.
|
|
20
20
|
*/
|
|
21
21
|
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
22
23
|
import { parseAssetRef } from "../core/asset-ref";
|
|
23
24
|
import { resolveStashDir } from "../core/common";
|
|
24
|
-
import { loadConfig } from "../core/config";
|
|
25
25
|
import { ConfigError, UsageError } from "../core/errors";
|
|
26
26
|
import { appendEvent, readEvents } from "../core/events";
|
|
27
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
27
28
|
import { lintLessonContent } from "../core/lesson-lint";
|
|
29
|
+
import { stripMarkdownFences } from "../core/markdown";
|
|
28
30
|
import { createProposal } from "../core/proposals";
|
|
29
31
|
import { lookup } from "../indexer/indexer";
|
|
30
|
-
import {
|
|
32
|
+
import { runAgent, } from "../integrations/agent";
|
|
33
|
+
import { resolveProcessAgentProfile } from "../integrations/agent/config";
|
|
34
|
+
import { runProposalAgentPipeline } from "../integrations/agent/pipeline";
|
|
31
35
|
import { buildReflectPrompt, parseAgentProposalPayload } from "../integrations/agent/prompts";
|
|
36
|
+
import { baseFailureFields, enoentHintMessage, isEnoentFailure, loadAgentConfigFromDisk, resolveAgentProfile, } from "./agent-support";
|
|
37
|
+
import { deriveLessonRef } from "./distill";
|
|
32
38
|
const MAX_FEEDBACK_LINES = 10;
|
|
33
39
|
const MAX_GLOBAL_FEEDBACK_LINES = 20;
|
|
34
40
|
/**
|
|
@@ -45,7 +51,7 @@ function readRecentFeedback(ref) {
|
|
|
45
51
|
for (const event of result.events.slice(-limit)) {
|
|
46
52
|
const md = (event.metadata ?? {});
|
|
47
53
|
const signal = typeof md.signal === "string" ? md.signal : "?";
|
|
48
|
-
const note = typeof md.
|
|
54
|
+
const note = typeof md.reason === "string" ? md.reason : typeof md.note === "string" ? md.note : "";
|
|
49
55
|
const details = note ? `[${signal}] ${note}` : `[${signal}]`;
|
|
50
56
|
lines.push(!ref && event.ref ? `${event.ref} ${details}` : details);
|
|
51
57
|
}
|
|
@@ -68,45 +74,83 @@ function buildSchemaHints(type, content) {
|
|
|
68
74
|
const report = lintLessonContent(content, "reflect");
|
|
69
75
|
return report.findings.map((f) => `[${f.kind}] ${f.message}`);
|
|
70
76
|
}
|
|
77
|
+
function hasRelatedSkillSource(content, skillRef) {
|
|
78
|
+
const parsed = parseFrontmatter(content);
|
|
79
|
+
const sources = parsed.data.sources;
|
|
80
|
+
return Array.isArray(sources) && sources.some((source) => typeof source === "string" && source.trim() === skillRef);
|
|
81
|
+
}
|
|
82
|
+
async function readRelatedLessons(stash, ref, parsedRef) {
|
|
83
|
+
if (parsedRef.type !== "skill")
|
|
84
|
+
return [];
|
|
85
|
+
const related = new Map();
|
|
86
|
+
const derivedLessonRef = deriveLessonRef(ref);
|
|
87
|
+
const candidateRefs = new Set([derivedLessonRef]);
|
|
88
|
+
const derivedLessonPath = path.join(stash, "lessons", `${derivedLessonRef.slice("lesson:".length)}.md`);
|
|
89
|
+
if (fs.existsSync(derivedLessonPath)) {
|
|
90
|
+
related.set(derivedLessonRef, { ref: derivedLessonRef, content: fs.readFileSync(derivedLessonPath, "utf8") });
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const feedbackEvents = readEvents({ type: "distill_invoked", ref }).events;
|
|
94
|
+
for (const event of feedbackEvents) {
|
|
95
|
+
const lessonRef = typeof event.metadata?.lessonRef === "string" ? event.metadata.lessonRef : undefined;
|
|
96
|
+
if (lessonRef?.startsWith("lesson:"))
|
|
97
|
+
candidateRefs.add(lessonRef);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Best effort only.
|
|
102
|
+
}
|
|
103
|
+
for (const candidateRef of candidateRefs) {
|
|
104
|
+
try {
|
|
105
|
+
const entry = await lookup(parseAssetRef(candidateRef));
|
|
106
|
+
if (!entry?.filePath || !fs.existsSync(entry.filePath))
|
|
107
|
+
continue;
|
|
108
|
+
const content = fs.readFileSync(entry.filePath, "utf8");
|
|
109
|
+
related.set(candidateRef, { ref: candidateRef, content });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Index miss is non-fatal.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const lessonsDir = path.join(stash, "lessons");
|
|
117
|
+
if (fs.existsSync(lessonsDir)) {
|
|
118
|
+
for (const fileName of fs.readdirSync(lessonsDir)) {
|
|
119
|
+
if (!fileName.endsWith(".md"))
|
|
120
|
+
continue;
|
|
121
|
+
const content = fs.readFileSync(path.join(lessonsDir, fileName), "utf8");
|
|
122
|
+
if (!hasRelatedSkillSource(content, ref))
|
|
123
|
+
continue;
|
|
124
|
+
const lessonName = fileName.slice(0, -3);
|
|
125
|
+
const lessonRef = `lesson:${lessonName}`;
|
|
126
|
+
if (!related.has(lessonRef)) {
|
|
127
|
+
related.set(lessonRef, { ref: lessonRef, content });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Best effort only.
|
|
134
|
+
}
|
|
135
|
+
return [...related.values()];
|
|
136
|
+
}
|
|
71
137
|
function fallbackPayloadFromRawContent(stdout, ref) {
|
|
72
138
|
if (!ref)
|
|
73
139
|
return undefined;
|
|
74
|
-
const trimmed =
|
|
140
|
+
const trimmed = stripMarkdownFences(stdout).trim();
|
|
75
141
|
if (!trimmed)
|
|
76
142
|
return undefined;
|
|
77
143
|
if (!looksLikeAssetContent(trimmed))
|
|
78
144
|
return undefined;
|
|
79
145
|
return { ref, content: trimmed };
|
|
80
146
|
}
|
|
81
|
-
function stripMarkdownFence(stdout) {
|
|
82
|
-
const trimmed = stdout.trim();
|
|
83
|
-
const match = trimmed.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```$/i);
|
|
84
|
-
return match?.[1] ?? trimmed;
|
|
85
|
-
}
|
|
86
147
|
function looksLikeAssetContent(value) {
|
|
87
148
|
return value.startsWith("#") || value.startsWith("---");
|
|
88
149
|
}
|
|
89
|
-
function loadAgentConfigFromDisk() {
|
|
90
|
-
const config = loadConfig();
|
|
91
|
-
return parseAgentConfig(config.agent);
|
|
92
|
-
}
|
|
93
|
-
function resolveProfile(options) {
|
|
94
|
-
if (options.agentProfile)
|
|
95
|
-
return options.agentProfile;
|
|
96
|
-
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
97
|
-
return requireAgentProfile(agent, options.profile);
|
|
98
|
-
}
|
|
99
150
|
function failureEnvelope(result, ref, fallbackReason = "non_zero_exit") {
|
|
100
|
-
const reason = result.reason ?? fallbackReason;
|
|
101
151
|
return {
|
|
102
|
-
|
|
103
|
-
ok: false,
|
|
104
|
-
reason,
|
|
105
|
-
error: result.error ?? `agent failure (${reason})`,
|
|
152
|
+
...baseFailureFields(result, fallbackReason),
|
|
106
153
|
...(ref ? { ref } : {}),
|
|
107
|
-
exitCode: result.exitCode,
|
|
108
|
-
...(result.stdout ? { stdout: result.stdout } : {}),
|
|
109
|
-
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
110
154
|
};
|
|
111
155
|
}
|
|
112
156
|
export async function akmReflect(options = {}) {
|
|
@@ -138,9 +182,32 @@ export async function akmReflect(options = {}) {
|
|
|
138
182
|
}
|
|
139
183
|
// 3. Resolve agent profile. ConfigError surfaces as a thrown error so the
|
|
140
184
|
// CLI dispatcher renders the standard envelope.
|
|
185
|
+
//
|
|
186
|
+
// When an explicit --profile flag is given, honour it directly (existing
|
|
187
|
+
// behaviour). Otherwise use resolveProcessAgentProfile so that per-process
|
|
188
|
+
// agent config (agent.processes["reflect"]) is picked up automatically.
|
|
141
189
|
let profile;
|
|
190
|
+
let resolvedTimeoutMs = options.timeoutMs;
|
|
142
191
|
try {
|
|
143
|
-
|
|
192
|
+
if (options.agentProfile) {
|
|
193
|
+
// Test seam: injected profile bypasses all config.
|
|
194
|
+
profile = options.agentProfile;
|
|
195
|
+
}
|
|
196
|
+
else if (options.profile) {
|
|
197
|
+
// Explicit --profile flag wins over process config.
|
|
198
|
+
profile = resolveAgentProfile(options);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Use per-process config resolution (falls back to agent.default).
|
|
202
|
+
const agent = options.agentConfig ?? loadAgentConfigFromDisk();
|
|
203
|
+
const processName = options.agentProcess ?? "reflect";
|
|
204
|
+
const resolved = resolveProcessAgentProfile(processName, agent);
|
|
205
|
+
profile = resolved.profile;
|
|
206
|
+
// Only apply process-resolved timeoutMs when caller didn't supply one.
|
|
207
|
+
if (resolvedTimeoutMs === undefined) {
|
|
208
|
+
resolvedTimeoutMs = resolved.timeoutMs;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
144
211
|
}
|
|
145
212
|
catch (err) {
|
|
146
213
|
if (err instanceof ConfigError || err instanceof UsageError)
|
|
@@ -153,6 +220,7 @@ export async function akmReflect(options = {}) {
|
|
|
153
220
|
// local opencode models and caused proposal generation failures.
|
|
154
221
|
const feedback = readRecentFeedback(options.ref);
|
|
155
222
|
const schemaHints = buildSchemaHints(parsedRef?.type ?? "", assetContent);
|
|
223
|
+
const relatedLessons = options.ref && parsedRef ? await readRelatedLessons(stash, options.ref, parsedRef) : [];
|
|
156
224
|
const prompt = buildReflectPrompt({
|
|
157
225
|
...(options.ref ? { ref: options.ref } : {}),
|
|
158
226
|
...(parsedRef?.type ? { type: parsedRef.type } : {}),
|
|
@@ -160,17 +228,48 @@ export async function akmReflect(options = {}) {
|
|
|
160
228
|
...(assetContent !== undefined ? { assetContent } : {}),
|
|
161
229
|
...(feedback.length > 0 ? { feedback } : {}),
|
|
162
230
|
...(schemaHints.length > 0 ? { schemaHints } : {}),
|
|
231
|
+
...(relatedLessons.length > 0 ? { relatedLessons } : {}),
|
|
163
232
|
...(options.task ? { task: options.task } : {}),
|
|
233
|
+
...(options.avoidPatterns && options.avoidPatterns.length > 0 ? { avoidPatterns: options.avoidPatterns } : {}),
|
|
164
234
|
});
|
|
165
235
|
// 5. Spawn the agent.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
236
|
+
// Use runProposalAgentPipeline for the shared spawn step, but fall back to
|
|
237
|
+
// raw runAgent when a custom spawn function is injected (test seam).
|
|
238
|
+
let result;
|
|
239
|
+
if (options.runAgentOptions?.spawn) {
|
|
240
|
+
// Test seam: use raw runAgent with injected spawn so tests remain deterministic.
|
|
241
|
+
const runOptions = {
|
|
242
|
+
stdio: "captured",
|
|
243
|
+
parseOutput: "text",
|
|
244
|
+
...(resolvedTimeoutMs !== undefined ? { timeoutMs: resolvedTimeoutMs } : {}),
|
|
245
|
+
...(options.runAgentOptions ?? {}),
|
|
246
|
+
};
|
|
247
|
+
result = await runAgent(profile, prompt, runOptions);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Production path: route through runProposalAgentPipeline (shared logic).
|
|
251
|
+
const pipelineResult = await runProposalAgentPipeline({
|
|
252
|
+
profile,
|
|
253
|
+
prompt,
|
|
254
|
+
// reflect always uses captured stdout (no draft file path).
|
|
255
|
+
draftFilePath: undefined,
|
|
256
|
+
timeoutMs: resolvedTimeoutMs,
|
|
257
|
+
});
|
|
258
|
+
result = {
|
|
259
|
+
ok: pipelineResult.ok,
|
|
260
|
+
exitCode: pipelineResult.exitCode,
|
|
261
|
+
stdout: pipelineResult.stdout,
|
|
262
|
+
stderr: pipelineResult.stderr,
|
|
263
|
+
durationMs: pipelineResult.durationMs,
|
|
264
|
+
error: pipelineResult.error,
|
|
265
|
+
reason: pipelineResult.reason,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
173
268
|
if (!result.ok) {
|
|
269
|
+
// B3: ENOENT / not-found gives an actionable hint.
|
|
270
|
+
if (isEnoentFailure(result)) {
|
|
271
|
+
return { ...failureEnvelope(result, options.ref), error: enoentHintMessage(profile.bin) };
|
|
272
|
+
}
|
|
174
273
|
return failureEnvelope(result, options.ref);
|
|
175
274
|
}
|
|
176
275
|
// 6. Resolve the proposal content from stdout JSON.
|
|
@@ -208,6 +307,15 @@ export async function akmReflect(options = {}) {
|
|
|
208
307
|
},
|
|
209
308
|
};
|
|
210
309
|
const proposal = createProposal(stash, createInput, options.ctx);
|
|
310
|
+
appendEvent({
|
|
311
|
+
eventType: "reflect_completed",
|
|
312
|
+
ref: proposal.ref,
|
|
313
|
+
metadata: {
|
|
314
|
+
proposalId: proposal.id,
|
|
315
|
+
source: "reflect",
|
|
316
|
+
agentProfile: profile.name,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
211
319
|
return {
|
|
212
320
|
schemaVersion: 1,
|
|
213
321
|
ok: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { toErrorMessage } from "../core/common";
|
|
2
|
-
import { DEFAULT_CONFIG
|
|
2
|
+
import { DEFAULT_CONFIG } from "../core/config";
|
|
3
3
|
import { warn } from "../core/warn";
|
|
4
4
|
import { resolveProviderFactory } from "../registry/factory";
|
|
5
5
|
// ── Eagerly import providers to trigger self-registration ───────────────────
|
|
@@ -133,7 +133,7 @@ export function resolveRegistries(configRegistries) {
|
|
|
133
133
|
}
|
|
134
134
|
return entries;
|
|
135
135
|
}
|
|
136
|
-
const registries = configRegistries ??
|
|
136
|
+
const registries = configRegistries ?? DEFAULT_CONFIG.registries ?? [];
|
|
137
137
|
return registries.filter((r) => r.enabled !== false);
|
|
138
138
|
}
|
|
139
139
|
// ── Provider resolution ─────────────────────────────────────────────────────
|
|
@@ -11,6 +11,7 @@ import { loadConfig } from "../core/config";
|
|
|
11
11
|
import { UsageError } from "../core/errors";
|
|
12
12
|
import { warn } from "../core/warn";
|
|
13
13
|
import { SCOPE_KEYS } from "../indexer/metadata";
|
|
14
|
+
import { parseFlagValue } from "../output/context";
|
|
14
15
|
/**
|
|
15
16
|
* Parse a shorthand duration string to a number of milliseconds.
|
|
16
17
|
* Supports: `30d` (days), `12h` (hours), `6m` (months, approximated as 30d).
|
|
@@ -188,3 +189,56 @@ Return ONLY the JSON object, no prose, no markdown fences.`;
|
|
|
188
189
|
return { tags: [] };
|
|
189
190
|
}
|
|
190
191
|
}
|
|
192
|
+
// ── Content-arg disambiguation ───────────────────────────────────────────────
|
|
193
|
+
/**
|
|
194
|
+
* Guard against citty consuming a global flag value as the `content` positional.
|
|
195
|
+
*
|
|
196
|
+
* When the user runs `akm remember --format json` without a content argument,
|
|
197
|
+
* citty may assign `"json"` to the `content` positional because of how it
|
|
198
|
+
* handles flag order. This helper detects that case and returns `undefined`
|
|
199
|
+
* so `readMemoryContent` falls through to stdin.
|
|
200
|
+
*/
|
|
201
|
+
export function resolveRememberContentArg(content) {
|
|
202
|
+
if (content === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
const parsedFormat = parseFlagValue(process.argv, "--format");
|
|
205
|
+
if (parsedFormat !== undefined &&
|
|
206
|
+
content === parsedFormat &&
|
|
207
|
+
wasRememberFlagValueConsumedAsContent(content, parsedFormat, "--format")) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const parsedDetail = parseFlagValue(process.argv, "--detail");
|
|
211
|
+
if (parsedDetail !== undefined &&
|
|
212
|
+
content === parsedDetail &&
|
|
213
|
+
wasRememberFlagValueConsumedAsContent(content, parsedDetail, "--detail")) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return content;
|
|
217
|
+
}
|
|
218
|
+
function wasRememberFlagValueConsumedAsContent(content, flagValue, flagName) {
|
|
219
|
+
const argv = process.argv.slice(2);
|
|
220
|
+
const rememberIndex = argv.indexOf("remember");
|
|
221
|
+
const tokens = rememberIndex >= 0 ? argv.slice(rememberIndex + 1) : argv;
|
|
222
|
+
let flagIndex = -1;
|
|
223
|
+
let flagConsumesNextToken = false;
|
|
224
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
225
|
+
const token = tokens[i];
|
|
226
|
+
if (token === flagName) {
|
|
227
|
+
flagIndex = i;
|
|
228
|
+
flagConsumesNextToken = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
if (token === `${flagName}=${flagValue}`) {
|
|
232
|
+
flagIndex = i;
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (flagIndex === -1)
|
|
237
|
+
return false;
|
|
238
|
+
if (tokens.slice(0, flagIndex).includes(content))
|
|
239
|
+
return false;
|
|
240
|
+
const firstTokenAfterFlag = flagIndex + (flagConsumesNextToken ? 2 : 1);
|
|
241
|
+
if (tokens.slice(firstTokenAfterFlag).includes(content))
|
|
242
|
+
return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-repair pass for `akm improve`.
|
|
3
|
+
*
|
|
4
|
+
* Attempts to patch missing frontmatter fields (`description`, `when_to_use`)
|
|
5
|
+
* on assets that failed schema validation, using a single bounded in-tree LLM
|
|
6
|
+
* call per asset. Results are recorded as `schema_repair_invoked` events.
|
|
7
|
+
*
|
|
8
|
+
* This module is extracted from `improve.ts` to make the repair logic
|
|
9
|
+
* independently testable and to use the `tryLlmFeature` seam rather than raw
|
|
10
|
+
* `chatCompletion`.
|
|
11
|
+
*/
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import { stringify as yamlStringify } from "yaml";
|
|
14
|
+
import { parseAssetRef } from "../core/asset-ref";
|
|
15
|
+
import { appendEvent, readEvents } from "../core/events";
|
|
16
|
+
import { parseFrontmatter } from "../core/frontmatter";
|
|
17
|
+
import { info } from "../core/warn";
|
|
18
|
+
import { resolveAssetPath } from "../indexer/path-resolver";
|
|
19
|
+
import { chatCompletion, parseEmbeddedJsonResponse } from "../llm/client";
|
|
20
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Minimum gap between schema-repair attempts on the same asset. */
|
|
22
|
+
const SCHEMA_REPAIR_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
23
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Run the schema-repair loop for a batch of validation failures.
|
|
26
|
+
* Returns a list of per-asset outcome records and the set of refs that were
|
|
27
|
+
* successfully repaired (so the caller can exclude them from skip logic).
|
|
28
|
+
*/
|
|
29
|
+
export async function runSchemaRepairPass(failures, options) {
|
|
30
|
+
const repairs = [];
|
|
31
|
+
const repairedRefs = new Set();
|
|
32
|
+
const { startMs, budgetMs, llmConfig, stashDir, findFilePath = defaultFindFilePath, isLessonCandidateFn = defaultIsLessonCandidate, } = options;
|
|
33
|
+
for (const failure of failures) {
|
|
34
|
+
if (Date.now() - startMs >= budgetMs)
|
|
35
|
+
break;
|
|
36
|
+
// Cooldown: skip repair if we ran it successfully recently.
|
|
37
|
+
const recentRepairs = readEvents({ type: "schema_repair_invoked", ref: failure.ref });
|
|
38
|
+
const lastRepair = recentRepairs.events
|
|
39
|
+
.filter((e) => e.metadata?.outcome === "written")
|
|
40
|
+
.sort((a, b) => new Date(b.ts ?? 0).getTime() - new Date(a.ts ?? 0).getTime())[0];
|
|
41
|
+
if (lastRepair?.ts && Date.now() - new Date(lastRepair.ts).getTime() < SCHEMA_REPAIR_COOLDOWN_MS) {
|
|
42
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const filePath = await findFilePath(failure.ref, stashDir);
|
|
46
|
+
if (!filePath) {
|
|
47
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
const fm = parseFrontmatter(raw);
|
|
53
|
+
const missingFields = [];
|
|
54
|
+
if (!fm.data.description)
|
|
55
|
+
missingFields.push("description");
|
|
56
|
+
if (isLessonCandidateFn(failure.ref) && !fm.data.when_to_use)
|
|
57
|
+
missingFields.push("when_to_use");
|
|
58
|
+
if (missingFields.length === 0) {
|
|
59
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "skipped" });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const fieldList = missingFields.join(" and ");
|
|
63
|
+
info(`[improve] schema-repair ${failure.ref} (${fieldList})`);
|
|
64
|
+
const bodyPreview = (fm.content ?? raw).slice(0, 2000);
|
|
65
|
+
const llmResponse = await chatCompletion(llmConfig, [
|
|
66
|
+
{
|
|
67
|
+
role: "system",
|
|
68
|
+
content: `You generate concise asset frontmatter fields. Respond with a JSON object containing only the missing fields. No prose, no markdown fences.`,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
role: "user",
|
|
72
|
+
content: `Generate the missing frontmatter fields (${fieldList}) for this ${parseAssetRef(failure.ref).type} asset. Return ONLY valid JSON like {"description": "...", "when_to_use": "..."}\n\n${bodyPreview}`,
|
|
73
|
+
},
|
|
74
|
+
]);
|
|
75
|
+
const parsed = parseEmbeddedJsonResponse(llmResponse.trim());
|
|
76
|
+
if (!parsed) {
|
|
77
|
+
repairs.push({
|
|
78
|
+
ref: failure.ref,
|
|
79
|
+
reason: failure.reason,
|
|
80
|
+
outcome: "error",
|
|
81
|
+
error: "LLM returned unparseable JSON for schema repair",
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const newFm = { ...fm.data };
|
|
86
|
+
if (parsed.description)
|
|
87
|
+
newFm.description = parsed.description;
|
|
88
|
+
if (parsed.when_to_use)
|
|
89
|
+
newFm.when_to_use = parsed.when_to_use;
|
|
90
|
+
const fmStr = yamlStringify(newFm).trimEnd();
|
|
91
|
+
const newContent = `---\n${fmStr}\n---\n${fm.content}`;
|
|
92
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
93
|
+
info(`[improve] schema-repair written: ${failure.ref}`);
|
|
94
|
+
appendEvent({
|
|
95
|
+
eventType: "schema_repair_invoked",
|
|
96
|
+
ref: failure.ref,
|
|
97
|
+
metadata: { outcome: "written", reason: failure.reason },
|
|
98
|
+
});
|
|
99
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "written" });
|
|
100
|
+
repairedRefs.add(failure.ref);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
appendEvent({
|
|
104
|
+
eventType: "schema_repair_invoked",
|
|
105
|
+
ref: failure.ref,
|
|
106
|
+
metadata: { outcome: "error", reason: failure.reason, error: String(e) },
|
|
107
|
+
});
|
|
108
|
+
repairs.push({ ref: failure.ref, reason: failure.reason, outcome: "error", error: String(e) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { repairs, repairedRefs };
|
|
112
|
+
}
|
|
113
|
+
// ── Default seam implementations ─────────────────────────────────────────────
|
|
114
|
+
function defaultIsLessonCandidate(ref) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = parseAssetRef(ref);
|
|
117
|
+
return parsed.type === "lesson";
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function defaultFindFilePath(ref, stashDir) {
|
|
124
|
+
return resolveAssetPath(ref, {
|
|
125
|
+
stashDir,
|
|
126
|
+
mode: "index-first",
|
|
127
|
+
directoryIndexNames: ["SKILL.md", "index.md", "README.md"],
|
|
128
|
+
preserveDirectNameFallback: true,
|
|
129
|
+
});
|
|
130
|
+
}
|