akm-cli 0.9.12 → 0.9.13
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 +61 -0
- package/dist/assets/workflows/workflow-template.md +4 -0
- package/dist/commands/improve/improve.js +1 -0
- package/dist/commands/lint/base-linter.js +10 -0
- package/dist/commands/proposal/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- package/dist/core/config/config-walker.js +7 -3
- package/dist/core/config/config.js +21 -12
- package/dist/core/config/schema/primitives.js +8 -2
- package/dist/core/errors.js +2 -0
- package/dist/llm/client.js +12 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/output/shapes.js +46 -1
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/scripts/akm-migrate-node.js +323 -172
- package/dist/scripts/akm-migrate.js +323 -172
- package/dist/storage/repositories/workflow-runs-repository.js +118 -10
- package/dist/workflows/exec/run-workflow.js +1 -1
- package/dist/workflows/exec/step-work.js +41 -0
- package/dist/workflows/parser.js +1 -1
- package/dist/workflows/runtime/runs.js +29 -5
- package/docs/reference/cli.md +18 -0
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
6
6
|
import { openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
|
|
7
7
|
import { borrowScopedStateDb, withStateDbScope } from "../../core/state-db-scope.js";
|
|
8
|
+
import { sleepSync } from "../../runtime.js";
|
|
8
9
|
import { escapeLikePattern } from "../like-pattern.js";
|
|
9
10
|
import { resolveStorageLocations } from "../locations.js";
|
|
10
11
|
import { insertEventOnce, insertEventStrict } from "./events-repository.js";
|
|
@@ -21,6 +22,51 @@ function assertAttemptReservationLease(input, run) {
|
|
|
21
22
|
throw new UsageError(`Workflow run ${input.runId} engine lease expired before durable dispatch reservation.`, "RESOURCE_ALREADY_EXISTS");
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether `error` is one of the specific SQLite conditions a run-lease
|
|
27
|
+
* statement can throw under real cross-process contention on the same row:
|
|
28
|
+
* `SQLITE_BUSY`/`SQLITE_LOCKED` (both drivers), or the message text a
|
|
29
|
+
* transient contention blip has been observed producing, "database is
|
|
30
|
+
* locked", "disk I/O error", or "database disk image is malformed".
|
|
31
|
+
* Matching on this set alone is never sufficient to call something lease
|
|
32
|
+
* contention — see {@link WorkflowRunsRepository.acquireEngineLease}, which
|
|
33
|
+
* additionally requires a fresh read confirming a live lease before
|
|
34
|
+
* substituting the lease-held message for the original error.
|
|
35
|
+
*/
|
|
36
|
+
function isLeaseContentionSqliteError(error) {
|
|
37
|
+
const code = error?.code;
|
|
38
|
+
if (code === "SQLITE_BUSY" || code === "SQLITE_LOCKED")
|
|
39
|
+
return true;
|
|
40
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
+
return (message.includes("database is locked") ||
|
|
42
|
+
message.includes("disk I/O error") ||
|
|
43
|
+
message.includes("database disk image is malformed"));
|
|
44
|
+
}
|
|
45
|
+
const LEASE_RETRY_ATTEMPTS = 4;
|
|
46
|
+
const LEASE_RETRY_BASE_DELAY_MS = 15;
|
|
47
|
+
/**
|
|
48
|
+
* Retry a single lease statement across a short, bounded set of attempts when
|
|
49
|
+
* it throws one of {@link isLeaseContentionSqliteError}'s conditions —
|
|
50
|
+
* absorbing a blip that a fresh attempt on the same connection clears on its
|
|
51
|
+
* own. Any other error, or the same error surviving every attempt, propagates
|
|
52
|
+
* unchanged; this never converts a persistent failure into a false success.
|
|
53
|
+
*/
|
|
54
|
+
function runLeaseStatementWithRetry(fn) {
|
|
55
|
+
let lastError;
|
|
56
|
+
for (let attempt = 0; attempt < LEASE_RETRY_ATTEMPTS; attempt += 1) {
|
|
57
|
+
try {
|
|
58
|
+
return fn();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!isLeaseContentionSqliteError(error))
|
|
62
|
+
throw error;
|
|
63
|
+
lastError = error;
|
|
64
|
+
if (attempt < LEASE_RETRY_ATTEMPTS - 1)
|
|
65
|
+
sleepSync(LEASE_RETRY_BASE_DELAY_MS * 2 ** attempt);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw lastError;
|
|
69
|
+
}
|
|
24
70
|
/**
|
|
25
71
|
* Repository owning every raw SQL statement against `workflow_runs` and
|
|
26
72
|
* `workflow_run_steps`. It is DB-location-agnostic: the lifecycle helper
|
|
@@ -365,25 +411,57 @@ export class WorkflowRunsRepository {
|
|
|
365
411
|
* A live lease held by anyone (including a stale copy of the same holder)
|
|
366
412
|
* is NOT reclaimable through this method; the single UPDATE is the whole
|
|
367
413
|
* claim, so two racing invocations cannot both win.
|
|
414
|
+
*
|
|
415
|
+
* The UPDATE can throw instead of cleanly returning `changes: 0` under real
|
|
416
|
+
* cross-process contention on this row: a `SQLITE_BUSY`/`SQLITE_LOCKED`
|
|
417
|
+
* from two engines racing the same statement, occasionally surfacing as
|
|
418
|
+
* "database is locked" or even "database disk image is malformed" text that
|
|
419
|
+
* reads as corruption but is not. `runLeaseStatementWithRetry` absorbs a
|
|
420
|
+
* blip that a fresh attempt clears on its own. If it is still failing after
|
|
421
|
+
* every retry, the row is read fresh (a plain SELECT, far less likely to
|
|
422
|
+
* trip whatever the write hit) to get independent evidence of what is
|
|
423
|
+
* actually going on: a live lease there means this really was contention,
|
|
424
|
+
* so the caller gets the same lease-held message `akm workflow run` already
|
|
425
|
+
* shows for the clean (non-throwing) case, now with `RUN_LEASE_HELD`. No
|
|
426
|
+
* live lease — or the verifying read itself fails — means the error was
|
|
427
|
+
* never actually about the lease, so it is rethrown exactly as raised.
|
|
428
|
+
* Nothing here invents a diagnosis from error text alone or suppresses a
|
|
429
|
+
* genuine SQLite failure.
|
|
368
430
|
*/
|
|
369
431
|
acquireEngineLease(runId, holder, until, now) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
432
|
+
try {
|
|
433
|
+
const result = runLeaseStatementWithRetry(() => this.db
|
|
434
|
+
.prepare(`UPDATE workflow_runs
|
|
435
|
+
SET engine_lease_holder = ?, engine_lease_until = ?
|
|
436
|
+
WHERE id = ? AND status = 'active'
|
|
437
|
+
AND (engine_lease_holder IS NULL OR engine_lease_until IS NULL OR engine_lease_until < ?)`)
|
|
438
|
+
.run(holder, until, runId, now));
|
|
439
|
+
return Number(result.changes) > 0;
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
if (!isLeaseContentionSqliteError(error))
|
|
443
|
+
throw error;
|
|
444
|
+
const row = this.tryReadLeaseColumns(runId);
|
|
445
|
+
if (row?.engine_lease_holder && row.engine_lease_until && row.engine_lease_until >= now) {
|
|
446
|
+
throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row.engine_lease_holder} ` +
|
|
447
|
+
`(run lease expires ${row.engine_lease_until}). A second \`akm workflow run\` would race it — ` +
|
|
448
|
+
`wait for that invocation to finish or for the lease to expire.`, "RUN_LEASE_HELD");
|
|
449
|
+
}
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
377
452
|
}
|
|
378
453
|
/**
|
|
379
454
|
* Extend the lease expiry — only while `holder` still owns it. Returns
|
|
380
455
|
* false when the lease was lost (expired and claimed by another engine),
|
|
381
|
-
* so the caller can stop driving instead of racing the new owner.
|
|
456
|
+
* so the caller can stop driving instead of racing the new owner. Wrapped
|
|
457
|
+
* in the same transient-error retry as {@link acquireEngineLease}; a
|
|
458
|
+
* renewal that still fails after retries is rethrown as-is (no confirmed
|
|
459
|
+
* "lost lease" diagnosis to substitute, unlike the acquire case above).
|
|
382
460
|
*/
|
|
383
461
|
renewEngineLease(runId, holder, until) {
|
|
384
|
-
const result = this.db
|
|
462
|
+
const result = runLeaseStatementWithRetry(() => this.db
|
|
385
463
|
.prepare("UPDATE workflow_runs SET engine_lease_until = ? WHERE id = ? AND engine_lease_holder = ? AND status = 'active'")
|
|
386
|
-
.run(until, runId, holder);
|
|
464
|
+
.run(until, runId, holder));
|
|
387
465
|
return Number(result.changes) > 0;
|
|
388
466
|
}
|
|
389
467
|
/**
|
|
@@ -396,6 +474,36 @@ export class WorkflowRunsRepository {
|
|
|
396
474
|
.prepare("UPDATE workflow_runs SET engine_lease_holder = NULL, engine_lease_until = NULL WHERE id = ? AND engine_lease_holder = ? AND status <> 'failed'")
|
|
397
475
|
.run(runId, holder);
|
|
398
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Self-heal an engine lease its holder crashed without releasing: once
|
|
479
|
+
* `engine_lease_until` has passed, clear it so a read (`workflow status`,
|
|
480
|
+
* `workflow list`) stops reporting a run as engine-driven when the engine is
|
|
481
|
+
* long gone — mirroring the maintenance barrier's self-reclaim of a wedged
|
|
482
|
+
* sentinel (`tryAcquireMaintenanceBarrier`) rather than a bespoke mechanism.
|
|
483
|
+
* The WHERE clause repeats the exact (holder, until) snapshot the caller
|
|
484
|
+
* read, so a lease renewed or re-acquired in between never gets clobbered —
|
|
485
|
+
* same compare-and-swap shape as the claim above. Never touches a lease
|
|
486
|
+
* that is still live.
|
|
487
|
+
*/
|
|
488
|
+
reclaimExpiredEngineLease(runId, holder, until, now) {
|
|
489
|
+
if (until >= now)
|
|
490
|
+
return false;
|
|
491
|
+
const result = this.db
|
|
492
|
+
.prepare(`UPDATE workflow_runs
|
|
493
|
+
SET engine_lease_holder = NULL, engine_lease_until = NULL
|
|
494
|
+
WHERE id = ? AND engine_lease_holder = ? AND engine_lease_until = ? AND engine_lease_until < ?`)
|
|
495
|
+
.run(runId, holder, until, now);
|
|
496
|
+
return Number(result.changes) > 0;
|
|
497
|
+
}
|
|
498
|
+
/** Best-effort lease-column read used only to confirm genuine contention after {@link acquireEngineLease} exhausts its retries. `undefined` on any failure — never a diagnosis, just "couldn't confirm". */
|
|
499
|
+
tryReadLeaseColumns(runId) {
|
|
500
|
+
try {
|
|
501
|
+
return (this.db.prepare("SELECT engine_lease_holder, engine_lease_until FROM workflow_runs WHERE id = ?").get(runId) ?? undefined);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
399
507
|
// ── durable v4 append-only dispatch attempts (migration 022) ─────────────
|
|
400
508
|
getUnitAttempts(runId, unitId) {
|
|
401
509
|
return this.db
|
|
@@ -194,7 +194,7 @@ async function acquireRunLease(runId, holder) {
|
|
|
194
194
|
const row = repo.getRunById(runId);
|
|
195
195
|
throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
|
|
196
196
|
`(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
|
|
197
|
-
`wait for that invocation to finish or for the lease to expire
|
|
197
|
+
`wait for that invocation to finish or for the lease to expire.`, "RUN_LEASE_HELD");
|
|
198
198
|
}));
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
@@ -18,6 +18,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
18
18
|
import unitPreambleTemplate from "../../assets/prompts/workflow-unit-preamble.md" with { type: "text" };
|
|
19
19
|
import { UsageError } from "../../core/errors.js";
|
|
20
20
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
21
|
+
import { parseEmbeddedJsonResponse } from "../../core/parse.js";
|
|
21
22
|
import { canonicalInputJson, validateInputs } from "../../execution/input-contract.js";
|
|
22
23
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
23
24
|
import { canonicalJson } from "../ir/plan-hash.js";
|
|
@@ -599,6 +600,41 @@ export function validateStepArtifact(plan, evidence) {
|
|
|
599
600
|
return (`Step "${plan.stepId}" artifact failed validation against the step's declared output schema: ` +
|
|
600
601
|
`${errors.join("; ")}.`);
|
|
601
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Warn-only check of each successful unit's own promoted value against its
|
|
605
|
+
* template's declared `schema` (`unit.output`) — the one field a harness that
|
|
606
|
+
* cannot request structured output drops during lowering (`untranslated-field`,
|
|
607
|
+
* field `outputSchema`), after which nothing else ever compares the returned
|
|
608
|
+
* text to it. Unlike {@link validateStepArtifact} this never fails the step:
|
|
609
|
+
* a harness that DID honor the schema already returned a compliant `result`
|
|
610
|
+
* (this re-check then finds nothing), and one that could not is exactly the
|
|
611
|
+
* case this exists to surface — the run continues either way.
|
|
612
|
+
*/
|
|
613
|
+
export function unitSchemaWarning(plan, units) {
|
|
614
|
+
const schema = stepTemplate(plan)?.schema;
|
|
615
|
+
if (!schema)
|
|
616
|
+
return undefined;
|
|
617
|
+
const mismatches = [];
|
|
618
|
+
for (const unit of units) {
|
|
619
|
+
if (!unit.ok)
|
|
620
|
+
continue;
|
|
621
|
+
const candidate = unit.result !== undefined
|
|
622
|
+
? unit.result
|
|
623
|
+
: unit.text !== undefined
|
|
624
|
+
? parseEmbeddedJsonResponse(unit.text)
|
|
625
|
+
: undefined;
|
|
626
|
+
if (candidate === undefined) {
|
|
627
|
+
mismatches.push(`unit "${unit.unitId}" produced no structured output to check`);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const errors = validateJsonSchemaSubset(candidate, schema);
|
|
631
|
+
if (errors.length > 0)
|
|
632
|
+
mismatches.push(`unit "${unit.unitId}": ${errors.join("; ")}`);
|
|
633
|
+
}
|
|
634
|
+
if (mismatches.length === 0)
|
|
635
|
+
return undefined;
|
|
636
|
+
return `Output does not match the unit's declared schema (advisory; the run continued): ${mismatches.join("; ")}.`;
|
|
637
|
+
}
|
|
602
638
|
/**
|
|
603
639
|
* Build the summary the completion-criteria gate judges for a step (addendum
|
|
604
640
|
* R2, "typed artifacts, honest gates"): a one-line unit count followed by the
|
|
@@ -743,6 +779,11 @@ export function reduceStepOutcomes(plan, reducer, isFanOut, onError, units) {
|
|
|
743
779
|
artifactSchemaFailure = true;
|
|
744
780
|
}
|
|
745
781
|
}
|
|
782
|
+
if (!artifactSchemaFailure) {
|
|
783
|
+
const schemaWarning = unitSchemaWarning(plan, units);
|
|
784
|
+
if (schemaWarning !== undefined)
|
|
785
|
+
summary += ` ${schemaWarning}`;
|
|
786
|
+
}
|
|
746
787
|
// P3b §3.4: a composed child workflow that blocked is carried on the
|
|
747
788
|
// failed unit's LIVE-ONLY `childRun` field (child-workflow.ts's
|
|
748
789
|
// driveChildWorkflowUnit). Surfaced here, unconditionally on the unit
|
package/dist/workflows/parser.js
CHANGED
|
@@ -249,7 +249,7 @@ function bindStepSections(headings, lines, bodyStartLine, totalLines, path, decl
|
|
|
249
249
|
if (!declaredIds.has(h.text)) {
|
|
250
250
|
errors.push({
|
|
251
251
|
line: h.line,
|
|
252
|
-
message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line} — no step "${h.text}" is declared in frontmatter "steps:". Level-2 headings must exactly match a declared step id.`,
|
|
252
|
+
message: `Unexpected level-2 heading "## ${h.text}" on line ${h.line} — no step "${h.text}" is declared in frontmatter "steps:". Level-2 headings must exactly match a declared step id. To document something that is not a step, use a level-3 heading.`,
|
|
253
253
|
});
|
|
254
254
|
continue;
|
|
255
255
|
}
|
|
@@ -637,14 +637,33 @@ function readWorkflowRunOrPrefix(repo, specifier) {
|
|
|
637
637
|
const run = findRunByIdOrPrefix(repo, specifier);
|
|
638
638
|
if (!run)
|
|
639
639
|
throw new NotFoundError(`Workflow run "${specifier}" not found.`, "WORKFLOW_NOT_FOUND");
|
|
640
|
-
return run;
|
|
640
|
+
return reclaimOrphanedEngineLease(repo, run);
|
|
641
641
|
}
|
|
642
642
|
function readWorkflowRun(repo, runId) {
|
|
643
643
|
const run = repo.getRunById(runId);
|
|
644
644
|
if (!run) {
|
|
645
645
|
throw new NotFoundError(`Workflow run "${runId}" not found.`, "WORKFLOW_NOT_FOUND");
|
|
646
646
|
}
|
|
647
|
-
return run;
|
|
647
|
+
return reclaimOrphanedEngineLease(repo, run);
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Self-heal a run's engine lease once it has expired — the orphaned-lease
|
|
651
|
+
* case where an engine crashed without releasing it — mirroring the
|
|
652
|
+
* maintenance barrier's self-reclaim of a wedged sentinel, applied at the
|
|
653
|
+
* points a caller actually asks "what is this run's state". Never touches a
|
|
654
|
+
* live lease: {@link WorkflowRunsRepository.reclaimExpiredEngineLease} is a
|
|
655
|
+
* compare-and-swap on the exact (holder, until) this call observed, so a
|
|
656
|
+
* lease renewed or re-acquired between the read and this write is left alone.
|
|
657
|
+
*/
|
|
658
|
+
function reclaimOrphanedEngineLease(repo, run) {
|
|
659
|
+
const { engine_lease_holder: holder, engine_lease_until: until } = run;
|
|
660
|
+
if (!holder || !until)
|
|
661
|
+
return run;
|
|
662
|
+
const now = new Date().toISOString();
|
|
663
|
+
if (until >= now)
|
|
664
|
+
return run;
|
|
665
|
+
repo.reclaimExpiredEngineLease(run.id, holder, until, now);
|
|
666
|
+
return { ...run, engine_lease_holder: null, engine_lease_until: null };
|
|
648
667
|
}
|
|
649
668
|
function readWorkflowRunSteps(repo, runId) {
|
|
650
669
|
return repo.getStepsForRun(runId);
|
|
@@ -758,8 +777,13 @@ function toWorkflowRunSummary(run) {
|
|
|
758
777
|
executionSupport: plan.support,
|
|
759
778
|
// Surface the engine lease (holder id + expiry — never workflow-authored
|
|
760
779
|
// content) so `workflow run`/`status` show which native execution
|
|
761
|
-
// invocation currently holds the run lease.
|
|
762
|
-
|
|
780
|
+
// invocation currently holds the run lease. Gated on `until` still being
|
|
781
|
+
// in the future: a crashed engine's lease self-expires, and a run
|
|
782
|
+
// whose holder is provably gone must stop reading as engine-driven the
|
|
783
|
+
// instant that happens, not just once something next attempts to acquire
|
|
784
|
+
// it (`readWorkflowRun`/`readWorkflowRunOrPrefix` also reclaim the DB
|
|
785
|
+
// columns outright on the same condition).
|
|
786
|
+
...(run.engine_lease_holder && run.engine_lease_until && run.engine_lease_until >= new Date().toISOString()
|
|
763
787
|
? { engineLease: { holder: run.engine_lease_holder, until: run.engine_lease_until } }
|
|
764
788
|
: {}),
|
|
765
789
|
// P3b (spec §4.5): all three optional and conditionally spread, so every
|
|
@@ -786,7 +810,7 @@ function assertLeaseAllowsSpineAdvance(run, leaseHolder) {
|
|
|
786
810
|
return; // expired ⇒ claimable, not live
|
|
787
811
|
throw new UsageError(`Workflow run ${run.id} is being driven by engine ${run.engine_lease_holder} ` +
|
|
788
812
|
`(run lease expires ${run.engine_lease_until}). The engine owns the step spine while it runs — ` +
|
|
789
|
-
`wait for it to finish or for the lease to expire before advancing steps manually
|
|
813
|
+
`wait for it to finish or for the lease to expire before advancing steps manually.`, "RUN_LEASE_HELD");
|
|
790
814
|
}
|
|
791
815
|
function toWorkflowRunStepState(step) {
|
|
792
816
|
return {
|
package/docs/reference/cli.md
CHANGED
|
@@ -79,6 +79,24 @@ For local materialized assets, `editHint` is added only when `editable` is
|
|
|
79
79
|
or use `action` (or curate `followUp`). Registry-only results have no local
|
|
80
80
|
`path`, `editable`, or `editHint`.
|
|
81
81
|
|
|
82
|
+
### The `results` collection alias
|
|
83
|
+
|
|
84
|
+
Every list-returning command names its collection field differently —
|
|
85
|
+
`search` returns `hits`, `curate` returns `items`, `proposal list` returns
|
|
86
|
+
`proposals`, `bundle list` returns `sources`, `env list` returns `envs`,
|
|
87
|
+
`secret list` returns `secrets`, `registry list` returns `registries`,
|
|
88
|
+
`registry search` returns `hits`, `workflow list` returns `runs`,
|
|
89
|
+
`task history` returns `rows`, `log list` returns `events`. A caller that does
|
|
90
|
+
not already know each command's key cannot write one accessor across all of
|
|
91
|
+
them.
|
|
92
|
+
|
|
93
|
+
Every one of these commands also carries a `results` field — the identical
|
|
94
|
+
array, not a copy — alongside its semantic key, in every `--format`/`--detail`
|
|
95
|
+
combination and both `--shape human` (the default) and `--shape agent`. Code
|
|
96
|
+
written against a single command should keep using its semantic key for
|
|
97
|
+
clarity; code that needs to handle several list commands uniformly can read
|
|
98
|
+
`results` and never maintain a per-command lookup table.
|
|
99
|
+
|
|
82
100
|
### `--shape summary`
|
|
83
101
|
|
|
84
102
|
Valid **only on `akm show`**. Every other command rejects `--shape summary`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|