akm-cli 0.10.261016-beta → 0.10.261018-beta
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 +144 -0
- package/README.md +8 -10
- package/SECURITY.md +7 -99
- package/STABILITY.md +57 -98
- package/dist/assets/opencode/akm-plugin.js +103 -1
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +2 -2
- package/dist/assets/stash-skeleton/facts/conventions/organization.md +11 -17
- package/dist/cli.js +32 -41
- package/dist/commands/feedback-cli.js +5 -4
- package/dist/commands/health.js +15 -16
- package/dist/commands/proposal/proposal-cli.js +1 -1
- package/dist/commands/read/registry-search.js +4 -1
- package/dist/commands/read/search.js +6 -6
- package/dist/commands/sources/self-update.js +9 -5
- package/dist/commands/tasks/tasks.js +12 -13
- package/dist/core/adapter/adapters/agent-skills-adapter.js +7 -7
- package/dist/core/adapter/adapters/akm-task-adapter.js +1 -1
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/adapters/claude-adapter.js +1 -1
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -1
- package/dist/core/adapter/adapters/generic-files-adapter.js +1 -1
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +1 -1
- package/dist/core/adapter/adapters/opencode-adapter.js +1 -1
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +1 -1
- package/dist/core/bundle-rename.js +17 -7
- package/dist/core/config/convert-engine-references.js +23 -7
- package/dist/core/config/convert-engines.js +94 -28
- package/dist/core/config/convert-models-overlay.js +14 -2
- package/dist/indexer/bundle-identity-guard.js +6 -6
- package/dist/indexer/index-written-assets.js +3 -3
- package/dist/indexer/indexer.js +12 -8
- package/dist/indexer/materialize-embeddings.js +12 -2
- package/dist/indexer/passes/metadata.js +103 -46
- package/dist/indexer/search/ranking.js +7 -3
- package/dist/indexer/search/search-fields.js +1 -1
- package/dist/indexer/usage/usage-events.js +30 -8
- package/dist/indexer/walk/walker.js +1 -1
- package/dist/integrations/agent/spawn.js +7 -2
- package/dist/integrations/harnesses/opencode/agent-builder.js +10 -1
- package/dist/integrations/harnesses/opencode/runtime/binary.js +6 -3
- package/dist/integrations/harnesses/opencode/runtime/index.js +18 -9
- package/dist/integrations/harnesses/opencode/session-log-v2.js +11 -1
- package/dist/output/shapes/helpers.js +3 -3
- package/dist/output/text/command-format.js +5 -6
- package/dist/scripts/akm-migrate-node.js +122 -36
- package/dist/scripts/akm-migrate.js +122 -36
- package/dist/sources/providers/git-provider.js +2 -4
- package/dist/sources/providers/provider-utils.js +8 -13
- package/dist/sources/snapshot-fetchers/website-ingest.js +6 -7
- package/dist/storage/repositories/events-repository.js +15 -0
- package/dist/storage/repositories/improve-ledger-repository.js +13 -0
- package/dist/storage/repositories/index-entries-repository.js +43 -5
- package/dist/storage/repositories/outcome-repository.js +12 -0
- package/dist/storage/repositories/proposals-repository.js +5 -5
- package/dist/storage/repositories/salience-repository.js +13 -0
- package/dist/storage/repositories/task-history-repository.js +8 -7
- package/dist/storage/repositories/workflow-runs-repository.js +10 -8
- package/docs/README.md +24 -60
- package/docs/agents/README.md +3 -8
- package/docs/architecture/README.md +18 -20
- package/docs/architecture/adapters.md +40 -43
- package/docs/architecture/architecture.md +180 -154
- package/docs/architecture/improvement.md +95 -91
- package/docs/architecture/internals/classification.md +101 -119
- package/docs/architecture/internals/fresh-host-rebuild-runbook.md +33 -35
- package/docs/architecture/internals/health-advisories.md +59 -46
- package/docs/architecture/internals/improve-workflow.md +166 -693
- package/docs/architecture/internals/indexing.md +65 -465
- package/docs/architecture/internals/storage-locations.md +301 -848
- package/docs/architecture/persisted-data-compat.md +6 -6
- package/docs/architecture/specs/okf-support.md +67 -95
- package/docs/architecture/specs/ref.md +67 -125
- package/docs/architecture/testing/testing-workflow.md +108 -168
- package/docs/architecture/workflow-engine.md +34 -33
- package/docs/guides/README.md +5 -5
- package/docs/guides/author-bundles.md +72 -98
- package/docs/guides/author-workflows.md +11 -18
- package/docs/guides/bundles.md +91 -123
- package/docs/guides/capture-knowledge.md +10 -9
- package/docs/guides/concepts.md +45 -30
- package/docs/guides/discover-and-load.md +39 -32
- package/docs/guides/getting-started.md +24 -24
- package/docs/guides/improve-the-library.md +98 -80
- package/docs/guides/recipes/headless-install.md +4 -4
- package/docs/guides/recipes/website-source.md +9 -5
- package/docs/guides/run-workflows.md +10 -11
- package/docs/guides/scheduling.md +11 -17
- package/docs/guides/use-with-any-agent.md +15 -14
- package/docs/guides/wikis.md +6 -9
- package/docs/integration/bundling-akm.md +21 -26
- package/docs/maintainers/README.md +2 -2
- package/docs/maintainers/local-development.md +3 -16
- package/docs/migration/README.md +2 -1
- package/docs/migration/release-notes/0.10.0.md +196 -23
- package/docs/migration/release-notes/README.md +2 -2
- package/docs/migration/v0.9-to-v0.10.md +103 -12
- package/docs/product-surface.md +10 -8
- package/docs/reference/README.md +26 -15
- package/docs/reference/asset-types.md +11 -12
- package/docs/reference/bundle-types.md +6 -5
- package/docs/reference/cli.md +52 -28
- package/docs/reference/configuration.md +91 -73
- package/docs/reference/data-and-telemetry.md +204 -421
- package/docs/reference/env-and-secrets.md +80 -83
- package/docs/reference/memory.md +1 -1
- package/docs/reference/refs.md +23 -36
- package/docs/reference/registry.md +138 -348
- package/docs/reference/supported-formats.md +8 -8
- package/docs/reference/tasks.md +13 -19
- package/docs/reference/website-sources.md +23 -15
- package/docs/reference/workflow-schema.md +28 -47
- package/docs/reference/workflows.md +33 -37
- package/package.json +1 -1
|
@@ -136,7 +136,7 @@ export function validateStashEntry(entry) {
|
|
|
136
136
|
const sources = normalizeNonEmptyStringList(e.sources);
|
|
137
137
|
if (sources)
|
|
138
138
|
result.sources = sources;
|
|
139
|
-
//
|
|
139
|
+
// `category` must survive the projection. Non-string values are
|
|
140
140
|
// dropped, not coerced.
|
|
141
141
|
if (typeof e.category === "string" && e.category.trim().length > 0) {
|
|
142
142
|
result.category = e.category.trim();
|
|
@@ -346,7 +346,7 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
346
346
|
const quality = asNonEmptyString(fmData.quality);
|
|
347
347
|
if (quality)
|
|
348
348
|
entry.quality = normalizeQuality(quality);
|
|
349
|
-
//
|
|
349
|
+
// Capture the `category:` frontmatter key (e.g. `convention`,
|
|
350
350
|
// `meta` on facts) must land on the indexed entry so category-keyed
|
|
351
351
|
// policies can see it. Trimmed; blank/non-string values are ignored.
|
|
352
352
|
const category = asNonEmptyString(fmData.category);
|
|
@@ -384,13 +384,13 @@ export function applyCuratedFrontmatter(entry, fmData) {
|
|
|
384
384
|
entry.evidenceSources = evidenceSources;
|
|
385
385
|
if (typeof fmData.evidence === "string" && fmData.evidence.trim().length > 0)
|
|
386
386
|
entry.evidence = fmData.evidence;
|
|
387
|
-
//
|
|
387
|
+
// Capture the parent ref for derived memories.
|
|
388
388
|
// Memory-inference writes `source: "memories/<parent>"` and `inferred: true`
|
|
389
389
|
// (and a derived child name suffix `.derived`). We mirror that source ref into
|
|
390
390
|
// `entry.derivedFrom` so the indexer can populate the dedicated `derived_from`
|
|
391
|
-
// column.
|
|
392
|
-
//
|
|
393
|
-
//
|
|
391
|
+
// column. The column is stored as the `memories/<name>` conceptId, matching
|
|
392
|
+
// the `getDerivedForParent` lookup key (search-hit-enrichers) so producer and
|
|
393
|
+
// consumer speak one grammar.
|
|
394
394
|
// Non-derived entries leave this field unset.
|
|
395
395
|
if (entry.type === "memory") {
|
|
396
396
|
const isDerivedByName = entry.name.toLowerCase().endsWith(".derived");
|
|
@@ -473,7 +473,7 @@ function isWikiRole(value) {
|
|
|
473
473
|
/**
|
|
474
474
|
* Parse one `verified:` family value into the `IndexDocument.provenance` shape.
|
|
475
475
|
* Accepts OKF v0.2's list form and its documented single-mapping shorthand
|
|
476
|
-
* ("consumers MUST treat a bare mapping as a one-element list",
|
|
476
|
+
* ("consumers MUST treat a bare mapping as a one-element list", OKF v0.2 §5.2).
|
|
477
477
|
* Tolerant: an entry without a usable `by` is dropped individually.
|
|
478
478
|
*/
|
|
479
479
|
function parseVerifiedFamily(value) {
|
|
@@ -491,9 +491,9 @@ function parseVerifiedFamily(value) {
|
|
|
491
491
|
}
|
|
492
492
|
/**
|
|
493
493
|
* Extract the OKF v0.2 provenance families that `promoteProposal` stamps onto
|
|
494
|
-
* accepted AKM-native proposals (
|
|
494
|
+
* accepted AKM-native proposals (#730), and apply them to the entry.
|
|
495
495
|
*
|
|
496
|
-
* The on-disk shape is deliberately **hybrid** (
|
|
496
|
+
* The on-disk shape is deliberately **hybrid** (#730 review):
|
|
497
497
|
* `generated:` and `verified:` are stamped BARE at the top level, exactly as
|
|
498
498
|
* OKF v0.2 spells them, because neither has any pre-existing AKM consumer — so
|
|
499
499
|
* a third-party OKF v0.2 reader treating an AKM stash as an OKF bundle sees
|
|
@@ -505,8 +505,8 @@ function parseVerifiedFamily(value) {
|
|
|
505
505
|
* non-strings).
|
|
506
506
|
*
|
|
507
507
|
* The nested `provenance.generatedBy` / `.generatedAt` / `.verified` spellings
|
|
508
|
-
* are still read as a fallback so assets stamped by an earlier build
|
|
509
|
-
*
|
|
508
|
+
* are still read as a fallback so assets stamped by an earlier build keep
|
|
509
|
+
* resolving. Tolerant throughout: any malformed sub-field is
|
|
510
510
|
* dropped individually rather than rejecting the whole block.
|
|
511
511
|
*/
|
|
512
512
|
export function applyProvenanceFrontmatter(entry, fmData) {
|
|
@@ -545,8 +545,8 @@ export function applyProvenanceFrontmatter(entry, fmData) {
|
|
|
545
545
|
entry.provenance = result;
|
|
546
546
|
}
|
|
547
547
|
// AKM-stash indexing policy (env/vaults/secrets sensitive-marker + wiki-infra
|
|
548
|
-
// exclusions)
|
|
549
|
-
// abstention
|
|
548
|
+
// exclusions) lives in the `akm` adapter's `recognize` as path/stat-based
|
|
549
|
+
// abstention. See
|
|
550
550
|
// `akmStashAbstains` in `src/core/adapter/adapters/akm-adapter.ts`.
|
|
551
551
|
/**
|
|
552
552
|
* Extract `@param` JSDoc tags from a script file's leading comment block.
|
|
@@ -764,11 +764,9 @@ function mergeAliases(existing, generated) {
|
|
|
764
764
|
/**
|
|
765
765
|
* Maximum native Markdown prose carried by the low-weight `content` field.
|
|
766
766
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
*
|
|
770
|
-
* input with no visible signal (the cut was reported via `warnVerbose`,
|
|
771
|
-
* silent unless `AKM_VERBOSE` was set). The remaining bound exists only to
|
|
767
|
+
* Set far past any real authored document, so ordinary long-form
|
|
768
|
+
* skills/knowledge docs never lose their tail from FTS or the embedding
|
|
769
|
+
* input. The bound exists only to
|
|
772
770
|
* stop a truly pathological single file (a committed data dump, a decompressed
|
|
773
771
|
* log) from ballooning index size — not to shave real content — so a caller
|
|
774
772
|
* that hits it is always told, unconditionally.
|
|
@@ -873,6 +871,58 @@ function isMarkdownFenceClosing(line, fence) {
|
|
|
873
871
|
return false;
|
|
874
872
|
return /^[\t ]*$/.test(line.slice(cursor));
|
|
875
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* Version of what {@link projectMarkdownContent} indexes. It is part of every
|
|
876
|
+
* directory's index fingerprint, so bumping it makes the next `akm index`
|
|
877
|
+
* re-project every directory once instead of keeping the old text for
|
|
878
|
+
* unchanged files. 2: identifiers from fenced code (akm#1260).
|
|
879
|
+
*/
|
|
880
|
+
export const CONTENT_PROJECTION_VERSION = 2;
|
|
881
|
+
/** At or above this length a token with a digit reads as a key or hash, not a name. */
|
|
882
|
+
const TOKEN_LIKE_MIN_LENGTH = 20;
|
|
883
|
+
/**
|
|
884
|
+
* The searchable identifiers of one line of fenced code, without its literal
|
|
885
|
+
* values (akm#1260). Fenced code holds the names agents look for (functions,
|
|
886
|
+
* classes, CLI flags, config keys, environment variable names, paths) and
|
|
887
|
+
* also example secrets, so it keeps names and drops values:
|
|
888
|
+
*
|
|
889
|
+
* - a quoted string survives only as a key (`"name":`) or an absolute path;
|
|
890
|
+
* every other quoted literal is dropped;
|
|
891
|
+
* - the value of an assignment (`KEY=value`, `key: value`, `"key": value`,
|
|
892
|
+
* `Authorization: Bearer token`) is dropped unless it is a dotted code
|
|
893
|
+
* reference (`process.env.CI`); the name stays;
|
|
894
|
+
* - long token-like strings with digits (keys, hashes) are dropped, base64
|
|
895
|
+
* included: its `/`, `+` and `=` do not cut it into short kept pieces;
|
|
896
|
+
* - camelCase, snake_case, dotted and dashed names also add their parts.
|
|
897
|
+
*
|
|
898
|
+
* Numbers, punctuation and operators never reach the result.
|
|
899
|
+
*/
|
|
900
|
+
export function fencedCodeIdentifiers(line) {
|
|
901
|
+
const cleaned = line
|
|
902
|
+
.replace(/(["'`])((?:\\.|(?!\1).)*)\1(\s*:)?/g, (match, _quote, body, colon, offset) => {
|
|
903
|
+
// Keep a key's colon when an unquoted value follows, so the assignment rule below drops that value.
|
|
904
|
+
if (colon)
|
|
905
|
+
return /^\s*["'`]/.test(line.slice(offset + match.length)) ? ` ${body} ` : ` ${body}: `;
|
|
906
|
+
return /^\/[\w./-]+$/.test(body) ? ` ${body} ` : " ";
|
|
907
|
+
})
|
|
908
|
+
.replace(/([A-Za-z_][A-Za-z0-9_.-]*\s*[:=]\s*)((?:[Bb]earer|[Bb]asic|[Tt]oken|[Dd]igest)\s+\S+|\S+)/g, (_match, name, value) => /^[!(]*[A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)+[()\]}]*[,;]?$/.test(value) ? `${name} ${value}` : name)
|
|
909
|
+
// Base64 is one token: its `/`, `+` and `=` would otherwise cut a key body into short pieces that are kept.
|
|
910
|
+
.replace(/(?<![\w.-])[A-Za-z0-9+/=]{20,}(?![\w.-])/g, (run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run) ? " " : run);
|
|
911
|
+
const words = [];
|
|
912
|
+
for (let token of cleaned.match(/--?[A-Za-z][A-Za-z0-9-]*|[A-Za-z_][A-Za-z0-9_.-]*/g) ?? []) {
|
|
913
|
+
token = token.replace(/[.-]+$/, "");
|
|
914
|
+
const bare = token.replace(/^-+/, "");
|
|
915
|
+
if (bare.length < 2)
|
|
916
|
+
continue;
|
|
917
|
+
if (bare.length >= TOKEN_LIKE_MIN_LENGTH && /\d/.test(bare))
|
|
918
|
+
continue;
|
|
919
|
+
words.push(token);
|
|
920
|
+
const parts = bare.split(/[_.-]|(?<=[a-z0-9])(?=[A-Z])/).filter((part) => part.length > 1);
|
|
921
|
+
if (parts.length > 1)
|
|
922
|
+
words.push(...parts);
|
|
923
|
+
}
|
|
924
|
+
return words.join(" ");
|
|
925
|
+
}
|
|
876
926
|
function stripMarkdownHtmlComments(line, state) {
|
|
877
927
|
let cursor = 0;
|
|
878
928
|
let visible = "";
|
|
@@ -1048,17 +1098,23 @@ function stripMarkdownLinkDestinations(text, nesting = 0) {
|
|
|
1048
1098
|
}
|
|
1049
1099
|
/**
|
|
1050
1100
|
* Derive the one low-weight search projection for an AKM-native Markdown body.
|
|
1051
|
-
* Frontmatter is removed by the caller. This projection drops comments,
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1101
|
+
* Frontmatter is removed by the caller. This projection drops comments, raw
|
|
1102
|
+
* link targets, and structural punctuation while retaining prose, headings,
|
|
1103
|
+
* link labels, and inline identifiers. From fenced code it keeps only the
|
|
1104
|
+
* identifiers ({@link fencedCodeIdentifiers}): names, flags, keys and paths,
|
|
1105
|
+
* never literal values (akm#1260). It never receives secret/env/session bytes;
|
|
1106
|
+
* that policy is enforced at the adapter metadata boundary below.
|
|
1107
|
+
*
|
|
1108
|
+
* Changing what this returns changes the indexed text of every Markdown entry,
|
|
1109
|
+
* so bump {@link CONTENT_PROJECTION_VERSION} with it: the indexer re-projects
|
|
1110
|
+
* every directory once on the next `akm index`.
|
|
1056
1111
|
*/
|
|
1057
1112
|
export function projectMarkdownContent(body, truncationInfo) {
|
|
1058
1113
|
const lines = body.split(/\r?\n/);
|
|
1059
1114
|
const innerBlock = findInnerFrontmatterBlock(lines);
|
|
1060
1115
|
const start = innerBlock && isFrontmatterShaped(lines, innerBlock) ? innerBlock.close + 1 : 0;
|
|
1061
1116
|
const projected = [];
|
|
1117
|
+
const fenced = [];
|
|
1062
1118
|
forEachVisibleMarkdownLine(lines, start, (_index, visible) => {
|
|
1063
1119
|
let trimmed = visible.trim();
|
|
1064
1120
|
if (!trimmed || /^(-{3,}|\*{3,}|_{3,}|=+)$/.test(trimmed))
|
|
@@ -1077,8 +1133,13 @@ export function projectMarkdownContent(body, truncationInfo) {
|
|
|
1077
1133
|
trimmed = trimmed.replace(/\s+/g, " ").trim();
|
|
1078
1134
|
if (trimmed)
|
|
1079
1135
|
projected.push(trimmed);
|
|
1136
|
+
}, (_index, fencedLine) => {
|
|
1137
|
+
const identifiers = fencedCodeIdentifiers(fencedLine);
|
|
1138
|
+
if (identifiers)
|
|
1139
|
+
fenced.push(identifiers);
|
|
1080
1140
|
});
|
|
1081
|
-
|
|
1141
|
+
// Fenced identifiers follow the prose, so a head-truncated embedding input keeps the prose first.
|
|
1142
|
+
const text = [...projected, ...fenced].join(" ").replace(/\s+/g, " ").trim();
|
|
1082
1143
|
if (!text)
|
|
1083
1144
|
return undefined;
|
|
1084
1145
|
if (truncationInfo)
|
|
@@ -1088,8 +1149,10 @@ export function projectMarkdownContent(body, truncationInfo) {
|
|
|
1088
1149
|
/**
|
|
1089
1150
|
* Visit each line from `start` that lies outside fenced code, with HTML
|
|
1090
1151
|
* comments removed (`visible`); fence lines themselves are never visited.
|
|
1152
|
+
* `visitFenced`, when given, receives each raw line inside a fence (not the
|
|
1153
|
+
* opening or closing fence line), from the same fence state.
|
|
1091
1154
|
*/
|
|
1092
|
-
function forEachVisibleMarkdownLine(lines, start, visit) {
|
|
1155
|
+
function forEachVisibleMarkdownLine(lines, start, visit, visitFenced) {
|
|
1093
1156
|
let fence;
|
|
1094
1157
|
const htmlComment = { inComment: false };
|
|
1095
1158
|
for (let index = start; index < lines.length; index += 1) {
|
|
@@ -1097,6 +1160,8 @@ function forEachVisibleMarkdownLine(lines, start, visit) {
|
|
|
1097
1160
|
if (fence) {
|
|
1098
1161
|
if (isMarkdownFenceClosing(rawLine, fence))
|
|
1099
1162
|
fence = undefined;
|
|
1163
|
+
else
|
|
1164
|
+
visitFenced?.(index, rawLine);
|
|
1100
1165
|
continue;
|
|
1101
1166
|
}
|
|
1102
1167
|
// A leading fence owns the whole line, including any info string that
|
|
@@ -1174,13 +1239,12 @@ export function projectMarkdownFragmentContent(raw) {
|
|
|
1174
1239
|
/**
|
|
1175
1240
|
* Priorities 1-2 of the metadata pipeline — package.json (P1), `.md`
|
|
1176
1241
|
* frontmatter (P2), and script `@param`/comment metadata (P2b) — everything
|
|
1177
|
-
* that runs BEFORE the renderer-contributor step (P3).
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1242
|
+
* that runs BEFORE the renderer-contributor step (P3). Shared by the `akm`
|
|
1243
|
+
* adapter's synchronous `recognize` and the live index-drain path; the two
|
|
1244
|
+
* differ ONLY in how they obtain the
|
|
1180
1245
|
* P3 renderer metadata (the adapter uses the synchronous
|
|
1181
1246
|
* `foldRecognizedMetadata`), guaranteeing P1/P2/P4 parity by construction.
|
|
1182
|
-
*
|
|
1183
|
-
* block. Mutates `entry` in place.
|
|
1247
|
+
* Mutates `entry` in place.
|
|
1184
1248
|
*/
|
|
1185
1249
|
export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
1186
1250
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1207,7 +1271,7 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1207
1271
|
entry.parameters = fmParams;
|
|
1208
1272
|
// Pass wiki-pattern frontmatter through onto the entry
|
|
1209
1273
|
applyWikiFrontmatter(entry, parsed.data);
|
|
1210
|
-
//
|
|
1274
|
+
// #730: reread the namespaced `provenance:` block promoteProposal stamps.
|
|
1211
1275
|
applyProvenanceFrontmatter(entry, parsed.data);
|
|
1212
1276
|
// Native Markdown has one bounded low-weight body projection. Sensitive
|
|
1213
1277
|
// types and raw session/checkpoint material never cross this boundary.
|
|
@@ -1220,7 +1284,7 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1220
1284
|
entry.content = contentProjection;
|
|
1221
1285
|
if (truncationInfo.truncated) {
|
|
1222
1286
|
entry.contentTruncated = true;
|
|
1223
|
-
// Unconditional, not warnVerbose: this bound
|
|
1287
|
+
// Unconditional, not warnVerbose: this bound sits far past any
|
|
1224
1288
|
// real document, so tripping it means
|
|
1225
1289
|
// something unusual is in the bundle and the operator should see
|
|
1226
1290
|
// it without having to pass --verbose.
|
|
@@ -1252,8 +1316,8 @@ export function applyPreContributorFields(entry, file, ctx, pkgMeta) {
|
|
|
1252
1316
|
* Priority 4 of the metadata pipeline — filename-heuristic fallbacks (P4) that
|
|
1253
1317
|
* run AFTER the renderer-contributor step (P3): a filename description when none
|
|
1254
1318
|
* was set, path/dir-derived tags, tag normalization, and alias generation.
|
|
1255
|
-
*
|
|
1256
|
-
*
|
|
1319
|
+
* Shared with {@link applyPreContributorFields} by both pipeline paths.
|
|
1320
|
+
* Mutates `entry`.
|
|
1257
1321
|
*/
|
|
1258
1322
|
export function applyPostContributorFields(entry, file, canonicalName, dirPath) {
|
|
1259
1323
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -1267,7 +1331,7 @@ export function applyPostContributorFields(entry, file, canonicalName, dirPath)
|
|
|
1267
1331
|
if (!entry.tags || entry.tags.length === 0) {
|
|
1268
1332
|
entry.tags = extractTagsFromPath(file, dirPath);
|
|
1269
1333
|
}
|
|
1270
|
-
// Stash-organization conventions
|
|
1334
|
+
// Stash-organization conventions: directory (scope/domain) tokens
|
|
1271
1335
|
// always reach the tags column, even when the author set explicit tags, so
|
|
1272
1336
|
// nested assets keep the exact-tag ranking boost for their scope token.
|
|
1273
1337
|
// Derived from canonicalName (the ref subpath) rather than the filesystem
|
|
@@ -1284,13 +1348,6 @@ export function applyPostContributorFields(entry, file, canonicalName, dirPath)
|
|
|
1284
1348
|
// Heuristic search hints are too noisy to be useful for search quality
|
|
1285
1349
|
entry.filename = path.basename(file);
|
|
1286
1350
|
}
|
|
1287
|
-
// The pre-0.9.0 flat-walk matcher-pass metadata source was DELETED in Chunk 5
|
|
1288
|
-
// F4a M-core-3. Its role (recognize a stash root's files into durable entries)
|
|
1289
|
-
// is now the `akm` adapter's `recognize`, drained by `indexer/scan/drain-dir.ts`
|
|
1290
|
-
// (`drainDirDocuments` for the live indexer, `recognizeStashEntries` for the
|
|
1291
|
-
// `manifest` fallback / `registry` index builder / metadata unit tests). The
|
|
1292
|
-
// flip is the F4 engine swap; the shadow-parity gate proved recognize produced
|
|
1293
|
-
// the identical entries before the old pass was removed.
|
|
1294
1351
|
export function buildMetadataSkipWarning(filePath, assetType, error) {
|
|
1295
1352
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1296
1353
|
// Workflow errors are already multi-line `path:line — message` blocks; print
|
|
@@ -1327,9 +1384,9 @@ function normalizeTerms(values) {
|
|
|
1327
1384
|
if (!cleaned)
|
|
1328
1385
|
continue;
|
|
1329
1386
|
normalized.add(cleaned);
|
|
1330
|
-
//
|
|
1331
|
-
//
|
|
1332
|
-
// edge cases like "kubernetes" and "status" that
|
|
1387
|
+
// No de-pluralization here: the FTS5 porter stemmer (configured with
|
|
1388
|
+
// `tokenize='porter unicode61'`) handles stemming correctly, including
|
|
1389
|
+
// edge cases like "kubernetes" and "status" that a naive s-strip mangles.
|
|
1333
1390
|
}
|
|
1334
1391
|
return Array.from(normalized);
|
|
1335
1392
|
}
|
|
@@ -1437,7 +1494,7 @@ export function extractTagsFromPath(filePath, rootDir) {
|
|
|
1437
1494
|
* Unlike {@link extractTagsFromPath} this never tokenizes the filename
|
|
1438
1495
|
* segment: it exists so a nested asset's directory (scope/domain) tokens can
|
|
1439
1496
|
* be merged into explicit author tags without dragging every filename word
|
|
1440
|
-
* into exact-tag matching
|
|
1497
|
+
* into exact-tag matching.
|
|
1441
1498
|
* Tokenization mirrors `extractTagsFromPath`: each segment splits on `-`/`_`/
|
|
1442
1499
|
* `.`, lowercased, single-character tokens dropped. A name with no directory
|
|
1443
1500
|
* segments yields no tags.
|
|
@@ -41,9 +41,13 @@ export function reciprocalRankFusion(channels) {
|
|
|
41
41
|
* 1 + w·(ln(1+pos) − ln(1+neg)) moves a score by about 2% / 3% / 5% / 9% for
|
|
42
42
|
* 1 / 2 / 5 / 20 positives at 0.03. Measured on the retrieval suite's fused
|
|
43
43
|
* lists, 5 positives lift a hit from rank 10 by a median of 3 places (from rank 5,
|
|
44
|
-
* by 2) and 20 by 4 to 5
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* by 2) and 20 by 4 to 5. What a vote can pass depends on the channels. In
|
|
45
|
+
* keyword-only search every fused score is exactly 1 / (RRF_K + rank), so
|
|
46
|
+
* neighbours in the first 20 sit 1.3 to 1.6% apart and one vote (about 2%) moves
|
|
47
|
+
* a hit one place (through rank 38) whatever the keyword margin between them. When both channels contribute, their
|
|
48
|
+
* ranks spread the scores and a vote passes only near-ties. At 0.05 the same
|
|
49
|
+
* votes lift it 4 to 5 and 6 to 7 places, and two wrong votes cost about 1.6
|
|
50
|
+
* times the nDCG@10 they cost at 0.03.
|
|
47
51
|
*/
|
|
48
52
|
export const FEEDBACK_WEIGHT = 0.03;
|
|
49
53
|
/**
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - tags: tags + aliases joined
|
|
14
14
|
* - hints: searchHints + examples + usage + intent fields
|
|
15
15
|
* - content: bounded native/adapter body projection + TOC headings + parameters
|
|
16
|
-
* (
|
|
16
|
+
* (the catch-all; BM25 weighs all five fields equally)
|
|
17
17
|
*/
|
|
18
18
|
export function buildSearchFields(entry) {
|
|
19
19
|
const name = entry.name.replace(/[-_]/g, " ");
|
|
@@ -69,20 +69,28 @@ export function insertUsageEventStrict(db, event) {
|
|
|
69
69
|
// ── Query ────────────────────────────────────────────────────────────────────
|
|
70
70
|
/**
|
|
71
71
|
* User feedback counts for many assets at once, keyed by durable `entry_ref`.
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* A negative that carried an exact fix does not count once an accepted
|
|
73
|
+
* `feedback` proposal for the asset exists at or after it (the rule `akm
|
|
74
|
+
* improve` uses for a fix already acted on); reverting that proposal makes it
|
|
75
|
+
* count again. Refs with no user feedback are absent from the map. Read in one
|
|
76
|
+
* query per chunk of refs.
|
|
74
77
|
*/
|
|
75
78
|
export function countFeedbackSignalsByRef(db, refs) {
|
|
76
79
|
const counts = new Map();
|
|
77
80
|
for (let i = 0; i < refs.length; i += SQLITE_CHUNK_SIZE) {
|
|
78
81
|
const chunk = refs.slice(i, i + SQLITE_CHUNK_SIZE);
|
|
79
82
|
const rows = db
|
|
80
|
-
.prepare(`SELECT entry_ref,
|
|
81
|
-
SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
82
|
-
SUM(CASE WHEN signal = 'negative'
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
.prepare(`SELECT u.entry_ref,
|
|
84
|
+
SUM(CASE WHEN u.signal = 'positive' THEN 1 ELSE 0 END) AS pos,
|
|
85
|
+
SUM(CASE WHEN u.signal = 'negative' AND NOT (
|
|
86
|
+
json_extract(u.metadata, '$.fix') IS NOT NULL AND EXISTS (
|
|
87
|
+
SELECT 1 FROM proposals p
|
|
88
|
+
WHERE p.ref = u.entry_ref AND p.source = 'feedback' AND p.status = 'accepted'
|
|
89
|
+
AND julianday(p.created_at) >= julianday(u.created_at))
|
|
90
|
+
) THEN 1 ELSE 0 END) AS neg
|
|
91
|
+
FROM usage_events u
|
|
92
|
+
WHERE u.event_type = 'feedback' AND u.source = 'user' AND u.entry_ref IN (${chunk.map(() => "?").join(",")})
|
|
93
|
+
GROUP BY u.entry_ref`)
|
|
86
94
|
.all(...chunk);
|
|
87
95
|
for (const row of rows)
|
|
88
96
|
counts.set(row.entry_ref, { pos: row.pos, neg: row.neg });
|
|
@@ -150,3 +158,17 @@ export function purgeOldUsageEvents(db, retentionDays) {
|
|
|
150
158
|
/* Table may not exist yet */
|
|
151
159
|
}
|
|
152
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Rewrite every `usage_events.entry_ref` naming `oldBundleId` to `newBundleId`
|
|
163
|
+
* (`akm bundle rename`). Search ranking counts feedback by `entry_ref`, so an
|
|
164
|
+
* event left under the old prefix stops counting for the renamed bundle's
|
|
165
|
+
* assets. `entry_id` is untouched: the rename keeps every entry's row id. A
|
|
166
|
+
* legacy unqualified ref is not matched by the `<old>//` prefix and is left alone.
|
|
167
|
+
* Returns the number of rows rewritten.
|
|
168
|
+
*/
|
|
169
|
+
export function renameUsageEventRefs(db, oldBundleId, newBundleId) {
|
|
170
|
+
const prefix = `${oldBundleId}//`;
|
|
171
|
+
return Number(db
|
|
172
|
+
.prepare(`UPDATE usage_events SET entry_ref = ? || substr(entry_ref, length(?) + 1) WHERE substr(entry_ref, 1, length(?)) = ?`)
|
|
173
|
+
.run(newBundleId, oldBundleId, prefix, prefix).changes);
|
|
174
|
+
}
|
|
@@ -112,7 +112,7 @@ function walkStashGit(stashRoot, options) {
|
|
|
112
112
|
? ignoredResult.stdout.toString("utf8")
|
|
113
113
|
: String(ignoredResult.stdout ?? "");
|
|
114
114
|
// Data-hygiene filename skips: the legacy metadata sidecar (never indexed as
|
|
115
|
-
// content
|
|
115
|
+
// content) plus git dot-files.
|
|
116
116
|
const SKIP_FILES = new Set([".stash.json", ".gitignore", ".gitattributes"]);
|
|
117
117
|
const stdout = Buffer.isBuffer(result.stdout) ? result.stdout.toString("utf8") : String(result.stdout ?? "");
|
|
118
118
|
const files = [...stdout.split("\0"), ...ignoredStdout.split("\0")]
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* The command comes from the harness (`harness.build`); this module never
|
|
15
15
|
* touches a model. Agents are reachable only via shell-out.
|
|
16
16
|
*/
|
|
17
|
+
import path from "node:path";
|
|
17
18
|
import { ConfigError } from "../../core/errors.js";
|
|
18
19
|
import { childProcessEnv } from "../../core/paths.js";
|
|
19
20
|
import { runManagedSubprocess, streamCaptureFailure } from "../../core/subprocess.js";
|
|
@@ -86,10 +87,14 @@ export async function runBuiltCommand(label, { argv, env, stdin }, options, stdi
|
|
|
86
87
|
// and the optional stdin payload. This keeps the agent-specific surface:
|
|
87
88
|
// the failure taxonomy.
|
|
88
89
|
const start = Date.now();
|
|
90
|
+
// A program started after `cd <dir>` has PWD equal to <dir>, and OpenCode takes its directory from PWD before its
|
|
91
|
+
// own working directory: the PWD akm inherits names where akm was started, so a run given a directory names it. One
|
|
92
|
+
// path is both, so they agree even where the kernel would resolve a `..` after a symbolic link elsewhere.
|
|
93
|
+
const cwd = options.cwd ? path.resolve(options.cwd) : undefined;
|
|
89
94
|
const result = await runManagedSubprocess([...argv], {
|
|
90
95
|
capture: stdioMode === "captured",
|
|
91
|
-
env: { ...env },
|
|
92
|
-
...(
|
|
96
|
+
env: { ...env, ...(cwd ? { PWD: cwd } : {}) },
|
|
97
|
+
...(cwd ? { cwd } : {}),
|
|
93
98
|
...(stdin !== undefined ? { stdin } : {}),
|
|
94
99
|
timeoutMs,
|
|
95
100
|
...(options.signal ? { signal: options.signal } : {}),
|
|
@@ -60,9 +60,17 @@ const MODEL_WORK_AGENT_DEFINITION = {
|
|
|
60
60
|
],
|
|
61
61
|
};
|
|
62
62
|
const CONFIG_CONTENT_ENV = "OPENCODE_CONFIG_CONTENT";
|
|
63
|
+
/**
|
|
64
|
+
* OpenCode's built-in browser plugin, removed from model work (a `-` entry in `plugins` removes a plugin, OpenCode 2.0.0
|
|
65
|
+
* and later). From 2.2.0 it appends a deny of its own skill after every agent's rules, and a rule on one skill keeps
|
|
66
|
+
* the `skill` tool listed for an agent that denies every tool; without the plugin, the `*` deny removes the tool again.
|
|
67
|
+
* Model work denies the browser anyway.
|
|
68
|
+
*/
|
|
69
|
+
const WITHOUT_BROWSER_PLUGIN = "-opencode.browser";
|
|
63
70
|
/**
|
|
64
71
|
* The inline config model work runs with: the user's own `OPENCODE_CONFIG_CONTENT` as it is, with akm's agent set on
|
|
65
|
-
* `agents.akm-model-work`, the one key akm's definition wins
|
|
72
|
+
* `agents.akm-model-work`, the one key akm's definition wins, and {@link WITHOUT_BROWSER_PLUGIN} after the user's
|
|
73
|
+
* `plugins`. OpenCode 2 reads that native `agents` map over the
|
|
66
74
|
* legacy `agent` one, so a same-named entry the user keeps in `agent` is left out rather than left to conflict. The
|
|
67
75
|
* child's value is built from the parent's, so a provider, model or key the user keeps only there stays. It is read as
|
|
68
76
|
* OpenCode reads it, as JSONC (comments and trailing commas), and written back as JSON; content that reads only once
|
|
@@ -78,6 +86,7 @@ function modelWorkConfig() {
|
|
|
78
86
|
}
|
|
79
87
|
const config = {
|
|
80
88
|
...user,
|
|
89
|
+
plugins: [...(Array.isArray(user?.plugins) ? user.plugins : []), WITHOUT_BROWSER_PLUGIN],
|
|
81
90
|
agents: { ...asRecord(user?.agents), [MODEL_WORK_AGENT]: MODEL_WORK_AGENT_DEFINITION },
|
|
82
91
|
};
|
|
83
92
|
const legacy = asRecord(user?.agent);
|
|
@@ -25,10 +25,13 @@ import { info } from "../../../../core/warn.js";
|
|
|
25
25
|
* The OpenCode release akm's own runtime runs. It is pinned because the runtime depends on things no OpenCode release
|
|
26
26
|
* promises: the inline config's shape, the `--standalone` flag, the plugin API, the text `run` prints, and the
|
|
27
27
|
* `opencode.db` tables that `../session-log-v2.ts` reads. To move it, change this version and `PACKAGES` if the release
|
|
28
|
-
* adds or drops a platform package, then run `tests/integration/opencode-runtime-sessions.test.ts`
|
|
29
|
-
*
|
|
28
|
+
* adds or drops a platform package, then run `tests/integration/opencode-runtime-sessions.test.ts` and
|
|
29
|
+
* `tests/integration/opencode-runtime-loop-guard.test.ts` against the new binary (they fetch it; the plugin ends a
|
|
30
|
+
* looping run by throwing from a hook, which OpenCode might one day handle differently, and tells the model by changing
|
|
31
|
+
* the body of a request, which it might one day stop letting a plugin do) and read
|
|
32
|
+
* `docs/reference/configuration.md` ("Without a harness") for what the version changes.
|
|
30
33
|
*/
|
|
31
|
-
export const OPENCODE_RUNTIME_VERSION = "2.
|
|
34
|
+
export const OPENCODE_RUNTIME_VERSION = "2.2.0";
|
|
32
35
|
/** The platform packages of `@opencode/cli` at {@link OPENCODE_RUNTIME_VERSION}. */
|
|
33
36
|
const PACKAGES = new Set([
|
|
34
37
|
"@opencode/cli-linux-x64",
|
|
@@ -23,7 +23,7 @@ import { openDatabase } from "../../../../storage/database.js";
|
|
|
23
23
|
import { resolveAkmInvocation } from "../../../../tasks/resolve-akm-bin.js";
|
|
24
24
|
import { buildChildEnv, runBuiltCommand, } from "../../../agent/spawn.js";
|
|
25
25
|
import { akmOpencodeDatabasePath, akmRuntimeDir } from "../roots.js";
|
|
26
|
-
import { v2SessionTokens } from "../session-log-v2.js";
|
|
26
|
+
import { v2SessionFailure, v2SessionTokens } from "../session-log-v2.js";
|
|
27
27
|
import { ensureRuntimeBinary, OPENCODE_RUNTIME_VERSION, runtimeBinaryPath } from "./binary.js";
|
|
28
28
|
import { runtimeArgv, runtimeConfig, runtimeEnv, runtimePrepareArgv } from "./command.js";
|
|
29
29
|
/**
|
|
@@ -61,10 +61,11 @@ function installPlugin(directory) {
|
|
|
61
61
|
/**
|
|
62
62
|
* OpenCode makes and migrates its database when a process starts, and two processes doing that to one database break
|
|
63
63
|
* each other (one fails with "Standalone server exited before reporting readiness"; on 2.1.4 two runs started together
|
|
64
|
-
* on a fresh directory lose one of them every time
|
|
65
|
-
* its own, under a lock file in the runtime directory that every
|
|
66
|
-
* leaves a marker naming the version; a run that finds the marker
|
|
67
|
-
* lock is reclaimed. The run itself reports anything the preparation
|
|
64
|
+
* on a fresh directory lose one of them every time; on 2.2.0, 4 of 24 starts in groups of eight failed). So the first
|
|
65
|
+
* start of this OpenCode version is a preparing run of its own, under a lock file in the runtime directory that every
|
|
66
|
+
* run, of this akm process or another, waits on, and it leaves a marker naming the version; a run that finds the marker
|
|
67
|
+
* and the database starts at once. A dead holder's lock is reclaimed. The run itself reports anything the preparation
|
|
68
|
+
* does not.
|
|
68
69
|
*/
|
|
69
70
|
async function prepareDatabase(directory, command, opts) {
|
|
70
71
|
const marker = path.join(directory, `prepared-${OPENCODE_RUNTIME_VERSION}`);
|
|
@@ -100,9 +101,9 @@ async function prepareDatabase(directory, command, opts) {
|
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
|
-
* The run's session as OpenCode recorded it, read once the process has ended: its id
|
|
104
|
-
*
|
|
105
|
-
*
|
|
104
|
+
* The run's session as OpenCode recorded it, read once the process has ended: its id, the tokens its turn used, which a
|
|
105
|
+
* workflow's `budget.max_tokens` and the unit journal count, and the message its turn failed with, if it did. Nothing
|
|
106
|
+
* when the run made no session. Best-effort: a database that cannot be read leaves the run's result as it is.
|
|
106
107
|
*/
|
|
107
108
|
function sessionRecord(sessionId) {
|
|
108
109
|
try {
|
|
@@ -111,9 +112,11 @@ function sessionRecord(sessionId) {
|
|
|
111
112
|
const tokens = v2SessionTokens(db, sessionId);
|
|
112
113
|
if (!tokens)
|
|
113
114
|
return {};
|
|
115
|
+
const failure = v2SessionFailure(db, sessionId);
|
|
114
116
|
return {
|
|
115
117
|
sessionId,
|
|
116
118
|
usage: { inputTokens: tokens.input, outputTokens: tokens.output, reasoningTokens: tokens.reasoning },
|
|
119
|
+
...(failure ? { failure } : {}),
|
|
117
120
|
};
|
|
118
121
|
}
|
|
119
122
|
finally {
|
|
@@ -160,7 +163,13 @@ export const opencodeRuntime = {
|
|
|
160
163
|
writeKeyFile(keyFile, connection.apiKey);
|
|
161
164
|
await prepareDatabase(directory, { argv: runtimePrepareArgv(binary), env }, opts);
|
|
162
165
|
const result = await runBuiltCommand(label, { argv: runtimeArgv(connection, dispatch, binary, session), env, stdin: dispatch.prompt }, opts, "captured");
|
|
163
|
-
|
|
166
|
+
const { failure, ...record } = sessionRecord(session);
|
|
167
|
+
return {
|
|
168
|
+
...result,
|
|
169
|
+
...record,
|
|
170
|
+
// OpenCode prints why the turn failed on stderr, which a workflow does not keep; the error carries it.
|
|
171
|
+
...(result.reason === "non_zero_exit" && failure ? { error: `${result.error}: ${failure}` } : {}),
|
|
172
|
+
};
|
|
164
173
|
}
|
|
165
174
|
finally {
|
|
166
175
|
fs.rmSync(keyFile, { force: true });
|
|
@@ -141,7 +141,7 @@ export function listV2Sessions(db, sinceMs) {
|
|
|
141
141
|
}
|
|
142
142
|
/**
|
|
143
143
|
* The tokens one session's turns used: OpenCode records them on each assistant message and keeps their sums on the
|
|
144
|
-
* session row (equal, measured on 2.1.4). Undefined when the session is not in the file.
|
|
144
|
+
* session row (equal, measured on 2.1.4 and 2.2.0). Undefined when the session is not in the file.
|
|
145
145
|
*/
|
|
146
146
|
export function v2SessionTokens(db, sessionId) {
|
|
147
147
|
const row = db
|
|
@@ -149,6 +149,16 @@ export function v2SessionTokens(db, sessionId) {
|
|
|
149
149
|
.get(sessionId);
|
|
150
150
|
return row ? { input: row.tokens_input, output: row.tokens_output, reasoning: row.tokens_reasoning } : undefined;
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The message a session's turn failed with, as OpenCode recorded it on the session's `idle` message (an error a plugin
|
|
154
|
+
* hook threw, a provider's refusal). Undefined when the session is not in the file or its turn did not fail.
|
|
155
|
+
*/
|
|
156
|
+
export function v2SessionFailure(db, sessionId) {
|
|
157
|
+
const row = db
|
|
158
|
+
.prepare("SELECT json_extract(data, '$.error.message') AS message FROM session_message WHERE session_id = ? AND type = 'idle' AND json_extract(data, '$.outcome') = 'failed' ORDER BY seq DESC LIMIT 1")
|
|
159
|
+
.get(sessionId);
|
|
160
|
+
return typeof row?.message === "string" && row.message.trim() !== "" ? row.message : undefined;
|
|
161
|
+
}
|
|
152
162
|
/** One session's events, from its messages in order. Throws when the session is not in the file. */
|
|
153
163
|
export function readV2Session(db, harness, sessionId, filePath) {
|
|
154
164
|
const row = db
|
|
@@ -71,7 +71,7 @@ export function shapeProposalEntry(entry, detail) {
|
|
|
71
71
|
if (detail === "normal") {
|
|
72
72
|
// `confidence` and `gateDecision` (#577) explain why a proposal is pending,
|
|
73
73
|
// so they are projected at `normal` for `akm proposal list/show` when present.
|
|
74
|
-
// `retirement`
|
|
74
|
+
// `retirement` is the same kind of field for a consolidate
|
|
75
75
|
// `retire` proposal: without it, "list/show" carries no reason a reviewer
|
|
76
76
|
// could act on (its `payload.content` is empty by design — a retire
|
|
77
77
|
// proposal deletes, it does not write).
|
|
@@ -278,7 +278,7 @@ export function shapeSearchHit(hit, detail) {
|
|
|
278
278
|
}
|
|
279
279
|
// Stash hit (local or remote)
|
|
280
280
|
// `ref` is included at `brief` so agents can run `akm show <ref>` without
|
|
281
|
-
// needing --detail full or --detail agent
|
|
281
|
+
// needing --detail full or --detail agent.
|
|
282
282
|
if (detail === "brief") {
|
|
283
283
|
return pickFields(hit, ["type", "name", "ref", "action", "estimatedTokens", "keys"]);
|
|
284
284
|
}
|
|
@@ -376,7 +376,7 @@ export function shapeShowOutput(result, detail, shape = "human") {
|
|
|
376
376
|
const base = pickFields(result, [
|
|
377
377
|
"type",
|
|
378
378
|
"name",
|
|
379
|
-
// ref is always projected, same as path/editable below
|
|
379
|
+
// ref is always projected, same as path/editable below.
|
|
380
380
|
"ref",
|
|
381
381
|
"origin",
|
|
382
382
|
"action",
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* events/history, search/curate, and the stash-lifecycle verbs (init,
|
|
8
8
|
* index, list, add, remove, update, upgrade, clone).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* sibling module: unlike `show`/`workflow`/`proposal` (each a stateful,
|
|
10
|
+
* Unlike `show`/`workflow`/`proposal` (each a stateful,
|
|
12
11
|
* multi-branch renderer with its own module), these are flat
|
|
13
12
|
* field-extraction formatters with no shared state.
|
|
14
13
|
*/
|
|
@@ -308,7 +307,7 @@ export function formatSearchPlain(r, detail) {
|
|
|
308
307
|
if (parts.length > 0)
|
|
309
308
|
lines.push(`timing: ${parts.join(", ")}`);
|
|
310
309
|
}
|
|
311
|
-
//
|
|
310
|
+
// When stash hits exist, tell the agent the next required step so it
|
|
312
311
|
// doesn't skip `akm show` and write from training memory instead.
|
|
313
312
|
if (hits.length >= 1) {
|
|
314
313
|
// Prefer skill/command/agent type hits for the "Next:" ref — knowledge docs are
|
|
@@ -517,7 +516,7 @@ export function formatAddPlain(r) {
|
|
|
517
516
|
// writes a desired locator to config — nothing is synced until a later
|
|
518
517
|
// `akm bundle update`. Force-fitting the declarative shape's `added`/`entry`/`message`
|
|
519
518
|
// fields into the "Installed <ref> (N scanned...)" wording produced
|
|
520
|
-
// "Installed undefined (0 directories scanned...)"
|
|
519
|
+
// "Installed undefined (0 directories scanned...)" because it
|
|
521
520
|
// implied a sync that never happened. `AddResponse` always carries an
|
|
522
521
|
// `index` block (even when zero entries were scanned); `SourceAddResult`
|
|
523
522
|
// never does — that is the reliable shape discriminator used below to
|
|
@@ -630,11 +629,11 @@ export function formatBundleRenamePlain(r) {
|
|
|
630
629
|
return lines.join("\n");
|
|
631
630
|
}
|
|
632
631
|
export function formatUpdatePlain(r) {
|
|
633
|
-
//
|
|
632
|
+
// `processed` alone would conflate three cases into one empty array —
|
|
634
633
|
// a true no-op (--all with nothing configured), a source this call flatly
|
|
635
634
|
// never looked at (a plain source under --all), and a plain git/website
|
|
636
635
|
// source that WAS just synced successfully but has no `UpdateResultItem`
|
|
637
|
-
// shape to report (no version/lock to diff). All three
|
|
636
|
+
// shape to report (no version/lock to diff). All three would render as the
|
|
638
637
|
// same misleading "nothing to update". `plainSynced` and `skipped` (see
|
|
639
638
|
// sources/types.ts) cover the other two so this can report accurately.
|
|
640
639
|
const processed = r.processed;
|