akm-cli 0.7.0-rc1 → 0.7.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/dist/src/cli.js +100 -16
- package/dist/src/commands/config-cli.js +42 -0
- package/dist/src/commands/history.js +78 -7
- package/dist/src/commands/registry-search.js +69 -6
- package/dist/src/commands/search.js +30 -3
- package/dist/src/commands/show.js +29 -0
- package/dist/src/commands/source-add.js +5 -1
- package/dist/src/commands/source-manage.js +7 -1
- package/dist/src/core/config.js +28 -0
- package/dist/src/indexer/db-search.js +1 -0
- package/dist/src/indexer/indexer.js +16 -2
- package/dist/src/indexer/matchers.js +1 -1
- package/dist/src/indexer/search-source.js +4 -2
- package/dist/src/integrations/agent/profiles.js +1 -1
- package/dist/src/integrations/agent/spawn.js +67 -16
- package/dist/src/integrations/github.js +9 -3
- package/dist/src/llm/embedders/remote.js +37 -3
- package/dist/src/output/cli-hints.js +15 -2
- package/dist/src/output/renderers.js +3 -1
- package/dist/src/output/shapes.js +8 -1
- package/dist/src/output/text.js +156 -3
- package/dist/src/registry/build-index.js +5 -4
- package/dist/src/registry/providers/static-index.js +3 -1
- package/dist/src/setup/setup.js +9 -0
- package/dist/src/wiki/wiki.js +54 -6
- package/dist/src/workflows/runs.js +37 -3
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +1 -1
- package/dist/tests/bench/attribution.test.js +24 -23
- package/dist/tests/bench/cleanup.js +31 -0
- package/dist/tests/bench/cli.js +366 -31
- package/dist/tests/bench/cli.test.js +282 -14
- package/dist/tests/bench/corpus.js +3 -0
- package/dist/tests/bench/corpus.test.js +10 -10
- package/dist/tests/bench/doctor.js +525 -0
- package/dist/tests/bench/driver.js +77 -22
- package/dist/tests/bench/driver.test.js +142 -1
- package/dist/tests/bench/environment.js +233 -0
- package/dist/tests/bench/environment.test.js +199 -0
- package/dist/tests/bench/evolve.js +67 -0
- package/dist/tests/bench/evolve.test.js +12 -4
- package/dist/tests/bench/failure-modes.test.js +52 -3
- package/dist/tests/bench/feedback-integrity.test.js +3 -2
- package/dist/tests/bench/leakage.test.js +105 -2
- package/dist/tests/bench/learning-curve.test.js +3 -2
- package/dist/tests/bench/metrics.js +102 -26
- package/dist/tests/bench/metrics.test.js +10 -4
- package/dist/tests/bench/opencode-config.js +194 -0
- package/dist/tests/bench/opencode-config.test.js +370 -0
- package/dist/tests/bench/report.js +73 -9
- package/dist/tests/bench/report.test.js +59 -10
- package/dist/tests/bench/run-config.js +355 -0
- package/dist/tests/bench/run-config.test.js +298 -0
- package/dist/tests/bench/run-curate-test.js +32 -0
- package/dist/tests/bench/run-failing-tasks.js +56 -0
- package/dist/tests/bench/run-full-bench.js +51 -0
- package/dist/tests/bench/run-items36-targeted.js +69 -0
- package/dist/tests/bench/run-nano-quick.js +42 -0
- package/dist/tests/bench/run-waveg-targeted.js +62 -0
- package/dist/tests/bench/runner.js +257 -94
- package/dist/tests/bench/tmp.js +90 -0
- package/dist/tests/bench/trajectory.js +2 -2
- package/dist/tests/bench/verifier.js +6 -1
- package/dist/tests/bench/workflow-spec.js +11 -24
- package/dist/tests/bench/workflow-spec.test.js +1 -1
- package/dist/tests/bench/workflow-trace.js +34 -0
- package/dist/tests/cli-errors.test.js +1 -0
- package/dist/tests/commands/history.test.js +195 -0
- package/dist/tests/config.test.js +25 -0
- package/dist/tests/e2e.test.js +23 -2
- package/dist/tests/fixtures/stashes/load.js +1 -1
- package/dist/tests/fixtures/stashes/load.test.js +11 -2
- package/dist/tests/indexer.test.js +12 -1
- package/dist/tests/output-baseline.test.js +2 -1
- package/dist/tests/output-shapes-unit.test.js +3 -1
- package/dist/tests/registry-build-index.test.js +17 -1
- package/dist/tests/registry-providers/static-index.test.js +34 -0
- package/dist/tests/registry-search.test.js +200 -0
- package/dist/tests/remember-frontmatter.test.js +11 -13
- package/dist/tests/source-qa-fixes.test.js +18 -0
- package/dist/tests/source-registry.test.js +3 -3
- package/dist/tests/source-source.test.js +61 -1
- package/dist/tests/workflow-qa-fixes.test.js +18 -0
- package/package.json +1 -1
|
@@ -30,6 +30,27 @@
|
|
|
30
30
|
*
|
|
31
31
|
* NOT in scope: live tailing, persistence, evaluation. Those belong to #256.
|
|
32
32
|
*/
|
|
33
|
+
/** Runtime set of all valid workflow trace event names. Single source of truth. */
|
|
34
|
+
export const WORKFLOW_TRACE_EVENT_NAMES = new Set([
|
|
35
|
+
"agent_started",
|
|
36
|
+
"akm_search",
|
|
37
|
+
"akm_show",
|
|
38
|
+
"akm_feedback",
|
|
39
|
+
"akm_reflect",
|
|
40
|
+
"akm_distill",
|
|
41
|
+
"akm_propose",
|
|
42
|
+
"akm_proposal_accept",
|
|
43
|
+
"akm_workflow_start",
|
|
44
|
+
"akm_workflow_next",
|
|
45
|
+
"akm_workflow_complete",
|
|
46
|
+
"akm_workflow_finish",
|
|
47
|
+
"workspace_read",
|
|
48
|
+
"workspace_write",
|
|
49
|
+
"first_workspace_write",
|
|
50
|
+
"test_run",
|
|
51
|
+
"verifier_run",
|
|
52
|
+
"agent_finished",
|
|
53
|
+
]);
|
|
33
54
|
/* ─── Caps (documented contract) ──────────────────────────────────────────── */
|
|
34
55
|
/** Hard cap on total events per trace. Prevents a noisy run from OOM-ing the harness. */
|
|
35
56
|
export const MAX_EVENT_COUNT = 4096;
|
|
@@ -182,6 +203,9 @@ const AKM_EVENT_TYPE_MAP = {
|
|
|
182
203
|
distill_invoked: "akm_distill",
|
|
183
204
|
propose_invoked: "akm_propose",
|
|
184
205
|
promoted: "akm_proposal_accept",
|
|
206
|
+
workflow_started: "akm_workflow_start",
|
|
207
|
+
workflow_step_completed: "akm_workflow_complete",
|
|
208
|
+
workflow_finished: "akm_workflow_finish",
|
|
185
209
|
};
|
|
186
210
|
function fromAkmEvent(ev, run, options, originalIndex) {
|
|
187
211
|
if (!ev || typeof ev !== "object")
|
|
@@ -354,6 +378,16 @@ function classifyArgs(command, argv) {
|
|
|
354
378
|
return { type: "akm_distill", command, args: argv };
|
|
355
379
|
case "propose":
|
|
356
380
|
return { type: "akm_propose", command, args: argv };
|
|
381
|
+
case "workflow": {
|
|
382
|
+
const sub = rest[0];
|
|
383
|
+
if (sub === "start")
|
|
384
|
+
return { type: "akm_workflow_start", command, args: argv, assetRef: rest[1] };
|
|
385
|
+
if (sub === "next")
|
|
386
|
+
return { type: "akm_workflow_next", command, args: argv, assetRef: rest[1] };
|
|
387
|
+
if (sub === "complete")
|
|
388
|
+
return { type: "akm_workflow_complete", command, args: argv };
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
357
391
|
default:
|
|
358
392
|
return null;
|
|
359
393
|
}
|
|
@@ -59,6 +59,7 @@ describe("CLI error handling", () => {
|
|
|
59
59
|
const parsed = JSON.parse(stderr.trim());
|
|
60
60
|
expect(parsed.ok).toBe(false);
|
|
61
61
|
expect(typeof parsed.error).toBe("string");
|
|
62
|
+
expect(parsed.code).toBe("INVALID_FLAG_VALUE");
|
|
62
63
|
});
|
|
63
64
|
test("search --source invalid prints hint about source", () => {
|
|
64
65
|
const { stderr, status } = runCli("search", "test", "--source", "invalid");
|
|
@@ -5,6 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { akmHistory } from "../../src/commands/history";
|
|
7
7
|
import { saveConfig } from "../../src/core/config";
|
|
8
|
+
import { appendEvent } from "../../src/core/events";
|
|
8
9
|
import { getDbPath } from "../../src/core/paths";
|
|
9
10
|
import { closeDatabase, openDatabase } from "../../src/indexer/db";
|
|
10
11
|
import { akmIndex } from "../../src/indexer/indexer";
|
|
@@ -221,3 +222,197 @@ describe("akm history CLI", () => {
|
|
|
221
222
|
expect(typeof parsed.error).toBe("string");
|
|
222
223
|
});
|
|
223
224
|
});
|
|
225
|
+
describe("akmHistory --include-proposals", () => {
|
|
226
|
+
test("sources field is ['usage_events'] by default", async () => {
|
|
227
|
+
const db = openDatabase(":memory:");
|
|
228
|
+
try {
|
|
229
|
+
ensureUsageEventsSchema(db);
|
|
230
|
+
const result = await akmHistory({ db });
|
|
231
|
+
expect(result.sources).toEqual(["usage_events"]);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
closeDatabase(db);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
test("sources field includes events.jsonl when includeProposals is true", async () => {
|
|
238
|
+
const eventsFile = path.join(makeTempDir("akm-history-events-"), "events.jsonl");
|
|
239
|
+
const db = openDatabase(":memory:");
|
|
240
|
+
try {
|
|
241
|
+
ensureUsageEventsSchema(db);
|
|
242
|
+
const result = await akmHistory({
|
|
243
|
+
db,
|
|
244
|
+
includeProposals: true,
|
|
245
|
+
eventsCtx: { filePath: eventsFile },
|
|
246
|
+
});
|
|
247
|
+
expect(result.sources).toEqual(["usage_events", "events.jsonl"]);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
closeDatabase(db);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
test("proposal accept event (promoted) appears in history with --include-proposals", async () => {
|
|
254
|
+
const eventsFile = path.join(makeTempDir("akm-history-proposal-"), "events.jsonl");
|
|
255
|
+
const db = openDatabase(":memory:");
|
|
256
|
+
try {
|
|
257
|
+
ensureUsageEventsSchema(db);
|
|
258
|
+
// Simulate `akm proposal accept` emitting a promoted event.
|
|
259
|
+
appendEvent({
|
|
260
|
+
eventType: "promoted",
|
|
261
|
+
ref: "skill:deploy",
|
|
262
|
+
metadata: { proposalId: "prop-001", source: "reflect", assetPath: "/stash/skills/deploy.md" },
|
|
263
|
+
}, { filePath: eventsFile });
|
|
264
|
+
const result = await akmHistory({
|
|
265
|
+
db,
|
|
266
|
+
includeProposals: true,
|
|
267
|
+
eventsCtx: { filePath: eventsFile },
|
|
268
|
+
});
|
|
269
|
+
expect(result.totalCount).toBe(1);
|
|
270
|
+
const promoted = result.entries.find((e) => e.eventType === "promoted");
|
|
271
|
+
expect(promoted).toBeDefined();
|
|
272
|
+
expect(promoted?.ref).toBe("skill:deploy");
|
|
273
|
+
expect(promoted?.eventType).toBe("promoted");
|
|
274
|
+
// Metadata from the proposal should be accessible.
|
|
275
|
+
expect(promoted?.metadata?.proposalId).toBe("prop-001");
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
closeDatabase(db);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
test("proposal reject event (rejected) appears in history with --include-proposals", async () => {
|
|
282
|
+
const eventsFile = path.join(makeTempDir("akm-history-reject-"), "events.jsonl");
|
|
283
|
+
const db = openDatabase(":memory:");
|
|
284
|
+
try {
|
|
285
|
+
ensureUsageEventsSchema(db);
|
|
286
|
+
appendEvent({
|
|
287
|
+
eventType: "rejected",
|
|
288
|
+
ref: "memory:old-draft",
|
|
289
|
+
metadata: { proposalId: "prop-002", source: "reflect", reason: "outdated" },
|
|
290
|
+
}, { filePath: eventsFile });
|
|
291
|
+
const result = await akmHistory({
|
|
292
|
+
db,
|
|
293
|
+
includeProposals: true,
|
|
294
|
+
eventsCtx: { filePath: eventsFile },
|
|
295
|
+
});
|
|
296
|
+
expect(result.totalCount).toBe(1);
|
|
297
|
+
const rejected = result.entries.find((e) => e.eventType === "rejected");
|
|
298
|
+
expect(rejected).toBeDefined();
|
|
299
|
+
expect(rejected?.ref).toBe("memory:old-draft");
|
|
300
|
+
expect(rejected?.metadata?.reason).toBe("outdated");
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
closeDatabase(db);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
test("non-proposal events in events.jsonl are excluded even with --include-proposals", async () => {
|
|
307
|
+
const eventsFile = path.join(makeTempDir("akm-history-filter-"), "events.jsonl");
|
|
308
|
+
const db = openDatabase(":memory:");
|
|
309
|
+
try {
|
|
310
|
+
ensureUsageEventsSchema(db);
|
|
311
|
+
// These event types should NOT appear in history even with --include-proposals.
|
|
312
|
+
appendEvent({ eventType: "add", ref: "skill:deploy" }, { filePath: eventsFile });
|
|
313
|
+
appendEvent({ eventType: "reflect_invoked", ref: "memory:alpha" }, { filePath: eventsFile });
|
|
314
|
+
// Only this one should appear.
|
|
315
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p1", source: "reflect" } }, { filePath: eventsFile });
|
|
316
|
+
const result = await akmHistory({
|
|
317
|
+
db,
|
|
318
|
+
includeProposals: true,
|
|
319
|
+
eventsCtx: { filePath: eventsFile },
|
|
320
|
+
});
|
|
321
|
+
expect(result.entries.every((e) => e.eventType === "promoted" || e.eventType === "rejected")).toBe(true);
|
|
322
|
+
expect(result.entries.length).toBe(1);
|
|
323
|
+
expect(result.entries[0]?.eventType).toBe("promoted");
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
closeDatabase(db);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
test("usage events and proposal events are merged chronologically", async () => {
|
|
330
|
+
const eventsFile = path.join(makeTempDir("akm-history-merge-"), "events.jsonl");
|
|
331
|
+
const db = openDatabase(":memory:");
|
|
332
|
+
try {
|
|
333
|
+
ensureUsageEventsSchema(db);
|
|
334
|
+
// Insert usage events with explicit timestamps (early, then late).
|
|
335
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("show", "skill:deploy", 1, "2026-01-01 10:00:00");
|
|
336
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("feedback", "skill:deploy", 1, "2026-01-03 12:00:00");
|
|
337
|
+
// Append a proposal event between the two usage events.
|
|
338
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p3", source: "reflect" } }, {
|
|
339
|
+
filePath: eventsFile,
|
|
340
|
+
now: () => new Date("2026-01-02T09:00:00Z").getTime(),
|
|
341
|
+
});
|
|
342
|
+
const result = await akmHistory({
|
|
343
|
+
db,
|
|
344
|
+
includeProposals: true,
|
|
345
|
+
eventsCtx: { filePath: eventsFile },
|
|
346
|
+
});
|
|
347
|
+
expect(result.totalCount).toBe(3);
|
|
348
|
+
const types = result.entries.map((e) => e.eventType);
|
|
349
|
+
// Chronological: show (Jan 1), promoted (Jan 2), feedback (Jan 3)
|
|
350
|
+
expect(types).toEqual(["show", "promoted", "feedback"]);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
closeDatabase(db);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
test("--include-proposals ref filter shows only matching ref events", async () => {
|
|
357
|
+
const eventsFile = path.join(makeTempDir("akm-history-ref-filter-"), "events.jsonl");
|
|
358
|
+
const db = openDatabase(":memory:");
|
|
359
|
+
try {
|
|
360
|
+
ensureUsageEventsSchema(db);
|
|
361
|
+
// Two proposal events for different refs.
|
|
362
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p1", source: "reflect" } }, { filePath: eventsFile });
|
|
363
|
+
appendEvent({ eventType: "rejected", ref: "memory:draft", metadata: { proposalId: "p2", source: "reflect" } }, { filePath: eventsFile });
|
|
364
|
+
const result = await akmHistory({
|
|
365
|
+
db,
|
|
366
|
+
ref: "skill:deploy",
|
|
367
|
+
includeProposals: true,
|
|
368
|
+
eventsCtx: { filePath: eventsFile },
|
|
369
|
+
});
|
|
370
|
+
// Only the promoted event for skill:deploy should appear.
|
|
371
|
+
expect(result.ref).toBe("skill:deploy");
|
|
372
|
+
expect(result.entries.every((e) => e.ref === "skill:deploy")).toBe(true);
|
|
373
|
+
const promoted = result.entries.find((e) => e.eventType === "promoted");
|
|
374
|
+
expect(promoted).toBeDefined();
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
closeDatabase(db);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
test("akm history --include-proposals CLI flag surfaces proposal lifecycle events", async () => {
|
|
381
|
+
const stashDir = makeTempDir("akm-history-cli-proposals-");
|
|
382
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
383
|
+
const cacheDir = makeTempDir("akm-history-cli-cache-");
|
|
384
|
+
process.env.XDG_CACHE_HOME = cacheDir;
|
|
385
|
+
saveConfig({ semanticSearchMode: "off" });
|
|
386
|
+
writeFile(path.join(stashDir, "memories", "alpha.md"), "---\ndescription: alpha memory\n---\nAlpha.\n");
|
|
387
|
+
await akmIndex({ stashDir, full: true });
|
|
388
|
+
// We can't easily run a full accept without a real proposal, so instead
|
|
389
|
+
// write a promoted event directly to events.jsonl to verify the CLI flag.
|
|
390
|
+
const eventsFile = path.join(cacheDir, "akm", "events.jsonl");
|
|
391
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
392
|
+
const promoted = {
|
|
393
|
+
schemaVersion: 1,
|
|
394
|
+
ts: new Date().toISOString(),
|
|
395
|
+
eventType: "promoted",
|
|
396
|
+
ref: "memory:alpha",
|
|
397
|
+
metadata: { proposalId: "p-cli-test", source: "reflect", assetPath: "memories/alpha.md" },
|
|
398
|
+
};
|
|
399
|
+
fs.appendFileSync(eventsFile, `${JSON.stringify(promoted)}\n`);
|
|
400
|
+
const result = runCli(["history", "--include-proposals", "--ref", "memory:alpha", "--format=json"]);
|
|
401
|
+
expect(result.status).toBe(0);
|
|
402
|
+
const parsed = parseJsonOutput(result);
|
|
403
|
+
const entries = parsed.entries;
|
|
404
|
+
expect(Array.isArray(entries)).toBe(true);
|
|
405
|
+
// The promoted event should appear.
|
|
406
|
+
const promotedEntry = entries.find((e) => e.eventType === "promoted");
|
|
407
|
+
expect(promotedEntry).toBeDefined();
|
|
408
|
+
expect(promotedEntry?.ref).toBe("memory:alpha");
|
|
409
|
+
// Sources should include events.jsonl.
|
|
410
|
+
expect(Array.isArray(parsed.sources)).toBe(true);
|
|
411
|
+
expect(parsed.sources.includes("events.jsonl")).toBe(true);
|
|
412
|
+
// Verify text output also shows the proposal event.
|
|
413
|
+
const text = runCli(["history", "--include-proposals", "--ref", "memory:alpha", "--format=text"]);
|
|
414
|
+
expect(text.status).toBe(0);
|
|
415
|
+
expect(text.stdout).toContain("[promoted]");
|
|
416
|
+
expect(text.stdout).toContain("events.jsonl");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -267,6 +267,31 @@ describe("loadConfig", () => {
|
|
|
267
267
|
expect(hint).toContain("akm remove");
|
|
268
268
|
}
|
|
269
269
|
});
|
|
270
|
+
test("throws ConfigError when config contains legacy stashes[]", () => {
|
|
271
|
+
writeRawConfig(getConfigPath(), JSON.stringify({
|
|
272
|
+
stashes: [{ type: "filesystem", path: "/legacy-stash" }],
|
|
273
|
+
}));
|
|
274
|
+
expect(() => loadConfig()).toThrow(ConfigError);
|
|
275
|
+
expect(() => loadConfig()).toThrow("legacy `stashes[]` config key");
|
|
276
|
+
});
|
|
277
|
+
test("throws ConfigError when installed npm entry is marked writable", () => {
|
|
278
|
+
writeRawConfig(getConfigPath(), JSON.stringify({
|
|
279
|
+
installed: [
|
|
280
|
+
{
|
|
281
|
+
id: "npm:left-pad",
|
|
282
|
+
source: "npm",
|
|
283
|
+
ref: "npm:left-pad",
|
|
284
|
+
artifactUrl: "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
|
|
285
|
+
stashRoot: "/tmp/left-pad",
|
|
286
|
+
cacheDir: "/tmp/cache",
|
|
287
|
+
installedAt: "2026-05-01T00:00:00.000Z",
|
|
288
|
+
writable: true,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
}));
|
|
292
|
+
expect(() => loadConfig()).toThrow(ConfigError);
|
|
293
|
+
expect(() => loadConfig()).toThrow("writable: true is only supported on filesystem and git sources");
|
|
294
|
+
});
|
|
270
295
|
test("recomputes merged config when cwd changes", () => {
|
|
271
296
|
const firstProject = makeTmpDir();
|
|
272
297
|
const secondProject = makeTmpDir();
|
package/dist/tests/e2e.test.js
CHANGED
|
@@ -500,8 +500,28 @@ describe("Scenario: CLI subprocess execution", () => {
|
|
|
500
500
|
expect(json.hits.length).toBeGreaterThan(0);
|
|
501
501
|
expect(json.hits.every((h) => h.type === "knowledge")).toBe(true);
|
|
502
502
|
});
|
|
503
|
+
// Issue 9: empty query must list all assets rather than throwing UsageError.
|
|
504
|
+
// The CLI contract (--help text) promises omitting the query returns all
|
|
505
|
+
// indexed assets. The akmSearch function handles empty queries end-to-end
|
|
506
|
+
// via getAllEntries (DB path) and the substring-fallback's query-less branch.
|
|
507
|
+
test("cli: akm search with no query lists all assets (empty-query listing)", async () => {
|
|
508
|
+
const result = runCli("search");
|
|
509
|
+
expect(result.exitCode).toBe(0);
|
|
510
|
+
const json = parseJson(result.stdout);
|
|
511
|
+
expect(json.hits).toBeInstanceOf(Array);
|
|
512
|
+
expect(json.hits.length).toBeGreaterThan(0);
|
|
513
|
+
// Brief output: ref is NOT present (only full/agent levels include ref)
|
|
514
|
+
expect(json.hits.every((h) => h.type !== undefined)).toBe(true);
|
|
515
|
+
expect(json.hits.every((h) => h.name !== undefined)).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
test("cli: akm search with no query and --type filters by type", async () => {
|
|
518
|
+
const result = runCli("search", "--type", "skill");
|
|
519
|
+
expect(result.exitCode).toBe(0);
|
|
520
|
+
const json = parseJson(result.stdout);
|
|
521
|
+
expect(json.hits).toBeInstanceOf(Array);
|
|
522
|
+
expect(json.hits.every((h) => h.type === "skill")).toBe(true);
|
|
523
|
+
});
|
|
503
524
|
test("cli: akm search --limit 2 respects limit", async () => {
|
|
504
|
-
// QA #14: empty query now throws UsageError (exit 2); use a real query
|
|
505
525
|
const result = runCli("search", "docker", "--limit", "2");
|
|
506
526
|
expect(result.exitCode).toBe(0);
|
|
507
527
|
const json = parseJson(result.stdout);
|
|
@@ -513,7 +533,8 @@ describe("Scenario: CLI subprocess execution", () => {
|
|
|
513
533
|
const json = parseJson(result.stdout);
|
|
514
534
|
expect(json.source).toBeUndefined();
|
|
515
535
|
expect(json.timing).toBeUndefined();
|
|
516
|
-
|
|
536
|
+
// REC-03: ref is now included at brief detail so agents can run `akm show <ref>`
|
|
537
|
+
expect(json.hits.every((h) => h.ref !== undefined)).toBe(true);
|
|
517
538
|
expect(json.hits.every((h) => h.type !== undefined)).toBe(true);
|
|
518
539
|
expect(json.hits.every((h) => h.name !== undefined)).toBe(true);
|
|
519
540
|
expect(json.hits.every((h) => h.action !== undefined)).toBe(true);
|
|
@@ -120,7 +120,7 @@ export function loadFixtureStash(name, options = {}) {
|
|
|
120
120
|
process.env.AKM_STASH_DIR = priorAkmStashDir;
|
|
121
121
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
122
122
|
};
|
|
123
|
-
return { stashDir, cleanup, contentHash };
|
|
123
|
+
return { stashDir, cleanup, contentHash, ...(!options.skipIndex ? { indexCacheHome: cacheHome } : {}) };
|
|
124
124
|
}
|
|
125
125
|
// ── Internals ───────────────────────────────────────────────────────────────
|
|
126
126
|
function fixtureSourceDir(name) {
|
|
@@ -81,8 +81,17 @@ describe("fixtureContentHash", () => {
|
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
describe("listFixtures", () => {
|
|
84
|
-
test("returns all
|
|
84
|
+
test("returns all shipped fixtures, sorted", () => {
|
|
85
85
|
const names = listFixtures();
|
|
86
|
-
expect(names).toEqual([
|
|
86
|
+
expect(names).toEqual([
|
|
87
|
+
"az-cli",
|
|
88
|
+
"docker-homelab",
|
|
89
|
+
"drillbit",
|
|
90
|
+
"inkwell",
|
|
91
|
+
"minimal",
|
|
92
|
+
"multi-domain",
|
|
93
|
+
"noisy",
|
|
94
|
+
"ranking-baseline",
|
|
95
|
+
]);
|
|
87
96
|
});
|
|
88
97
|
});
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, expect, mock, spyOn, test } from "bun:test";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { saveConfig } from "../src/core/config";
|
|
5
6
|
import { getDbPath } from "../src/core/paths";
|
|
6
7
|
import { closeDatabase, DB_VERSION, getAllEntries, getEmbeddingCount, getMeta, openDatabase } from "../src/indexer/db";
|
|
7
8
|
import { akmIndex, buildFileBasenameMap, matchEntryToFile } from "../src/indexer/indexer";
|
|
@@ -13,7 +14,7 @@ let embedBatchImpl;
|
|
|
13
14
|
const actualEmbedBatch = embedderModule.embedBatch;
|
|
14
15
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
15
16
|
const originalXdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
16
|
-
mock.module("../src/embedder.js", () => ({
|
|
17
|
+
mock.module("../src/llm/embedder.js", () => ({
|
|
17
18
|
...embedderModule,
|
|
18
19
|
embedBatch: (texts, embeddingConfig) => embedBatchImpl ? embedBatchImpl(texts, embeddingConfig) : actualEmbedBatch(texts, embeddingConfig),
|
|
19
20
|
}));
|
|
@@ -138,6 +139,15 @@ test("akmIndex handles markdown assets", async () => {
|
|
|
138
139
|
const result = await akmIndex({ stashDir });
|
|
139
140
|
expect(result.totalEntries).toBe(2);
|
|
140
141
|
});
|
|
142
|
+
test("akmIndex classifies flat markdown files under skills/ as skill assets", async () => {
|
|
143
|
+
const stashDir = tmpStash();
|
|
144
|
+
writeFile(path.join(stashDir, "skills", "deploy.md"), "---\ndescription: Deploy skill\n---\n# Deploy\n");
|
|
145
|
+
await akmIndex({ stashDir, full: true });
|
|
146
|
+
const db = openDatabase();
|
|
147
|
+
const skillEntry = getAllEntries(db).find((row) => row.entry.type === "skill" && row.entry.name === "deploy");
|
|
148
|
+
expect(skillEntry).toBeDefined();
|
|
149
|
+
closeDatabase(db);
|
|
150
|
+
});
|
|
141
151
|
test("akmIndex includes wiki raw files but excludes infrastructure files from the primary stash index", async () => {
|
|
142
152
|
const stashDir = tmpStash();
|
|
143
153
|
writeFile(path.join(stashDir, "wikis", "research", "schema.md"), "---\ndescription: Schema\n---\n# Schema\n");
|
|
@@ -511,6 +521,7 @@ test("usage_events are re-linked after full reindex", async () => {
|
|
|
511
521
|
test("incremental reindex clears embeddings when provider fingerprint changes", async () => {
|
|
512
522
|
const stashDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-fp-"));
|
|
513
523
|
process.env.AKM_STASH_DIR = stashDir;
|
|
524
|
+
saveConfig({ semanticSearchMode: "auto" });
|
|
514
525
|
embedBatchImpl = async (texts) => texts.map((_text, index) => {
|
|
515
526
|
const embedding = new Float32Array(384);
|
|
516
527
|
embedding[0] = index + 1;
|
|
@@ -83,7 +83,8 @@ describe("output baseline", () => {
|
|
|
83
83
|
const json = JSON.parse(output);
|
|
84
84
|
// hits is always present; warnings may appear when semantic search is pending
|
|
85
85
|
expect(Object.keys(json)).toContain("hits");
|
|
86
|
-
|
|
86
|
+
// REC-03: ref is now included at brief detail so agents can run `akm show <ref>`
|
|
87
|
+
expect(Object.keys(json.hits[0] ?? {}).sort()).toEqual(["action", "estimatedTokens", "name", "ref", "type"]);
|
|
87
88
|
});
|
|
88
89
|
test("search normal detail includes description capped at 250 characters", () => {
|
|
89
90
|
const stashDir = makeTempDir("akm-output-stash-");
|
|
@@ -69,10 +69,12 @@ describe("shapeSearchHit — local stash hits", () => {
|
|
|
69
69
|
tags: ["ops"],
|
|
70
70
|
whyMatched: "name match",
|
|
71
71
|
};
|
|
72
|
-
test("brief keeps only type/name/action/estimatedTokens", () => {
|
|
72
|
+
test("brief keeps only type/name/ref/action/estimatedTokens", () => {
|
|
73
|
+
// REC-03: ref is now included at brief so agents can run `akm show <ref>`
|
|
73
74
|
expect(shapeSearchHit(fullHit, "brief")).toEqual({
|
|
74
75
|
type: "skill",
|
|
75
76
|
name: "deploy",
|
|
77
|
+
ref: "skill:deploy",
|
|
76
78
|
action: "akm show skill:deploy",
|
|
77
79
|
estimatedTokens: 120,
|
|
78
80
|
});
|
|
@@ -3,7 +3,7 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { buildRegistryIndex } from "../src/registry/build-index";
|
|
6
|
+
import { buildRegistryIndex, writeRegistryIndex } from "../src/registry/build-index";
|
|
7
7
|
const CLI = path.join(import.meta.dir, "..", "src", "cli.ts");
|
|
8
8
|
const tempDirs = [];
|
|
9
9
|
const servers = [];
|
|
@@ -105,6 +105,22 @@ afterAll(() => {
|
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
describe("buildRegistryIndex", () => {
|
|
108
|
+
test("writeRegistryIndex defaults under the cache registry-build directory", () => {
|
|
109
|
+
const cacheHome = makeTempDir("akm-registry-cache-");
|
|
110
|
+
const originalCacheHome = process.env.XDG_CACHE_HOME;
|
|
111
|
+
process.env.XDG_CACHE_HOME = cacheHome;
|
|
112
|
+
try {
|
|
113
|
+
const outPath = writeRegistryIndex({ version: 3, updatedAt: "2026-05-01T00:00:00.000Z", stashes: [] });
|
|
114
|
+
expect(outPath).toBe(path.join(cacheHome, "akm", "registry-build", "index.json"));
|
|
115
|
+
expect(fs.existsSync(outPath)).toBe(true);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
if (originalCacheHome === undefined)
|
|
119
|
+
delete process.env.XDG_CACHE_HOME;
|
|
120
|
+
else
|
|
121
|
+
process.env.XDG_CACHE_HOME = originalCacheHome;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
108
124
|
test("builds a v2 index from discovery and manual entries", async () => {
|
|
109
125
|
const fixtureRoot = makeTempDir("akm-registry-build-fixture-");
|
|
110
126
|
const npmPackageDir = path.join(fixtureRoot, "package");
|
|
@@ -201,4 +201,38 @@ describe("StaticIndexProvider", () => {
|
|
|
201
201
|
expect(hit?.installRef).toBe("github:vercel-labs/agent-skills");
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
|
+
describe("registry version contract", () => {
|
|
205
|
+
test("version 3 index parses without warnings (canonical format)", async () => {
|
|
206
|
+
const srv = serveJson(FIXTURE_INDEX); // FIXTURE_INDEX has version: 3
|
|
207
|
+
const provider = makeProvider(srv.url);
|
|
208
|
+
const result = await provider.search({ query: "agent", limit: 10 });
|
|
209
|
+
expect(result.warnings ?? []).toHaveLength(0);
|
|
210
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
211
|
+
});
|
|
212
|
+
test("version 2 index parses without warnings (live official registry format)", async () => {
|
|
213
|
+
const v2Index = { ...FIXTURE_INDEX, version: 2 };
|
|
214
|
+
const srv = serveJson(v2Index);
|
|
215
|
+
const provider = makeProvider(srv.url);
|
|
216
|
+
const result = await provider.search({ query: "agent", limit: 10 });
|
|
217
|
+
expect(result.warnings ?? []).toHaveLength(0);
|
|
218
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
219
|
+
});
|
|
220
|
+
test("version 2 index returns correct kit hits", async () => {
|
|
221
|
+
const v2Index = { ...FIXTURE_INDEX, version: 2 };
|
|
222
|
+
const srv = serveJson(v2Index);
|
|
223
|
+
const provider = makeProvider(srv.url);
|
|
224
|
+
const kits = await provider.searchKits({ text: "agent", limit: 10 });
|
|
225
|
+
expect(kits.length).toBeGreaterThan(0);
|
|
226
|
+
expect(kits.some((k) => k.id === "github:vercel-labs/agent-skills")).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
test("version 1 index returns null (unsupported)", async () => {
|
|
229
|
+
// version 1 is explicitly unsupported per schema comment
|
|
230
|
+
const v1Index = { ...FIXTURE_INDEX, version: 1 };
|
|
231
|
+
const srv = serveJson(v1Index);
|
|
232
|
+
const provider = makeProvider(srv.url);
|
|
233
|
+
const result = await provider.search({ query: "agent", limit: 10 });
|
|
234
|
+
// No hits because the parser returns null for unsupported versions
|
|
235
|
+
expect(result.hits).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
204
238
|
});
|