akm-cli 0.9.15 → 0.9.16-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/assets/hints/cli-hints-full.md +13 -6
  3. package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
  4. package/dist/cli/retired-commands.js +0 -2
  5. package/dist/commands/env/env-binding.js +4 -4
  6. package/dist/commands/env/env-cli.js +3 -3
  7. package/dist/commands/improve/improve-cli.js +19 -14
  8. package/dist/commands/improve/reflect.js +23 -2
  9. package/dist/commands/lint/base-linter.js +9 -0
  10. package/dist/commands/lint/env-key-rules.js +2 -2
  11. package/dist/commands/proposal/propose.js +15 -1
  12. package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
  13. package/dist/commands/proposal/validators/proposal-validators.js +5 -4
  14. package/dist/commands/read/search.js +33 -4
  15. package/dist/commands/read/show.js +21 -2
  16. package/dist/commands/registry-cli.js +5 -5
  17. package/dist/commands/sources/add-cli.js +59 -16
  18. package/dist/commands/sources/bundle-cli.js +35 -11
  19. package/dist/commands/sources/bundle-config-ops.js +30 -0
  20. package/dist/commands/sources/dangerous-env-audit.js +4 -4
  21. package/dist/commands/sources/installed-stashes.js +43 -28
  22. package/dist/commands/sources/source-add.js +33 -17
  23. package/dist/commands/sources/source-manage.js +34 -12
  24. package/dist/commands/sources/stash-skeleton.js +6 -3
  25. package/dist/commands/tasks/explain.js +4 -1
  26. package/dist/commands/tasks/tasks-cli.js +31 -9
  27. package/dist/commands/tasks/tasks.js +239 -194
  28. package/dist/commands/tasks/validate.js +20 -32
  29. package/dist/core/activation-policy.js +4 -4
  30. package/dist/core/adapter/adapters/akm-adapter.js +5 -0
  31. package/dist/core/adapter/execution-source.js +10 -29
  32. package/dist/core/config/config-schema.js +64 -8
  33. package/dist/core/config/config-sources.js +96 -2
  34. package/dist/core/config/config.js +190 -24
  35. package/dist/core/config/legacy-source-shape-shim.js +9 -0
  36. package/dist/core/config/schema/execution.js +23 -0
  37. package/dist/core/config/schema/experimental.js +1 -1
  38. package/dist/core/config/schema/scheduler.js +20 -0
  39. package/dist/core/config/schema/search.js +1 -1
  40. package/dist/core/config/schema/sources-bundles.js +32 -1
  41. package/dist/core/content-safety.js +52 -0
  42. package/dist/core/maintenance-barrier.js +6 -6
  43. package/dist/core/type-presentation.js +1 -1
  44. package/dist/core/write-source.js +13 -8
  45. package/dist/indexer/bundle-identity-guard.js +45 -8
  46. package/dist/indexer/indexer.js +1 -1
  47. package/dist/indexer/materialize-embeddings.js +15 -1
  48. package/dist/indexer/search/search-source.js +29 -11
  49. package/dist/integrations/agent/execution-lowering.js +3 -2
  50. package/dist/integrations/agent/execution-preparation.js +32 -1
  51. package/dist/integrations/agent/prompts.js +1 -1
  52. package/dist/integrations/agent/request-lowering.js +3 -2
  53. package/dist/llm/client.js +2 -1
  54. package/dist/output/shapes/passthrough.js +2 -0
  55. package/dist/registry/resolve.js +37 -10
  56. package/dist/scripts/akm-migrate-node.js +13644 -9894
  57. package/dist/scripts/akm-migrate.js +12626 -8876
  58. package/dist/setup/setup.js +3 -3
  59. package/dist/setup/steps/tasks.js +29 -36
  60. package/dist/sources/providers/git-install.js +17 -11
  61. package/dist/sources/providers/git-provider.js +12 -5
  62. package/dist/sources/providers/git-stash.js +38 -16
  63. package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
  64. package/dist/storage/repositories/index-vec-repository.js +100 -0
  65. package/dist/tasks/activation-config.js +90 -0
  66. package/dist/tasks/backends/cron.js +9 -0
  67. package/dist/tasks/backends/launchd.js +1 -0
  68. package/dist/tasks/backends/schtasks.js +2 -0
  69. package/dist/tasks/embedded.js +4 -5
  70. package/dist/tasks/scheduler-binding.js +2 -2
  71. package/dist/tasks/scheduler-sync-preview.js +8 -1
  72. package/dist/tasks/scheduler-sync.js +19 -10
  73. package/dist/tasks/source/parse-task-source.js +10 -113
  74. package/dist/tasks/source/project-v4.js +2 -2
  75. package/dist/tasks/source/task-source-v4.js +4 -12
  76. package/dist/tasks/source/task-to-v3.js +4 -12
  77. package/dist/tasks/source/task-to-v4.js +40 -7
  78. package/docs/migration/README.md +1 -0
  79. package/docs/migration/release-notes/0.9.16.md +72 -0
  80. package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
  81. package/docs/reference/cli.md +37 -29
  82. package/docs/reference/configuration.md +48 -5
  83. package/docs/reference/tasks.md +34 -29
  84. package/package.json +1 -1
  85. package/schemas/akm-config.json +112 -4
  86. package/schemas/akm-task.json +1 -2
