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.
Files changed (57) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/STABILITY.md +2 -2
  3. package/dist/akm +62 -29
  4. package/dist/akm-migrate +38 -19
  5. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  6. package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +5 -3
  7. package/dist/commands/improve/consolidate.js +11 -0
  8. package/dist/commands/improve/improve-cli.js +27 -7
  9. package/dist/commands/improve/ledger.js +7 -3
  10. package/dist/commands/improve/loop-stages.js +4 -3
  11. package/dist/commands/improve/preparation.js +40 -10
  12. package/dist/commands/improve/reflect.js +46 -22
  13. package/dist/commands/improve/retrieval-gate.js +127 -0
  14. package/dist/commands/improve/retrieval-scope.js +77 -0
  15. package/dist/commands/read/curate.js +41 -30
  16. package/dist/commands/read/show.js +55 -2
  17. package/dist/commands/sources/info.js +3 -0
  18. package/dist/commands/tasks/tasks-cli.js +10 -12
  19. package/dist/commands/tasks/tasks.js +57 -56
  20. package/dist/commands/tasks/validate.js +27 -46
  21. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  22. package/dist/core/adapter/adapters/akm-metadata.js +31 -0
  23. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  24. package/dist/core/improve-result.js +4 -1
  25. package/dist/core/non-task-input.js +20 -0
  26. package/dist/core/paths.js +0 -4
  27. package/dist/indexer/db/graph-db.js +0 -32
  28. package/dist/indexer/graph/graph-extraction.js +3 -1
  29. package/dist/indexer/indexer.js +1 -3
  30. package/dist/indexer/links/declared-links.js +90 -0
  31. package/dist/indexer/scan/doc-to-entry.js +1 -0
  32. package/dist/indexer/usage/usage-events.js +34 -0
  33. package/dist/llm/graph-extract.js +26 -37
  34. package/dist/output/shapes/helpers.js +3 -0
  35. package/dist/output/text/show-format.js +16 -0
  36. package/dist/scripts/akm-migrate-node.js +6377 -6437
  37. package/dist/scripts/akm-migrate.js +6860 -6920
  38. package/dist/storage/repositories/index-entries-repository.js +12 -6
  39. package/dist/storage/repositories/index-entry-schema.js +18 -1
  40. package/dist/storage/repositories/index-links-repository.js +143 -0
  41. package/dist/storage/repositories/index-schema.js +27 -0
  42. package/dist/storage/repositories/proposals-repository.js +4 -0
  43. package/dist/tasks/backends/cron.js +80 -43
  44. package/dist/tasks/backends/launchd.js +28 -15
  45. package/dist/tasks/backends/schtasks.js +25 -10
  46. package/dist/tasks/run/load-task.js +1 -1
  47. package/dist/tasks/scheduler-binding.js +4 -2
  48. package/dist/tasks/scheduler-invocation.js +127 -235
  49. package/dist/tasks/scheduler-sync.js +13 -8
  50. package/dist/tasks/source/parse-task-source.js +22 -126
  51. package/dist/tasks/source/task-to-v4.js +463 -87
  52. package/docs/migration/release-notes/0.9.17.md +7 -5
  53. package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
  54. package/docs/reference/cli.md +26 -10
  55. package/docs/reference/tasks.md +58 -40
  56. package/package.json +1 -1
  57. 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
- }, config.defaultBundle ?? (isPrimaryStash ? indexedEntry.bundleId : undefined));
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` and `converts`
439
- * (a v2/v3 document the in-memory shim converted successfully) are both
440
- * successful (exit 0); `blocked`/`invalid`/`not-a-task` are diagnosed
441
- * defects the caller must act on (exit 1, mirroring `task sync`'s own
442
- * `failures.length > 0 -> EXIT_CODES.GENERAL`). A missing path or an
443
- * unreadable file never reaches this function at all — `akmTaskValidate`
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" || result.outcome === "converts" ? undefined : EXIT_CODES.GENERAL;
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 their own descriptor no longer " +
482
- "resolves to a live bundle (corrupt/missing --scheduler-context, or the owning bundle directory is " +
483
- "gone). Defaults to a dry-run preview — zero scheduler writes. Requires --yes to remove anything; " +
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 { schedulerContextDescriptor, schedulerContextPath, validateSchedulerContextDescriptor, writeSchedulerContextDescriptor, } from "../../tasks/scheduler-invocation.js";
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, publish, warnings } = await buildSchedulerSyncPlan(deps, bundleTarget, options);
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 descriptor, no row.
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
- ...(runtime.options ? { installOptions: runtime.options } : {}),
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?.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\`.`);
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 descriptor rows are written with. The descriptor follows
464
- * the current policy on every sync; a row that is already installed keeps its
465
- * launcher unless `--rebind` (see `installOptionsFor`). An injected backend
466
- * (tests) renders with its own defaults.
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 its own descriptor names (#846). */
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 = row.contextPath ? resolveInstalledOwnerPath(row.contextPath) : undefined;
475
+ const ownerBundlePath = installedRowBundleDir(row);
493
476
  return ownerBundlePath !== undefined ? { ...row, ownerBundlePath } : row;
494
477
  });
