akm-cli 0.9.17-alpha.5 → 0.9.17-alpha.7

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 (55) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/STABILITY.md +2 -2
  3. package/dist/akm +7 -7
  4. package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
  5. package/dist/commands/improve/consolidate.js +11 -0
  6. package/dist/commands/improve/execution.js +5 -5
  7. package/dist/commands/improve/improve-cli.js +27 -7
  8. package/dist/commands/improve/improve-strategies.js +3 -0
  9. package/dist/commands/improve/ledger.js +7 -3
  10. package/dist/commands/improve/loop-stages.js +5 -4
  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 +15 -49
  16. package/dist/commands/read/show.js +2 -81
  17. package/dist/commands/tasks/tasks-cli.js +10 -12
  18. package/dist/commands/tasks/tasks.js +57 -56
  19. package/dist/commands/tasks/validate.js +27 -46
  20. package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
  21. package/dist/core/config/config.js +1 -1
  22. package/dist/core/config/schema/improve-processes.js +4 -3
  23. package/dist/core/config/schema/index-config.js +4 -23
  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 +8 -81
  28. package/dist/indexer/graph/graph-extraction.js +74 -227
  29. package/dist/indexer/graph/graph-related.js +5 -4
  30. package/dist/indexer/indexer.js +1 -3
  31. package/dist/indexer/search/db-search.js +10 -1
  32. package/dist/indexer/usage/usage-events.js +34 -0
  33. package/dist/llm/feature-gate.js +0 -3
  34. package/dist/llm/graph-extract.js +81 -41
  35. package/dist/scripts/akm-migrate-node.js +7148 -7174
  36. package/dist/scripts/akm-migrate.js +8718 -8744
  37. package/dist/storage/repositories/index-entries-repository.js +5 -7
  38. package/dist/storage/repositories/index-schema.js +16 -34
  39. package/dist/storage/repositories/proposals-repository.js +4 -0
  40. package/dist/tasks/backends/cron.js +80 -43
  41. package/dist/tasks/backends/launchd.js +28 -15
  42. package/dist/tasks/backends/schtasks.js +25 -10
  43. package/dist/tasks/run/load-task.js +1 -1
  44. package/dist/tasks/scheduler-binding.js +4 -2
  45. package/dist/tasks/scheduler-invocation.js +127 -235
  46. package/dist/tasks/scheduler-sync.js +13 -8
  47. package/dist/tasks/source/parse-task-source.js +22 -126
  48. package/dist/tasks/source/task-to-v3.js +1 -55
  49. package/dist/tasks/source/task-to-v4.js +1 -13
  50. package/docs/migration/v0.9.1-to-v0.9.2.md +7 -3
  51. package/docs/reference/cli.md +13 -4
  52. package/docs/reference/configuration.md +12 -0
  53. package/docs/reference/tasks.md +58 -40
  54. package/package.json +1 -1
  55. package/schemas/akm-config.json +0 -6
@@ -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
  }
@@ -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
+ }
@@ -33,7 +33,7 @@ export { FEEDBACK_FAILURE_MODES } from "./config-schema.js";
33
33
  * combined prompt size well under common 8K/16K context windows (each body is
34
34
  * sliced to ~500 chars in the graph-extract prompt builder).
35
35
  */
36
- export const DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE = 4;
36
+ const DEFAULT_GRAPH_EXTRACTION_BATCH_SIZE = 4;
37
37
  /**
38
38
  * Approximate character budget per asset body inside a batched
39
39
  * graph-extraction prompt — used by {@link resolveBatchSize} to derive a
@@ -183,9 +183,10 @@ const MEMORY_INFERENCE_PROCESS_FIELDS = {
183
183
  cls: clsField,
184
184
  };
185
185
  /**
186
- * GraphExtraction process fields: improve-owned graph extraction scope and
187
- * batching. Passed to the invocation directly and never inherited from
188
- * standalone index.graph.
186
+ * GraphExtraction process fields: one strategy's graph extraction scope and
187
+ * batching. `includeTypes` and `batchSize` override `index.graph`'s
188
+ * `graphExtractionIncludeTypes` and `graphExtractionBatchSize`; unset, the
189
+ * pass reads those.
189
190
  */
190
191
  const GRAPH_EXTRACTION_PROCESS_FIELDS = {
191
192
  // #624 P2: when set, rank eligible files by utility_scores DESC and process
@@ -32,22 +32,11 @@ const INDEX_PASS_RETIRED_KEYS = new Set([
32
32
  "maxTokens",
33
33
  "capabilities",
34
34
  ]);
35
- const INDEX_PASS_KNOWN_KEYS = new Set([
36
- "engine",
37
- "model",
38
- "timeoutMs",
39
- "enabled",
40
- "llm",
41
- "graphExtractionBatchSize",
42
- "graphExtractionIncludeTypes",
43
- "lazyGraphExtraction",
44
- ]);
45
35
  /**
46
- * Per-pass `index.<pass>` entry. Uses preprocess + manual validation so we can
47
- * emit targeted error messages ("Retired or misplaced engine setting",
48
- * "Unknown key `index.<pass>.<key>`")
49
- * instead of Zod's generic `Unrecognized key` / `Expected boolean, received
50
- * string` strings — keeps `akm` startup errors actionable.
36
+ * Per-pass `index.<pass>` entry. The preprocess names and drops the retired
37
+ * engine settings above with a targeted message. Any other unknown key is kept
38
+ * and named once by the config loader's schema walk, like an unknown key
39
+ * anywhere else in config.
51
40
  */
52
41
  export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
53
42
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -62,13 +51,6 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
62
51
  cleaned ??= { ...obj };
63
52
  delete cleaned[key];
64
53
  }