@@ -25,6 +25,7 @@ import { getDirname } from "../runtime.js";
25
25
  import { parseTaskSource } from "./source/parse-task-source.js";
26
26
  /** Directory holding the bundled task template categories. */
27
27
  const TASKS_ASSETS_DIR = path.join(getDirname(import.meta.url), "../assets/tasks");
28
+ const DEFAULT_DISABLED_TASKS = new Set(["improve/akm-improve-catchup"]);
28
29
  /**
29
30
  * Enumerate the embedded task templates from every category subdirectory of
30
31
  * the bundled assets directory. Sorted by category then id for deterministic
@@ -72,10 +73,8 @@ export function listEmbeddedTasks() {
72
73
  catch {
73
74
  continue;
74
75
  }
75
- // Shipped templates are task source v4 (spec docs/plans/specs/p4-deletions-closeout.md
76
- // §3.2.6, row B-24): a template's `enabled` is per schedule-binding, so
77
- // the single-cron display shape here reads the FIRST schedule entry —
78
- // every shipped template authors exactly one.
76
+ // Setup defaults are trusted application metadata, not bundle-authored
77
+ // task data. Task source can describe a schedule but cannot activate it.
79
78
  const task = parsed.v4;
80
79
  const [firstSchedule] = task.schedule;
81
80
  if (task.target.kind !== "run" || !firstSchedule)
@@ -86,7 +85,7 @@ export function listEmbeddedTasks() {
86
85
  command: task.target.run,
87
86
  schedule: firstSchedule.cron,
88
87
  description: task.description ?? "",
89
- enabled: firstSchedule.enabled,
88
+ enabled: !DEFAULT_DISABLED_TASKS.has(`${category}/${id}`),
90
89
  yaml,
91
90
  });
92
91
  }
@@ -41,7 +41,7 @@ export function compileTaskSchedulerBindings(input) {
41
41
  cron: schedule.cron,
42
42
  source: schedule.source,
43
43
  ordinal: schedule.ordinal,
44
- enabled: schedule.enabled ?? input.enabled,
44
+ enabled: true,
45
45
  invocation,
46
46
  });
47
47
  }));
@@ -286,7 +286,7 @@ export function canonicalSchedulerIdentity(logicalSource, ordinal, invocation) {
286
286
  }
287
287
  const canonicalInvocation = ["task", "run", taskId, "--bundle", parsed.bundle, "--scheduled"];
288
288
  if (!sameInvocation(invocation, canonicalInvocation)) {
289
- throw new UsageError("Task scheduler expectation invocation does not match its qualified source.", "INVALID_FLAG_VALUE");
289
+ throw new UsageError(`Task scheduler expectation invocation does not match its qualified source: expected ${JSON.stringify(canonicalInvocation)}, got ${JSON.stringify(invocation)}.`, "INVALID_FLAG_VALUE");
290
290
  }
291
291
  if (parsed.conceptId !== taskId && parsed.conceptId !== `tasks/${taskId}`) {
292
292
  throw new UsageError("Task scheduler expectation id does not match its qualified source.", "INVALID_FLAG_VALUE");
@@ -25,7 +25,14 @@ export function renderSchedulerPlanPreview(backend, operations, unchanged = [],
25
25
  adds.push({ id: operation.binding.id, kind: "install" });
26
26
  }
27
27
  else {
28
- updates.push({ id: operation.binding.id, kind: "update" });
28
+ updates.push({
29
+ id: operation.binding.id,
30
+ kind: "update",
31
+ ...(operation.expected.fingerprint !== undefined
32
+ ? { installedFingerprint: operation.expected.fingerprint }
33
+ : {}),
34
+ ...(operation.resultFingerprint !== undefined ? { expectedFingerprint: operation.resultFingerprint } : {}),
35
+ });
29
36
  }
30
37
  }
31
38
  return Object.freeze({
@@ -51,6 +51,9 @@ export async function prepareSchedulerSyncSourceSet(input) {
51
51
  failures: compiled.failures,
52
52
  });
53
53
  }
54
+ function schedulerActivationKey(kind, ref) {
55
+ return `${kind}\0${ref}`;
56
+ }
54
57
  export function finalizeSchedulerSyncPlan(input, prepared) {
55
58
  const inspection = inspectionForPlan(input);
56
59
  const coherentInput = {
@@ -60,10 +63,8 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
60
63
  };
61
64
  const desired = prepared.desired;
62
65
  assertUniqueDesiredIds(desired);
63
- assertCoherentInspection(inspection, input.inspection !== undefined);
64
- assertUniqueInstalledIds(coherentInput.installed);
66
+ assertSchedulerBackendInspection(inspection, desired, input.inspection !== undefined);
65
67
  assertNoForeignIds(desired, coherentInput);
66
- assertSchedulerNativeArtifactOwnership(desired, inspection.artifacts);
67
68
  const scopedInstalled = coherentInput.installed.filter((entry) => belongsToBundle(entry, coherentInput));
68
69
  const present = new Map(scopedInstalled.map((entry) => [entry.id, entry]));
69
70
  const installed = [];
@@ -123,6 +124,12 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
123
124
  failures: prepared.failures,
124
125
  });
125
126
  }
127
+ /** Validate one coherent whole-backend read before deriving any mutation plan. */
128
+ export function assertSchedulerBackendInspection(inspection, desired = [], requireCompleteFingerprint = true) {
129
+ assertCoherentInspection(inspection, requireCompleteFingerprint);
130
+ assertUniqueInstalledIds(inspection.installed);
131
+ assertSchedulerNativeArtifactOwnership(desired, inspection.artifacts);
132
+ }
126
133
  /**
127
134
  * Build the exact removal operation for one installed binding: same
128
135
  * exact-native-fingerprint / ordinal-attribution safety checks
@@ -299,6 +306,10 @@ async function compileTaskSources(input, collector, out, failures) {
299
306
  const conceptId = relative.slice(0, -4);
300
307
  const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
301
308
  const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
309
+ if (input.enabledActivations &&
310
+ !input.enabledActivations.has(schedulerActivationKey("task", qualifiedRefForFailure))) {
311
+ continue;
312
+ }
302
313
  try {
303
314
  const physicalIdentity = guarded.physicalIdentity;
304
315
  const priorOwner = physicalOwners.get(physicalIdentity);
@@ -308,14 +319,11 @@ async function compileTaskSources(input, collector, out, failures) {
308
319
  physicalOwners.set(physicalIdentity, sourcePath);
309
320
  // Project BEFORE prepareTaskV3Execution so projectability is checked —
310
321
  // but build the scheduler bindings from the ORIGINAL task source v4
311
- // document, not the projection, which deliberately drops per-entry
312
- // `enabled` and `schedule[i].inputs` (D2-N5, project-v4.ts) —
322
+ // document, not the projection, which deliberately drops
323
+ // `schedule[i].inputs` (project-v4.ts) —
313
324
  // schedule-supplied inputs are delivered through the scheduler
314
325
  // binding's own compiled invocation tail (P2b Lane B, spec §4.4,
315
326
  // B-N3), not through the prepare-seam projection. A task source v4
316
- // document has no document-level `akm.enabled`, so `enabled: true` is
317
- // passed at the document level and every entry's own `enabled`
318
- // (always present, defaulted at parse time) decides.
319
327
  const parsed = parseTaskSource({
320
328
  yaml: guarded.content,
321
329
  filePath: sourcePath,
@@ -349,11 +357,9 @@ async function compileTaskSources(input, collector, out, failures) {
349
357
  id,
350
358
  qualifiedRef,
351
359
  ...(input.bundleTarget ? { bundleTarget: input.bundleTarget } : {}),
352
- enabled: true,
353
360
  schedules: parsed.v4.schedule.map((schedule) => ({
354
361
  cron: schedule.cron,
355
362
  ordinal: schedule.ordinal,
356
- enabled: schedule.enabled,
357
363
  source: `${relSource}:${schedule.source}`,
358
364
  // P2b Lane B (spec §4.4, B-N3): delivered through the compiled
359
365
  // binding's own invocation tail below — the F-B2 flip that closes
@@ -423,6 +429,9 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
423
429
  for (const [canonicalName, sources] of lookups) {
424
430
  const failurePath = sources[0]?.sourcePath ?? canonicalName;
425
431
  const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
432
+ if (input.enabledActivations && !input.enabledActivations.has(schedulerActivationKey("workflow", failureRef))) {
433
+ continue;
434
+ }
426
435
  try {
427
436
  if (sources.length > 1) {
428
437
  throw new WorkflowSourceCollisionError(input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName, sources.map((source) => source.relativePath));
@@ -2,138 +2,35 @@
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  /**
5
- * The task source version router (spec docs/plans/specs/p4-deletions-closeout.md
6
- * §3.2.2).
5
+ * Current task-source router.
7
6
  *
8
- * Runs the bounded YAML front end ONCE (`readBoundedTaskSourceYaml`), reads
9
- * `root.version`, and either dispatches into `parseTaskSourceV4Document` or
10
- * routes through the in-memory read shim below — no second parse of the v4
11
- * grammar itself, no re-serialization back to disk, no synthetic document
12
- * (the P1b §4.3 invariant this phase carries forward: the shim adds a pure
13
- * bytes-in/bytes-out detour, never a disk write).
14
- *
15
- * The terminal routing table:
16
- *
17
- * | root `version` | outcome |
18
- * |------------------------|-----------------------------------------------------------------|
19
- * | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
20
- * | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
21
- * | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
22
- * | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
23
- *
24
- * A missing or non-numeric `version:` is a MALFORMED v4 document, not a
25
- * legacy one — it routes into the v4 parser so the field error names the
26
- * one grammar `src` still accepts, rather than a generic "unsupported"
27
- * message that would send the user to the migrator for a document that was
28
- * never task v2 or v3 in the first place.
29
- *
30
- * task v2 and task v3 sources are no longer read as their own standing
31
- * grammar anywhere else in `src` — the only readers of that grammar are the
32
- * pure, byte-producing planners (`./task-to-v3.ts`, `./task-to-v4.ts`, and
33
- * the frozen v3 reader `./task-source-v3-frozen.ts`), reached either through
34
- * this shim (bytes in, bytes out, never touches disk) or through
35
- * `akm migrate apply` / `akm-migrate` (`scripts/akm-migrate`, which
36
- * additionally rewrites the file on disk once the user asks for that).
37
- * Policy: a deterministic byte transform is the tool's job, not the user's —
38
- * upgrading past a schema bump must not silently break a scheduled task, so
39
- * v2/v3 files keep reading successfully at the cost of a one-line
40
- * deprecation warning, and `akm migrate apply` remains available to rewrite
41
- * the file and silence it. The front end's own pre-version failures (source
42
- * not a string, source too large, YAML parse/warning/expansion) render with
43
- * the label `task source` (row B-17, closing the "task v3 source" label
44
- * wart P2a's §3.4 recorded).
7
+ * Runtime code accepts only the current v4 grammar. Historical v2/v3 files
8
+ * and the former source-owned `schedule[].enabled` field are handled only by
9
+ * the explicit `akm migrate` executable; execution never translates legacy
10
+ * bytes in memory.
45
11
  */
