akm-cli 0.9.14 → 0.9.15-beta.2
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 +559 -0
- package/STABILITY.md +6 -3
- package/dist/akm +54 -1
- package/dist/akm-migrate +34 -1
- package/dist/assets/prompts/reflect-feedback-framing.md +1 -0
- package/dist/assets/prompts/reflect-llm-framed-contract.md +2 -0
- package/dist/assets/prompts/reflect-llm-schema-contract.md +2 -0
- package/dist/assets/tasks/core/improve.yml +1 -1
- package/dist/assets/tasks/core/index-refresh.yml +1 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +1 -1
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +1 -1
- package/dist/cli/retired-commands.js +0 -1
- package/dist/cli/shared.js +9 -0
- package/dist/cli/unknown-flags.js +1 -0
- package/dist/cli.js +40 -3
- package/dist/commands/config-cli.js +85 -3
- package/dist/commands/env/env-cli.js +1 -42
- package/dist/commands/env/env.js +1 -1
- package/dist/commands/env/secret-cli.js +1 -2
- package/dist/commands/health/checks.js +357 -63
- package/dist/commands/health/engine-usage.js +45 -0
- package/dist/commands/health/improve-metrics.js +18 -0
- package/dist/commands/health/llm-usage.js +41 -1
- package/dist/commands/health/plugin-staleness.js +7 -3
- package/dist/commands/health/version-drift.js +93 -0
- package/dist/commands/health/windows.js +3 -1
- package/dist/commands/health.js +44 -9
- package/dist/commands/improve/consolidate/chunking.js +4 -2
- package/dist/commands/improve/improve-cli.js +99 -5
- package/dist/commands/improve/improve-report.js +154 -0
- package/dist/commands/improve/improve-result-file.js +45 -33
- package/dist/commands/improve/improve-strategies.js +133 -3
- package/dist/commands/improve/improve-usage-report.js +182 -0
- package/dist/commands/improve/improve.js +40 -3
- package/dist/commands/improve/locks.js +28 -78
- package/dist/commands/improve/planner.js +1 -0
- package/dist/commands/improve/preparation.js +9 -1
- package/dist/commands/improve/reflect.js +44 -4
- package/dist/commands/models-cli.js +50 -1
- package/dist/commands/proposal/repository.js +8 -3
- package/dist/commands/proposal/validators/proposal-quality-validators.js +41 -6
- package/dist/commands/proposal/validators/proposal-validators.js +24 -0
- package/dist/commands/read/search-cli.js +38 -2
- package/dist/commands/read/show.js +103 -4
- package/dist/commands/sources/info.js +5 -1
- package/dist/commands/sources/installed-stashes.js +58 -16
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +48 -0
- package/dist/commands/tasks/tasks-cli.js +49 -2
- package/dist/commands/workflow-cli.js +86 -12
- package/dist/core/asset/markdown-fragments.js +35 -0
- package/dist/core/config/config-schema.js +14 -0
- package/dist/core/config/config.js +302 -24
- package/dist/core/config/schema/embedding.js +41 -0
- package/dist/core/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/file-lock.js +49 -15
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/parent-watchdog.js +64 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +107 -0
- package/dist/core/sensitive-marker-path.js +19 -0
- package/dist/core/state-db.js +74 -14
- package/dist/indexer/index-rebuild-lock.js +73 -0
- package/dist/indexer/index-writer-lock.js +40 -1
- package/dist/indexer/index-written-assets.js +29 -1
- package/dist/indexer/indexer.js +93 -29
- package/dist/indexer/materialize-embeddings.js +564 -48
- package/dist/indexer/search/db-search.js +49 -2
- package/dist/indexer/search/search-source.js +23 -1
- package/dist/integrations/agent/engine-resolution.js +96 -6
- package/dist/integrations/agent/execution-definitions.js +6 -15
- package/dist/integrations/agent/execution-lowering.js +6 -1
- package/dist/integrations/agent/execution-preparation.js +1 -1
- package/dist/integrations/agent/model-map.js +123 -20
- package/dist/integrations/agent/prompts.js +40 -8
- package/dist/integrations/agent/runner-dispatch.js +9 -3
- package/dist/integrations/agent/runner.js +2 -0
- package/dist/llm/client.js +8 -3
- package/dist/llm/embedder.js +20 -8
- package/dist/llm/embedders/local.js +10 -2
- package/dist/llm/embedders/remote.js +497 -32
- package/dist/output/shapes/helpers.js +38 -2
- package/dist/output/shapes/models-list.js +16 -0
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/shapes.js +4 -0
- package/dist/output/text/command-format.js +29 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/improve-report.js +27 -0
- package/dist/{commands/env/marker-path.js → output/text/models.js} +4 -3
- package/dist/output/text/show-format.js +4 -0
- package/dist/output/text.js +4 -0
- package/dist/scripts/akm-migrate-node.js +25146 -21759
- package/dist/scripts/akm-migrate.js +24271 -20885
- package/dist/storage/repositories/embedding-salvage-repository.js +184 -0
- package/dist/storage/repositories/improve-runs-repository.js +34 -0
- package/dist/storage/repositories/index-fts-repository.js +49 -6
- package/dist/storage/repositories/index-schema.js +16 -0
- package/dist/storage/repositories/index-vec-repository.js +30 -0
- package/dist/storage/repositories/workflow-runs-repository.js +55 -18
- package/dist/tasks/backends/cron.js +14 -7
- package/dist/tasks/run/run-native-task.js +23 -1
- package/dist/tasks/run/run-workflow-task.js +16 -0
- package/dist/workflows/exec/child-workflow.js +2 -2
- package/dist/workflows/exec/dispatch-redaction.js +21 -9
- package/dist/workflows/exec/run-workflow.js +6 -5
- package/dist/workflows/runtime/runs.js +33 -5
- package/docs/migration/release-notes/0.9.15.md +133 -0
- package/docs/migration/release-notes/README.md +5 -0
- package/docs/reference/cli.md +271 -30
- package/docs/reference/configuration.md +234 -21
- package/docs/reference/data-and-telemetry.md +8 -0
- package/docs/reference/tasks.md +16 -1
- package/docs/reference/workflow-schema.md +5 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +47 -0
|
@@ -30,8 +30,14 @@ import { akmShowUnified } from "./show.js";
|
|
|
30
30
|
* rejected — the command then runs against the DEFAULT `--from` value
|
|
31
31
|
* (local) instead of the source the caller named, with exit 0 and no error.
|
|
32
32
|
* Reject it explicitly instead.
|
|
33
|
+
*
|
|
34
|
+
* Exported so `akm task list` (`../tasks/tasks-cli.ts`, #951) — a pure
|
|
35
|
+
* delegating alias for `akm search --type task` — applies the identical
|
|
36
|
+
* guard rather than a second copy of it; a task-list-scoped `--source` must
|
|
37
|
+
* fail exactly like `search`'s does, not fall through to citty's silent
|
|
38
|
+
* unknown-flag absorption.
|
|
33
39
|
*/
|
|
34
|
-
function rejectRetiredSourceFlag() {
|
|
40
|
+
export function rejectRetiredSourceFlag() {
|
|
35
41
|
if (!getParsedInvocation().hasFlag("--source"))
|
|
36
42
|
return;
|
|
37
43
|
throw new UsageError("`--source` was renamed to `--from` in 0.9. Use `--from local|registry|all` instead.", "INVALID_FLAG_VALUE");
|
|
@@ -141,7 +147,7 @@ export const searchCommand = defineJsonCommand({
|
|
|
141
147
|
export const curateCommand = defineJsonCommand({
|
|
142
148
|
meta: {
|
|
143
149
|
name: "curate",
|
|
144
|
-
description: "Pick the assets worth loading for a task. Unlike `akm search`, this
|
|
150
|
+
description: "Pick the assets worth loading for a task. Unlike `akm search`, this attaches a preview and run details per hit, adds related support refs, and summarizes the set — the usual starting point for an agent.",
|
|
145
151
|
},
|
|
146
152
|
args: {
|
|
147
153
|
// Optional in citty so run() is invoked when omitted; we re-validate
|
|
@@ -283,6 +289,18 @@ export const showCommand = defineJsonCommand({
|
|
|
283
289
|
type: "string",
|
|
284
290
|
description: "Scope filter (repeatable): --filter user=<id> --filter agent=<id> --filter run=<id> --filter channel=<name>. Narrows resolution to assets whose frontmatter scope matches. Same axis as `akm search --filter`.",
|
|
285
291
|
},
|
|
292
|
+
context: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Fragment presentation: exact (default) returns only the selected section; lead returns bounded indexed-safe document lead plus the explicitly labelled selected match.",
|
|
295
|
+
},
|
|
296
|
+
"max-tokens": {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "Approximate context budget in tokens (four characters per token). Requires --context lead; mutually exclusive with --max-chars.",
|
|
299
|
+
},
|
|
300
|
+
"max-chars": {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "Exact context budget in characters. Requires --context lead; mutually exclusive with --max-tokens.",
|
|
303
|
+
},
|
|
286
304
|
// Declared as the POSITIVE name with `default: true` — see the
|
|
287
305
|
// `project-context` comment on `searchCommand` above for why a flag NAME
|
|
288
306
|
// must never start with `no-`.
|
|
@@ -331,10 +349,22 @@ export const showCommand = defineJsonCommand({
|
|
|
331
349
|
// commands share one spelling for the scope-narrowing axis).
|
|
332
350
|
const scopeTokens = parseAllFlagValues("--filter");
|
|
333
351
|
const scope = parseScopeFilterFlags(scopeTokens, "--filter");
|
|
352
|
+
const contextMode = parseFragmentContextMode(typeof args.context === "string" ? args.context : undefined);
|
|
353
|
+
const maxTokens = parsePositiveIntFlag(args["max-tokens"] ?? undefined, "--max-tokens");
|
|
354
|
+
const maxChars = parsePositiveIntFlag(args["max-chars"] ?? undefined, "--max-chars");
|
|
355
|
+
if (maxTokens !== undefined && maxChars !== undefined) {
|
|
356
|
+
throw new UsageError("--max-tokens and --max-chars are mutually exclusive.", "INVALID_FLAG_VALUE");
|
|
357
|
+
}
|
|
358
|
+
const maxContextChars = maxChars ?? (maxTokens !== undefined ? maxTokens * 4 : undefined);
|
|
359
|
+
if (maxContextChars !== undefined && !Number.isSafeInteger(maxContextChars)) {
|
|
360
|
+
throw new UsageError("Fragment context budget is too large.", "INVALID_FLAG_VALUE");
|
|
361
|
+
}
|
|
334
362
|
const skipLogging = args["track-usage"] === false;
|
|
335
363
|
const result = await akmShowUnified({
|
|
336
364
|
ref: args.ref,
|
|
337
365
|
detail: showDetail,
|
|
366
|
+
contextMode,
|
|
367
|
+
maxContextChars,
|
|
338
368
|
scope,
|
|
339
369
|
skipLogging,
|
|
340
370
|
eventSource: resolveUsageEventSource(),
|
|
@@ -342,3 +372,9 @@ export const showCommand = defineJsonCommand({
|
|
|
342
372
|
output("show", result);
|
|
343
373
|
},
|
|
344
374
|
});
|
|
375
|
+
export function parseFragmentContextMode(raw) {
|
|
376
|
+
const normalized = raw?.trim().toLowerCase() || "exact";
|
|
377
|
+
if (normalized === "exact" || normalized === "lead")
|
|
378
|
+
return normalized;
|
|
379
|
+
throw new UsageError(`Invalid --context value: "${raw}". Expected exact or lead.`, "INVALID_FLAG_VALUE");
|
|
380
|
+
}
|
|
@@ -23,7 +23,7 @@ import { assetPathForName, stashDirFor } from "../../core/asset/asset-placement.
|
|
|
23
23
|
import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
24
24
|
import { parseFrontmatter } from "../../core/asset/frontmatter.js";
|
|
25
25
|
import { extractSection, markdownFragmentSlugs } from "../../core/asset/markdown.js";
|
|
26
|
-
import { fragmentForSelector } from "../../core/asset/markdown-fragments.js";
|
|
26
|
+
import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js";
|
|
27
27
|
import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
28
28
|
import { META_DIR, parseMetaRef, readMetaFile } from "../../core/asset/stash-meta.js";
|
|
29
29
|
import { asNonEmptyString, isWithin } from "../../core/common.js";
|
|
@@ -65,6 +65,7 @@ import { getActiveWorkflowRun } from "../../workflows/runtime/runs.js";
|
|
|
65
65
|
*/
|
|
66
66
|
export async function akmShowUnified(input) {
|
|
67
67
|
const ref = input.ref.trim();
|
|
68
|
+
validateFragmentContextRequest(input);
|
|
68
69
|
// 0a. Stash `.meta/` convention: `[origin//]meta[:name]` direct-reads a
|
|
69
70
|
// human-authored orientation doc from the stash's `.meta/` directory.
|
|
70
71
|
// These files are not indexed (the walker skips dot-dirs), so they are
|
|
@@ -148,6 +149,32 @@ async function showStashMeta(metaRef) {
|
|
|
148
149
|
function hasAnyScopeKey(scope) {
|
|
149
150
|
return Boolean(scope.user || scope.agent || scope.run || scope.channel);
|
|
150
151
|
}
|
|
152
|
+
function validateFragmentContextRequest(input) {
|
|
153
|
+
const mode = input.contextMode ?? "exact";
|
|
154
|
+
if (mode !== "exact" && mode !== "lead") {
|
|
155
|
+
throw new UsageError(`Invalid --context value: "${String(mode)}". Expected exact or lead.`, "INVALID_FLAG_VALUE");
|
|
156
|
+
}
|
|
157
|
+
if (input.maxContextChars !== undefined &&
|
|
158
|
+
(!Number.isSafeInteger(input.maxContextChars) || input.maxContextChars <= 0)) {
|
|
159
|
+
throw new UsageError("Fragment context budget must be a positive safe integer.", "INVALID_FLAG_VALUE");
|
|
160
|
+
}
|
|
161
|
+
if (mode !== "lead" && input.maxContextChars !== undefined) {
|
|
162
|
+
throw new UsageError("--max-chars and --max-tokens require --context lead.", "INVALID_FLAG_VALUE");
|
|
163
|
+
}
|
|
164
|
+
if (mode !== "lead")
|
|
165
|
+
return;
|
|
166
|
+
if (parseMetaRef(input.ref)) {
|
|
167
|
+
throw new UsageError("--context lead requires an indexed Markdown asset fragment.", "INVALID_FLAG_VALUE");
|
|
168
|
+
}
|
|
169
|
+
const parsed = parseBundleRef(input.ref);
|
|
170
|
+
if (!parsed.fragment) {
|
|
171
|
+
throw new UsageError("--context lead requires a fragment-qualified ref.", "INVALID_FLAG_VALUE");
|
|
172
|
+
}
|
|
173
|
+
const type = typeNameFromConceptId(parsed.conceptId)?.type;
|
|
174
|
+
if (type === "env" || type === "secret") {
|
|
175
|
+
throw new UsageError(`--context lead is unavailable for sensitive ${type} assets.`, "INVALID_FLAG_VALUE");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
151
178
|
/**
|
|
152
179
|
* Read the asset file's frontmatter and verify its `scope_*` keys satisfy
|
|
153
180
|
* every supplied filter. Throws a {@link NotFoundError} on mismatch so the
|
|
@@ -184,6 +211,7 @@ function enforceScopeOrThrow(filePath, ref, scope) {
|
|
|
184
211
|
}
|
|
185
212
|
/** @internal Use akmShowUnified() for all external callers. */
|
|
186
213
|
export async function showLocal(input) {
|
|
214
|
+
validateFragmentContextRequest(input);
|
|
187
215
|
const parsed = parseBundleRef(input.ref);
|
|
188
216
|
warnSensitiveFragmentUnsupported(parsed);
|
|
189
217
|
const assetParts = typeNameFromConceptId(parsed.conceptId);
|
|
@@ -235,14 +263,19 @@ export async function showLocal(input) {
|
|
|
235
263
|
}
|
|
236
264
|
const fileCtx = buildFileContext(sourceStashDir, assetPath);
|
|
237
265
|
const presentedName = indexedEntry.name;
|
|
238
|
-
const
|
|
266
|
+
const opaqueFragmentSelector = parsed.fragment?.startsWith("akm-fragment-") === true;
|
|
267
|
+
// Friendly heading selectors retain the source-live exact response contract.
|
|
268
|
+
// Only resolve the indexed-safe revision when the selector itself is opaque,
|
|
269
|
+
// or when the caller explicitly opts into indexed-safe contextual output.
|
|
270
|
+
const indexedFragment = parsed.fragment && (opaqueFragmentSelector || input.contextMode === "lead")
|
|
239
271
|
? withIndexDb((db) => getIndexedMarkdownFragment(db, indexedEntry.itemRef, parsed.fragment))
|
|
240
272
|
: undefined;
|
|
273
|
+
const indexedFragmentContent = opaqueFragmentSelector ? indexedFragment?.content : undefined;
|
|
241
274
|
const indexedRenderer = rendererForIndexedEntry(indexedEntry, fileCtx);
|
|
242
275
|
let response;
|
|
243
276
|
try {
|
|
244
277
|
if (indexedRenderer === null) {
|
|
245
|
-
response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment,
|
|
278
|
+
response = buildIndexedProjectionResponse(indexedEntry, assetPath, parsed.fragment, indexedFragmentContent);
|
|
246
279
|
}
|
|
247
280
|
else {
|
|
248
281
|
const match = typeof indexedRenderer === "string" ? indexedMatch(indexedEntry, indexedRenderer) : recognizeMatch(fileCtx);
|
|
@@ -263,7 +296,7 @@ export async function showLocal(input) {
|
|
|
263
296
|
warn(`Fragment "#${parsed.fragment}" was ignored: ${makeBundleRef(parsed.bundle, parsed.conceptId)} is not a Markdown document, so heading fragments do not apply. Showing the whole asset.`);
|
|
264
297
|
}
|
|
265
298
|
else {
|
|
266
|
-
applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName,
|
|
299
|
+
applyMarkdownFragment(response, fileCtx.content(), parsed.fragment, presentedName, indexedFragmentContent);
|
|
267
300
|
}
|
|
268
301
|
}
|
|
269
302
|
}
|
|
@@ -280,6 +313,41 @@ export async function showLocal(input) {
|
|
|
280
313
|
conceptId: indexedEntry.conceptId,
|
|
281
314
|
bundleId: indexedEntry.bundleId,
|
|
282
315
|
}, config.defaultBundle ?? (isPrimaryStash ? indexedEntry.bundleId : undefined));
|
|
316
|
+
if (parsed.fragment && indexedFragment) {
|
|
317
|
+
const selectedFragmentId = indexedFragment.fragments[indexedFragment.ordinal].fragmentId;
|
|
318
|
+
const selectedRef = `${canonicalRef}#${selectedFragmentId}`;
|
|
319
|
+
const parentEstimatedTokens = typeof indexedEntry.document?.fileSize === "number"
|
|
320
|
+
? Math.round(indexedEntry.document.fileSize / 4)
|
|
321
|
+
: Math.round(indexedFragment.parentChars / 4);
|
|
322
|
+
Object.assign(response, {
|
|
323
|
+
selectedRef,
|
|
324
|
+
parentRef: canonicalRef,
|
|
325
|
+
fragmentOrdinal: indexedFragment.ordinal + 1,
|
|
326
|
+
fragmentCount: indexedFragment.count,
|
|
327
|
+
startLine: indexedFragment.startLine,
|
|
328
|
+
endLine: indexedFragment.endLine,
|
|
329
|
+
...(indexedFragment.previousFragmentId
|
|
330
|
+
? { previousRef: `${canonicalRef}#${indexedFragment.previousFragmentId}` }
|
|
331
|
+
: {}),
|
|
332
|
+
...(indexedFragment.nextFragmentId ? { nextRef: `${canonicalRef}#${indexedFragment.nextFragmentId}` } : {}),
|
|
333
|
+
fragmentChars: indexedFragment.fragmentChars,
|
|
334
|
+
fragmentEstimatedTokens: Math.round(indexedFragment.fragmentChars / 4),
|
|
335
|
+
parentChars: indexedFragment.parentChars,
|
|
336
|
+
parentEstimatedTokens,
|
|
337
|
+
contextMode: input.contextMode ?? "exact",
|
|
338
|
+
contextTruncated: false,
|
|
339
|
+
});
|
|
340
|
+
if (input.contextMode === "lead") {
|
|
341
|
+
const contextMaxChars = input.maxContextChars ?? MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS;
|
|
342
|
+
const contextual = buildMarkdownLeadContext(indexedFragment.fragments, indexedFragment.ordinal, contextMaxChars);
|
|
343
|
+
applyMarkdownResponsePayload(response, contextual.content);
|
|
344
|
+
response.contextMaxChars = contextMaxChars;
|
|
345
|
+
response.contextTruncated = contextual.truncated;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else if (input.contextMode === "lead") {
|
|
349
|
+
throw new NotFoundError(`Indexed-safe fragment context is unavailable for ${input.ref}.`);
|
|
350
|
+
}
|
|
283
351
|
if (response.type === "workflow")
|
|
284
352
|
response.action = buildWorkflowAction(canonicalRef);
|
|
285
353
|
// 07 P1-D: provenance-aware toolPolicy CEILING. An agent's self-declared
|
|
@@ -541,6 +609,14 @@ function applyMarkdownFragment(response, raw, fragment, name, indexedFragmentCon
|
|
|
541
609
|
else
|
|
542
610
|
response.content = section;
|
|
543
611
|
}
|
|
612
|
+
function applyMarkdownResponsePayload(response, content) {
|
|
613
|
+
if (response.template !== undefined)
|
|
614
|
+
response.template = content;
|
|
615
|
+
else if (response.prompt !== undefined)
|
|
616
|
+
response.prompt = content;
|
|
617
|
+
else
|
|
618
|
+
response.content = content;
|
|
619
|
+
}
|
|
544
620
|
function requireMarkdownSection(content, fragment, name) {
|
|
545
621
|
const section = extractSection(content, fragment);
|
|
546
622
|
if (section)
|
|
@@ -572,6 +648,7 @@ function buildBriefResponse(full, assetPath) {
|
|
|
572
648
|
...(summary.origin !== undefined ? { origin: summary.origin } : {}),
|
|
573
649
|
...(full.editable !== undefined ? { editable: full.editable } : {}),
|
|
574
650
|
...(full.editHint ? { editHint: full.editHint } : {}),
|
|
651
|
+
...fragmentResponseProjection(full),
|
|
575
652
|
};
|
|
576
653
|
}
|
|
577
654
|
/**
|
|
@@ -607,6 +684,28 @@ function buildSummaryResponse(full, assetPath) {
|
|
|
607
684
|
...(full.origin !== undefined ? { origin: full.origin } : {}),
|
|
608
685
|
...(full.editable !== undefined ? { editable: full.editable } : {}),
|
|
609
686
|
...(full.editHint ? { editHint: full.editHint } : {}),
|
|
687
|
+
...fragmentResponseProjection(full),
|
|
610
688
|
};
|
|
611
689
|
return summary;
|
|
612
690
|
}
|
|
691
|
+
function fragmentResponseProjection(full) {
|
|
692
|
+
if (!full.selectedRef)
|
|
693
|
+
return {};
|
|
694
|
+
return {
|
|
695
|
+
selectedRef: full.selectedRef,
|
|
696
|
+
parentRef: full.parentRef,
|
|
697
|
+
fragmentOrdinal: full.fragmentOrdinal,
|
|
698
|
+
fragmentCount: full.fragmentCount,
|
|
699
|
+
startLine: full.startLine,
|
|
700
|
+
endLine: full.endLine,
|
|
701
|
+
previousRef: full.previousRef,
|
|
702
|
+
nextRef: full.nextRef,
|
|
703
|
+
fragmentChars: full.fragmentChars,
|
|
704
|
+
fragmentEstimatedTokens: full.fragmentEstimatedTokens,
|
|
705
|
+
parentChars: full.parentChars,
|
|
706
|
+
parentEstimatedTokens: full.parentEstimatedTokens,
|
|
707
|
+
contextMode: full.contextMode,
|
|
708
|
+
contextMaxChars: full.contextMaxChars,
|
|
709
|
+
contextTruncated: full.contextTruncated,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
@@ -5,7 +5,7 @@ import { placementTypes } from "../../core/asset/asset-placement.js";
|
|
|
5
5
|
import { resolveStashDir } from "../../core/common.js";
|
|
6
6
|
import { getSources, loadConfig } from "../../core/config/config.js";
|
|
7
7
|
import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
|
|
8
|
-
import { getDbPath } from "../../core/paths.js";
|
|
8
|
+
import { getCacheDir, getConfigDir, getDataDir, getDbPath, getStateDir } from "../../core/paths.js";
|
|
9
9
|
import { formatRegistryUrl } from "../../core/registry-url.js";
|
|
10
10
|
import { error } from "../../core/warn.js";
|
|
11
11
|
import { closeDatabase, openExistingDatabase } from "../../storage/repositories/index-connection.js";
|
|
@@ -69,6 +69,10 @@ export function assembleInfo(options) {
|
|
|
69
69
|
version: pkgVersion,
|
|
70
70
|
bundleDir: stashDir,
|
|
71
71
|
defaultBundle,
|
|
72
|
+
dataDir: getDataDir(),
|
|
73
|
+
configDir: getConfigDir(),
|
|
74
|
+
cacheDir: getCacheDir(),
|
|
75
|
+
stateDir: getStateDir(),
|
|
72
76
|
assetTypes,
|
|
73
77
|
searchModes,
|
|
74
78
|
semanticSearch: {
|
|
@@ -26,7 +26,7 @@ import { beginImmediateTransaction, getStateDbPath, openStateDatabase } from "..
|
|
|
26
26
|
import { warn } from "../../core/warn.js";
|
|
27
27
|
import { resolveGitContentRoot } from "../../core/write-source.js";
|
|
28
28
|
import { withAssetMutationLease } from "../../indexer/index-writer-lock.js";
|
|
29
|
-
import { akmIndex } from "../../indexer/indexer.js";
|
|
29
|
+
import { akmIndex, runEmbeddingPass } from "../../indexer/indexer.js";
|
|
30
30
|
import { compareAndSwapLockfileSnapshot, publishLockfileUpdate, readLockfile, readLockfileForUpdate, } from "../../integrations/lockfile.js";
|
|
31
31
|
import { parseRegistryRef } from "../../registry/resolve.js";
|
|
32
32
|
import { sha256Hex } from "../../runtime.js";
|
|
@@ -350,11 +350,19 @@ export async function akmRemove(input) {
|
|
|
350
350
|
},
|
|
351
351
|
};
|
|
352
352
|
}
|
|
353
|
-
/**
|
|
353
|
+
/**
|
|
354
|
+
* Read the current index generation without creating or hydrating anything.
|
|
355
|
+
* This path never ran an embedding pass (#954, field-report follow-up), so it has no
|
|
356
|
+
* `verification` to report — {@link buildUpdateResponse} falls back to the
|
|
357
|
+
* two facts it can actually know (whether semantic search is configured on
|
|
358
|
+
* at all) rather than fabricating verification numbers (`entryCount: 0`,
|
|
359
|
+
* `ok: true`) for a run that never verified anything.
|
|
360
|
+
*/
|
|
354
361
|
function readCurrentIndexSummary() {
|
|
355
362
|
const db = openReadonlyExistingDatabase(getDbPath());
|
|
356
|
-
if (!db)
|
|
363
|
+
if (!db) {
|
|
357
364
|
return { mode: "incremental", totalEntries: 0, directoriesScanned: 0, directoriesSkipped: 0 };
|
|
365
|
+
}
|
|
358
366
|
try {
|
|
359
367
|
return {
|
|
360
368
|
mode: "incremental",
|
|
@@ -388,6 +396,12 @@ function buildUpdateResponse(stashDir, target, all, processed, opts) {
|
|
|
388
396
|
directoriesScanned: index.directoriesScanned,
|
|
389
397
|
directoriesSkipped: index.directoriesSkipped,
|
|
390
398
|
...(index.scanComplete !== undefined ? { scanComplete: index.scanComplete } : {}),
|
|
399
|
+
// A real embedding pass (`akmIndex`/`runEmbeddingPass`) reports its own
|
|
400
|
+
// verified `semanticStatus`. When no pass ran this update (the
|
|
401
|
+
// no-op/nothing-configured fallback) the only two facts known without
|
|
402
|
+
// fabricating a verification are whether semantic search is off at all
|
|
403
|
+
// or, if not, that its state is simply unverified this run.
|
|
404
|
+
semanticStatus: index.verification?.semanticStatus ?? (finalConfig.semanticSearchMode === "off" ? "disabled" : "pending"),
|
|
391
405
|
},
|
|
392
406
|
};
|
|
393
407
|
}
|
|
@@ -499,6 +513,45 @@ function closeUnifiedUpdateTransaction(transaction, committed) {
|
|
|
499
513
|
: `[akm bundle update] rolled back, but closing its database handles failed: ${String(closeError)}`);
|
|
500
514
|
}
|
|
501
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Run the embedding phase AFTER the coordinator's atomic commit, on its own
|
|
518
|
+
* fresh connection (#954). Before this, `akmUpdate` called
|
|
519
|
+
* `akmIndex()` for its embedding phase too, INSIDE this same unified
|
|
520
|
+
* `BEGIN IMMEDIATE` — so every per-batch commit the materializer opened
|
|
521
|
+
* nested as an unobservable SAVEPOINT, and a SIGKILL mid-run lost every
|
|
522
|
+
* embedding of the run rather than just the one in flight. The
|
|
523
|
+
* ambient-transaction drift guard (#954) now rejects that outright, so
|
|
524
|
+
* `akmIndex`'s own embedding phase is skipped for a deferred update
|
|
525
|
+
* transaction and this runs instead, once content/lock/index/state are
|
|
526
|
+
* already durably committed.
|
|
527
|
+
*
|
|
528
|
+
* A failing pass (provider down, timeout) does NOT fail the update: the
|
|
529
|
+
* bundle content and index are already committed successfully, exactly like
|
|
530
|
+
* a plain `akm index` whose embedding phase fails — only the reported
|
|
531
|
+
* `verification` reflects the shortfall (`semanticStatus: "blocked"`).
|
|
532
|
+
*/
|
|
533
|
+
async function runPostCommitEmbeddingPass(index) {
|
|
534
|
+
const config = loadConfig();
|
|
535
|
+
let db;
|
|
536
|
+
try {
|
|
537
|
+
const embeddingDim = config.embedding?.dimension;
|
|
538
|
+
db = openIndexDatabase(getDbPath(), embeddingDim ? { embeddingDim } : undefined);
|
|
539
|
+
const { verification } = await runEmbeddingPass({ db, config, onProgress: () => { } });
|
|
540
|
+
return { ...index, verification };
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
544
|
+
warn(`[akm bundle update] post-commit embedding pass failed: ${message}`);
|
|
545
|
+
return {
|
|
546
|
+
...index,
|
|
547
|
+
verification: { ...index.verification, ok: false, semanticStatus: "blocked", message },
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
if (db)
|
|
552
|
+
closeDatabase(db);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
502
555
|
function pathAtOrBelow(candidate, root) {
|
|
503
556
|
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
504
557
|
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
@@ -779,14 +832,8 @@ async function publishPreparedPlainUpdate(id, ref, prepared, stashDir, allowInse
|
|
|
779
832
|
updateTransactionHook("before-commit", id, { db: transaction.db });
|
|
780
833
|
commitUnifiedUpdateTransaction(transaction);
|
|
781
834
|
committed = true;
|
|
782
|
-
try {
|
|
783
|
-
transaction.deferred.afterCommit?.();
|
|
784
|
-
}
|
|
785
|
-
catch (error) {
|
|
786
|
-
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
787
|
-
}
|
|
788
835
|
prepared.publication?.commit();
|
|
789
|
-
return index;
|
|
836
|
+
return await runPostCommitEmbeddingPass(index);
|
|
790
837
|
}
|
|
791
838
|
catch (error) {
|
|
792
839
|
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
@@ -1093,12 +1140,6 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
|
|
|
1093
1140
|
updateTransactionHook("before-commit", managed.installId, { db: transaction.db });
|
|
1094
1141
|
commitUnifiedUpdateTransaction(transaction);
|
|
1095
1142
|
committed = true;
|
|
1096
|
-
try {
|
|
1097
|
-
transaction.deferred.afterCommit?.();
|
|
1098
|
-
}
|
|
1099
|
-
catch (error) {
|
|
1100
|
-
warn(`[akm bundle update] committed, but semantic status refresh failed: ${String(error)}`);
|
|
1101
|
-
}
|
|
1102
1143
|
}
|
|
1103
1144
|
catch (error) {
|
|
1104
1145
|
let recoveryError = transaction ? rollbackUnifiedUpdateTransaction(transaction) : undefined;
|
|
@@ -1146,6 +1187,7 @@ async function updateManagedInstall(managed, force, yes, stashDir, allowInsecure
|
|
|
1146
1187
|
}
|
|
1147
1188
|
}
|
|
1148
1189
|
prepared.publication?.commit();
|
|
1190
|
+
index = await runPostCommitEmbeddingPass(index);
|
|
1149
1191
|
if (movedRoot) {
|
|
1150
1192
|
const currentConfig = loadConfig();
|
|
1151
1193
|
const currentLocks = readLockfile();
|
|
@@ -174,10 +174,10 @@ export function getAkmBinaryName() {
|
|
|
174
174
|
return "akm-windows-x64.exe";
|
|
175
175
|
throw new ConfigError(`Unsupported platform for binary upgrade: ${platform}/${arch}`, "UNSUPPORTED_PLATFORM");
|
|
176
176
|
}
|
|
177
|
-
export async function checkForUpdate(currentVersion) {
|
|
177
|
+
export async function checkForUpdate(currentVersion, fetchOptions) {
|
|
178
178
|
const installMethod = detectInstallMethod();
|
|
179
179
|
const url = `https://api.github.com/repos/${REPO}/releases/latest`;
|
|
180
|
-
const response = await fetchWithRetry(url, { headers: githubHeaders() });
|
|
180
|
+
const response = await fetchWithRetry(url, { headers: githubHeaders() }, fetchOptions);
|
|
181
181
|
if (!response.ok) {
|
|
182
182
|
throw new Error(`Failed to check for updates: ${response.status} ${response.statusText}`);
|
|
183
183
|
}
|
|
@@ -42,10 +42,13 @@ import { resolveBundleWriteTarget } from "../../core/mutation-target.js";
|
|
|
42
42
|
import { getCacheDir } from "../../core/paths.js";
|
|
43
43
|
import { clearLogFile, info, isVerbose, setLogFile } from "../../core/warn.js";
|
|
44
44
|
import { resolveWriteTarget } from "../../core/write-source.js";
|
|
45
|
+
import { releaseIndexRebuildLock, tryAcquireIndexRebuildLock } from "../../indexer/index-rebuild-lock.js";
|
|
45
46
|
import { akmIndex } from "../../indexer/indexer.js";
|
|
46
47
|
import { getHyphenatedBoolean, getOutputMode } from "../../output/context.js";
|
|
47
48
|
import { inferAssetName, mergeXrefsIntoContent, readKnowledgeInput, resolveSupersedesForWrite, resolveSupersedesWriteTarget, resolveXrefsForWrite, writeMarkdownAsset, } from "../read/knowledge.js";
|
|
48
49
|
import { assembleInfo } from "./info.js";
|
|
50
|
+
/** Matches the high-frequency per-committed-batch progress line (#954), excluded from non-verbose JSON-mode stderr. */
|
|
51
|
+
const EMBEDDED_BATCH_PROGRESS_PATTERN = /^Embedded \d+\/\d+ entries\.$/;
|
|
49
52
|
export const indexCommand = defineCommand({
|
|
50
53
|
meta: { name: "index", description: "Build search index (incremental by default; --full forces full reindex)" },
|
|
51
54
|
args: {
|
|
@@ -66,6 +69,16 @@ export const indexCommand = defineCommand({
|
|
|
66
69
|
description: "When combined with --clean, report stale entries without deleting them.",
|
|
67
70
|
default: false,
|
|
68
71
|
},
|
|
72
|
+
reembed: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
description: "Force re-embedding of every entry, bypassing the embedding-model-rename compatibility check.",
|
|
75
|
+
default: false,
|
|
76
|
+
},
|
|
77
|
+
"skip-if-locked": {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
description: "If another `akm index` run already holds the rebuild lock, skip gracefully (exit 0) instead of contending with it. Use for scheduled/opportunistic index runs so they don't pile up against a longer run in progress.",
|
|
80
|
+
default: false,
|
|
81
|
+
},
|
|
69
82
|
},
|
|
70
83
|
async run({ args }) {
|
|
71
84
|
await runWithJsonErrors(async () => {
|
|
@@ -75,6 +88,27 @@ export const indexCommand = defineCommand({
|
|
|
75
88
|
if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
|
|
76
89
|
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
77
90
|
}
|
|
91
|
+
// #956: opt-in, non-blocking rebuild lock — never gates a human-typed
|
|
92
|
+
// `akm index` (it only warns and contends), but a scheduled/opportunistic
|
|
93
|
+
// caller can pass --skip-if-locked to step aside instead of piling up
|
|
94
|
+
// behind a run already in progress. Acquired before any other side
|
|
95
|
+
// effect (log file, spinner) so a skip does neither.
|
|
96
|
+
const lockAcquisition = tryAcquireIndexRebuildLock(args["skip-if-locked"]);
|
|
97
|
+
if (lockAcquisition.state === "skipped") {
|
|
98
|
+
output("index", {
|
|
99
|
+
ok: true,
|
|
100
|
+
skipped: {
|
|
101
|
+
reason: "lock-held",
|
|
102
|
+
pid: lockAcquisition.holder.pid,
|
|
103
|
+
// #956: the launcher pid (when known) alongside the pid that
|
|
104
|
+
// actually holds the lock — every process listing and task log
|
|
105
|
+
// shows the launcher pid, not the bun/node child's.
|
|
106
|
+
launcherPid: lockAcquisition.holder.launcherPid,
|
|
107
|
+
startedAt: lockAcquisition.holder.startedAt,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
78
112
|
const outputMode = getOutputMode();
|
|
79
113
|
const controller = new AbortController();
|
|
80
114
|
const abort = () => controller.abort(new Error("index interrupted"));
|
|
@@ -98,6 +132,7 @@ export const indexCommand = defineCommand({
|
|
|
98
132
|
full: args.full,
|
|
99
133
|
clean: args.clean,
|
|
100
134
|
dryRun: args["dry-run"],
|
|
135
|
+
reembed: args.reembed,
|
|
101
136
|
onProgress: ({ phase, message, processed, total }) => {
|
|
102
137
|
latestMessage = message;
|
|
103
138
|
const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
|
|
@@ -108,6 +143,17 @@ export const indexCommand = defineCommand({
|
|
|
108
143
|
spin.stop(`${progressPrefix}${message}`);
|
|
109
144
|
spin.start(`${progressPrefix}${message}`);
|
|
110
145
|
}
|
|
146
|
+
else if (!EMBEDDED_BATCH_PROGRESS_PATTERN.test(message)) {
|
|
147
|
+
// Non-verbose, non-text (JSON/yaml/etc) mode: silence used to be
|
|
148
|
+
// total until the run finished (#954) — a stalled
|
|
149
|
+
// run looked identical to "nothing written". Phase-start
|
|
150
|
+
// messages and the embedding heartbeat now reach stderr here
|
|
151
|
+
// too; the high-frequency per-batch `Embedded N/M entries.`
|
|
152
|
+
// line (emitted after every committed batch)
|
|
153
|
+
// is deliberately excluded — that would be spam, not a
|
|
154
|
+
// heartbeat.
|
|
155
|
+
info(`[index:${phase}] ${progressPrefix}${message}`);
|
|
156
|
+
}
|
|
111
157
|
},
|
|
112
158
|
signal: controller.signal,
|
|
113
159
|
});
|
|
@@ -126,6 +172,8 @@ export const indexCommand = defineCommand({
|
|
|
126
172
|
clearLogFile();
|
|
127
173
|
process.off("SIGINT", abort);
|
|
128
174
|
process.off("SIGTERM", abort);
|
|
175
|
+
if (lockAcquisition.state === "acquired")
|
|
176
|
+
releaseIndexRebuildLock(lockAcquisition.ownership);
|
|
129
177
|
}
|
|
130
178
|
});
|
|
131
179
|
},
|
|
@@ -29,7 +29,11 @@ import { getParsedInvocation } from "../../cli/invocation.js";
|
|
|
29
29
|
import { parsePositiveIntFlag } from "../../cli/parse-args.js";
|
|
30
30
|
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
|
|
31
31
|
import { UsageError } from "../../core/errors.js";
|
|
32
|
+
import { resolveUsageEventSource } from "../../indexer/usage/usage-events.js";
|
|
33
|
+
import { getOutputMode } from "../../output/context.js";
|
|
32
34
|
import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
|
|
35
|
+
import { akmSearch, parseSearchSource } from "../read/search.js";
|
|
36
|
+
import { rejectRetiredSourceFlag } from "../read/search-cli.js";
|
|
33
37
|
import { akmTaskExplain } from "./explain.js";
|
|
34
38
|
import { akmTasksAdd, akmTasksDoctor, akmTasksHistory, akmTasksPrune, akmTasksRun, akmTasksSync, akmTasksSyncPlan, } from "./tasks.js";
|
|
35
39
|
import { akmTaskValidate } from "./validate.js";
|
|
@@ -480,6 +484,47 @@ const tasksPruneCommand = defineJsonCommand({
|
|
|
480
484
|
outputWithExitCode("task-prune", result, taskPruneExitCode(result));
|
|
481
485
|
},
|
|
482
486
|
});
|
|
487
|
+
/**
|
|
488
|
+
* #951: `akm task list` — a pure, zero-logic delegating alias for
|
|
489
|
+
* `akm search --type task`. 0.9.0 removed `task list` because it was a
|
|
490
|
+
* second, redundant IMPLEMENTATION of task listing, not because the
|
|
491
|
+
* spelling itself was off-limits; this reuses `search`'s exact envelope
|
|
492
|
+
* (its `results` alias comes along for free) instead of adding a second one.
|
|
493
|
+
* `query`/`--limit`/`--from` are handled identically to `searchCommand.run()`
|
|
494
|
+
* for the same inputs, including the retired `--source` guard (`../read/
|
|
495
|
+
* search-cli.ts`'s `rejectRetiredSourceFlag`, reused rather than copied) so
|
|
496
|
+
* `akm task list --source x` fails with the same actionable rename message
|
|
497
|
+
* as `akm search --type task --source x` instead of citty's silent absorb.
|
|
498
|
+
*/
|
|
499
|
+
const tasksListCommand = defineJsonCommand({
|
|
500
|
+
meta: { name: "list", description: "List task assets (alias for `akm search --type task`)" },
|
|
501
|
+
args: {
|
|
502
|
+
query: {
|
|
503
|
+
type: "positional",
|
|
504
|
+
description: "Search query (omit to list all tasks)",
|
|
505
|
+
required: false,
|
|
506
|
+
default: "",
|
|
507
|
+
},
|
|
508
|
+
limit: { type: "string", description: "Maximum number of results" },
|
|
509
|
+
from: { type: "string", description: "Search source (local|registry|all)", default: "local" },
|
|
510
|
+
},
|
|
511
|
+
async run({ args }) {
|
|
512
|
+
rejectRetiredSourceFlag();
|
|
513
|
+
const query = (args.query ?? "").trim();
|
|
514
|
+
const limit = parsePositiveIntFlag(args.limit ?? undefined);
|
|
515
|
+
const source = parseSearchSource(args.from);
|
|
516
|
+
const outputMode = getOutputMode();
|
|
517
|
+
const result = await akmSearch({
|
|
518
|
+
query,
|
|
519
|
+
type: "task",
|
|
520
|
+
limit,
|
|
521
|
+
source,
|
|
522
|
+
eventSource: resolveUsageEventSource(),
|
|
523
|
+
attributionProjection: outputMode.shape === "agent" ? "agent" : outputMode.detail,
|
|
524
|
+
});
|
|
525
|
+
output("search", result);
|
|
526
|
+
},
|
|
527
|
+
});
|
|
483
528
|
export const taskCommand = defineGroupCommand({
|
|
484
529
|
meta: {
|
|
485
530
|
name: "task",
|
|
@@ -491,12 +536,14 @@ export const taskCommand = defineGroupCommand({
|
|
|
491
536
|
explain: tasksExplainCommand,
|
|
492
537
|
validate: tasksValidateCommand,
|
|
493
538
|
history: tasksHistoryCommand,
|
|
539
|
+
list: tasksListCommand,
|
|
494
540
|
sync: tasksSyncCommand,
|
|
495
541
|
prune: tasksPruneCommand,
|
|
496
542
|
doctor: tasksDoctorCommand,
|
|
497
543
|
},
|
|
498
|
-
// Bare `akm task` reports scheduler diagnostics.
|
|
499
|
-
// tasks
|
|
544
|
+
// Bare `akm task` reports scheduler diagnostics. Deeper inspection of
|
|
545
|
+
// individual tasks is the generic `akm show <bundle//tasks/id>`; `list`
|
|
546
|
+
// above is a delegating alias for `akm search --type task` (#951).
|
|
500
547
|
// No `defaultRun`: bare `akm task` is a usage error (exit 2), the canonical
|
|
501
548
|
// bare-group behavior — owner ruling 12. Run `akm task doctor` for what the
|
|
502
549
|
// bare form used to run.
|