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.
- package/CHANGELOG.md +52 -0
- package/dist/assets/hints/cli-hints-full.md +13 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +3 -6
- package/dist/cli/retired-commands.js +0 -2
- package/dist/commands/env/env-binding.js +4 -4
- package/dist/commands/env/env-cli.js +3 -3
- package/dist/commands/improve/improve-cli.js +19 -14
- package/dist/commands/improve/reflect.js +23 -2
- package/dist/commands/lint/base-linter.js +9 -0
- package/dist/commands/lint/env-key-rules.js +2 -2
- package/dist/commands/proposal/propose.js +15 -1
- package/dist/commands/proposal/validators/proposal-quality-validators.js +40 -3
- package/dist/commands/proposal/validators/proposal-validators.js +5 -4
- package/dist/commands/read/search.js +33 -4
- package/dist/commands/read/show.js +21 -2
- package/dist/commands/registry-cli.js +5 -5
- package/dist/commands/sources/add-cli.js +59 -16
- package/dist/commands/sources/bundle-cli.js +35 -11
- package/dist/commands/sources/bundle-config-ops.js +30 -0
- package/dist/commands/sources/dangerous-env-audit.js +4 -4
- package/dist/commands/sources/installed-stashes.js +43 -28
- package/dist/commands/sources/source-add.js +33 -17
- package/dist/commands/sources/source-manage.js +34 -12
- package/dist/commands/sources/stash-skeleton.js +6 -3
- package/dist/commands/tasks/explain.js +4 -1
- package/dist/commands/tasks/tasks-cli.js +31 -9
- package/dist/commands/tasks/tasks.js +239 -194
- package/dist/commands/tasks/validate.js +20 -32
- package/dist/core/activation-policy.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +5 -0
- package/dist/core/adapter/execution-source.js +10 -29
- package/dist/core/config/config-schema.js +64 -8
- package/dist/core/config/config-sources.js +96 -2
- package/dist/core/config/config.js +190 -24
- package/dist/core/config/legacy-source-shape-shim.js +9 -0
- package/dist/core/config/schema/execution.js +23 -0
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/scheduler.js +20 -0
- package/dist/core/config/schema/search.js +1 -1
- package/dist/core/config/schema/sources-bundles.js +32 -1
- package/dist/core/content-safety.js +52 -0
- package/dist/core/maintenance-barrier.js +6 -6
- package/dist/core/type-presentation.js +1 -1
- package/dist/core/write-source.js +13 -8
- package/dist/indexer/bundle-identity-guard.js +45 -8
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/materialize-embeddings.js +15 -1
- package/dist/indexer/search/search-source.js +29 -11
- package/dist/integrations/agent/execution-lowering.js +3 -2
- package/dist/integrations/agent/execution-preparation.js +32 -1
- package/dist/integrations/agent/prompts.js +1 -1
- package/dist/integrations/agent/request-lowering.js +3 -2
- package/dist/llm/client.js +2 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/registry/resolve.js +37 -10
- package/dist/scripts/akm-migrate-node.js +13644 -9894
- package/dist/scripts/akm-migrate.js +12626 -8876
- package/dist/setup/setup.js +3 -3
- package/dist/setup/steps/tasks.js +29 -36
- package/dist/sources/providers/git-install.js +17 -11
- package/dist/sources/providers/git-provider.js +12 -5
- package/dist/sources/providers/git-stash.js +38 -16
- package/dist/sources/snapshot-fetchers/website-ingest.js +3 -3
- package/dist/storage/repositories/index-vec-repository.js +100 -0
- package/dist/tasks/activation-config.js +90 -0
- package/dist/tasks/backends/cron.js +9 -0
- package/dist/tasks/backends/launchd.js +1 -0
- package/dist/tasks/backends/schtasks.js +2 -0
- package/dist/tasks/embedded.js +4 -5
- package/dist/tasks/scheduler-binding.js +2 -2
- package/dist/tasks/scheduler-sync-preview.js +8 -1
- package/dist/tasks/scheduler-sync.js +19 -10
- package/dist/tasks/source/parse-task-source.js +10 -113
- package/dist/tasks/source/project-v4.js +2 -2
- package/dist/tasks/source/task-source-v4.js +4 -12
- package/dist/tasks/source/task-to-v3.js +4 -12
- package/dist/tasks/source/task-to-v4.js +40 -7
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.16.md +72 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +6 -9
- package/docs/reference/cli.md +37 -29
- package/docs/reference/configuration.md +48 -5
- package/docs/reference/tasks.md +34 -29
- package/package.json +1 -1
- package/schemas/akm-config.json +112 -4
- package/schemas/akm-task.json +1 -2
package/dist/tasks/embedded.js
CHANGED
|
@@ -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
|
-
//
|
|
76
|
-
//
|
|
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:
|
|
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:
|
|
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(
|
|
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({
|
|
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
|
-
|
|
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
|
|
312
|
-
// `
|
|
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
|
-
*
|
|
6
|
-
* §3.2.2).
|
|
5
|
+
* Current task-source router.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 {
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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", "
|
|
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,
|
|
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,
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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 =
|
|
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) => (
|
|
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,
|
package/docs/migration/README.md
CHANGED
|
@@ -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
|
-
-
|
|
138
|
-
|
|
139
|
-
`enabled
|
|
140
|
-
|
|
141
|
-
|
|
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 `
|
|
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
|
|