495
478
  }
496
- /** Best-effort recovery of an installed binding's owning bundle path (#846). */
497
- function resolveInstalledOwnerPath(contextPath) {
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 validateSchedulerContextDescriptor(contextPath).environment.AKM_BUNDLE_DIR;
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: its own
519
- * descriptor does not load (`invalid-context`) or names a bundle directory
520
- * that is gone (`dead-bundle-path`). A row that still resolves to a live
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 ownerBundlePath;
525
- try {
526
- ownerBundlePath = validateSchedulerContextDescriptor(entry.contextPath).environment.AKM_BUNDLE_DIR;
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
- catch {
529
- return "invalid-context";
526
+ else {
527
+ bundleDir = entry.environment?.AKM_BUNDLE_DIR;
530
528
  }
531
- if (ownerBundlePath !== undefined && !fs.existsSync(ownerBundlePath))
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 their descriptor no longer resolves to a live bundle. It scans
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
- try {
673
- validateSchedulerContextDescriptor(entry.contextPath);
674
- }
675
- catch {
676
- status.push("invalid-context");
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 = [...binding.filter((part) => path.isAbsolute(part)), entry.contextPath];
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 must accept precisely its derived
738
- * owner name after the scheduler context restores the environment.
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-routing shim `parseTaskSource`
13
- * (`src/tasks/source/parse-task-source.ts`) already applies for every other
14
- * task-source reader (`akm task sync`'s `compileTaskSources` included) —
15
- * this module never forks a second parser or a second v2/v3 migration
16
- * planner. `readBoundedTaskSourceYaml` / `peekTaskSourceVersion` / `own` are
17
- * the SAME front-end helpers that shim itself calls first; they are used
18
- * here only to recover the file's ORIGINALLY DECLARED schema version for
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`/`converts` here. Cron dialect is checked against
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 directly (declared `version: 4`)
50
- * and passes both sync gates.
51
- * - `converts` — declared `version: 2` or `3`; the deterministic
52
- * in-memory migrator produced a valid v4 document that
53
- * passes both sync gates. A declared `version: 4`
54
- * document whose only defect is a retired
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 (directly or via a SUCCESSFUL v2/v3
66
- * conversion) but fails one of the two sync gates
67
- * above, OR the YAML itself does not parse at all
68
- * (a genuine syntax error, not merely a non-task
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
- // `parseTaskSource` only ever throws for a declared version 2/3 via the
146
- // unmigratable-conversion branch (see this file's header) — no separate
147
- // message check needed to recognize "blocked" here.
148
- if (declaredVersion === 2 || declaredVersion === 3) {
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 2/3/4 version to reach here.
155
+ // undefined — the router requires a numeric version 4 to reach here.
167
156
  const sourceVersion = declaredVersion ?? 4;
168
- // The document itself parsed (directly, or via a successful v2/v3
169
- // conversion) — now the two gates `compileTaskSources` runs before
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: sourceVersion === 2 || sourceVersion === 3 || convertedFromRetiredScheduleEnabled ? "converts" : "valid",
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 native as of P4 — a `version: 3` or `version: 2` document
21
- * is auto-read through the in-memory migration shim in
22
- * `parse-task-source.ts`; only a version the shim's deterministic planners
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 { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
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-v3
129
- * `.yml` file. The full parser keeps this disjoint from unrelated YAML and
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
+ }