akm-cli 0.9.14 → 0.9.15-beta.1
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 +397 -0
- package/STABILITY.md +6 -3
- 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 +3 -2
- 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 +27 -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/self-update.js +2 -2
- package/dist/commands/sources/stash-cli.js +31 -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/env-secret-ref.js +58 -5
- package/dist/core/errors.js +30 -0
- package/dist/core/improve-result.js +51 -0
- package/dist/core/loopback.js +17 -0
- package/dist/core/paths.js +11 -0
- package/dist/core/run-lock.js +96 -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 +21 -1
- package/dist/indexer/indexer.js +18 -17
- package/dist/indexer/materialize-embeddings.js +282 -32
- package/dist/indexer/search/db-search.js +49 -2
- 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 +188 -21
- 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 +24798 -21732
- package/dist/scripts/akm-migrate.js +23408 -20343
- 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-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-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 +52 -0
- package/docs/migration/release-notes/README.md +4 -0
- package/docs/reference/cli.md +245 -29
- package/docs/reference/configuration.md +180 -19
- 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 +8 -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: {
|
|
@@ -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,6 +42,7 @@ 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";
|
|
@@ -66,6 +67,16 @@ export const indexCommand = defineCommand({
|
|
|
66
67
|
description: "When combined with --clean, report stale entries without deleting them.",
|
|
67
68
|
default: false,
|
|
68
69
|
},
|
|
70
|
+
reembed: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Force re-embedding of every entry, bypassing the embedding-model-rename compatibility check.",
|
|
73
|
+
default: false,
|
|
74
|
+
},
|
|
75
|
+
"skip-if-locked": {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
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.",
|
|
78
|
+
default: false,
|
|
79
|
+
},
|
|
69
80
|
},
|
|
70
81
|
async run({ args }) {
|
|
71
82
|
await runWithJsonErrors(async () => {
|
|
@@ -75,6 +86,23 @@ export const indexCommand = defineCommand({
|
|
|
75
86
|
if (getHyphenatedBoolean(args, "re-enrich") || getParsedInvocation().getFlagValue("--re-enrich") !== undefined) {
|
|
76
87
|
throw new UsageError("`akm index --re-enrich` has been removed. Re-enrichment of index-time LLM passes is not exposed in this slice.");
|
|
77
88
|
}
|
|
89
|
+
// #956: opt-in, non-blocking rebuild lock — never gates a human-typed
|
|
90
|
+
// `akm index` (it only warns and contends), but a scheduled/opportunistic
|
|
91
|
+
// caller can pass --skip-if-locked to step aside instead of piling up
|
|
92
|
+
// behind a run already in progress. Acquired before any other side
|
|
93
|
+
// effect (log file, spinner) so a skip does neither.
|
|
94
|
+
const lockAcquisition = tryAcquireIndexRebuildLock(args["skip-if-locked"]);
|
|
95
|
+
if (lockAcquisition.state === "skipped") {
|
|
96
|
+
output("index", {
|
|
97
|
+
ok: true,
|
|
98
|
+
skipped: {
|
|
99
|
+
reason: "lock-held",
|
|
100
|
+
pid: lockAcquisition.holder.pid,
|
|
101
|
+
startedAt: lockAcquisition.holder.startedAt,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
78
106
|
const outputMode = getOutputMode();
|
|
79
107
|
const controller = new AbortController();
|
|
80
108
|
const abort = () => controller.abort(new Error("index interrupted"));
|
|
@@ -98,6 +126,7 @@ export const indexCommand = defineCommand({
|
|
|
98
126
|
full: args.full,
|
|
99
127
|
clean: args.clean,
|
|
100
128
|
dryRun: args["dry-run"],
|
|
129
|
+
reembed: args.reembed,
|
|
101
130
|
onProgress: ({ phase, message, processed, total }) => {
|
|
102
131
|
latestMessage = message;
|
|
103
132
|
const progressPrefix = processed !== undefined && total !== undefined ? `[${processed}/${total}] ` : "";
|
|
@@ -126,6 +155,8 @@ export const indexCommand = defineCommand({
|
|
|
126
155
|
clearLogFile();
|
|
127
156
|
process.off("SIGINT", abort);
|
|
128
157
|
process.off("SIGTERM", abort);
|
|
158
|
+
if (lockAcquisition.state === "acquired")
|
|
159
|
+
releaseIndexRebuildLock(lockAcquisition.ownership);
|
|
129
160
|
}
|
|
130
161
|
});
|
|
131
162
|
},
|
|
@@ -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.
|
|
@@ -11,7 +11,8 @@ import { getStringArg } from "../cli/parse-args.js";
|
|
|
11
11
|
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output, outputWithExitCode } from "../cli/shared.js";
|
|
12
12
|
import { armAbortDeadline } from "../core/abort-deadline.js";
|
|
13
13
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
14
|
-
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
14
|
+
import { NotFoundError, TransientError, UsageError } from "../core/errors.js";
|
|
15
|
+
import { warn } from "../core/warn.js";
|
|
15
16
|
import { akmIndex } from "../indexer/indexer.js";
|
|
16
17
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
17
18
|
import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
@@ -30,10 +31,16 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
30
31
|
"diagnostic text). Diagnostics only — step evidence stays deterministic and is unaffected.",
|
|
31
32
|
default: false,
|
|
32
33
|
},
|
|
34
|
+
"all-scopes": {
|
|
35
|
+
type: "boolean",
|
|
36
|
+
description: "When resolving a workflow ref (not a run id), search every scope instead of only the current one (#942).",
|
|
37
|
+
default: false,
|
|
38
|
+
},
|
|
33
39
|
},
|
|
34
40
|
async run({ args }) {
|
|
35
41
|
const target = args.target;
|
|
36
42
|
const includeUnits = args.units === true;
|
|
43
|
+
const allScopes = args["all-scopes"] === true;
|
|
37
44
|
const resolvedRunId = await resolveWorkflowRunTarget(target);
|
|
38
45
|
if (resolvedRunId !== undefined) {
|
|
39
46
|
const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
|
|
@@ -41,8 +48,9 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
41
48
|
return;
|
|
42
49
|
}
|
|
43
50
|
let runs;
|
|
51
|
+
let scopeKey;
|
|
44
52
|
try {
|
|
45
|
-
({ runs } = await listWorkflowRuns({ workflowRef: target }));
|
|
53
|
+
({ runs, scopeKey } = await listWorkflowRuns({ workflowRef: target, allScopes }));
|
|
46
54
|
}
|
|
47
55
|
catch (error) {
|
|
48
56
|
if (!target.includes(":") && !target.includes("/")) {
|
|
@@ -51,8 +59,17 @@ const workflowStatusCommand = defineJsonCommand({
|
|
|
51
59
|
throw error;
|
|
52
60
|
}
|
|
53
61
|
const mostRecent = runs[0];
|
|
54
|
-
if (!mostRecent)
|
|
62
|
+
if (!mostRecent) {
|
|
63
|
+
// #942: name the scope actually searched and point at `--all-scopes`
|
|
64
|
+
// rather than a bare "not found" — the ref-fallthrough lookup is
|
|
65
|
+
// scope-local by default, so "no runs" here means "none in THIS
|
|
66
|
+
// scope", not "none anywhere". Already searching every scope (or no
|
|
67
|
+
// real scope was filtered on) has nothing more specific to suggest.
|
|
68
|
+
if (!allScopes && scopeKey !== null) {
|
|
69
|
+
throw new NotFoundError(`No workflow runs found for ${target} in scope ${scopeKey}.`, "WORKFLOW_NOT_FOUND", `Run 'akm workflow status ${target} --all-scopes' to search every scope.`);
|
|
70
|
+
}
|
|
55
71
|
throw new NotFoundError(`No workflow runs found for ${target}`, "WORKFLOW_NOT_FOUND");
|
|
72
|
+
}
|
|
56
73
|
const result = await getWorkflowStatus(mostRecent.id, { includeUnits });
|
|
57
74
|
output("workflow-status", result);
|
|
58
75
|
},
|
|
@@ -70,12 +87,20 @@ const workflowListCommand = defineJsonCommand({
|
|
|
70
87
|
description: "Also include child workflow runs (hidden by default, P3b)",
|
|
71
88
|
default: false,
|
|
72
89
|
},
|
|
90
|
+
"all-scopes": {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
description: "Search every scope instead of only the current one (#942). The envelope's top-level `scopeKey` is " +
|
|
93
|
+
"`null` with this flag, otherwise the scope that was searched — so an empty `runs: []` is never " +
|
|
94
|
+
'indistinguishable from "nothing anywhere".',
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
73
97
|
},
|
|
74
98
|
async run({ args }) {
|
|
75
99
|
const result = await listWorkflowRuns({
|
|
76
100
|
workflowRef: args.ref,
|
|
77
101
|
activeOnly: args.active,
|
|
78
102
|
includeChildren: args.children,
|
|
103
|
+
allScopes: args["all-scopes"],
|
|
79
104
|
});
|
|
80
105
|
output("workflow-list", result);
|
|
81
106
|
},
|
|
@@ -145,6 +170,20 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
145
170
|
output("workflow-create", { ok: true, ...result });
|
|
146
171
|
},
|
|
147
172
|
});
|
|
173
|
+
/**
|
|
174
|
+
* `--skip-if-locked` (#948) eligibility: only these two named, retryable
|
|
175
|
+
* `TransientError` codes (#948 addendum — moved off UsageError, exit 75) turn
|
|
176
|
+
* a `workflow run` failure into a graceful skip — `RUN_LEASE_HELD` (another
|
|
177
|
+
* engine invocation is driving THIS run, `workflow-runs-repository.ts`'s
|
|
178
|
+
* single-driver lease) and `STATE_DB_CONTENDED` (an unrelated akm process is
|
|
179
|
+
* writing state.db right now, `core/state-db.ts`'s BEGIN IMMEDIATE retry
|
|
180
|
+
* exhaustion). Every other error — a bad flag, an unresolvable target —
|
|
181
|
+
* still fails loudly.
|
|
182
|
+
*/
|
|
183
|
+
const WORKFLOW_RUN_SKIP_REASONS = {
|
|
184
|
+
RUN_LEASE_HELD: "lock-held",
|
|
185
|
+
STATE_DB_CONTENDED: "state-db-contended",
|
|
186
|
+
};
|
|
148
187
|
const workflowRunCommand = defineJsonCommand({
|
|
149
188
|
meta: {
|
|
150
189
|
name: "run",
|
|
@@ -161,6 +200,13 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
161
200
|
"(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
|
|
162
201
|
default: false,
|
|
163
202
|
},
|
|
203
|
+
"skip-if-locked": {
|
|
204
|
+
type: "boolean",
|
|
205
|
+
description: "If another akm process already holds this run's engine lease, or state.db is busy with another " +
|
|
206
|
+
"writer, skip gracefully (exit 0) instead of failing (exit 75). Use for high-frequency scheduled runs " +
|
|
207
|
+
"so they don't pile up failures while a longer-running invocation is in progress.",
|
|
208
|
+
default: false,
|
|
209
|
+
},
|
|
164
210
|
},
|
|
165
211
|
async run({ args, rawArgs }) {
|
|
166
212
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
@@ -168,6 +214,7 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
168
214
|
const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
|
|
169
215
|
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0);
|
|
170
216
|
const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
|
|
217
|
+
const skipIfLocked = args["skip-if-locked"];
|
|
171
218
|
const controller = new AbortController();
|
|
172
219
|
let signalExitCode;
|
|
173
220
|
const interrupt = (signal) => {
|
|
@@ -185,14 +232,31 @@ const workflowRunCommand = defineJsonCommand({
|
|
|
185
232
|
reason: `Workflow run timed out after ${timeoutMs}ms.`,
|
|
186
233
|
});
|
|
187
234
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
235
|
+
let result;
|
|
236
|
+
try {
|
|
237
|
+
result = await runWorkflowSteps({
|
|
238
|
+
target: args.target,
|
|
239
|
+
parameterFlags,
|
|
240
|
+
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
241
|
+
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
242
|
+
newRun: args.new,
|
|
243
|
+
signal: controller.signal,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
// #948: `--skip-if-locked` extends improve's "another run already
|
|
248
|
+
// holds this" skip semantics to `workflow run`. Only these two named,
|
|
249
|
+
// retryable TransientError codes are eligible (#948 addendum — moved
|
|
250
|
+
// off UsageError) — a bad flag or malformed input still fails loudly
|
|
251
|
+
// even with the flag set.
|
|
252
|
+
if (skipIfLocked && err instanceof TransientError && WORKFLOW_RUN_SKIP_REASONS[err.code]) {
|
|
253
|
+
const reason = WORKFLOW_RUN_SKIP_REASONS[err.code];
|
|
254
|
+
warn(`[workflow] ${err.message} skipping (--skip-if-locked)`);
|
|
255
|
+
output("workflow-run", { ok: true, target: args.target, skipped: { reason, message: err.message } });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
throw err;
|
|
259
|
+
}
|
|
196
260
|
// The abort is observed between steps, so a deadline landing in the run's
|
|
197
261
|
// final bookkeeping fires on a run that then finishes. Reporting that as
|
|
198
262
|
// timed out would send an operator to resume a run with nothing left to
|
|
@@ -222,7 +286,17 @@ const WORKFLOW_RUN_VALUE_FLAGS = new Set([
|
|
|
222
286
|
"shape",
|
|
223
287
|
"output",
|
|
224
288
|
]);
|
|
225
|
-
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set([
|
|
289
|
+
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set([
|
|
290
|
+
"quiet",
|
|
291
|
+
"verbose",
|
|
292
|
+
"help",
|
|
293
|
+
"no-quiet",
|
|
294
|
+
"no-verbose",
|
|
295
|
+
"new",
|
|
296
|
+
"no-new",
|
|
297
|
+
"skip-if-locked",
|
|
298
|
+
"no-skip-if-locked",
|
|
299
|
+
]);
|
|
226
300
|
export function parseWorkflowParameterFlags(rawArgs, target) {
|
|
227
301
|
const flags = [];
|
|
228
302
|
let targetSeen = false;
|
|
@@ -12,6 +12,8 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { markdownHeadingSlug, parseMarkdownToc } from "./markdown.js";
|
|
13
13
|
export const MARKDOWN_FRAGMENT_MAX_CHARS = 1600;
|
|
14
14
|
export const MARKDOWN_FRAGMENT_PREFIX = "akm-fragment-";
|
|
15
|
+
export const MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS = 3200;
|
|
16
|
+
export const MARKDOWN_FRAGMENT_SELECTED_LABEL = "[Selected matching fragment]";
|
|
15
17
|
function hash(text) {
|
|
16
18
|
return createHash("sha256").update(text).digest("hex");
|
|
17
19
|
}
|
|
@@ -144,3 +146,36 @@ export function splitMarkdownFragments(body, maxChars = MARKDOWN_FRAGMENT_MAX_CH
|
|
|
144
146
|
export function fragmentForSelector(body, selector) {
|
|
145
147
|
return splitMarkdownFragments(body).find((fragment) => fragment.fragmentId === selector || fragment.headingSlug === selector);
|
|
146
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Assemble the document lead and selected fragment under one hard character
|
|
151
|
+
* budget. The selected match is always labelled and last. When both pieces do
|
|
152
|
+
* not fit, lead bytes are discarded before any selected-fragment bytes so the
|
|
153
|
+
* evidence that caused retrieval remains intact whenever the caller's budget
|
|
154
|
+
* can hold it.
|
|
155
|
+
*/
|
|
156
|
+
export function buildMarkdownLeadContext(fragments, selectedOrdinal, maxChars = MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS) {
|
|
157
|
+
if (!Number.isSafeInteger(maxChars) || maxChars <= 0) {
|
|
158
|
+
throw new RangeError("Markdown fragment context maxChars must be a positive safe integer");
|
|
159
|
+
}
|
|
160
|
+
const selected = fragments[selectedOrdinal];
|
|
161
|
+
if (!selected)
|
|
162
|
+
throw new RangeError(`Markdown fragment ordinal ${selectedOrdinal} is out of range`);
|
|
163
|
+
const selectedBlock = `${MARKDOWN_FRAGMENT_SELECTED_LABEL}\n${selected.text}`;
|
|
164
|
+
if (selectedBlock.length > maxChars) {
|
|
165
|
+
return { content: selectedBlock.slice(0, maxChars), truncated: true };
|
|
166
|
+
}
|
|
167
|
+
const lead = fragments[0];
|
|
168
|
+
if (!lead || lead.ordinal === selected.ordinal)
|
|
169
|
+
return { content: selectedBlock, truncated: false };
|
|
170
|
+
const separator = "\n\n";
|
|
171
|
+
const availableLeadChars = maxChars - selectedBlock.length - separator.length;
|
|
172
|
+
if (availableLeadChars <= 0)
|
|
173
|
+
return { content: selectedBlock, truncated: true };
|
|
174
|
+
const leadText = lead.text.slice(0, availableLeadChars).trimEnd();
|
|
175
|
+
if (!leadText)
|
|
176
|
+
return { content: selectedBlock, truncated: true };
|
|
177
|
+
return {
|
|
178
|
+
content: `${leadText}${separator}${selectedBlock}`,
|
|
179
|
+
truncated: leadText.length < lead.text.length,
|
|
180
|
+
};
|
|
181
|
+
}
|