65
- else if (!INDEX_PASS_KNOWN_KEYS.has(key)) {
66
- warnOnce(`index-pass:unknown:${dotted}`, `Unknown key \`${dotted}\` ignored. Per-pass entries support ` +
67
- "`engine`, `model`, `timeoutMs`, `enabled`, `llm`, `graphExtractionBatchSize`, " +
68
- "`graphExtractionIncludeTypes`, and `lazyGraphExtraction`.");
69
- cleaned ??= { ...obj };
70
- delete cleaned[key];
71
- }
72
54
  }
73
55
  return cleaned ?? raw;
74
56
  }, z
@@ -81,7 +63,6 @@ export const IndexPassConfigSchema = z.preprocess((raw, ctx) => {
81
63
  graphExtractionBatchSize: positiveInt.optional(),
82
64
  // Accept-any until Chunk 2 (WI-9.6c) — no longer enum-restricted.
83
65
  graphExtractionIncludeTypes: z.array(z.string().min(1)).nonempty().optional(),
84
- lazyGraphExtraction: z.boolean().optional(),
85
66
  })
86
67
  .passthrough());
87
68
  const MetadataEnhanceSchema = z.object({ enabled: z.boolean().optional() }).passthrough();
@@ -306,6 +306,8 @@ function validateImprovePlan(value, dryRun, plannedRefNames) {
306
306
  fail("plan.limits.totalCeiling must equal plan.limits.effective + plan.limits.additiveReplayAllowance");
307
307
  }
308
308
  const gateNames = new Set(["profile", "cleanup", "validation", "signal", "disk", "limit"]);
309
+ // Plans stored before 0.9.17-alpha.7 (#986) have no retrieval gate.
310
+ const optionalGateNames = new Set(["retrieval"]);
309
311
  if (!Array.isArray(value.gates))
310
312
  fail("plan.gates must be an array");
311
313
  const gateRemovedByName = new Map();
@@ -313,8 +315,9 @@ function validateImprovePlan(value, dryRun, plannedRefNames) {
313
315
  if (!isRecord(gate))
314
316
  fail("plan.gates entries must be objects");
315
317
  requireExactFields(gate, new Set(["name", "removed", "reason"]));
316
- if (typeof gate.name !== "string" || !gateNames.has(gate.name))
318
+ if (typeof gate.name !== "string" || !(gateNames.has(gate.name) || optionalGateNames.has(gate.name))) {
317
319
  fail("plan.gates.name is invalid");
320
+ }
318
321
  requireCount(gate, "removed", "plan.gates entry");
319
322
  if (typeof gate.reason !== "string")
320
323
  fail("plan.gates.reason must be a string");
@@ -0,0 +1,20 @@
1
+ // This Source Code Form is subject to the terms of the Mozilla Public
2
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
+ /** The line of `src/assets/stash-skeleton/README.md` that reaches curate verbatim as a query. */
5
+ const STASH_README_LINE = "This is an **AKM stash** — a structured knowledge repository that stores reusable";
6
+ /**
7
+ * What the (trimmed) curate input is when it is not a task, else undefined.
8
+ * Harness and tool envelopes (`<task-notification>…`, `<system-reminder>…`,
9
+ * `<cross-session-message …>…`) start with a tag and close one, and the stash
10
+ * README line arrives verbatim; on the retrieval suite neither shape occurs in
11
+ * a real query. Length is not a signal: prompts over 2,000 characters found
12
+ * relevant assets at about the rate of shorter long prompts.
13
+ */
14
+ export function nonTaskInput(query) {
15
+ if (query.startsWith("<") && query.includes("</"))
16
+ return "a harness or tool envelope";
17
+ if (query === STASH_README_LINE)
18
+ return "the akm stash README boilerplate";
19
+ return undefined;
20
+ }
@@ -249,10 +249,6 @@ export function getIndexRebuildLockPath() {
249
249
  export function getStateDbPathInDataDir() {
250
250
  return path.join(getDataDir(), "state.db");
251
251
  }
252
- /** Content-addressed scheduler runtime descriptors. */
253
- export function getTaskContextDir(env = process.env) {
254
- return path.join(getDataDir(env), "tasks", "context");
255
- }
256
252
  /** Path to the akm.lock file in $DATA. */
257
253
  export function getLockfilePath() {
258
254
  return path.join(getDataDir(), "akm.lock");