46
12
  import { UsageError } from "../../core/errors.js";
47
- import { warn } from "../../core/warn.js";
48
13
  import { readBoundedTaskSourceYaml } from "./bounded-document.js";
49
- import { parseTaskSourceV4, parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION, } from "./task-source-v4.js";
50
- import { planTaskToV3File } from "./task-to-v3.js";
51
- import { planTaskToV4File } from "./task-to-v4.js";
52
- /** Read the root `version` field without over-accepting non-number values (e.g. the string `"4"`). */
14
+ import { parseTaskSourceV4Document, TASK_SOURCE_V4_VERSION } from "./task-source-v4.js";
53
15
  export function peekTaskSourceVersion(root) {
54
16
  if (root === null || typeof root !== "object" || Array.isArray(root))
55
17
  return undefined;
56
18
  const value = root.version;
57
19
  return typeof value === "number" ? value : undefined;
58
20
  }
59
- const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
60
- /**
61
- * Thrown only when the deterministic conversion itself could not produce a
62
- * task source v4 document — a case where a person must decide the intended
63
- * behavior (e.g. an ambiguous shell command), not one the migrator can just
64
- * be re-run to fix. `reason`/`detail` are the SAME blocked outcome
65
- * `akm migrate status`/`apply` reports for this file, so the message names
66
- * the actual decision instead of pointing at a command that will report the
67
- * identical block.
68
- */
69
- function unmigratableVersionError(filePath, version, reason, detail) {
70
- return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
71
- }
72
21
  function unsupportedVersionError(filePath, version) {
73
- return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
74
- }
75
- /**
76
- * Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
77
- * chained v2->v3->v4) migration planner(s) — never touches disk, never
78
- * writes the file, never re-reads it from disk. Returns the produced v4
79
- * YAML text, or the blocked reason/detail when the deterministic conversion
80
- * cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
81
- * the same hard error this gate threw before the shim existed, now naming
82
- * that reason.
83
- */
84
- function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
85
- const bytes = Buffer.from(yaml, "utf8");
86
- // `writable`/`onDiskWritable` gate the DISK apply path's "don't touch a
87
- // read-only file" check inside the planners; this shim never writes
88
- // anything to disk, so that check does not apply here and must not block
89
- // an otherwise-legal read of a task file that happens to be read-only.
90
- const baseInput = {
91
- filePath,
92
- bytes,
93
- mode: 0o644,
94
- writable: true,
95
- onDiskWritable: true,
96
- ...(workspaceRoot ? { containmentRoot: workspaceRoot } : {}),
97
- };
98
- let v3Bytes;
99
- if (version === 3) {
100
- v3Bytes = bytes;
101
- }
102
- else {
103
- const v3Outcome = planTaskToV3File(baseInput);
104
- if (v3Outcome.status !== "changed")
105
- return { reason: v3Outcome.reason, detail: v3Outcome.detail };
106
- v3Bytes = v3Outcome.after;
107
- }
108
- const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
109
- if (v4Outcome.status !== "changed")
110
- return { reason: v4Outcome.reason, detail: v4Outcome.detail };
111
- return v4Outcome.after.toString("utf8");
22
+ return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Run `akm migrate apply --dry-run`, review the plan, then run `akm migrate apply`.");
112
23
  }
