akm-cli 0.9.3 → 0.9.4
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 +73 -1
- package/README.md +1 -1
- package/SECURITY.md +1 -1
- package/STABILITY.md +1 -1
- package/dist/akm +2 -2
- package/dist/akm-migrate +2 -2
- package/dist/cli.js +5 -5
- package/dist/commands/health/improve-metrics.js +17 -0
- package/dist/commands/health/windows.js +2 -2
- package/dist/commands/health.js +2 -2
- package/dist/commands/improve/preparation.js +8 -1
- package/dist/commands/lint/index.js +3 -7
- package/dist/commands/tasks/tasks-cli.js +28 -3
- package/dist/commands/tasks/tasks.js +29 -1
- package/dist/core/adapter/adapters/akm-adapter.js +21 -14
- package/dist/core/adapter/adapters/akm-lint.js +3 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +9 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +13 -11
- package/dist/core/adapter/recognize-match.js +1 -20
- package/dist/core/asset/asset-placement.js +21 -2
- package/dist/core/common.js +21 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +6 -89
- package/dist/indexer/search/db-search.js +6 -0
- package/dist/indexer/walk/matchers.js +0 -22
- package/dist/output/shapes/helpers.js +19 -1
- package/dist/output/shapes/passthrough.js +1 -0
- package/dist/output/text/command-format.js +4 -0
- package/dist/registry/pinned-request-helper.js +2 -2
- package/dist/registry/pinned-transport.js +6 -6
- package/dist/scripts/akm-migrate-node.js +12624 -12555
- package/dist/scripts/akm-migrate.js +12624 -12555
- package/dist/storage/repositories/proposals-repository.js +33 -1
- package/dist/storage/repositories/task-history-repository.js +22 -10
- package/dist/tasks/run/task-history.js +23 -3
- package/dist/tasks/scheduler-sync-preview.js +43 -0
- package/dist/tasks/scheduler-sync.js +1 -0
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/parse-task-source.js +77 -11
- package/dist/tasks/source/task-source-v3-frozen.js +428 -0
- package/dist/tasks/source/task-to-v3.js +500 -0
- package/dist/tasks/source/task-to-v4.js +467 -0
- package/docs/reference/cli.md +8 -3
- package/package.json +4 -4
|
@@ -0,0 +1,467 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Pure, byte-producing task-v3 to task-source-v4 migration planner (spec
|
|
6
|
+
* docs/plans/specs/p2b-input-bindings.md §1.3, §1.7 C-N1, §5). Mirrors
|
|
7
|
+
* `task-to-v3.ts`'s fail-closed ladder exactly: the INPUT side is read as a
|
|
8
|
+
* raw record by a vendored bounded-YAML reader (never the typed
|
|
9
|
+
* `parseTaskV3Yaml`, which would normalize away exactly the value bytes this
|
|
10
|
+
* migrator must preserve — a duration string like "5m", or a bare numeric
|
|
11
|
+
* `timeout`, would be converted to milliseconds by the real parser). The
|
|
12
|
+
* OUTPUT side is validated through the REAL `parseTaskSourceV4` before a
|
|
13
|
+
* "changed" outcome is ever handed back (C-N1, B-71).
|
|
14
|
+
*
|
|
15
|
+
* `inputs:` is never invented — the migrator translates structure, not
|
|
16
|
+
* intent (spec §5.3).
|
|
17
|
+
*/
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
21
|
+
import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
|
|
22
|
+
import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
|
|
23
|
+
import { parseTaskSourceV4 } from "./task-source-v4.js";
|
|
24
|
+
/** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
|
|
25
|
+
const V3_TOP_LEVEL_KEYS = new Set([
|
|
26
|
+
"version",
|
|
27
|
+
"name",
|
|
28
|
+
"uses",
|
|
29
|
+
"run",
|
|
30
|
+
"with",
|
|
31
|
+
"env",
|
|
32
|
+
"shell",
|
|
33
|
+
"working-directory",
|
|
34
|
+
"akm",
|
|
35
|
+
"on",
|
|
36
|
+
]);
|
|
37
|
+
/** The closed v3 `akm.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
38
|
+
const V3_AKM_KEYS = new Set([
|
|
39
|
+
"schedule",
|
|
40
|
+
"enabled",
|
|
41
|
+
"description",
|
|
42
|
+
"when_to_use",
|
|
43
|
+
"tags",
|
|
44
|
+
"agent",
|
|
45
|
+
"engine",
|
|
46
|
+
"model",
|
|
47
|
+
"inference",
|
|
48
|
+
"outputSchema",
|
|
49
|
+
"tools",
|
|
50
|
+
"timeout",
|
|
51
|
+
"redact",
|
|
52
|
+
"maxSteps",
|
|
53
|
+
"maxRetries",
|
|
54
|
+
]);
|
|
55
|
+
/** The closed v3 `on.*` key set, vendored from `src/tasks/source-v3.ts`. */
|
|
56
|
+
const V3_ON_KEYS = new Set(["schedule", "workflow_dispatch"]);
|
|
57
|
+
/** `akm.*` keys hoisted verbatim to the identical top-level v4 key (schedule/enabled handled separately). */
|
|
58
|
+
const AKM_HOIST_KEYS = [
|
|
59
|
+
"description",
|
|
60
|
+
"when_to_use",
|
|
61
|
+
"tags",
|
|
62
|
+
"agent",
|
|
63
|
+
"engine",
|
|
64
|
+
"model",
|
|
65
|
+
"inference",
|
|
66
|
+
"tools",
|
|
67
|
+
"timeout",
|
|
68
|
+
"redact",
|
|
69
|
+
"maxSteps",
|
|
70
|
+
"maxRetries",
|
|
71
|
+
];
|
|
72
|
+
function hash(bytes) {
|
|
73
|
+
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function causeMessage(cause) {
|
|
76
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
77
|
+
}
|
|
78
|
+
function base(input) {
|
|
79
|
+
const inspectionIdentity = input.inspectionIdentity
|
|
80
|
+
? Object.freeze({
|
|
81
|
+
file: Object.freeze({ ...input.inspectionIdentity.file }),
|
|
82
|
+
root: Object.freeze({ ...input.inspectionIdentity.root }),
|
|
83
|
+
...(input.inspectionIdentity.bundleRoot
|
|
84
|
+
? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
|
|
85
|
+
: {}),
|
|
86
|
+
})
|
|
87
|
+
: undefined;
|
|
88
|
+
return {
|
|
89
|
+
filePath: input.filePath,
|
|
90
|
+
before: Buffer.from(input.bytes),
|
|
91
|
+
beforeHash: hash(input.bytes),
|
|
92
|
+
mode: input.mode,
|
|
93
|
+
writable: input.writable,
|
|
94
|
+
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
95
|
+
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
96
|
+
...(inspectionIdentity ? { inspectionIdentity } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function blocked(input, reason, detail) {
|
|
100
|
+
return Object.freeze({ status: "blocked", ...base(input), reason, ...(detail ? { detail } : {}) });
|
|
101
|
+
}
|
|
102
|
+
function plainRecord(value, label) {
|
|
103
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
104
|
+
throw new Error(`${label} must be a mapping`);
|
|
105
|
+
}
|
|
106
|
+
const prototype = Object.getPrototypeOf(value);
|
|
107
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
108
|
+
throw new Error(`${label} must use a plain or null prototype`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
function exactString(value, label, nonempty = false) {
|
|
113
|
+
if (typeof value !== "string" || (nonempty && value.trim().length === 0)) {
|
|
114
|
+
throw new Error(`${label} must be ${nonempty ? "a non-empty " : "a "}string`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Vendored raw-record reader (mirrors `task-to-v3.ts`'s `parseLegacyTaskYaml`
|
|
120
|
+
* exactly). Reading the RAW decoded record — rather than the typed
|
|
121
|
+
* `parseTaskV3Yaml` — keeps every field's original value bytes (a duration
|
|
122
|
+
* string, a bare millisecond integer, an env value's exact type) intact for
|
|
123
|
+
* verbatim re-emission; a typed v3 parse would normalize several of these
|
|
124
|
+
* away (C-N1).
|
|
125
|
+
*/
|
|
126
|
+
function parseV3RawYaml(input) {
|
|
127
|
+
if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
|
|
128
|
+
throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
|
|
129
|
+
}
|
|
130
|
+
let source;
|
|
131
|
+
try {
|
|
132
|
+
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
throw new Error("task YAML contains invalid UTF-8 bytes");
|
|
136
|
+
}
|
|
137
|
+
const lineCounter = new LineCounter();
|
|
138
|
+
let document;
|
|
139
|
+
try {
|
|
140
|
+
document = parseDocument(source, { lineCounter, uniqueKeys: true });
|
|
141
|
+
}
|
|
142
|
+
catch (cause) {
|
|
143
|
+
throw new Error(`invalid YAML: ${causeMessage(cause)}`);
|
|
144
|
+
}
|
|
145
|
+
const [parseError] = document.errors;
|
|
146
|
+
if (parseError)
|
|
147
|
+
throw new Error(`invalid YAML: ${parseError.message.split("\n")[0]}`);
|
|
148
|
+
const [parseWarning] = document.warnings;
|
|
149
|
+
if (parseWarning)
|
|
150
|
+
throw new Error(`unsupported YAML construct: ${parseWarning.message}`);
|
|
151
|
+
assertBoundedTaskYamlDocument(document, {
|
|
152
|
+
filePath: input.filePath,
|
|
153
|
+
sourceLabel: "task v3 migration source",
|
|
154
|
+
lineCounter,
|
|
155
|
+
});
|
|
156
|
+
return { data: plainRecord(document.toJS({ maxAliasCount: 0 }), "task YAML"), source };
|
|
157
|
+
}
|
|
158
|
+
/** Convert one already-validated v3 raw record to final task source v4 bytes. */
|
|
159
|
+
function planV3DataToV4(input, data) {
|
|
160
|
+
const unknownTop = Object.keys(data).filter((key) => !V3_TOP_LEVEL_KEYS.has(key));
|
|
161
|
+
if (unknownTop.length > 0) {
|
|
162
|
+
return blocked(input, "invalid-v3-task", `unknown v3 field(s): ${unknownTop.join(", ")}`);
|
|
163
|
+
}
|
|
164
|
+
const hasUses = Object.hasOwn(data, "uses");
|
|
165
|
+
const hasRun = Object.hasOwn(data, "run");
|
|
166
|
+
if (hasUses === hasRun) {
|
|
167
|
+
return blocked(input, "invalid-v3-task", "requires exactly one executable selector: uses or run");
|
|
168
|
+
}
|
|
169
|
+
let akm;
|
|
170
|
+
if (Object.hasOwn(data, "akm")) {
|
|
171
|
+
try {
|
|
172
|
+
akm = plainRecord(data.akm, "akm");
|
|
173
|
+
}
|
|
174
|
+
catch (cause) {
|
|
175
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
176
|
+
}
|
|
177
|
+
const unknownAkm = Object.keys(akm).filter((key) => !V3_AKM_KEYS.has(key));
|
|
178
|
+
if (unknownAkm.length > 0) {
|
|
179
|
+
return blocked(input, "unrecognized-akm-member", `akm has unknown field(s): ${unknownAkm.join(", ")}`);
|
|
180
|
+
}
|
|
181
|
+
if (Object.hasOwn(akm, "enabled") && typeof akm.enabled !== "boolean") {
|
|
182
|
+
return blocked(input, "invalid-v3-task", "akm.enabled must be a boolean");
|
|
183
|
+
}
|
|
184
|
+
if (Object.hasOwn(akm, "schedule") && typeof akm.schedule !== "string") {
|
|
185
|
+
return blocked(input, "invalid-v3-task", "akm.schedule must be a string");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const hasOn = Object.hasOwn(data, "on");
|
|
189
|
+
let onRecord;
|
|
190
|
+
if (hasOn) {
|
|
191
|
+
try {
|
|
192
|
+
onRecord = plainRecord(data.on, "on");
|
|
193
|
+
}
|
|
194
|
+
catch (cause) {
|
|
195
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
196
|
+
}
|
|
197
|
+
// Mirrors the frozen v3 reader's own `parseOn` gates exactly
|
|
198
|
+
// (task-source-v3-frozen.ts:395-427): an empty `on: {}` declares no
|
|
199
|
+
// trigger at all, and `on.workflow_dispatch` accepts only null or an
|
|
200
|
+
// empty mapping (inputs are unsupported in v3). The frozen parser
|
|
201
|
+
// rejects both shapes outright, so this migrator must too — translating
|
|
202
|
+
// a document the v3 oracle would refuse to parse into runnable v4 bytes
|
|
203
|
+
// would launder invalid input into a valid, schedule-less task.
|
|
204
|
+
if (Object.keys(onRecord).length === 0) {
|
|
205
|
+
return blocked(input, "invalid-v3-task", "on must declare schedule and/or workflow_dispatch.");
|
|
206
|
+
}
|
|
207
|
+
const unknownOn = Object.keys(onRecord).filter((key) => !V3_ON_KEYS.has(key));
|
|
208
|
+
if (unknownOn.length > 0) {
|
|
209
|
+
return blocked(input, "invalid-v3-task", `on has unknown field(s): ${unknownOn.join(", ")}`);
|
|
210
|
+
}
|
|
211
|
+
if (Object.hasOwn(onRecord, "workflow_dispatch") && onRecord.workflow_dispatch !== null) {
|
|
212
|
+
let dispatchMapping;
|
|
213
|
+
try {
|
|
214
|
+
dispatchMapping = plainRecord(onRecord.workflow_dispatch, "on.workflow_dispatch");
|
|
215
|
+
}
|
|
216
|
+
catch (cause) {
|
|
217
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
218
|
+
}
|
|
219
|
+
if (Object.keys(dispatchMapping).length > 0) {
|
|
220
|
+
return blocked(input, "invalid-v3-task", "on.workflow_dispatch must be null or an empty mapping; inputs are unsupported.");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const hasAkmSchedule = akm !== undefined && Object.hasOwn(akm, "schedule");
|
|
225
|
+
if (hasAkmSchedule && hasOn) {
|
|
226
|
+
return blocked(input, "ambiguous-scheduling-source", "declares both akm.schedule and on:; task v3 requires exactly one scheduling source and the migrator will not guess which one wins.");
|
|
227
|
+
}
|
|
228
|
+
if (!hasAkmSchedule && !hasOn) {
|
|
229
|
+
return blocked(input, "invalid-v3-task", "requires exactly one scheduling source: akm.schedule or on.");
|
|
230
|
+
}
|
|
231
|
+
const hasWith = Object.hasOwn(data, "with");
|
|
232
|
+
let usesTarget;
|
|
233
|
+
if (hasUses) {
|
|
234
|
+
let usesValue;
|
|
235
|
+
try {
|
|
236
|
+
usesValue = exactString(data.uses, "uses", true);
|
|
237
|
+
}
|
|
238
|
+
catch (cause) {
|
|
239
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
usesTarget = classifyTaskV3Uses(usesValue);
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
246
|
+
}
|
|
247
|
+
if (usesTarget.kind === "github-action") {
|
|
248
|
+
return blocked(input, "github-action-target-removed", `"${usesValue}" is a github-action target; the github-action uses: variant was removed in task source v4. Use commands/, scripts/, workflows/, or akm/command instead.`);
|
|
249
|
+
}
|
|
250
|
+
if (hasWith && usesTarget.kind !== "builtin-command") {
|
|
251
|
+
return blocked(input, "with-on-non-command-target", `a with: block on "${usesValue}" (a non-akm/command target) has no task source v4 equivalent; task-call inputs are declared and bound separately.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else if (hasWith) {
|
|
255
|
+
return blocked(input, "invalid-v3-task", "with is legal only with uses");
|
|
256
|
+
}
|
|
257
|
+
if (!input.writable || input.onDiskWritable === false) {
|
|
258
|
+
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
259
|
+
}
|
|
260
|
+
const enabledFalse = akm !== undefined && akm.enabled === false;
|
|
261
|
+
let scheduleField;
|
|
262
|
+
// Several independent translation facts can need reporting on the SAME
|
|
263
|
+
// file (a manual-only trigger AND a dropped output schema, say), so
|
|
264
|
+
// notices accumulate and are joined into the single `notice` string the
|
|
265
|
+
// outcome carries.
|
|
266
|
+
const notices = [];
|
|
267
|
+
if (hasAkmSchedule) {
|
|
268
|
+
const cron = akm.schedule;
|
|
269
|
+
scheduleField = enabledFalse ? [{ cron, enabled: false }] : cron;
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const rawSchedule = onRecord !== undefined && Object.hasOwn(onRecord, "schedule") ? onRecord.schedule : undefined;
|
|
273
|
+
if (rawSchedule !== undefined) {
|
|
274
|
+
if (!Array.isArray(rawSchedule) || rawSchedule.length === 0) {
|
|
275
|
+
return blocked(input, "invalid-v3-task", "on.schedule must be a non-empty list of {cron} records");
|
|
276
|
+
}
|
|
277
|
+
const crons = [];
|
|
278
|
+
for (const entry of rawSchedule) {
|
|
279
|
+
let record;
|
|
280
|
+
try {
|
|
281
|
+
record = plainRecord(entry, "on.schedule[]");
|
|
282
|
+
}
|
|
283
|
+
catch (cause) {
|
|
284
|
+
return blocked(input, "invalid-v3-task", causeMessage(cause));
|
|
285
|
+
}
|
|
286
|
+
const keys = Object.keys(record);
|
|
287
|
+
if (keys.length !== 1 || keys[0] !== "cron" || typeof record.cron !== "string" || record.cron.length === 0) {
|
|
288
|
+
return blocked(input, "invalid-v3-task", "each on.schedule entry must be exactly {cron: <non-empty string>}");
|
|
289
|
+
}
|
|
290
|
+
crons.push(record.cron);
|
|
291
|
+
}
|
|
292
|
+
scheduleField = crons.map((cron) => (enabledFalse ? { cron, enabled: false } : { cron }));
|
|
293
|
+
}
|
|
294
|
+
else if (enabledFalse) {
|
|
295
|
+
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.");
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
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.");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const out = { version: 4 };
|
|
302
|
+
if (Object.hasOwn(data, "name"))
|
|
303
|
+
out.name = data.name;
|
|
304
|
+
if (hasUses)
|
|
305
|
+
out.uses = data.uses;
|
|
306
|
+
else
|
|
307
|
+
out.run = data.run;
|
|
308
|
+
if (Object.hasOwn(data, "shell"))
|
|
309
|
+
out.shell = data.shell;
|
|
310
|
+
if (hasWith)
|
|
311
|
+
out.with = data.with;
|
|
312
|
+
if (Object.hasOwn(data, "env"))
|
|
313
|
+
out.env = data.env;
|
|
314
|
+
if (Object.hasOwn(data, "working-directory"))
|
|
315
|
+
out["working-directory"] = data["working-directory"];
|
|
316
|
+
if (scheduleField !== undefined)
|
|
317
|
+
out.schedule = scheduleField;
|
|
318
|
+
if (akm) {
|
|
319
|
+
for (const key of AKM_HOIST_KEYS) {
|
|
320
|
+
if (Object.hasOwn(akm, key))
|
|
321
|
+
out[key] = akm[key];
|
|
322
|
+
}
|
|
323
|
+
// v3's `akm.outputSchema: null` means "no schema" (accepted verbatim by
|
|
324
|
+
// the frozen v3 reader, task-source-v3-frozen.ts:256-258); v4's
|
|
325
|
+
// `output:` has no null form (parseOutputSchema always requires a
|
|
326
|
+
// mapping). Omitting the key is the faithful v4 equivalent of an
|
|
327
|
+
// explicit v3 null — emitting `output: null` would fail the real
|
|
328
|
+
// parseTaskSourceV4 validation below and block the whole file.
|
|
329
|
+
if (Object.hasOwn(akm, "outputSchema") && akm.outputSchema !== null) {
|
|
330
|
+
// v4 accepts `output:` ONLY on a command target — `uses: commands/<ref>`
|
|
331
|
+
// or `uses: akm/command` (src/tasks/source/task-source-v4.ts's
|
|
332
|
+
// `targetConsumesOutputSchema`). v3 enforced no such rule: the frozen v3
|
|
333
|
+
// reader accepts `akm.outputSchema` on ANY target kind
|
|
334
|
+
// (task-source-v3-frozen.ts:256-263), and on `run:`/`uses: scripts/`/
|
|
335
|
+
// `uses: workflows/` it was equally inert there — nothing ever consumed
|
|
336
|
+
// it. Hoisting it unconditionally would therefore emit bytes the real
|
|
337
|
+
// parseTaskSourceV4 below rejects, blocking a valid, previously-runnable
|
|
338
|
+
// v3 file — and one blocked file aborts the whole plan
|
|
339
|
+
// (`applyTaskToV4MigrationPlan`, ./task-files-to-v4.ts). Dropping an
|
|
340
|
+
// already-inert field and SAYING SO is the faithful translation, and
|
|
341
|
+
// keeps spec row B-66 / §5.3's `changed` guarantee intact.
|
|
342
|
+
if (usesTarget !== undefined && (usesTarget.kind === "command" || usesTarget.kind === "builtin-command")) {
|
|
343
|
+
out.output = akm.outputSchema;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
const targetLabel = usesTarget === undefined ? "a run: target" : `the "${usesTarget.ref}" target`;
|
|
347
|
+
notices.push(`akm.outputSchema was dropped rather than hoisted to output: — task source v4 accepts output: only with a command target (uses: commands/<ref> or uses: akm/command), and ${targetLabel} never consumed the schema in v3 either, so nothing enforceable was lost.`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const afterYaml = stringifyYaml(out);
|
|
352
|
+
const after = Buffer.from(afterYaml, "utf8");
|
|
353
|
+
try {
|
|
354
|
+
parseTaskSourceV4({
|
|
355
|
+
yaml: afterYaml,
|
|
356
|
+
filePath: input.filePath,
|
|
357
|
+
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
catch (cause) {
|
|
361
|
+
return blocked(input, "generated-v4-validation-failed", causeMessage(cause));
|
|
362
|
+
}
|
|
363
|
+
const notice = notices.join(" ");
|
|
364
|
+
return Object.freeze({
|
|
365
|
+
status: "changed",
|
|
366
|
+
...base(input),
|
|
367
|
+
reason: "task-converted",
|
|
368
|
+
after,
|
|
369
|
+
afterHash: hash(after),
|
|
370
|
+
...(notice ? { notice } : {}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
/** Plan exactly one source file without touching disk. */
|
|
374
|
+
export function planTaskToV4File(input) {
|
|
375
|
+
let data;
|
|
376
|
+
let source;
|
|
377
|
+
try {
|
|
378
|
+
({ data, source } = parseV3RawYaml(input));
|
|
379
|
+
}
|
|
380
|
+
catch (cause) {
|
|
381
|
+
return blocked(input, "invalid-task-yaml", causeMessage(cause));
|
|
382
|
+
}
|
|
383
|
+
if (data.version === 4) {
|
|
384
|
+
try {
|
|
385
|
+
parseTaskSourceV4({
|
|
386
|
+
yaml: source,
|
|
387
|
+
filePath: input.filePath,
|
|
388
|
+
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
389
|
+
});
|
|
390
|
+
return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
|
|
391
|
+
}
|
|
392
|
+
catch (cause) {
|
|
393
|
+
return blocked(input, "invalid-v4-task", causeMessage(cause));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Not this generation's document to validate — v2 grammar is entirely
|
|
397
|
+
// generation 1's domain (task-to-v3.ts). Reported skipped, not blocked
|
|
398
|
+
// (see TaskToV4Skipped's own header).
|
|
399
|
+
if (data.version === 2) {
|
|
400
|
+
return Object.freeze({
|
|
401
|
+
status: "skipped",
|
|
402
|
+
...base(input),
|
|
403
|
+
reason: "pending-v2-to-v3-migration",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
if (data.version !== 3) {
|
|
407
|
+
return blocked(input, "unsupported-task-version", `expected version 2, 3, or 4, got ${String(data.version)}`);
|
|
408
|
+
}
|
|
409
|
+
return planV3DataToV4(input, data);
|
|
410
|
+
}
|
|
411
|
+
function generationFor(files) {
|
|
412
|
+
const digest = crypto.createHash("sha256");
|
|
413
|
+
digest.update("akm-task-to-v4-plan-v1\0");
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
digest.update(file.filePath);
|
|
416
|
+
digest.update("\0");
|
|
417
|
+
digest.update(file.status);
|
|
418
|
+
digest.update("\0");
|
|
419
|
+
digest.update(file.reason);
|
|
420
|
+
digest.update("\0");
|
|
421
|
+
digest.update(String(file.mode));
|
|
422
|
+
digest.update("\0");
|
|
423
|
+
digest.update(file.writable ? "writable" : "read-only");
|
|
424
|
+
digest.update("\0");
|
|
425
|
+
digest.update(file.onDiskWritable === false ? "disk-read-only" : "disk-writable-or-unspecified");
|
|
426
|
+
digest.update("\0");
|
|
427
|
+
if (file.containmentRoot)
|
|
428
|
+
digest.update(file.containmentRoot);
|
|
429
|
+
digest.update("\0");
|
|
430
|
+
digest.update(file.beforeHash);
|
|
431
|
+
digest.update("\0");
|
|
432
|
+
if (file.status === "changed")
|
|
433
|
+
digest.update(file.afterHash);
|
|
434
|
+
digest.update("\0");
|
|
435
|
+
if (file.detail)
|
|
436
|
+
digest.update(file.detail);
|
|
437
|
+
digest.update("\0");
|
|
438
|
+
if (file.status === "changed" && file.notice)
|
|
439
|
+
digest.update(file.notice);
|
|
440
|
+
digest.update("\0");
|
|
441
|
+
}
|
|
442
|
+
return digest.digest("hex");
|
|
443
|
+
}
|
|
444
|
+
/** Build/fingerprint a plan from already-derived immutable outcomes. */
|
|
445
|
+
export function taskToV4PlanFromOutcomes(outcomes) {
|
|
446
|
+
const files = [...outcomes].sort((left, right) => left.filePath < right.filePath ? -1 : left.filePath > right.filePath ? 1 : 0);
|
|
447
|
+
for (let index = 1; index < files.length; index += 1) {
|
|
448
|
+
const previous = files[index - 1];
|
|
449
|
+
const current = files[index];
|
|
450
|
+
if (previous && current && path.resolve(previous.filePath) === path.resolve(current.filePath)) {
|
|
451
|
+
throw new Error(`duplicate task migration file path: ${current.filePath}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return Object.freeze({ schemaVersion: 1, generation: generationFor(files), files: Object.freeze(files) });
|
|
455
|
+
}
|
|
456
|
+
/** Plan a complete, stable file set. Input order cannot change the result. */
|
|
457
|
+
export function planTaskToV4Migration(inputs) {
|
|
458
|
+
const sorted = [...inputs].sort((left, right) => left.filePath < right.filePath ? -1 : left.filePath > right.filePath ? 1 : 0);
|
|
459
|
+
let previous;
|
|
460
|
+
for (const current of sorted) {
|
|
461
|
+
if (previous && path.resolve(previous.filePath) === path.resolve(current.filePath)) {
|
|
462
|
+
throw new Error(`duplicate task migration file path: ${current.filePath}`);
|
|
463
|
+
}
|
|
464
|
+
previous = current;
|
|
465
|
+
}
|
|
466
|
+
return taskToV4PlanFromOutcomes(sorted.map(planTaskToV4File));
|
|
467
|
+
}
|
package/docs/reference/cli.md
CHANGED
|
@@ -393,6 +393,11 @@ availability:
|
|
|
393
393
|
- **`origin`** -- The source bundle (e.g. `npm:@scope/pkg`), present only for
|
|
394
394
|
managed source assets; surfaced at `full` only
|
|
395
395
|
- **`id`** -- Registry-level identifier (registry hits only)
|
|
396
|
+
- **`matchStage`** -- Which stage of the progressive AND->OR lexical search
|
|
397
|
+
ladder produced the hit: `exact` (strict AND), `prefix` (prefix AND), or
|
|
398
|
+
`relaxed` (OR/prefix-OR recovery). Omitted for hits with no FTS component
|
|
399
|
+
(e.g. a pure-semantic hybrid match) and for registry hits; surfaced at
|
|
400
|
+
`normal`, `full`, and `--shape agent`
|
|
396
401
|
|
|
397
402
|
The default brief shape is intentionally small. The exact field set per
|
|
398
403
|
detail level (and per `--shape`) is authoritative in
|
|
@@ -402,9 +407,9 @@ assembled into the shape registry by the `src/output/shapes.ts` barrel:
|
|
|
402
407
|
| Level | Local bundle hits | Registry hits |
|
|
403
408
|
| --- | --- | --- |
|
|
404
409
|
| `brief` (default) | `type`, `name`, `ref`, `action`, `estimatedTokens` | `name`, `installRef`, `score` |
|
|
405
|
-
| `normal` | `type`, `name`, `description`, `action`, `score`, `estimatedTokens`, optional `warnings`/`quality`/`keys` | `name`, `description`, `action`, `installRef`, `score`, optional `warnings` |
|
|
406
|
-
| `full` | full hit object (includes `ref`, `origin`, `tags`, `whyMatched`, optional `warnings`, optional `quality`, timings, bundle metadata) | full hit object |
|
|
407
|
-
| `--shape agent` | `name`, `ref`, `type`, `path`, `editable`, conditional `editHint`, `description`, `action`, `score`, optional `estimatedTokens`/`keys` | no local access fields |
|
|
410
|
+
| `normal` | `type`, `name`, `description`, `action`, `score`, `estimatedTokens`, optional `warnings`/`quality`/`keys`/`matchStage` | `name`, `description`, `action`, `installRef`, `score`, optional `warnings` |
|
|
411
|
+
| `full` | full hit object (includes `ref`, `origin`, `tags`, `whyMatched`, optional `warnings`, optional `quality`, optional `matchStage`, timings, bundle metadata) | full hit object |
|
|
412
|
+
| `--shape agent` | `name`, `ref`, `type`, `path`, `editable`, conditional `editHint`, `description`, `action`, `score`, optional `estimatedTokens`/`keys`/`matchStage` | no local access fields |
|
|
408
413
|
|
|
409
414
|
`--shape summary` is **not valid on `search`** — see
|
|
410
415
|
[`--shape summary`](#--shape-summary) above; it is a usage error (exit 2)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cursor.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MPL-2.0",
|
|
37
37
|
"pinNotes": {
|
|
38
38
|
"@opencode-ai/sdk@1.2.20": "Exact pin. The SDK surface we use (createOpencodeClient + session.create/prompt/delete in src/integrations/harnesses/opencode-sdk/sdk-runner.ts) is stable across 1.x, but the SDK has shipped 5+ minor versions of unrelated provider/registry churn. akm-cli is a global CLI install so the pin is isolated from user-project deps. Re-test sdk-runner before bumping. Note this package is an HTTP client only — it declares no dependencies and its own createOpencodeServer spawns `opencode serve` — so it does NOT make the opencode binary available; that install is separate and is what every SDK-path probe checks for.",
|
|
39
|
-
"better-sqlite3@12.11.1": "Exact pin (#790). This is the SQLite driver akm loads on Node (src/storage/database.ts); Bun never touches it. The pin is about PREBUILT BINARIES, not API surface. better-sqlite3 ships one prebuild per Node ABI as a GitHub release asset and its install script is `prebuild-install || node-gyp rebuild` — so any (version, Node ABI) pair with no prebuild silently COMPILES FROM SOURCE against the headers of whatever Node is on the machine that day. The 11.x line predates Node 24 and declares no `engines` at all: its newest release (11.10.0, 2025-05-08) publishes ABI 108/115/127/131 (Node 18/20/22/23) and nothing for ABI 137 (Node 24). Node 24.19.0 then changed the public `node_object_wrap.h` so `node::ObjectWrap`'s ctor/dtor register and unregister an environment cleanup hook; a from-source 11.x build against those headers aborts at teardown in `Statement::~Statement()` with `RemoveEnvironmentCleanupHook ... Assertion (env) != nullptr`, intermittently, depending on GC timing. 12.11.1 publishes ABI 127/137/141/147 (Node 22/24/25/26) and declares `engines: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x`, so
|
|
39
|
+
"better-sqlite3@12.11.1": "Exact pin (#790). This is the SQLite driver akm loads on Node (src/storage/database.ts); Bun never touches it. The pin is about PREBUILT BINARIES, not API surface. better-sqlite3 ships one prebuild per Node ABI as a GitHub release asset and its install script is `prebuild-install || node-gyp rebuild` — so any (version, Node ABI) pair with no prebuild silently COMPILES FROM SOURCE against the headers of whatever Node is on the machine that day. The 11.x line predates Node 24 and declares no `engines` at all: its newest release (11.10.0, 2025-05-08) publishes ABI 108/115/127/131 (Node 18/20/22/23) and nothing for ABI 137 (Node 24). Node 24.19.0 then changed the public `node_object_wrap.h` so `node::ObjectWrap`'s ctor/dtor register and unregister an environment cleanup hook; a from-source 11.x build against those headers aborts at teardown in `Statement::~Statement()` with `RemoveEnvironmentCleanupHook ... Assertion (env) != nullptr`, intermittently, depending on GC timing. 12.11.1 publishes ABI 127/137/141/147 (Node 22/24/25/26) and declares `engines: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x`, so every Node akm supports installs a prebuilt binary and never compiles. Before bumping: confirm the target version publishes a prebuild for EVERY Node major in `engines` (probe https://github.com/WiseLibs/better-sqlite3/releases/download/vX.Y.Z/better-sqlite3-vX.Y.Z-node-vABI-linux-x64.tar.gz), not just that the version is newer. 13.x is the eventual destination — it moved to node-addon-api/N-API with prebuilds bundled in the npm tarball and no install script, which retires this failure mode entirely — but it is a fresh major rewrite of the binding, so it wants its own soak, not a patch release. The CI node-smoke job installs this exact string by reading it back out of this file (.github/workflows/ci.yml), so the two cannot drift.",
|
|
40
40
|
"@huggingface/transformers@4.2.0": "Exact semantic-search dependency pin. Re-run the real-model semantic gate before changing it."
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"akm-migrate": "dist/akm-migrate"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
|
-
"preinstall": "node -e \"var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>=
|
|
69
|
+
"preinstall": "node -e \"var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>=22;if(ok){process.exit(0)}console.error('\\n ERROR: the akm-cli npm package requires Node.js >= 22.\\n A working Bun >= 1.0 on PATH is optional and preferred for akm and akm-migrate.\\n Upgrade Node.js (https://nodejs.org), or install the runtime-free standalone binary:\\n curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
|
|
70
70
|
"build": "rm -rf dist && bun scripts/gen-config-schema.ts &&bun run tsc --project ./tsconfig.build.json && bun scripts/copy-assets.ts && bun scripts/fix-esm-extensions.ts",
|
|
71
71
|
"check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
|
|
72
72
|
"check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"sqlite-vec": "^0.1.9"
|
|
115
115
|
},
|
|
116
116
|
"engines": {
|
|
117
|
-
"node": ">=
|
|
117
|
+
"node": ">=22"
|
|
118
118
|
},
|
|
119
119
|
"dependencies": {
|
|
120
120
|
"@clack/prompts": "^1.3.0",
|