akm-cli 0.9.17-alpha.6 → 0.9.17-alpha.8
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 +285 -0
- package/STABILITY.md +2 -2
- package/dist/akm +62 -29
- package/dist/akm-migrate +38 -19
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +5 -3
- package/dist/commands/improve/consolidate.js +11 -0
- package/dist/commands/improve/improve-cli.js +27 -7
- package/dist/commands/improve/ledger.js +7 -3
- package/dist/commands/improve/loop-stages.js +4 -3
- package/dist/commands/improve/preparation.js +40 -10
- package/dist/commands/improve/reflect.js +46 -22
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +77 -0
- package/dist/commands/read/curate.js +41 -30
- package/dist/commands/read/show.js +55 -2
- package/dist/commands/sources/info.js +3 -0
- package/dist/commands/tasks/tasks-cli.js +10 -12
- package/dist/commands/tasks/tasks.js +57 -56
- package/dist/commands/tasks/validate.js +27 -46
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/adapter/adapters/akm-metadata.js +31 -0
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/improve-result.js +4 -1
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/paths.js +0 -4
- package/dist/indexer/db/graph-db.js +0 -32
- package/dist/indexer/graph/graph-extraction.js +3 -1
- package/dist/indexer/indexer.js +1 -3
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/usage/usage-events.js +34 -0
- package/dist/llm/graph-extract.js +26 -37
- package/dist/output/shapes/helpers.js +3 -0
- package/dist/output/text/show-format.js +16 -0
- package/dist/scripts/akm-migrate-node.js +6377 -6437
- package/dist/scripts/akm-migrate.js +6860 -6920
- package/dist/storage/repositories/index-entries-repository.js +12 -6
- package/dist/storage/repositories/index-entry-schema.js +18 -1
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-schema.js +27 -0
- package/dist/storage/repositories/proposals-repository.js +4 -0
- package/dist/tasks/backends/cron.js +80 -43
- package/dist/tasks/backends/launchd.js +28 -15
- package/dist/tasks/backends/schtasks.js +25 -10
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +4 -2
- package/dist/tasks/scheduler-invocation.js +127 -235
- package/dist/tasks/scheduler-sync.js +13 -8
- package/dist/tasks/source/parse-task-source.js +22 -126
- package/dist/tasks/source/task-to-v4.js +463 -87
- package/docs/migration/release-notes/0.9.17.md +7 -5
- package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
- package/docs/reference/cli.md +26 -10
- package/docs/reference/tasks.md +58 -40
- package/package.json +1 -1
- package/dist/tasks/source/task-to-v3.js +0 -507
|
@@ -24,7 +24,7 @@ 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
26
|
import { buildMarkdownLeadContext, fragmentForSelector, MARKDOWN_FRAGMENT_CONTEXT_DEFAULT_MAX_CHARS, } from "../../core/asset/markdown-fragments.js";
|
|
27
|
-
import { displayRef, typeNameFromConceptId } from "../../core/asset/resolve-ref.js";
|
|
27
|
+
import { displayRef, displayRefForConceptId, 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";
|
|
30
30
|
import { loadConfig } from "../../core/config/config.js";
|
|
@@ -43,6 +43,7 @@ import { buildFileContext, buildRenderContext, getRenderer, } from "../../indexe
|
|
|
43
43
|
import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
|
|
44
44
|
import { withIndexDb } from "../../storage/repositories/index-db.js";
|
|
45
45
|
import { getIndexedMarkdownFragment } from "../../storage/repositories/index-fts-repository.js";
|
|
46
|
+
import { readEntryLinks } from "../../storage/repositories/index-links-repository.js";
|
|
46
47
|
import { getCurrentWorkflowScopeKey } from "../../workflows/authoring/scope-key.js";
|
|
47
48
|
import { buildWorkflowAction } from "../../workflows/renderer.js";
|
|
48
49
|
import { getActiveWorkflowRun } from "../../workflows/runtime/runs.js";
|
|
@@ -318,12 +319,13 @@ export async function showLocal(input) {
|
|
|
318
319
|
response.type = indexedEntry.type;
|
|
319
320
|
response.name = indexedEntry.name;
|
|
320
321
|
const isPrimaryStash = source?.isDefault === true;
|
|
322
|
+
const displayDefaultBundle = config.defaultBundle ?? (isPrimaryStash ? indexedEntry.bundleId : undefined);
|
|
321
323
|
const canonicalRef = displayRef({
|
|
322
324
|
type: indexedEntry.type,
|
|
323
325
|
name: presentedName,
|
|
324
326
|
conceptId: indexedEntry.conceptId,
|
|
325
327
|
bundleId: indexedEntry.bundleId,
|
|
326
|
-
},
|
|
328
|
+
}, displayDefaultBundle);
|
|
327
329
|
if (parsed.fragment && indexedFragment) {
|
|
328
330
|
const selectedFragmentId = indexedFragment.fragments[indexedFragment.ordinal].fragmentId;
|
|
329
331
|
const selectedRef = `${canonicalRef}#${selectedFragmentId}`;
|
|
@@ -395,6 +397,7 @@ export async function showLocal(input) {
|
|
|
395
397
|
return { total: 0, hits: [] };
|
|
396
398
|
}
|
|
397
399
|
})(),
|
|
400
|
+
...showLinks(indexedEntry.itemRef, displayDefaultBundle),
|
|
398
401
|
};
|
|
399
402
|
const activeRun = await getActiveWorkflowRun(getCurrentWorkflowScopeKey());
|
|
400
403
|
if (activeRun) {
|
|
@@ -408,6 +411,56 @@ export async function showLocal(input) {
|
|
|
408
411
|
}
|
|
409
412
|
return fullResponse;
|
|
410
413
|
}
|
|
414
|
+
/** Refs listed per kind of declared link; `total` still counts them all (one memory is named by 1,437 others). */
|
|
415
|
+
const LINKS_PER_KIND = 10;
|
|
416
|
+
/**
|
|
417
|
+
* The declared links (#935) of an indexed asset, grouped by kind: outgoing in
|
|
418
|
+
* authored order, incoming by ref, and unresolved tokens as authored. Empty
|
|
419
|
+
* parts are omitted, and the field when nothing links either way.
|
|
420
|
+
*/
|
|
421
|
+
function showLinks(itemRef, defaultBundle) {
|
|
422
|
+
let rows;
|
|
423
|
+
try {
|
|
424
|
+
rows = withIndexDb((db) => readEntryLinks(db, itemRef));
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
rethrowIfTestIsolationError(err);
|
|
428
|
+
rethrowIfDataDirUnreadable(err);
|
|
429
|
+
return {};
|
|
430
|
+
}
|
|
431
|
+
const outgoing = [];
|
|
432
|
+
const unresolved = [];
|
|
433
|
+
for (const row of rows.outgoing) {
|
|
434
|
+
if (row.conceptId === undefined)
|
|
435
|
+
unresolved.push({ kind: row.kind, ref: row.raw ?? "" });
|
|
436
|
+
else
|
|
437
|
+
outgoing.push({ kind: row.kind, ref: displayRefForConceptId(row.conceptId, row.bundleId, defaultBundle) });
|
|
438
|
+
}
|
|
439
|
+
const incoming = rows.incoming.map((row) => ({
|
|
440
|
+
kind: row.kind,
|
|
441
|
+
ref: displayRefForConceptId(row.conceptId ?? "", row.bundleId, defaultBundle),
|
|
442
|
+
}));
|
|
443
|
+
const links = {
|
|
444
|
+
...groupLinks("outgoing", outgoing),
|
|
445
|
+
...groupLinks("incoming", incoming),
|
|
446
|
+
...groupLinks("unresolved", unresolved),
|
|
447
|
+
};
|
|
448
|
+
return Object.keys(links).length > 0 ? { links } : {};
|
|
449
|
+
}
|
|
450
|
+
function groupLinks(part, rows) {
|
|
451
|
+
if (rows.length === 0)
|
|
452
|
+
return {};
|
|
453
|
+
const groups = {};
|
|
454
|
+
for (const kind of [...new Set(rows.map((row) => row.kind))].sort())
|
|
455
|
+
groups[kind] = { total: 0, refs: [] };
|
|
456
|
+
for (const { kind, ref } of rows) {
|
|
457
|
+
const group = groups[kind];
|
|
458
|
+
group.total++;
|
|
459
|
+
if (group.refs.length < LINKS_PER_KIND)
|
|
460
|
+
group.refs.push(ref);
|
|
461
|
+
}
|
|
462
|
+
return { [part]: groups };
|
|
463
|
+
}
|
|
411
464
|
/**
|
|
412
465
|
* Warn and ignore body fragments for namespaces whose authored bytes are
|
|
413
466
|
* sensitive. `warnOnce`-keyed on the exact ref: `akmShowUnified` calls this
|
|
@@ -10,6 +10,7 @@ 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";
|
|
12
12
|
import { getEntryCount, getEntryCountByType } from "../../storage/repositories/index-entries-repository.js";
|
|
13
|
+
import { countLinksByKind } from "../../storage/repositories/index-links-repository.js";
|
|
13
14
|
import { getMeta } from "../../storage/repositories/index-meta-repository.js";
|
|
14
15
|
import { pkgVersion } from "../../version.js";
|
|
15
16
|
/**
|
|
@@ -99,9 +100,11 @@ function readIndexStats(resolvedPath) {
|
|
|
99
100
|
let db;
|
|
100
101
|
try {
|
|
101
102
|
db = openExistingDatabase(resolvedPath);
|
|
103
|
+
const links = countLinksByKind(db);
|
|
102
104
|
return {
|
|
103
105
|
entryCount: getEntryCount(db),
|
|
104
106
|
byType: getEntryCountByType(db),
|
|
107
|
+
...(Object.keys(links).length > 0 ? { links } : {}),
|
|
105
108
|
lastBuiltAt: getMeta(db, "builtAt") ?? null,
|
|
106
109
|
hasEmbeddings: getMeta(db, "hasEmbeddings") === "1",
|
|
107
110
|
};
|
|
@@ -435,17 +435,15 @@ const tasksDoctorCommand = defineJsonCommand({
|
|
|
435
435
|
},
|
|
436
436
|
});
|
|
437
437
|
/**
|
|
438
|
-
* #907: `akm task validate`'s exit-code contract — `valid`
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* throws a `UsageError` for those, which `defineJsonCommand`'s wrapping
|
|
445
|
-
* already maps to exit 2.
|
|
438
|
+
* #907: `akm task validate`'s exit-code contract — `valid` succeeds (exit
|
|
439
|
+
* 0); `blocked`/`invalid`/`not-a-task` are diagnosed defects the caller must
|
|
440
|
+
* act on (exit 1, mirroring `task sync`'s own `failures.length > 0 ->
|
|
441
|
+
* EXIT_CODES.GENERAL`). A missing path or an unreadable file never reaches
|
|
442
|
+
* this function at all — `akmTaskValidate` throws a `UsageError` for those,
|
|
443
|
+
* which `defineJsonCommand`'s wrapping already maps to exit 2.
|
|
446
444
|
*/
|
|
447
445
|
export function taskValidateExitCode(result) {
|
|
448
|
-
return result.outcome === "valid"
|
|
446
|
+
return result.outcome === "valid" ? undefined : EXIT_CODES.GENERAL;
|
|
449
447
|
}
|
|
450
448
|
const tasksValidateCommand = defineJsonCommand({
|
|
451
449
|
meta: {
|
|
@@ -478,9 +476,9 @@ export function taskPruneExitCode(result) {
|
|
|
478
476
|
const tasksPruneCommand = defineJsonCommand({
|
|
479
477
|
meta: {
|
|
480
478
|
name: "prune",
|
|
481
|
-
description: "Remove installed scheduler entries `sync` can never reclaim because
|
|
482
|
-
"
|
|
483
|
-
"
|
|
479
|
+
description: "Remove installed scheduler entries `sync` can never reclaim because they no longer resolve to a live " +
|
|
480
|
+
"bundle (the bundle directory a row names is gone, or an older row's --scheduler-context descriptor " +
|
|
481
|
+
"cannot be read). Defaults to a dry-run preview — zero scheduler writes. Requires --yes to remove anything; " +
|
|
484
482
|
"--id narrows removal to specific binding ids (comma-separated).",
|
|
485
483
|
},
|
|
486
484
|
args: {
|
|
@@ -41,7 +41,7 @@ import { readTaskHistory } from "../../tasks/run/task-history.js";
|
|
|
41
41
|
import { exitCodeForStatus } from "../../tasks/run/task-result.js";
|
|
42
42
|
import { parseSchedule, SCHEDULE_SUPPORTED_SUBSET_HINT } from "../../tasks/schedule.js";
|
|
43
43
|
import { compileTaskSchedulerBindings, schedulerBindingNativeId, } from "../../tasks/scheduler-binding.js";
|
|
44
|
-
import {
|
|
44
|
+
import { readLegacySchedulerContext, scheduledRowEnvironment } from "../../tasks/scheduler-invocation.js";
|
|
45
45
|
import { withSchedulerLock } from "../../tasks/scheduler-lock.js";
|
|
46
46
|
import { compileSchedulerSources, installedRowNativeId, installedRowOwner, installedRowScope, planSchedulerSync, renderSchedulerPlanPreview, scheduledInvocationBundle, } from "../../tasks/scheduler-sync.js";
|
|
47
47
|
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
@@ -301,9 +301,7 @@ export async function akmTasksHistory(input) {
|
|
|
301
301
|
*/
|
|
302
302
|
export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
303
303
|
return withSchedulerLock(async () => {
|
|
304
|
-
const { sched, plan,
|
|
305
|
-
if (publish && plan.operations.some((operation) => operation.kind !== "remove"))
|
|
306
|
-
publish();
|
|
304
|
+
const { sched, plan, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
|
|
307
305
|
const failed = new Set();
|
|
308
306
|
const failures = [...plan.failures];
|
|
309
307
|
for (const operation of plan.operations) {
|
|
@@ -343,7 +341,7 @@ export async function akmTasksSync(deps = {}, bundleTarget, options = {}) {
|
|
|
343
341
|
}
|
|
344
342
|
/**
|
|
345
343
|
* `akm task sync --dry-run` (#849): the exact plan `akmTasksSync` would
|
|
346
|
-
* apply, previewed. It writes nothing: no config, no
|
|
344
|
+
* apply, previewed. It writes nothing: no config, no row.
|
|
347
345
|
*/
|
|
348
346
|
export async function akmTasksSyncPlan(deps = {}, bundleTarget, options = {}) {
|
|
349
347
|
const { sched, plan } = await buildSchedulerSyncPlan(deps, bundleTarget, { ...options, dryRun: true });
|
|
@@ -440,7 +438,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
440
438
|
installed,
|
|
441
439
|
scopes,
|
|
442
440
|
...(sched.expectedSignature ? { expectedSignature: sched.expectedSignature.bind(sched) } : {}),
|
|
443
|
-
|
|
441
|
+
installOptions: runtime.options,
|
|
444
442
|
rebind: options.rebind === true,
|
|
445
443
|
extraRemovals,
|
|
446
444
|
keepRefs,
|
|
@@ -450,53 +448,45 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
|
|
|
450
448
|
? plan.operations.some((operation) => operation.kind !== "remove")
|
|
451
449
|
: plan.installed.length > 0;
|
|
452
450
|
if (runtime.via === "checkout" && writesLauncher) {
|
|
453
|
-
warnings.push(`Scheduled tasks now run akm from a source checkout (${runtime.options
|
|
451
|
+
warnings.push(`Scheduled tasks now run akm from a source checkout (${runtime.options.binding?.join(" ")}); they run whatever the checkout holds when they fire. Install akm with \`npm install --global akm-cli\` or a standalone release, then run \`akm task sync --rebind\`.`);
|
|
454
452
|
}
|
|
455
|
-
return {
|
|
456
|
-
sched,
|
|
457
|
-
plan: { ...plan, failures: [...failures, ...plan.failures] },
|
|
458
|
-
...(runtime.publish ? { publish: runtime.publish } : {}),
|
|
459
|
-
warnings,
|
|
460
|
-
};
|
|
453
|
+
return { sched, plan: { ...plan, failures: [...failures, ...plan.failures] }, warnings };
|
|
461
454
|
}
|
|
462
455
|
/**
|
|
463
|
-
* The launcher and
|
|
464
|
-
* the current policy on every sync; a row that is already installed
|
|
465
|
-
* launcher unless `--rebind` (see `installOptionsFor`). An injected
|
|
466
|
-
* (tests) renders with its own
|
|
456
|
+
* The launcher and environment rows are written with. The environment
|
|
457
|
+
* follows the current policy on every sync; a row that is already installed
|
|
458
|
+
* keeps its launcher unless `--rebind` (see `installOptionsFor`). An injected
|
|
459
|
+
* backend (tests) renders with its own launcher.
|
|
467
460
|
*/
|
|
468
461
|
function prepareSchedulerRuntime(deps) {
|
|
462
|
+
const environment = scheduledRowEnvironment();
|
|
469
463
|
if (deps.schedulerRuntime) {
|
|
470
464
|
const runtime = deps.schedulerRuntime();
|
|
471
|
-
return {
|
|
472
|
-
options: { binding: runtime.binding, contextPath: runtime.contextPath },
|
|
473
|
-
...(runtime.via ? { via: runtime.via } : {}),
|
|
474
|
-
};
|
|
465
|
+
return { options: { binding: runtime.binding, environment }, ...(runtime.via ? { via: runtime.via } : {}) };
|
|
475
466
|
}
|
|
476
467
|
if (deps.backend)
|
|
477
|
-
return {};
|
|
478
|
-
const descriptor = schedulerContextDescriptor();
|
|
468
|
+
return { options: { environment } };
|
|
479
469
|
const invocation = resolveAkmInvocation();
|
|
480
|
-
return {
|
|
481
|
-
options: { binding: invocation.argv, contextPath: schedulerContextPath(descriptor) },
|
|
482
|
-
// The content-addressed descriptor is written once, before the first row that references it.
|
|
483
|
-
publish: () => {
|
|
484
|
-
writeSchedulerContextDescriptor(descriptor);
|
|
485
|
-
},
|
|
486
|
-
via: invocation.via,
|
|
487
|
-
};
|
|
470
|
+
return { options: { binding: invocation.argv, environment }, via: invocation.via };
|
|
488
471
|
}
|
|
489
|
-
/** One read of the akm-owned rows, each attributed to the bundle path
|
|
472
|
+
/** One read of the akm-owned rows, each attributed to the bundle path it names (#846). */
|
|
490
473
|
async function listInstalledRows(sched) {
|
|
491
474
|
return (await sched.list()).map((row) => {
|
|
492
|
-
const ownerBundlePath =
|
|
475
|
+
const ownerBundlePath = installedRowBundleDir(row);
|
|
493
476
|
return ownerBundlePath !== undefined ? { ...row, ownerBundlePath } : row;
|
|
494
477
|
});
|
|
495
478
|
}
|
|
496
|
-
/**
|
|
497
|
-
|
|
479
|
+
/**
|
|
480
|
+
* The `AKM_BUNDLE_DIR` an installed row names: its inline one, or the one in
|
|
481
|
+
* the descriptor a row written before 0.9.17-alpha.7 references. Undefined
|
|
482
|
+
* when it names none, or its descriptor cannot be read — which never means
|
|
483
|
+
* "mine".
|
|
484
|
+
*/
|
|
485
|
+
function installedRowBundleDir(row) {
|
|
486
|
+
if (row.contextPath === undefined)
|
|
487
|
+
return row.environment?.AKM_BUNDLE_DIR;
|
|
498
488
|
try {
|
|
499
|
-
return
|
|
489
|
+
return readLegacySchedulerContext(row.contextPath).AKM_BUNDLE_DIR;
|
|
500
490
|
}
|
|
501
491
|
catch {
|
|
502
492
|
return undefined;
|
|
@@ -515,26 +505,34 @@ function installedSchedulerBundle(config, row) {
|
|
|
515
505
|
return bundleKeyForContentRoot(config, row.ownerBundlePath);
|
|
516
506
|
}
|
|
517
507
|
/**
|
|
518
|
-
* Why `akm task prune` (#851) would remove an installed row:
|
|
519
|
-
* descriptor
|
|
520
|
-
* that is gone (`dead-bundle-path`). A row
|
|
521
|
-
* bundle is never a candidate.
|
|
508
|
+
* Why `akm task prune` (#851) would remove an installed row: a row written
|
|
509
|
+
* before 0.9.17-alpha.7 whose descriptor cannot be read (`invalid-context`),
|
|
510
|
+
* or one naming a bundle directory that is gone (`dead-bundle-path`). A row
|
|
511
|
+
* that still resolves to a live bundle is never a candidate.
|
|
522
512
|
*/
|
|
523
513
|
function classifyPruneCandidate(entry) {
|
|
524
|
-
let
|
|
525
|
-
|
|
526
|
-
|
|
514
|
+
let bundleDir;
|
|
515
|
+
if (entry.contextPath !== undefined) {
|
|
516
|
+
try {
|
|
517
|
+
bundleDir = readLegacySchedulerContext(entry.contextPath).AKM_BUNDLE_DIR;
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return "invalid-context";
|
|
521
|
+
}
|
|
522
|
+
// Every descriptor akm wrote names the bundle; one that does not cannot be attributed.
|
|
523
|
+
if (bundleDir === undefined)
|
|
524
|
+
return "invalid-context";
|
|
527
525
|
}
|
|
528
|
-
|
|
529
|
-
|
|
526
|
+
else {
|
|
527
|
+
bundleDir = entry.environment?.AKM_BUNDLE_DIR;
|
|
530
528
|
}
|
|
531
|
-
if (
|
|
529
|
+
if (bundleDir !== undefined && !fs.existsSync(bundleDir))
|
|
532
530
|
return "dead-bundle-path";
|
|
533
531
|
return undefined;
|
|
534
532
|
}
|
|
535
533
|
/**
|
|
536
534
|
* `akm task prune` (#851): remove installed rows `sync` can never reclaim
|
|
537
|
-
* because
|
|
535
|
+
* because they no longer resolve to a live bundle. It scans
|
|
538
536
|
* every installed row, not one bundle's. Without `--yes` it only previews and
|
|
539
537
|
* writes nothing; `--id` narrows it to named candidates.
|
|
540
538
|
*/
|
|
@@ -655,7 +653,7 @@ function groupInstalledBindings(entries) {
|
|
|
655
653
|
}
|
|
656
654
|
groups.set(key, {
|
|
657
655
|
argv,
|
|
658
|
-
contextPath: entry.contextPath,
|
|
656
|
+
...(entry.contextPath !== undefined ? { contextPath: entry.contextPath } : {}),
|
|
659
657
|
taskIds: [entry.id],
|
|
660
658
|
status,
|
|
661
659
|
});
|
|
@@ -669,13 +667,15 @@ function inspectInstalledBinding(entry) {
|
|
|
669
667
|
status.push("checkout");
|
|
670
668
|
if (binding.some((part) => part === "akm" || part === "bun" || part === "node"))
|
|
671
669
|
status.push("path-selected");
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
670
|
+
if (entry.contextPath !== undefined) {
|
|
671
|
+
try {
|
|
672
|
+
readLegacySchedulerContext(entry.contextPath);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
status.push("invalid-context");
|
|
676
|
+
}
|
|
677
677
|
}
|
|
678
|
-
const absolutePaths =
|
|
678
|
+
const absolutePaths = binding.filter((part) => path.isAbsolute(part));
|
|
679
679
|
if (absolutePaths.some((part) => !fs.existsSync(part)))
|
|
680
680
|
status.push("missing-path");
|
|
681
681
|
if (status.length === 0)
|
|
@@ -734,8 +734,9 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
|
|
|
734
734
|
* New scheduler bindings always carry a canonical `--bundle <owner>` token,
|
|
735
735
|
* including an env-selected working stash. That stash need not be persisted in
|
|
736
736
|
* config (CI, one-shot tools, and fresh installs commonly use only
|
|
737
|
-
* AKM_BUNDLE_DIR), so its scheduled child
|
|
738
|
-
*
|
|
737
|
+
* AKM_BUNDLE_DIR), so its scheduled child — every row sets AKM_BUNDLE_DIR to
|
|
738
|
+
* the working stash it was synced from — must accept precisely its derived
|
|
739
|
+
* owner name.
|
|
739
740
|
*
|
|
740
741
|
* This is intentionally narrower than an unknown-bundle fallback: a configured
|
|
741
742
|
* source always wins, and an unconfigured selector is accepted only when it is
|
|
@@ -9,23 +9,20 @@
|
|
|
9
9
|
* a bundle/adapter/concept id at all — it reads exactly the path it was
|
|
10
10
|
* given and classifies it.
|
|
11
11
|
*
|
|
12
|
-
* Reuses the exact version
|
|
13
|
-
* (`src/tasks/source/parse-task-source.ts`)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* the report, because `parseTaskSource`'s own `ParsedTaskSource.version` is
|
|
20
|
-
* always `4` post-shim — it cannot answer "was this a v2/v3/v4 file?" on
|
|
21
|
-
* its own once a v2/v3 source has been converted in memory.
|
|
12
|
+
* Reuses the exact version router `parseTaskSource`
|
|
13
|
+
* (`src/tasks/source/parse-task-source.ts`) every other task-source reader
|
|
14
|
+
* uses (`akm task sync`'s `compileTaskSources` included) — this module never
|
|
15
|
+
* forks a second parser. `readBoundedTaskSourceYaml` /
|
|
16
|
+
* `peekTaskSourceVersion` / `own` are the SAME front-end helpers the router
|
|
17
|
+
* calls first; they are used here only to recover the file's DECLARED schema
|
|
18
|
+
* version for the report and to tell `blocked` from `invalid`.
|
|
22
19
|
*
|
|
23
20
|
* Beyond parsing, this module also runs the SAME two per-source gates
|
|
24
21
|
* `akm task sync`'s `compileTaskSources` runs before it ever installs a
|
|
25
22
|
* schedule — `assertTaskScheduleInputsSatisfyContract` and
|
|
26
23
|
* `assertTaskScheduleCronValid` (both extracted from `scheduler-sync.ts` for
|
|
27
24
|
* exactly this reuse) — so a file `sync` would reject can never
|
|
28
|
-
* be reported `valid
|
|
25
|
+
* be reported `valid` here. Cron dialect is checked against
|
|
29
26
|
* `backendNameForPlatform()`, the same platform default `sync` falls back
|
|
30
27
|
* to whenever it has no injected/native-inspected backend to hand (see
|
|
31
28
|
* `akmTasksAdd`, `src/commands/tasks/tasks.ts`); a bare file was never
|
|
@@ -46,27 +43,18 @@
|
|
|
46
43
|
*
|
|
47
44
|
* Outcome classification (mirrors `parse-task-source.ts`'s own routing
|
|
48
45
|
* table in its header, extended for the two gates above):
|
|
49
|
-
* - `valid` — parses as task source v4
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* `schedule[].enabled` key also reports `converts`
|
|
56
|
-
* (`sourceVersion` is still `4`) — it read through the
|
|
57
|
-
* same in-memory shim, not the direct v4 parse path.
|
|
58
|
-
* - `blocked` — declared `version: 2` or `3`; the migrator itself
|
|
59
|
-
* could not convert it (an ambiguous/unmigratable
|
|
60
|
-
* shape) — the ONLY way `parseTaskSource` ever throws
|
|
61
|
-
* for those two version numbers, so no message-text
|
|
62
|
-
* sniffing is needed to tell this apart from `invalid`.
|
|
46
|
+
* - `valid` — parses as task source v4 and passes both sync gates.
|
|
47
|
+
* - `blocked` — declared `version: 2` or `3`, or a `version: 4`
|
|
48
|
+
* document still carrying the retired
|
|
49
|
+
* `schedule[].enabled`: what `akm migrate apply`
|
|
50
|
+
* rewrites. The runtime reads only v4 (#987), so the
|
|
51
|
+
* reason names that command.
|
|
63
52
|
* - `invalid` — the document declares SOME version (`4`, or anything
|
|
64
53
|
* other than 2/3/4) but fails to parse/validate, OR it
|
|
65
|
-
* parsed
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* shape) — reported with the parser's own reason.
|
|
54
|
+
* parsed but fails one of the two sync gates above, OR
|
|
55
|
+
* the YAML itself does not parse at all (a genuine
|
|
56
|
+
* syntax error, not merely a non-task shape) — reported
|
|
57
|
+
* with the parser's own reason.
|
|
70
58
|
* - `not-a-task` — the document parses as YAML but never declares a
|
|
71
59
|
* `version:` field at all (or isn't a YAML mapping) —
|
|
72
60
|
* the strongest signal available that the file was
|
|
@@ -142,10 +130,11 @@ export async function akmTaskValidate(filePath) {
|
|
|
142
130
|
if (!(cause instanceof UsageError))
|
|
143
131
|
throw cause;
|
|
144
132
|
const reason = cause.message;
|
|
145
|
-
// `
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
133
|
+
// What `akm migrate apply` rewrites is the only refusal for these
|
|
134
|
+
// shapes (`parse-task-source.ts`), so no message-text sniffing is needed.
|
|
135
|
+
if (declaredVersion === 2 ||
|
|
136
|
+
declaredVersion === 3 ||
|
|
137
|
+
(declaredVersion === 4 && v4ScheduleHasRetiredEnabledKey(root))) {
|
|
149
138
|
return { ok: false, path: resolvedPath, sourceVersion: declaredVersion, outcome: "blocked", reason };
|
|
150
139
|
}
|
|
151
140
|
if (peekFailed) {
|
|
@@ -163,13 +152,10 @@ export async function akmTaskValidate(filePath) {
|
|
|
163
152
|
};
|
|
164
153
|
}
|
|
165
154
|
// Success is unreachable from any path that leaves `declaredVersion`
|
|
166
|
-
// undefined — the router requires a numeric
|
|
155
|
+
// undefined — the router requires a numeric version 4 to reach here.
|
|
167
156
|
const sourceVersion = declaredVersion ?? 4;
|
|
168
|
-
// The document itself parsed
|
|
169
|
-
//
|
|
170
|
-
// accepting it. A violation here is `invalid`, never `blocked`: the
|
|
171
|
-
// migrator already succeeded, so this is the same kind of defect a
|
|
172
|
-
// native v4 document with the identical schedule would have.
|
|
157
|
+
// The document itself parsed — now the two gates `compileTaskSources`
|
|
158
|
+
// runs before accepting it. A violation here is `invalid`.
|
|
173
159
|
try {
|
|
174
160
|
assertTaskScheduleInputsSatisfyContract(parsed.v4, resolvedPath);
|
|
175
161
|
assertTaskScheduleCronValid(parsed.v4, backend);
|
|
@@ -179,17 +165,12 @@ export async function akmTaskValidate(filePath) {
|
|
|
179
165
|
throw cause;
|
|
180
166
|
return { ok: false, path: resolvedPath, sourceVersion, outcome: "invalid", reason: cause.message };
|
|
181
167
|
}
|
|
182
|
-
// A declared `version: 4` document with a retired `schedule[].enabled`
|
|
183
|
-
// key parsed through `parseTaskSource`'s in-memory shim, not the direct
|
|
184
|
-
// v4 path, even though `sourceVersion` reads `4` — report it the same
|
|
185
|
-
// way a converted v2/v3 document is reported.
|
|
186
|
-
const convertedFromRetiredScheduleEnabled = sourceVersion === 4 && !peekFailed && v4ScheduleHasRetiredEnabledKey(root);
|
|
187
168
|
const id = path.parse(resolvedPath).name;
|
|
188
169
|
return {
|
|
189
170
|
ok: true,
|
|
190
171
|
path: resolvedPath,
|
|
191
172
|
sourceVersion,
|
|
192
|
-
outcome:
|
|
173
|
+
outcome: "valid",
|
|
193
174
|
resolved: buildResolved(id, parsed.v4),
|
|
194
175
|
};
|
|
195
176
|
}
|
|
@@ -193,6 +193,8 @@ const DOCUMENT_JSON_CARRIED_FIELDS = [
|
|
|
193
193
|
"wikiRole",
|
|
194
194
|
"sources",
|
|
195
195
|
"evidenceSources",
|
|
196
|
+
// #935: a workflow's step targets and a task's target, read as declared links.
|
|
197
|
+
"uses",
|
|
196
198
|
// D2 (#730): the OKF v0.2 provenance `promoteProposal` stamps onto AKM-native
|
|
197
199
|
// writes (generated/verified/sources, namespaced — see `types.ts`'s
|
|
198
200
|
// `OkfProvenance` doc). Carried so the akm adapter rereads what it wrote and
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* concern (Chunk 4/5), not recognition's. On the valid Chunk-0b fixture this
|
|
55
55
|
* distinction never triggers.
|
|
56
56
|
*/
|
|
57
|
+
import { parseBuiltinCommandAction } from "../../../commands/command/builtin-action.js";
|
|
57
58
|
import { scanEnvKeyNames } from "../../../commands/env/env.js";
|
|
58
59
|
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
|
|
59
60
|
import { compileWorkflowSource, workflowStepInstructions } from "../../../workflows/compile.js";
|
|
@@ -125,6 +126,25 @@ function applyFrontmatterDescriptionAndTags(fm, out) {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
129
|
+
/** The command a stored `akm/command` action runs; inline content names no asset. */
|
|
130
|
+
function storedCommandRef(action) {
|
|
131
|
+
return action?.kind === "stored" ? action.ref : undefined;
|
|
132
|
+
}
|
|
133
|
+
/** The asset a workflow step targets: its `uses:` ref, or the ref of a stored `akm/command`. Prose and `run:` steps target none. */
|
|
134
|
+
function workflowStepTarget(spec) {
|
|
135
|
+
if (!spec?.uses)
|
|
136
|
+
return undefined;
|
|
137
|
+
if (spec.uses !== "akm/command")
|
|
138
|
+
return spec.uses;
|
|
139
|
+
if (spec.commandMode !== "stored-ref")
|
|
140
|
+
return undefined;
|
|
141
|
+
try {
|
|
142
|
+
return storedCommandRef(parseBuiltinCommandAction(spec.with));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
128
148
|
/** Collect + finalize a searchHints set the way every contributor does (`Array.from(hints).filter(Boolean)`, assigned only when non-empty). */
|
|
129
149
|
function finalizeHints(out, hints) {
|
|
130
150
|
if (hints.size > 0)
|
|
@@ -245,6 +265,9 @@ export function foldRecognizedMetadata(rendererName, file) {
|
|
|
245
265
|
hints.add(`prompt:${target.command.ref}`);
|
|
246
266
|
else
|
|
247
267
|
hints.add(`uses:${target.uses.ref}`);
|
|
268
|
+
const used = target.uses.kind !== "builtin-command" ? target.uses.ref : storedCommandRef(target.command);
|
|
269
|
+
if (used)
|
|
270
|
+
out.uses = [used];
|
|
248
271
|
}
|
|
249
272
|
else {
|
|
250
273
|
hints.add(`run:${target.run}`);
|
|
@@ -321,6 +344,7 @@ export function foldRecognizedMetadata(rendererName, file) {
|
|
|
321
344
|
return out;
|
|
322
345
|
const plan = result.plan;
|
|
323
346
|
const hints = new Set();
|
|
347
|
+
const uses = new Set();
|
|
324
348
|
if (plan.preamble)
|
|
325
349
|
hints.add(plan.preamble);
|
|
326
350
|
for (const step of plan.steps) {
|
|
@@ -328,8 +352,13 @@ export function foldRecognizedMetadata(rendererName, file) {
|
|
|
328
352
|
hints.add(workflowStepInstructions(step));
|
|
329
353
|
if (step.gate.criteria[0])
|
|
330
354
|
hints.add(step.gate.criteria[0]);
|
|
355
|
+
const used = workflowStepTarget(step.spec);
|
|
356
|
+
if (used)
|
|
357
|
+
uses.add(used);
|
|
331
358
|
}
|
|
332
359
|
out.searchHints = Array.from(hints).filter(Boolean);
|
|
360
|
+
if (uses.size > 0)
|
|
361
|
+
out.uses = [...uses];
|
|
333
362
|
if (plan.paramSchemas) {
|
|
334
363
|
const parameters = Object.entries(plan.paramSchemas).map(([name, schema]) => {
|
|
335
364
|
const description = schema.description;
|
|
@@ -395,4 +424,6 @@ export function applyFoldedMetadata(entry, folded) {
|
|
|
395
424
|
entry.toc = folded.toc;
|
|
396
425
|
if (folded.parameters)
|
|
397
426
|
entry.parameters = folded.parameters;
|
|
427
|
+
if (folded.uses)
|
|
428
|
+
entry.uses = folded.uses;
|
|
398
429
|
}
|
|
@@ -17,11 +17,9 @@
|
|
|
17
17
|
* ── validate (spec §6 task validation column) ──
|
|
18
18
|
*
|
|
19
19
|
* Validation enters the canonical task source parser (`parseTaskSource`,
|
|
20
|
-
* task source v4
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
* cannot convert, or any other unsupported number, fails closed with
|
|
24
|
-
* `TASK_SCHEMA_VERSION_UNSUPPORTED`). That parser owns the closed key sets,
|
|
20
|
+
* which reads only task source v4 — a `version: 2` or `version: 3`
|
|
21
|
+
* document fails closed with `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming
|
|
22
|
+
* `akm migrate apply`, which converts it). That parser owns the closed key sets,
|
|
25
23
|
* the executable-selector XOR, hostile YAML policy, the `akm/command`
|
|
26
24
|
* builtin, bounds, and physical `working-directory` containment. The
|
|
27
25
|
* adapter only translates a parser failure into the format-family
|
|
@@ -33,7 +31,8 @@
|
|
|
33
31
|
*/
|
|
34
32
|
import fs from "node:fs";
|
|
35
33
|
import path from "node:path";
|
|
36
|
-
import {
|
|
34
|
+
import { readBoundedTaskSourceYaml } from "../../../tasks/source/bounded-document.js";
|
|
35
|
+
import { parseTaskSource, peekTaskSourceVersion } from "../../../tasks/source/parse-task-source.js";
|
|
37
36
|
import { TASK_EXTENSION, TASK_NEAR_MISS_EXTENSION, taskExtensionDetail, taskSourceErrorDetail, } from "../../../tasks/source-v3.js";
|
|
38
37
|
import { toPosix } from "../../common.js";
|
|
39
38
|
import { hashContent } from "./shared.js";
|
|
@@ -125,8 +124,11 @@ export const akmTaskAdapter = {
|
|
|
125
124
|
return ["."];
|
|
126
125
|
},
|
|
127
126
|
/**
|
|
128
|
-
* Install-time probe (§1.2): a root holding a top-level, valid task
|
|
129
|
-
* `.yml` file
|
|
127
|
+
* Install-time probe (§1.2): a root holding a top-level, valid task source
|
|
128
|
+
* v4 `.yml` file, or a task v2/v3 file that `akm migrate apply` has yet to
|
|
129
|
+
* convert (the runtime refuses those, so without this the migrator and
|
|
130
|
+
* `akm task sync` would not find them in a bundle whose adapter config does
|
|
131
|
+
* not record). The full parser keeps this disjoint from unrelated YAML and
|
|
130
132
|
* prevents probe semantics from drifting from validation semantics.
|
|
131
133
|
*/
|
|
132
134
|
looksLikeRoot(root) {
|
|
@@ -152,9 +154,28 @@ export const akmTaskAdapter = {
|
|
|
152
154
|
return true;
|
|
153
155
|
}
|
|
154
156
|
catch {
|
|
157
|
+
if (isLegacyTaskDocument(raw, entry.name))
|
|
158
|
+
return true;
|
|
155
159
|
// Continue probing the remaining top-level .yml files.
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
return false;
|
|
159
163
|
},
|
|
160
164
|
};
|
|
165
|
+
/** The executable keys each retired task schema version required one of. */
|
|
166
|
+
const LEGACY_TASK_TARGET_KEYS = {
|
|
167
|
+
2: ["workflow", "prompt", "command"],
|
|
168
|
+
3: ["uses", "run"],
|
|
169
|
+
};
|
|
170
|
+
/** A task v2/v3 document: its schema version, plus a target key that version used. */
|
|
171
|
+
function isLegacyTaskDocument(yaml, filePath) {
|
|
172
|
+
let root;
|
|
173
|
+
try {
|
|
174
|
+
root = readBoundedTaskSourceYaml({ yaml, filePath }, { sourceLabel: "task source" }).root;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const keys = LEGACY_TASK_TARGET_KEYS[peekTaskSourceVersion(root) ?? 0];
|
|
180
|
+
return keys !== undefined && keys.some((key) => Object.hasOwn(root, key));
|
|
181
|
+
}
|