113
- /** Parse task source YAML, routing per the terminal table above. */
114
24
  export function parseTaskSource(input) {
115
25
  const { root, lineAt } = readBoundedTaskSourceYaml(input, { sourceLabel: "task source" });
116
26
  const version = peekTaskSourceVersion(root);
117
27
  if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
118
- if (version === 2 || version === 3) {
119
- const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
120
- if (typeof shimmed === "string") {
121
- const v4 = parseTaskSourceV4({
122
- yaml: shimmed,
123
- filePath: input.filePath,
124
- ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
125
- });
126
- warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
127
- return Object.freeze({ version: 4, v4 });
128
- }
129
- throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
130
- }
131
28
  throw unsupportedVersionError(input.filePath, version);
132
29
  }
133
- const documentOptions = {
30
+ const v4 = parseTaskSourceV4Document(root, {
134
31
  filePath: input.filePath,
135
32
  ...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
136
33
  lineAt,
137
- };
138
- return Object.freeze({ version: 4, v4: parseTaskSourceV4Document(root, documentOptions) });
34
+ });
35
+ return Object.freeze({ version: 4, v4 });
139
36
  }
@@ -4,8 +4,8 @@
4
4
  /**
5
5
  * Map every top-level task source v4 execution control and D2-N7 survivor
6
6
  * into v3's `akm.*` shape, one field at a time (never a whole-object copy,
7
- * so a field task source v4 does not represent — `inputs`, per-binding
8
- * `enabled` — can never leak in by accident). Returns `undefined` when
7
+ * so a field task source v4 does not represent — such as `inputs` — can
8
+ * never leak in by accident). Returns `undefined` when
9
9
  * nothing maps, matching v3's own
10
10
  * convention of omitting the `akm` key entirely rather than emitting an
11
11
  * always-present empty object (`source-v3.ts:789`).
@@ -75,7 +75,7 @@ export const TASK_SOURCE_V4_TOP_LEVEL_KEYS = [
75
75
  "maxRetries",
76
76
  ];
77
77
  /** Closes one `schedule:` list entry (D2-N5). */
