akm-cli 0.9.5 → 0.9.7
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 +295 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env-cli.js +1 -2
- package/dist/commands/env/secret-cli.js +1 -5
- package/dist/commands/feedback-cli.js +0 -4
- package/dist/commands/health/checks.js +0 -32
- package/dist/commands/health/surfaces.js +2 -2
- package/dist/commands/health.js +6 -15
- package/dist/commands/improve/autonomy-gate.js +1 -1
- package/dist/commands/improve/consolidate.js +25 -26
- package/dist/commands/improve/distill.js +2 -2
- package/dist/commands/improve/extract.js +8 -1
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/improve.js +3 -5
- package/dist/commands/improve/locks.js +13 -5
- package/dist/commands/improve/loop-stages.js +8 -9
- package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
- package/dist/commands/improve/memory/memory-improve.js +2 -1
- package/dist/commands/improve/outcome-loop.js +0 -6
- package/dist/commands/improve/preparation.js +1 -1
- package/dist/commands/improve/reflect.js +21 -10
- package/dist/commands/lint/base-linter.js +141 -18
- package/dist/commands/lint/index.js +21 -10
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
- package/dist/commands/read/curate.js +47 -0
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/sources/bundle-config-ops.js +9 -4
- package/dist/commands/sources/info.js +14 -10
- package/dist/commands/tasks/tasks.js +12 -16
- package/dist/commands/workflow-cli.js +2 -2
- package/dist/core/asset/asset-placement.js +13 -2
- package/dist/core/asset/frontmatter.js +116 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +61 -24
- package/dist/core/config/config-io.js +2 -2
- package/dist/core/config/config-sources.js +32 -2
- package/dist/core/config/config.js +1 -1
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/schema/engines.js +9 -7
- package/dist/core/config/schema/primitives.js +0 -5
- package/dist/core/config/schema/search.js +2 -1
- package/dist/core/file-lock.js +2 -1
- package/dist/core/maintenance-barrier.js +2 -14
- package/dist/core/paths.js +0 -3
- package/dist/core/redaction.js +2 -2
- package/dist/core/spawn-env.js +8 -12
- package/dist/core/state/migrations.js +1 -12
- package/dist/core/state-db.js +9 -27
- package/dist/core/write-source.js +12 -19
- package/dist/execution/directory-identity.js +36 -10
- package/dist/indexer/graph/graph-boost.js +0 -4
- package/dist/indexer/index-writer-lock.js +43 -24
- package/dist/indexer/index-written-assets.js +5 -6
- package/dist/indexer/indexer.js +2 -39
- package/dist/indexer/materialize-embeddings.js +85 -41
- package/dist/indexer/search/db-search.js +15 -48
- package/dist/indexer/search/ranking-contributors.js +0 -25
- package/dist/indexer/search/ranking.js +3 -13
- package/dist/integrations/agent/builder-shared.js +0 -25
- package/dist/integrations/agent/model-map.js +2 -60
- package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
- package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
- package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
- package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
- package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
- package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
- package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
- package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
- package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
- package/dist/integrations/session-logs/index.js +0 -9
- package/dist/llm/client.js +75 -42
- package/dist/llm/embedder.js +7 -3
- package/dist/llm/embedders/remote.js +141 -42
- package/dist/registry/network.js +5 -37
- package/dist/runtime.js +2 -10
- package/dist/scripts/akm-migrate-node.js +73 -133
- package/dist/scripts/akm-migrate.js +73 -133
- package/dist/setup/engine-config.js +2 -5
- package/dist/setup/registry-stash-loader.js +0 -8
- package/dist/setup/setup.js +9 -46
- package/dist/setup/steps/connection-shared.js +10 -13
- package/dist/sources/providers/git-install.js +1 -1
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/engines/sqlite-migrations.js +20 -1
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -15
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +15 -20
- package/dist/tasks/backends/schtasks.js +18 -8
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/run-native-task.js +8 -6
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/source/bounded-document.js +2 -4
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/tasks/source/task-source-v3-frozen.js +5 -7
- package/dist/tasks/source/task-source-v4.js +5 -10
- package/dist/tasks/source/task-to-v3.js +5 -10
- package/dist/tasks/source/task-to-v4.js +1 -4
- package/dist/tasks/source-v3.js +6 -6
- package/dist/workflows/exec/native-executor.js +21 -31
- package/dist/workflows/exec/run-workflow.js +5 -6
- package/dist/workflows/exec/scheduler.js +3 -19
- package/dist/workflows/exec/step-work.js +1 -4
- package/dist/workflows/exec/unit-dispatch.js +2 -2
- package/dist/workflows/exec/worktree.js +1 -13
- package/dist/workflows/freeze/targets/child-workflow.js +2 -10
- package/dist/workflows/ir/plan-hash.js +4 -6
- package/dist/workflows/ir/schema-v4.js +0 -12
- package/dist/workflows/ir/schema.js +20 -31
- package/dist/workflows/parser.js +11 -52
- package/dist/workflows/renderer.js +2 -3
- package/dist/workflows/resource-limits.js +11 -41
- package/dist/workflows/runtime/runs.js +3 -4
- package/dist/workflows/source-ir/schema.js +14 -30
- package/dist/workflows/validator.js +1 -7
- package/docs/reference/cli.md +5 -1
- package/docs/reference/configuration.md +1 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +4 -23
- package/schemas/akm-task.json +1 -2
- package/schemas/akm-workflow.json +1 -13
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/indexer/search/semantic-status.js +0 -142
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
import fs from "node:fs";
|
|
35
35
|
import path from "node:path";
|
|
36
36
|
import { isScalar, parseDocument } from "yaml";
|
|
37
|
-
import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
37
|
+
import { assetPathCandidatesForName, assetPathForName, stashDirFor } from "../../core/asset/asset-placement.js";
|
|
38
38
|
import { BUNDLE_REF_RE } from "../../core/asset/asset-ref.js";
|
|
39
|
-
import { spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
39
|
+
import { removeFrontmatterListValues, spliceFrontmatterLine } from "../../core/asset/frontmatter.js";
|
|
40
40
|
import { checkUnquotedDescriptionColon } from "../../core/asset/frontmatter-lint.js";
|
|
41
|
+
import { isArchivedRelPath } from "../../core/asset/memory-archive.js";
|
|
41
42
|
import { typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
42
43
|
import { localDateStamp } from "../../core/common.js";
|
|
43
44
|
import { findFenceRegions } from "./markdown-insertion.js";
|
|
@@ -191,6 +192,37 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
191
192
|
if (resolveRefPathInStash(relPath, refType, refName, root) !== null)
|
|
192
193
|
return true;
|
|
193
194
|
}
|
|
195
|
+
// #884: a memory pruned by `analyzeMemoryCleanup` was ARCHIVED, not deleted —
|
|
196
|
+
// its bytes and identity live on under `.akm/memory-cleanup/archive`. Inbound
|
|
197
|
+
// belief edges to it are satisfied, not dangling, so resolve the tombstone
|
|
198
|
+
// rather than reporting `missing-ref`. Checked only after every live location
|
|
199
|
+
// misses: a tombstone must never shadow a real file, and the scan then costs
|
|
200
|
+
// one directory read per root instead of one per ref.
|
|
201
|
+
//
|
|
202
|
+
// Existence ONLY. `resolveRefPathInStash` deliberately does NOT consult the
|
|
203
|
+
// archive: it hands back a path callers MUTATE (SPEC-5 `--supersedes`
|
|
204
|
+
// demotion), and writing into an archived tombstone would corrupt the audit
|
|
205
|
+
// record while leaving the live stash untouched.
|
|
206
|
+
return memoryArchiveHasRef(refType, refName, stashRoots);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* True when `(refType, refName)` names a memory that prune archived in any
|
|
210
|
+
* root. Mirrors `resolveRefPathInStash`'s candidate set so a ref that resolved
|
|
211
|
+
* through the `.derived.md` twin (#882) still resolves once archived.
|
|
212
|
+
*/
|
|
213
|
+
function memoryArchiveHasRef(refType, refName, stashRoots) {
|
|
214
|
+
if (refType !== "memory")
|
|
215
|
+
return false; // only memories are ever archived
|
|
216
|
+
const typeDir = stashDirFor(refType);
|
|
217
|
+
if (typeDir === undefined)
|
|
218
|
+
return false;
|
|
219
|
+
const candidates = assetPathCandidatesForName(refType, typeDir, refName);
|
|
220
|
+
for (const root of stashRoots) {
|
|
221
|
+
for (const candidate of candidates) {
|
|
222
|
+
if (isArchivedRelPath(candidate, root))
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
194
226
|
return false;
|
|
195
227
|
}
|
|
196
228
|
/**
|
|
@@ -198,8 +230,9 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
198
230
|
* the same reachability rules (in the same order) as
|
|
199
231
|
* {@link refExistsInAnyStash}, which delegates here. Returns the absolute path
|
|
200
232
|
* of the file that makes the ref "exist" — for a multi-file skill directory
|
|
201
|
-
* that is its `SKILL.md` primary
|
|
202
|
-
*
|
|
233
|
+
* that is its `SKILL.md` primary, for a `memory` ref its `.derived.md` twin
|
|
234
|
+
* when the plain `.md` is absent (#882, see `assetPathCandidatesForName`) —
|
|
235
|
+
* or `null` when the ref does not resolve in this root.
|
|
203
236
|
*
|
|
204
237
|
* Extracted for SPEC-5 (`--supersedes` demotion): write commands need the
|
|
205
238
|
* superseded asset's actual file to mutate, and forking a second resolver
|
|
@@ -207,10 +240,14 @@ export function refExistsInAnyStash(relPath, refType, refName, stashRoots) {
|
|
|
207
240
|
* (the contract pins `refToRelPath` + `refExistsInAnyStash`; this is the
|
|
208
241
|
* shared internal both build on).
|
|
209
242
|
*/
|
|
210
|
-
export function resolveRefPathInStash(relPath,
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
243
|
+
export function resolveRefPathInStash(relPath, refType, refName, root) {
|
|
244
|
+
const typeDir = stashDirFor(refType);
|
|
245
|
+
const candidates = typeDir === undefined ? [relPath] : assetPathCandidatesForName(refType, typeDir, refName);
|
|
246
|
+
for (const candidate of candidates) {
|
|
247
|
+
const absPath = path.join(root, candidate);
|
|
248
|
+
if (fs.existsSync(absPath))
|
|
249
|
+
return absPath;
|
|
250
|
+
}
|
|
214
251
|
return null;
|
|
215
252
|
}
|
|
216
253
|
/**
|
|
@@ -281,11 +318,31 @@ function scanBundleRefs(scanBody, allRoots) {
|
|
|
281
318
|
*/
|
|
282
319
|
function classifyConceptRef(rawConceptId, allRoots) {
|
|
283
320
|
const conceptId = rawConceptId.split("#", 1)[0];
|
|
284
|
-
const parts = typeNameFromConceptId(conceptId);
|
|
321
|
+
const parts = typeNameFromConceptId(conceptId) ?? legacyTypeSlugParts(conceptId);
|
|
285
322
|
if (parts === undefined)
|
|
286
323
|
return null; // foreign type / not a local asset ref
|
|
287
324
|
return localRefMissingRelPath(parts.type, parts.name, allRoots);
|
|
288
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Read-only fallback for the retired `type:slug` xref grammar (see
|
|
328
|
+
* resolve-ref.ts Q-02 — the write boundary no longer emits or accepts it).
|
|
329
|
+
* Stash content written before that retirement still carries it in frontmatter
|
|
330
|
+
* `xrefs:` lists, and `typeNameFromConceptId` (conceptId-only) returns
|
|
331
|
+
* `undefined` for it, so without this it was silently skipped rather than
|
|
332
|
+
* validated (#882). `refToRelPath`/`refExistsInAnyStash` already key off a bare
|
|
333
|
+
* `(type, slug)` pair — the same shape `type:slug` already is — so this only
|
|
334
|
+
* needs to split the token; no new resolution logic.
|
|
335
|
+
*/
|
|
336
|
+
function legacyTypeSlugParts(rawConceptId) {
|
|
337
|
+
const colon = rawConceptId.indexOf(":");
|
|
338
|
+
if (colon <= 0)
|
|
339
|
+
return undefined;
|
|
340
|
+
const type = rawConceptId.slice(0, colon);
|
|
341
|
+
const name = rawConceptId.slice(colon + 1);
|
|
342
|
+
if (!name || name.includes("/") || name.includes(":"))
|
|
343
|
+
return undefined;
|
|
344
|
+
return stashDirFor(type) === undefined ? undefined : { type, name };
|
|
345
|
+
}
|
|
289
346
|
/**
|
|
290
347
|
* Returns an array of {ref, resolvedRelPath} for every local AKM ref in the
|
|
291
348
|
* PROSE body that does not resolve to a real file under any of the provided
|
|
@@ -364,6 +421,12 @@ function dedupeMissing(rows) {
|
|
|
364
421
|
* excluded because it can point at merged-away or pruned assets.
|
|
365
422
|
*/
|
|
366
423
|
const XREF_FRONTMATTER_KEYS = ["xrefs", "supersededBy", "contradictedBy"];
|
|
424
|
+
/**
|
|
425
|
+
* The belief-graph subset of {@link XREF_FRONTMATTER_KEYS} — the only channels
|
|
426
|
+
* `--prune-dangling-edges` will repair (#884). Typed as `readonly string[]` so
|
|
427
|
+
* `.includes` accepts any xref key without a cast.
|
|
428
|
+
*/
|
|
429
|
+
const BELIEF_EDGE_KEYS = ["supersededBy", "contradictedBy"];
|
|
367
430
|
/**
|
|
368
431
|
* Return the `refs:` array from frontmatter when it is present and is an
|
|
369
432
|
* array of strings; otherwise return `null` to signal the caller should
|
|
@@ -597,12 +660,22 @@ export function runBaseChecks(ctx) {
|
|
|
597
660
|
});
|
|
598
661
|
}
|
|
599
662
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
663
|
+
/**
|
|
664
|
+
* Flush accumulated mutations to disk. Called after the fixable checks above
|
|
665
|
+
* AND again at the very end, because the `missing-ref` section below can also
|
|
666
|
+
* mutate (`--prune-dangling-edges`, #884) and used to run PAST this point —
|
|
667
|
+
* its edits were computed and then silently dropped.
|
|
668
|
+
*
|
|
669
|
+
* Mirrors the two stub-delete call sites in `commands/lint/index.ts`
|
|
670
|
+
* (`appendMemoryStubIssue`/`appendWorkflowStubIssue`), which already report
|
|
671
|
+
* a failed mutation as `fixed: "failed"` instead of throwing. Without this
|
|
672
|
+
* the sweep aborted mid-run on the first unwritable file and the caller got
|
|
673
|
+
* an exception instead of a result naming the fixes that HAD landed.
|
|
674
|
+
*/
|
|
675
|
+
const flushIfModified = () => {
|
|
676
|
+
if (!modified)
|
|
677
|
+
return;
|
|
678
|
+
modified = false;
|
|
606
679
|
try {
|
|
607
680
|
fs.writeFileSync(ctx.filePath, currentRaw, "utf8");
|
|
608
681
|
// Propagate the mutated raw back so subclasses can re-parse if needed
|
|
@@ -615,7 +688,8 @@ export function runBaseChecks(ctx) {
|
|
|
615
688
|
issue.detail = `${issue.detail} — could not write fix: ${reason}`;
|
|
616
689
|
}
|
|
617
690
|
}
|
|
618
|
-
}
|
|
691
|
+
};
|
|
692
|
+
flushIfModified();
|
|
619
693
|
// ── 3. stale-path ──────────────────────────────────────────────────────
|
|
620
694
|
// M3: checkStalePath returns all stale matches; push one issue per path.
|
|
621
695
|
// M4: Also scan ctx.frontmatter for stale paths (absolute paths in frontmatter).
|
|
@@ -636,6 +710,26 @@ export function runBaseChecks(ctx) {
|
|
|
636
710
|
}
|
|
637
711
|
}
|
|
638
712
|
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
713
|
+
const missingRefPass = runMissingRefChecks(ctx, currentRaw, shouldRun, pendingFixes);
|
|
714
|
+
issues.push(...missingRefPass.issues);
|
|
715
|
+
if (missingRefPass.modified) {
|
|
716
|
+
currentRaw = missingRefPass.raw;
|
|
717
|
+
modified = true;
|
|
718
|
+
}
|
|
719
|
+
flushIfModified();
|
|
720
|
+
return issues;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The `missing-ref` pass, extracted from {@link runBaseChecks} so that function
|
|
724
|
+
* stays under the src fn-size ratchet after #884 gave this pass its own
|
|
725
|
+
* (opt-in) mutation path. Pure move plus the explicit state hand-off: `raw` in,
|
|
726
|
+
* `{raw, modified}` out, `issues`/`pendingFixes` appended in place.
|
|
727
|
+
*/
|
|
728
|
+
function runMissingRefChecks(ctx, raw, shouldRun, pendingFixes) {
|
|
729
|
+
const issues = [];
|
|
730
|
+
let currentRaw = raw;
|
|
731
|
+
let modified = false;
|
|
732
|
+
// ── 4. missing-ref ─────────────────────────────────────────────────────
|
|
639
733
|
// Carve-out for assets that declare an explicit `refs:` array in
|
|
640
734
|
// frontmatter (e.g. session-checkpoint memories captured by the
|
|
641
735
|
// claude-code hook). The frontmatter array is the *authoritative*
|
|
@@ -650,7 +744,9 @@ export function runBaseChecks(ctx) {
|
|
|
650
744
|
// still run `checkMissingRefs` against the array itself to catch
|
|
651
745
|
// refs that were valid at capture time but later removed from the
|
|
652
746
|
// stash.
|
|
653
|
-
if (shouldRun("missing-ref"))
|
|
747
|
+
if (!shouldRun("missing-ref"))
|
|
748
|
+
return { issues, raw, modified };
|
|
749
|
+
{
|
|
654
750
|
const explicitRefs = extractFrontmatterRefs(ctx.data, ctx.body);
|
|
655
751
|
// An explicit `refs:` array is a REF LIST (each value is a whole ref —
|
|
656
752
|
// short conceptIds included); a bare body is PROSE (anchored refs only).
|
|
@@ -691,6 +787,33 @@ export function runBaseChecks(ctx) {
|
|
|
691
787
|
if (values === null)
|
|
692
788
|
continue;
|
|
693
789
|
const missingXrefs = checkMissingRefsInList(values, ctx.stashRoot, ctx.extraStashRoots);
|
|
790
|
+
// #884 opt-in repair. Scoped to the BELIEF channels only: an edge whose
|
|
791
|
+
// target has neither a file nor a prune tombstone asserts a
|
|
792
|
+
// relationship to a memory that no longer exists in any form, and
|
|
793
|
+
// carrying it forward corrupts every belief-graph read. `xrefs` is
|
|
794
|
+
// excluded — a stale xref is an ordinary broken link (the 3 skill refs
|
|
795
|
+
// in #884), and repairing it by DELETION would throw away a pointer the
|
|
796
|
+
// author may simply need to re-target.
|
|
797
|
+
const repairable = ctx.pruneDanglingEdges === true && BELIEF_EDGE_KEYS.includes(key) && missingXrefs.length > 0;
|
|
798
|
+
if (repairable) {
|
|
799
|
+
const dropped = missingXrefs.map(({ ref }) => ref);
|
|
800
|
+
const rewritten = removeFrontmatterListValues(currentRaw, key, dropped);
|
|
801
|
+
if (rewritten !== null) {
|
|
802
|
+
currentRaw = rewritten;
|
|
803
|
+
modified = true;
|
|
804
|
+
for (const { ref, resolvedRelPath } of missingXrefs) {
|
|
805
|
+
const issue = {
|
|
806
|
+
file: ctx.relPath,
|
|
807
|
+
issue: "missing-ref",
|
|
808
|
+
detail: `dangling ${key} edge dropped: ${ref} (no file and no prune tombstone at ${resolvedRelPath})`,
|
|
809
|
+
fixed: true,
|
|
810
|
+
};
|
|
811
|
+
issues.push(issue);
|
|
812
|
+
pendingFixes.push(issue);
|
|
813
|
+
}
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
694
817
|
for (const { ref, resolvedRelPath } of missingXrefs) {
|
|
695
818
|
issues.push({
|
|
696
819
|
file: ctx.relPath,
|
|
@@ -702,5 +825,5 @@ export function runBaseChecks(ctx) {
|
|
|
702
825
|
}
|
|
703
826
|
}
|
|
704
827
|
}
|
|
705
|
-
return issues;
|
|
828
|
+
return { issues, raw: currentRaw, modified };
|
|
706
829
|
}
|
|
@@ -92,7 +92,7 @@ function collectWorkflowFiles(dir) {
|
|
|
92
92
|
continue;
|
|
93
93
|
const full = path.join(dir, entry.name);
|
|
94
94
|
if (entry.isDirectory()) {
|
|
95
|
-
if (
|
|
95
|
+
if (isAkmRegistryCachePath(full))
|
|
96
96
|
continue;
|
|
97
97
|
results.push(...collectWorkflowFiles(full));
|
|
98
98
|
continue;
|
|
@@ -185,11 +185,9 @@ function collectAdapterFiles(root, extensions) {
|
|
|
185
185
|
walk(full);
|
|
186
186
|
}
|
|
187
187
|
else if (entry.isFile() && matchesAdapterExtension(entry.name, extensions)) {
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
const segments = path.relative(root, full).split(/[\\/]/);
|
|
192
|
-
if (segments.includes(".cache") || segments.includes("registry"))
|
|
188
|
+
// Skip akm's OWN resolved registry-cache copies, not any user
|
|
189
|
+
// directory that happens to be named ".cache" or "registry".
|
|
190
|
+
if (isAkmRegistryCachePath(full))
|
|
193
191
|
continue;
|
|
194
192
|
results.push(full);
|
|
195
193
|
}
|
|
@@ -387,8 +385,8 @@ function assertFixTargetWritable(stashRoot, sources) {
|
|
|
387
385
|
return;
|
|
388
386
|
// Same error kind and code `write-source.ts#ensureWritable` raises for the
|
|
389
387
|
// identical refusal, so a scripted caller classifies both the same way.
|
|
390
|
-
throw new UsageError(`lint
|
|
391
|
-
"Run `akm lint` without --fix to report findings, or set `writable: true` on the bundle.", "INVALID_FLAG_VALUE");
|
|
388
|
+
throw new UsageError(`lint: bundle "${stashRoot}" is configured \`writable: false\`; refusing to modify it. ` +
|
|
389
|
+
"Run `akm lint` without --fix / --prune-dangling-edges to report findings, or set `writable: true` on the bundle.", "INVALID_FLAG_VALUE");
|
|
392
390
|
}
|
|
393
391
|
/** True when the issue represents a file deletion that was successfully applied. */
|
|
394
392
|
function isFileDeletion(issue) {
|
|
@@ -525,7 +523,9 @@ export function lintAssetFile(ctx, subdir) {
|
|
|
525
523
|
*/
|
|
526
524
|
function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
527
525
|
const fix = options.fix === true;
|
|
528
|
-
|
|
526
|
+
// #884: the dangling-edge repair writes too, so it clears the same
|
|
527
|
+
// read-only gate `--fix` does.
|
|
528
|
+
if (fix || options.pruneDanglingEdges === true)
|
|
529
529
|
assertFixTargetWritable(stashRoot, sources);
|
|
530
530
|
const fixed = [];
|
|
531
531
|
const flagged = [];
|
|
@@ -629,7 +629,18 @@ function lintAkmSweep(stashRoot, extraStashRoots, cfg, sources, options) {
|
|
|
629
629
|
try {
|
|
630
630
|
issues = [
|
|
631
631
|
...fileIssues,
|
|
632
|
-
...lintAssetFile({
|
|
632
|
+
...lintAssetFile({
|
|
633
|
+
filePath,
|
|
634
|
+
relPath,
|
|
635
|
+
raw,
|
|
636
|
+
data,
|
|
637
|
+
body,
|
|
638
|
+
frontmatter,
|
|
639
|
+
fix,
|
|
640
|
+
pruneDanglingEdges: options.pruneDanglingEdges === true,
|
|
641
|
+
stashRoot,
|
|
642
|
+
extraStashRoots,
|
|
643
|
+
}, subdir),
|
|
633
644
|
];
|
|
634
645
|
}
|
|
635
646
|
catch (e) {
|
|
@@ -196,13 +196,6 @@ export function hasHotCaptureMode(frontmatter) {
|
|
|
196
196
|
* with 80-90% overlap legitimately compress to well under 50% of the larger.
|
|
197
197
|
*/
|
|
198
198
|
export const MERGE_SHRINK_RATIO_MIN = 0.3;
|
|
199
|
-
/**
|
|
200
|
-
* Absolute floor (chars) for merged body. When sources are short (<~333 chars),
|
|
201
|
-
* `MERGE_SHRINK_RATIO_MIN × largerBodyLen` falls below this and the absolute
|
|
202
|
-
* floor dominates — prevents false positives on very terse memory pairs.
|
|
203
|
-
* Matches the existing `promote_source_too_small` floor of 100 chars.
|
|
204
|
-
*/
|
|
205
|
-
export const MERGE_ABSOLUTE_FLOOR_CHARS = 100;
|
|
206
199
|
// ── Reflect size gate ────────────────────────────────────────────────────────
|
|
207
200
|
/** Ratio lower-bound: proposed body must be at least this fraction of source. */
|
|
208
201
|
export const REFLECT_SHRINK_RATIO_MIN = 0.5;
|
|
@@ -24,6 +24,7 @@ import { enqueueGraphExtraction, hasGraphData } from "../../indexer/db/graph-db.
|
|
|
24
24
|
import { copySearchHitAttribution, getSearchHitAttribution, usageEventAttributionMetadata, } from "../../indexer/search/search-attribution.js";
|
|
25
25
|
import { findSourceForPath, resolveSourceEntries } from "../../indexer/search/search-source.js";
|
|
26
26
|
import { insertUsageEvent } from "../../indexer/usage/usage-events.js";
|
|
27
|
+
import { estimateTokenCount } from "../../llm/embedders/remote.js";
|
|
27
28
|
import { truncateDescription } from "../../output/shapes/helpers.js";
|
|
28
29
|
import { TELEMETRY_BUSY_TIMEOUT_MS, withIndexDb } from "../../storage/repositories/index-db.js";
|
|
29
30
|
import { findEntryIdByRef, getItemRefById } from "../../storage/repositories/index-entries-repository.js";
|
|
@@ -163,6 +164,52 @@ export async function curateSearchResults(query, result, limit, selectedType, ev
|
|
|
163
164
|
...(result.tip ? { tip: result.tip } : {}),
|
|
164
165
|
};
|
|
165
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Pack a curate result's stash hits into a single token-budgeted blob:
|
|
169
|
+
* resolve each hit's content via the SAME path `akm show` uses
|
|
170
|
+
* (`akmShowUnified` — this also means a `ref#fragment` hit packs just the
|
|
171
|
+
* matched section), then greedily accumulate hits, in the ranking order
|
|
172
|
+
* `curateSearchResults` already produced, until the next hit would exceed
|
|
173
|
+
* `budgetTokens`.
|
|
174
|
+
*
|
|
175
|
+
* Registry hits are never packed — only `CuratedStashItem`s (locked
|
|
176
|
+
* contract, AGENTS.md: registry results stay separate/opt-in).
|
|
177
|
+
*
|
|
178
|
+
* Truncation policy: drop whole hits from the tail of the ranked list first.
|
|
179
|
+
* The only exception is a single high-rank hit that alone exceeds the
|
|
180
|
+
* budget — that one hit is truncated to fit rather than dropping everything.
|
|
181
|
+
*/
|
|
182
|
+
export async function packCuratedHits(result, budgetTokens) {
|
|
183
|
+
const stashItems = result.items.filter((item) => item.source === "local");
|
|
184
|
+
const packed = [];
|
|
185
|
+
let used = 0;
|
|
186
|
+
for (const item of stashItems) {
|
|
187
|
+
let shown;
|
|
188
|
+
try {
|
|
189
|
+
shown = await akmShowUnified({ ref: item.ref, skipLogging: true });
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const content = shown.content ?? shown.template ?? shown.prompt ?? "";
|
|
195
|
+
const tokens = estimateTokenCount(content);
|
|
196
|
+
if (used + tokens <= budgetTokens) {
|
|
197
|
+
packed.push({ ref: item.ref, tokens, content });
|
|
198
|
+
used += tokens;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (packed.length === 0) {
|
|
202
|
+
const remaining = budgetTokens - used;
|
|
203
|
+
if (remaining > 0) {
|
|
204
|
+
const truncated = content.slice(0, remaining * 4);
|
|
205
|
+
packed.push({ ref: item.ref, tokens: estimateTokenCount(truncated), content: truncated });
|
|
206
|
+
used += estimateTokenCount(truncated);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return { query: result.query, budget: budgetTokens, tokens: used, items: packed };
|
|
212
|
+
}
|
|
166
213
|
async function enrichCuratedStashHit(query, hit, supportRefs, selectedRefs, eventSource) {
|
|
167
214
|
let shown;
|
|
168
215
|
try {
|
|
@@ -20,7 +20,8 @@ import { parseMetaRef } from "../../core/asset/stash-meta.js";
|
|
|
20
20
|
import { UsageError } from "../../core/errors.js";
|
|
21
21
|
import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
|
|
22
22
|
import { getOutputMode } from "../../output/context.js";
|
|
23
|
-
import {
|
|
23
|
+
import { deliverRendered } from "../../output/html-render.js";
|
|
24
|
+
import { akmCurate, packCuratedHits } from "./curate.js";
|
|
24
25
|
import { akmSearch, parseBeliefFilterMode, parseScopeFilterFlags, parseSearchSource } from "./search.js";
|
|
25
26
|
import { akmShowUnified } from "./show.js";
|
|
26
27
|
/**
|
|
@@ -153,6 +154,15 @@ export const curateCommand = defineJsonCommand({
|
|
|
153
154
|
},
|
|
154
155
|
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
155
156
|
from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
|
|
157
|
+
pack: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Pack the ranked stash hits' full content into a single token-budgeted blob instead of returning refs " +
|
|
160
|
+
"to follow up on individually — value is the max token budget, e.g. --pack 4000 (~4 chars/token, same " +
|
|
161
|
+
"estimator as embedding). Content is resolved the same way `akm show` resolves it, so a ref#fragment " +
|
|
162
|
+
"hit packs just that section. Registry hits (--from registry|all) are never packed. Not to be confused " +
|
|
163
|
+
"with a workflow asset's own `budget` field (a run-cost cap) — this is a context-size target for this " +
|
|
164
|
+
"one curate call.",
|
|
165
|
+
},
|
|
156
166
|
// Declared as the POSITIVE name with `default: true` — see the
|
|
157
167
|
// `project-context` comment on `searchCommand` above for why a flag NAME
|
|
158
168
|
// must never start with `no-`.
|
|
@@ -175,6 +185,7 @@ export const curateCommand = defineJsonCommand({
|
|
|
175
185
|
const source = parseSearchSource(args.from ?? "local");
|
|
176
186
|
const skipLogging = args["track-usage"] === false;
|
|
177
187
|
const outputMode = getOutputMode();
|
|
188
|
+
const packBudget = parsePositiveIntFlag(args.pack ?? undefined, "--pack");
|
|
178
189
|
const curated = await akmCurate({
|
|
179
190
|
query: args.query,
|
|
180
191
|
type,
|
|
@@ -184,9 +195,21 @@ export const curateCommand = defineJsonCommand({
|
|
|
184
195
|
eventSource: resolveUsageEventSource(),
|
|
185
196
|
attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
|
|
186
197
|
});
|
|
198
|
+
if (packBudget !== undefined) {
|
|
199
|
+
const packed = await packCuratedHits(curated, packBudget);
|
|
200
|
+
deliverRendered(outputMode.format === "text" ? formatCuratePackText(packed) : JSON.stringify(packed.items, null, 2), outputMode.outputPath);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
187
203
|
output("curate", curated);
|
|
188
204
|
},
|
|
189
205
|
});
|
|
206
|
+
/** Human-readable rendering for `akm curate --pack`: concatenated content per hit under a `## <ref>` header. */
|
|
207
|
+
function formatCuratePackText(packed) {
|
|
208
|
+
if (packed.items.length === 0) {
|
|
209
|
+
return `No packed content for "${packed.query}" (budget ${packed.budget} tokens).`;
|
|
210
|
+
}
|
|
211
|
+
return packed.items.map((item) => `## ${item.ref}\n\n${item.content}`).join("\n\n");
|
|
212
|
+
}
|
|
190
213
|
/**
|
|
191
214
|
* Reject `--scope` (either spelling) on `akm show` (E-3). `--scope` was
|
|
192
215
|
* removed in favor of `--filter` (R-047, guardrail 6 — no alias, must keep
|
|
@@ -13,20 +13,25 @@
|
|
|
13
13
|
* keying rules across call sites.
|
|
14
14
|
*/
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { primaryBundlePath } from "../../core/config/config.js";
|
|
16
|
+
import { bundleKeyForContentRoot, primaryBundlePath } from "../../core/config/config.js";
|
|
17
17
|
import { deriveBundleId } from "../../indexer/installations.js";
|
|
18
18
|
export { primaryBundlePath };
|
|
19
19
|
/**
|
|
20
20
|
* Upsert the primary filesystem bundle (`{ path, writable: true }`) and point
|
|
21
21
|
* `defaultBundle` at it. Reuses the current default key when it already names a
|
|
22
|
-
* filesystem bundle (so re-pointing the primary keeps a stable id)
|
|
23
|
-
*
|
|
22
|
+
* filesystem bundle (so re-pointing the primary keeps a stable id). Otherwise,
|
|
23
|
+
* reuses whichever configured bundle ALREADY resolves to `stashDir`'s content
|
|
24
|
+
* root (issue #870 — `AKM_BUNDLE_DIR`/`--dir` pointed at a directory already
|
|
25
|
+
* configured under a different id must not mint a second bundle for it); only
|
|
26
|
+
* when no bundle owns that root yet is a fresh slug-legal key derived.
|
|
24
27
|
*/
|
|
25
28
|
export function withPrimaryBundle(config, stashDir) {
|
|
26
29
|
const bundles = { ...(config.bundles ?? {}) };
|
|
27
30
|
let key = config.defaultBundle;
|
|
28
31
|
if (!key || !(key in bundles) || typeof bundles[key]?.path !== "string") {
|
|
29
|
-
key =
|
|
32
|
+
key =
|
|
33
|
+
bundleKeyForContentRoot(config, path.resolve(stashDir)) ??
|
|
34
|
+
deriveBundleId(undefined, stashDir, new Set(Object.keys(bundles)));
|
|
30
35
|
}
|
|
31
36
|
bundles[key] = {
|
|
32
37
|
...bundles[key],
|
|
@@ -8,7 +8,6 @@ import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-ac
|
|
|
8
8
|
import { getDbPath } from "../../core/paths.js";
|
|
9
9
|
import { formatRegistryUrl } from "../../core/registry-url.js";
|
|
10
10
|
import { error } from "../../core/warn.js";
|
|
11
|
-
import { getEffectiveSemanticStatus, readSemanticStatus } from "../../indexer/search/semantic-status.js";
|
|
12
11
|
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
13
12
|
import { getEntryCount, getEntryCountByType } from "../../storage/repositories/index-entries-repository.js";
|
|
14
13
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
@@ -29,13 +28,6 @@ export function assembleInfo(options) {
|
|
|
29
28
|
const defaultBundle = config.defaultBundle ?? null;
|
|
30
29
|
// Asset types (copy into a mutable array — `placementTypes()` returns readonly)
|
|
31
30
|
const assetTypes = [...placementTypes()];
|
|
32
|
-
const semanticRuntime = readSemanticStatus();
|
|
33
|
-
const semanticStatus = getEffectiveSemanticStatus(config, semanticRuntime);
|
|
34
|
-
// Search modes
|
|
35
|
-
const searchModes = ["fts"];
|
|
36
|
-
if (semanticStatus === "ready-js" || semanticStatus === "ready-vec") {
|
|
37
|
-
searchModes.push("semantic", "hybrid");
|
|
38
|
-
}
|
|
39
31
|
// Registries (strip sensitive fields like apiKey from options)
|
|
40
32
|
const registries = (config.registries ?? []).map((r) => ({
|
|
41
33
|
url: formatRegistryUrl(r.url),
|
|
@@ -58,6 +50,20 @@ export function assembleInfo(options) {
|
|
|
58
50
|
// health and search use, so info reads the same database they do.
|
|
59
51
|
const resolvedDbPath = options?.dbPath ?? getDbPath();
|
|
60
52
|
const indexStats = readIndexStats(resolvedDbPath);
|
|
53
|
+
// Semantic status is read live from the index's own state, not a cached
|
|
54
|
+
// verdict — a failed embed attempt at search time falls back to FTS and
|
|
55
|
+
// reports that in the search response, it never disables the mode here.
|
|
56
|
+
const semanticStatus = config.semanticSearchMode === "off"
|
|
57
|
+
? "disabled"
|
|
58
|
+
: !indexStats.hasEmbeddings
|
|
59
|
+
? "pending"
|
|
60
|
+
: indexStats.vecAvailable
|
|
61
|
+
? "ready-vec"
|
|
62
|
+
: "ready-js";
|
|
63
|
+
const searchModes = ["fts"];
|
|
64
|
+
if (semanticStatus === "ready-js" || semanticStatus === "ready-vec") {
|
|
65
|
+
searchModes.push("semantic", "hybrid");
|
|
66
|
+
}
|
|
61
67
|
return {
|
|
62
68
|
schemaVersion: 1,
|
|
63
69
|
version: pkgVersion,
|
|
@@ -68,8 +74,6 @@ export function assembleInfo(options) {
|
|
|
68
74
|
semanticSearch: {
|
|
69
75
|
mode: config.semanticSearchMode,
|
|
70
76
|
status: semanticStatus,
|
|
71
|
-
...(semanticRuntime?.reason ? { reason: semanticRuntime.reason } : {}),
|
|
72
|
-
...(semanticRuntime?.message ? { message: semanticRuntime.message } : {}),
|
|
73
77
|
},
|
|
74
78
|
registries,
|
|
75
79
|
sourceProviders,
|
|
@@ -39,7 +39,6 @@ import { assertSchedulerNativeArtifactOwnership, assertSchedulerSourceSnapshot,
|
|
|
39
39
|
import { renderSchedulerPlanPreview, renderSchedulerSyncPlanPreview, } from "../../tasks/scheduler-sync-preview.js";
|
|
40
40
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
41
41
|
import { projectTaskSourceV4 } from "../../tasks/source/project-v4.js";
|
|
42
|
-
import { TASK_V3_MAX_SOURCE_BYTES } from "../../tasks/source-v3.js";
|
|
43
42
|
import { normaliseTaskConceptId, normaliseTaskId } from "../../tasks/task-id.js";
|
|
44
43
|
import { applyAutonomyGate, configuredDirectAutonomyLanes, describeGatedLanes } from "../improve/autonomy-gate.js";
|
|
45
44
|
import { resolveImproveStrategy } from "../improve/improve-strategies.js";
|
|
@@ -971,13 +970,7 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
971
970
|
if (!rootStat.isDirectory()) {
|
|
972
971
|
throw new UsageError(`${root} is not a task source directory.`, "INVALID_FLAG_VALUE");
|
|
973
972
|
}
|
|
974
|
-
const common = {
|
|
975
|
-
filePath,
|
|
976
|
-
rootRealPath,
|
|
977
|
-
rootPhysicalIdentity: rootStat.ino === 0n ? `path:${rootRealPath}` : `inode:${rootStat.dev}:${rootStat.ino}`,
|
|
978
|
-
rootMtimeNs: String(rootStat.mtimeNs),
|
|
979
|
-
rootCtimeNs: String(rootStat.ctimeNs),
|
|
980
|
-
};
|
|
973
|
+
const common = { filePath, rootRealPath };
|
|
981
974
|
let descriptor;
|
|
982
975
|
try {
|
|
983
976
|
const noFollow = "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0;
|
|
@@ -986,9 +979,6 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
986
979
|
if (!before.isFile()) {
|
|
987
980
|
throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
|
|
988
981
|
}
|
|
989
|
-
if (before.size > BigInt(TASK_V3_MAX_SOURCE_BYTES)) {
|
|
990
|
-
throw new UsageError(`${filePath} exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) task source limit.`, "INVALID_FLAG_VALUE");
|
|
991
|
-
}
|
|
992
982
|
const bytes = fs.readFileSync(descriptor);
|
|
993
983
|
const after = fs.fstatSync(descriptor, { bigint: true });
|
|
994
984
|
if (!sameTaskSourceStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
|
|
@@ -1010,10 +1000,7 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
1010
1000
|
state: "present",
|
|
1011
1001
|
...common,
|
|
1012
1002
|
realPath,
|
|
1013
|
-
physicalIdentity: before.ino === 0n ? `path:${realPath}` : `inode:${before.dev}:${before.ino}`,
|
|
1014
1003
|
size: bytes.byteLength,
|
|
1015
|
-
mtimeNs: String(before.mtimeNs),
|
|
1016
|
-
ctimeNs: String(before.ctimeNs),
|
|
1017
1004
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1018
1005
|
bytesBase64: bytes.toString("base64"),
|
|
1019
1006
|
content,
|
|
@@ -1035,6 +1022,11 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
|
|
|
1035
1022
|
fs.closeSync(descriptor);
|
|
1036
1023
|
}
|
|
1037
1024
|
}
|
|
1025
|
+
// TOCTOU note: this compares CONTENT (state + sha256), not filesystem
|
|
1026
|
+
// identity (inode/mtime/ctime/directory timestamps) — the same split already
|
|
1027
|
+
// applied to the task migrator in 0.9.5. An unrelated touch to the file or
|
|
1028
|
+
// its containing directory must not trip a "changed after planning" refusal;
|
|
1029
|
+
// only a real content change should.
|
|
1038
1030
|
function assertTaskSourceExpectation(expected) {
|
|
1039
1031
|
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1040
1032
|
if (!sameTaskSourceExpectation(actual, expected)) {
|
|
@@ -1042,12 +1034,16 @@ function assertTaskSourceExpectation(expected) {
|
|
|
1042
1034
|
}
|
|
1043
1035
|
}
|
|
1044
1036
|
function sameTaskSourceExpectation(left, right) {
|
|
1045
|
-
|
|
1037
|
+
if (left.state !== right.state || left.filePath !== right.filePath || left.rootRealPath !== right.rootRealPath) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
if (left.state === "absent")
|
|
1041
|
+
return true;
|
|
1042
|
+
return left.sha256 === right.sha256;
|
|
1046
1043
|
}
|
|
1047
1044
|
function assertTaskSourceRestored(expected) {
|
|
1048
1045
|
const actual = captureTaskSourceExpectation(expected.filePath, expected.rootRealPath);
|
|
1049
1046
|
const restored = actual.state === expected.state &&
|
|
1050
|
-
actual.rootPhysicalIdentity === expected.rootPhysicalIdentity &&
|
|
1051
1047
|
(actual.state === "absent" ||
|
|
1052
1048
|
(expected.state === "present" && actual.sha256 === expected.sha256 && actual.content === expected.content));
|
|
1053
1049
|
if (!restored) {
|
|
@@ -20,7 +20,7 @@ import { deliverRendered } from "../output/html-render.js";
|
|
|
20
20
|
import { shapeForCommand } from "../output/shapes.js";
|
|
21
21
|
import { formatPlain } from "../output/text.js";
|
|
22
22
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
23
|
-
import {
|
|
23
|
+
import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
24
24
|
import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
25
25
|
import { akmWorkflowPlan } from "./workflow/plan.js";
|
|
26
26
|
const workflowStatusCommand = defineJsonCommand({
|
|
@@ -168,7 +168,7 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
168
168
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
169
169
|
const parameterFlags = parseWorkflowParameterFlags(rawArgs, args.target);
|
|
170
170
|
const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
|
|
171
|
-
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0
|
|
171
|
+
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0);
|
|
172
172
|
const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
|
|
173
173
|
const controller = new AbortController();
|
|
174
174
|
let signalExitCode;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import fs from "node:fs";
|
|
27
27
|
import path from "node:path";
|
|
28
|
-
import { SCRIPT_EXTENSIONS, WORKFLOW_EXTENSIONS } from "../recognition-util.js";
|
|
28
|
+
import { DERIVED_SUFFIX, SCRIPT_EXTENSIONS, WORKFLOW_EXTENSIONS } from "../recognition-util.js";
|
|
29
29
|
function toPosix(input) {
|
|
30
30
|
return input.replace(/\\/g, "/");
|
|
31
31
|
}
|
|
@@ -247,10 +247,21 @@ export function assetPathForName(assetType, typeRoot, name) {
|
|
|
247
247
|
* "default" alias is genuinely dual-owned: both `<dir>/.env` and
|
|
248
248
|
* `<dir>/default.env` derive the same canonical name (`toCanonicalName`
|
|
249
249
|
* above), so a physical-owner lookup must consider both without reading
|
|
250
|
-
* either file.
|
|
250
|
+
* either file. `memory` has a second, analogous duality (#882): a ref to
|
|
251
|
+
* `<name>` may own either `<name>.md` or the LLM-inferred `<name>.derived.md`
|
|
252
|
+
* twin — `.derived` is a provenance marker on the SAME identity, not part of
|
|
253
|
+
* the name (see `resolveParentRef`/`isDerivedMemory` in
|
|
254
|
+
* `commands/improve/memory/derived-ref.ts`, and the belief-edge identity
|
|
255
|
+
* channel's own `memory:<name>.derived` refs). The plain `.md` file wins when
|
|
256
|
+
* both exist, so it stays `primary` — first in the returned list — and every
|
|
257
|
+
* caller here already prefers the first candidate that exists on disk. Every
|
|
258
|
+
* other placement type has exactly one inverse spelling.
|
|
251
259
|
*/
|
|
252
260
|
export function assetPathCandidatesForName(assetType, typeRoot, name) {
|
|
253
261
|
const primary = assetPathForName(assetType, typeRoot, name);
|
|
262
|
+
if (assetType === "memory" && !name.endsWith(DERIVED_SUFFIX)) {
|
|
263
|
+
return [primary, assetPathForName(assetType, typeRoot, `${name}${DERIVED_SUFFIX}`)];
|
|
264
|
+
}
|
|
254
265
|
if (assetType !== "env")
|
|
255
266
|
return [primary];
|
|
256
267
|
const base = name === "default" ? "" : name.endsWith("/default") ? name.slice(0, -"default".length) : undefined;
|