78
- export const TASK_SOURCE_V4_SCHEDULE_KEYS = ["cron", "enabled", "inputs"];
78
+ export const TASK_SOURCE_V4_SCHEDULE_KEYS = ["cron", "inputs"];
79
79
  /**
80
80
  * The closed key set for one `inputs.<name>` declaration root (D2-N3). The
81
81
  * JSON-Schema-subset portion is DERIVED from
@@ -479,12 +479,6 @@ function parseScheduleEntry(entryRaw, index, contract, ctx) {
479
479
  sourceError(ctx, [...entryPath, "cron"], "is required.");
480
480
  const cron = stringField(entry.cron, ctx, [...entryPath, "cron"], { nonempty: true });
481
481
  noGithubExpression(cron, ctx, [...entryPath, "cron"]);
482
- let enabled = true;
483
- if (own(entry, "enabled")) {
484
- if (typeof entry.enabled !== "boolean")
485
- sourceError(ctx, [...entryPath, "enabled"], "must be a boolean.");
486
- enabled = entry.enabled;
487
- }
488
482
  let inputsLiteral = Object.freeze({});
489
483
  if (own(entry, "inputs")) {
490
484
  const inputsValue = asRecord(presentJsonValue(entry.inputs, ctx, [...entryPath, "inputs"]), ctx, [
@@ -516,7 +510,7 @@ function parseScheduleEntry(entryRaw, index, contract, ctx) {
516
510
  inputsLiteral = Object.freeze({ ...inputsValue });
517
511
  }
518
512
  checkScheduleEntryRunnable(inputsLiteral, contract, ctx, entryPath);
519
- return Object.freeze({ cron, enabled, inputs: inputsLiteral, source: `schedule[${index}].cron`, ordinal: index });
513
+ return Object.freeze({ cron, inputs: inputsLiteral, source: `schedule[${index}].cron`, ordinal: index });
520
514
  }
521
515
  function parseSchedule(input, contract, ctx) {
522
516
  if (!own(input, "schedule"))
@@ -530,12 +524,10 @@ function parseSchedule(input, contract, ctx) {
530
524
  // runnability contract, at the `schedule` key's own field path (it has
531
525
  // neither an ordinal nor an `inputs:` sub-path to point at).
532
526
  checkScheduleEntryRunnable(Object.freeze({}), contract, ctx, ["schedule"]);
533
- return Object.freeze([
534
- Object.freeze({ cron, enabled: true, inputs: Object.freeze({}), source: "schedule", ordinal: 0 }),
535
- ]);
527
+ return Object.freeze([Object.freeze({ cron, inputs: Object.freeze({}), source: "schedule", ordinal: 0 })]);
536
528
  }
537
529
  if (!Array.isArray(raw) || raw.length === 0) {
538
- sourceError(ctx, ["schedule"], "must be a non-empty string or a non-empty list of {cron, enabled?, inputs?} records.");
530
+ sourceError(ctx, ["schedule"], "must be a non-empty string or a non-empty list of {cron, inputs?} records.");
539
531
  }
540
532
  if (raw.length > TASK_V3_MAX_SCHEDULES) {
541
533
  sourceError(ctx, ["schedule"], `accepts at most ${TASK_V3_MAX_SCHEDULES} entries.`);
@@ -11,7 +11,6 @@ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../wo
11
11
  import { validateTaskId } from "../task-id.js";
12
12
  import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
13
13
  import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
14
- import { parseTaskSourceV4 } from "./task-source-v4.js";
15
14
  const V2_KEYS = new Set([
16
15
  "version",
17
16
  "name",
@@ -442,17 +441,10 @@ export function planTaskToV3File(input) {
442
441
  }
443
442
  }
444
443
  if (data.version === 4) {
445
- try {
446
- parseTaskSourceV4({
447
- yaml: source,
448
- filePath: input.filePath,
449
- ...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
450
- });
451
- return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
452
- }
453
- catch (cause) {
454
- return blocked(input, "invalid-v4-task", cause instanceof Error ? cause.message : String(cause));
455
- }
444
+ // Generation 1 owns v2 only. Do not validate v4 here: generation 2 must
445
+ // be allowed to recognize and remove the retired schedule[].enabled field
446
+ // before the current runtime parser sees those bytes.
447
+ return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
456
448
  }
457
449
  if (data.version !== 2) {
458
450
  return blocked(input, "unsupported-task-version", `expected version 2, 3, or 4, got ${String(data.version)}`);
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import crypto from "node:crypto";
19
19
  import path from "node:path";
20
- import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
20
+ import { isMap, isSeq, LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
21
21
  import { assertBoundedTaskYamlDocument } from "./bounded-document.js";
22
22
  import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
23
23
  import { parseTaskSourceV4 } from "./task-source-v4.js";
@@ -244,7 +244,6 @@ function planV3DataToV4(input, data) {
244
244
  if (!input.writable || input.onDiskWritable === false) {
245
245
  return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
246
246
  }
247
- const enabledFalse = akm !== undefined && akm.enabled === false;
248
247
  let scheduleField;
249
248
  // Several independent translation facts can need reporting on the SAME
250
249
  // file (a manual-only trigger AND a dropped output schema, say), so
@@ -253,7 +252,7 @@ function planV3DataToV4(input, data) {
253
252
  const notices = [];
254
253
  if (hasAkmSchedule) {
255
254
  const cron = akm.schedule;
256
- scheduleField = enabledFalse ? [{ cron, enabled: false }] : cron;
255
+ scheduleField = cron;
257
256
  }
258
257
  else {
259
258
  const rawSchedule = onRecord !== undefined && Object.hasOwn(onRecord, "schedule") ? onRecord.schedule : undefined;
@@ -276,10 +275,7 @@ function planV3DataToV4(input, data) {
276
275
  }
277
276
  crons.push(record.cron);
278
277
  }
279
- scheduleField = crons.map((cron) => (enabledFalse ? { cron, enabled: false } : { cron }));
280
- }
281
- else if (enabledFalse) {
282
- return blocked(input, "enabled-false-has-no-schedule-entry", "akm.enabled: false has no schedule entry to attach to (the only trigger is on.workflow_dispatch); task source v4 has no top-level enabled flag.");
278
+ scheduleField = crons.map((cron) => ({ cron }));
283
279
  }
284
280
  else {
285
281
  notices.push("schedule: is absent from the migrated document — the source's only trigger was on.workflow_dispatch (manual dispatch); task source v4 tasks are always runnable manually via `akm task run`, so no schedule: entry was emitted.");
@@ -368,6 +364,43 @@ export function planTaskToV4File(input) {
368
364
  return blocked(input, "invalid-task-yaml", causeMessage(cause));
369
365
  }
370
366
  if (data.version === 4) {
367
+ const document = parseDocument(source, { uniqueKeys: true });
368
+ const schedule = document.get("schedule", true);
369
+ let removed = false;
370
+ if (isSeq(schedule)) {
371
+ for (const entry of schedule.items) {
372
+ if (!isMap(entry) || !entry.has("enabled"))
373
+ continue;
374
+ entry.delete("enabled");
375
+ removed = true;
376
+ }
377
+ }
378
+ if (removed) {
379
+ if (!input.writable || input.onDiskWritable === false) {
380
+ return blocked(input, "read-only-source", !input.writable
381
+ ? "the owning source is not writable"
382
+ : "the source file or publication directory is read-only");
383
+ }
384
+ const after = Buffer.from(document.toString(), "utf8");
385
+ try {
386
+ parseTaskSourceV4({
387
+ yaml: after.toString("utf8"),
388
+ filePath: input.filePath,
389
+ ...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
390
+ });
391
+ }
392
+ catch (cause) {
393
+ return blocked(input, "generated-v4-validation-failed", causeMessage(cause));
394
+ }
395
+ return Object.freeze({
396
+ status: "changed",
397
+ ...base(input),
398
+ reason: "source-enablement-removed",
399
+ after,
400
+ afterHash: hash(after),
401
+ notice: "Removed source-owned schedule enablement; scheduler activation is now host-local config.",
402
+ });
403
+ }
371
404
  try {
372
405
  parseTaskSourceV4({
373
406
  yaml: source,
@@ -4,6 +4,7 @@ Upgrade guides and per-release migration notes.
4
4
 
5
5
  - [v0.9.1 -> v0.9.2 migration guide](v0.9.1-to-v0.9.2.md) -- Task-v2/task-v3 to task source v4 conversion, the durable-v4-family workflow boundary at executable `irVersion: 5`, and release behavior changes
6
6
  - [v0.9.2 release note](release-notes/0.9.2.md) -- Self-contained terminal upgrade summary shipped for `akm help migrate 0.9.2`
7
+ - [v0.9.16 release note](release-notes/0.9.16.md) -- Source-bound scheduler grants, local execution authority, and split unsafe overrides
7
8
  - [v0.8 -> current v0.9 migration guide](v0.8-to-v0.9.md) -- Package upgrade with fresh current config/state and explicit task conversion
8
9
  - [v0.7 -> v0.8 migration guide](v0.7-to-v0.8.md) -- Task schema and 0.8-era changes
9
10
  - [v0.5 -> v0.6 migration guide](https://github.com/itlackey/akm/blob/main/docs/migration/v0.5-to-v0.6.md) -- Terminology cut, registry schema v3, publisher changes
@@ -0,0 +1,72 @@
1
+ Migration notes for akm v0.9.16
2
+
3
+ Scheduled execution is now authorized by host-local config and bound to the
4
+ specific source installed under a bundle id. Existing
5
+ `scheduler.enabled` entries need a `sourceId` before the 0.9.16 runtime will
6
+ load them. Run this once after upgrading:
7
+
8
+ ```sh
9
+ akm migrate apply
10
+ akm task sync
11
+ ```
12
+
13
+ The migrator binds each existing grant to the bundle source currently in
14
+ config, drops grants whose bundle no longer exists, and writes a config backup
15
+ before changing the file. `akm migrate status` and `akm migrate apply
16
+ --dry-run` report this as a blocking migration because silently trusting a new
17
+ source would defeat the boundary. Task frontmatter cannot enable scheduling.
18
+ Use `akm task enable <bundle//tasks/name>` or `akm task disable ...`; a plain
19
+ `akm task sync` reconciles all enabled bundles and removes installed entries
20
+ for bundles that have since been disabled.
21
+
22
+ Replacing a bundle's source locator or component root invalidates its old
23
+ scheduler grants. Re-enable the reviewed task after the replacement. Normal
24
+ content updates, adapter detection, and website crawl-policy changes keep the
25
+ same source identity.
26
+ Removing a bundle revokes all grants owned by that bundle. A bundle with
27
+ `enabled: false` is inert for content reads, writes, indexing, execution, and
28
+ scheduling, and it cannot remain `defaultBundle` or `defaultWriteTarget`.
29
+ Explicit lifecycle operations such as `akm bundle update <name>` may still
30
+ refresh a disabled bundle without activating its content.
31
+
32
+ Two bundle ids may no longer point at the same physical content root, including
33
+ through symbolic links. This alias is not safe to rewrite automatically because
34
+ durable refs name the bundle id. Keep the id whose refs should survive and
35
+ remove the duplicate config entry; config errors name both ids and the shared
36
+ root.
37
+
38
+ Config inheritance now has a portable-data boundary. An inherited config may
39
+ supply ordinary portable settings, including engine endpoint and model fields,
40
+ but host authority is always local. Inherited bundle/default declarations,
41
+ scheduler grants, execution policy, credentials, executable paths/arguments,
42
+ setup state, registry declarations, and write-capable strategy hooks are
43
+ ignored with a warning. Bundle-relative `extends` paths are checked against
44
+ the bundle's physical root, including every link in an extends chain.
45
+
46
+ Commands and personas can no longer set `workspace`, `environment`, or
47
+ `runtime` in frontmatter. Those values select host execution context and now
48
+ produce a configuration error. Asset-requested tools are constrained by the
49
+ local allowlist:
50
+
51
+ ```json
52
+ {
53
+ "execution": {
54
+ "allowedTools": ["read_file", "search"]
55
+ }
56
+ }
57
+ ```
58
+
59
+ Use `"*"` only when every asset the host may execute is trusted. If an asset
60
+ requests tools that the selected transport cannot enforce, akm fails before
61
+ dispatch instead of sending an over-privileged request.
62
+
63
+ The old combined `--allow-insecure` switch has been removed. The two unrelated
64
+ decisions are now explicit:
65
+
66
+ - `--allow-insecure-transport` permits a reviewed plain-HTTP bundle or
67
+ registry endpoint.
68
+ - `--allow-dangerous-env-keys` permits reviewed dangerous environment-key
69
+ findings during bundle add/update or env activation.
70
+
71
+ Update scripts to use the narrow flag that matches the risk being accepted.
72
+ Neither flag implies the other.
@@ -104,7 +104,6 @@ with:
104
104
  content: Review the execution contract.
105
105
  schedule:
106
106
  - cron: "15 4 * * 1"
107
- enabled: false
108
107
  engine: reviewer
109
108
  model: exact-model-id
110
109
  timeout: 45000
@@ -134,12 +133,11 @@ earlier 0.9.x releases and are summarized further down):
134
133
  parses, runs with `akm task run`, and is silently skipped by
135
134
  `akm task sync` (zero bindings, zero failures) — it never has to declare
136
135
  a trigger just to be a valid document.
137
- - **`akm.enabled` becomes per-schedule-binding.** v3's single
138
- document-level `enabled: false` becomes that schedule entry's own
139
- `enabled: false` in v4 — there is no longer a document-level flag at all.
140
- A disabled task with no cron trigger (only `on.workflow_dispatch`) has no
141
- v4 representation and is `blocked` for manual review (see
142
- [The migration procedure](#the-migration-procedure)).
136
+ - **Source-owned enablement is removed.** Current task source v4 carries no
137
+ `enabled` field. `akm-migrate` removes v2/v3/v4 source flags and seeds the
138
+ host-local `scheduler.enabled` allow-list only from native scheduler
139
+ bindings it can prove are currently enabled. A source cannot activate
140
+ itself merely by being installed from a bundle.
143
141
  - **Typed `inputs:` with defaults and `required:`.** v4 tasks can declare
144
142
  named, bounded-JSON-Schema parameters, each optionally carrying a
145
143
  `default` or `required: true` (mutually exclusive). `akm task run`
@@ -224,7 +222,7 @@ by hand using this field mapping:
224
222
  |---|---|
225
223
  | `command:` (array, argv style) | `run:` (string) plus `shell:` |
226
224
  | `timeoutMs:` | `timeout:` |
227
- | `enabled:` (document level) | removed — use `schedule:` as a list of `{cron, enabled}` entries |
225
+ | `enabled:` (document level) | removed — use host-local `scheduler.enabled` (`akm task enable` / `disable`) |
228
226
  | `schedule:` (cron string) | still accepted as a bare string, or as the list form above |
229
227
 
230
228
  validate it, and rerun the preview. You can either write the replacement
@@ -248,7 +246,6 @@ Common blocked reasons and what to do about each:
248
246
  | `github-action-target-removed` | The task's `uses:` is a GitHub Action locator (`owner/repo[/path]@ref`); that spelling has no task source v4 equivalent. | Rewrite the target as `commands/`, `scripts/`, `workflows/`, or `akm/command` by hand. |
249
247
  | `with-on-non-command-target` | A `with:` block is authored on a target other than `uses: akm/command`. | Declare `inputs:` on the task instead; a workflow step composing it binds them with its own `with:`. |
250
248
  | `ambiguous-scheduling-source` | The document declares both `akm.schedule` and `on:`. | Pick one; the migrator will not guess which one wins. |
251
- | `enabled-false-has-no-schedule-entry` | `akm.enabled: false` with no cron trigger to attach it to (the only trigger is `on.workflow_dispatch`). | Task source v4 has no document-level `enabled` flag — decide whether the task should be scheduled (add a cron) or left manual-only (drop `akm.enabled`), then re-run. |
252
249
  | `read-only-source` | The owning source or file is not writable. | Move or re-source the file somewhere writable, or edit it by hand. |
253
250
  | `invalid-v3-task` | The v3 document itself is structurally invalid (unknown fields, missing selector, malformed trigger, etc). | Fix the underlying v3 document first — the migrator translates structure, it does not repair it. |
254
251