akm-cli 0.9.4 → 0.9.5
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 +160 -0
- package/dist/commands/improve/anti-collapse.js +4 -91
- package/dist/commands/proposal/validators/proposal-validators.js +12 -0
- package/dist/commands/read/search.js +14 -24
- package/dist/commands/tasks/tasks-cli.js +56 -3
- package/dist/commands/tasks/tasks.js +89 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/config/config-version-shim.js +101 -0
- package/dist/core/config/config.js +6 -6
- package/dist/core/improve-result.js +35 -14
- package/dist/execution/guarded-source.js +0 -10
- package/dist/indexer/passes/metadata.js +12 -4
- package/dist/indexer/scan/doc-to-entry.js +2 -0
- package/dist/indexer/search/search-fields.js +16 -1
- package/dist/output/shapes/passthrough.js +17 -5
- package/dist/scripts/akm-migrate-node.js +120 -112
- package/dist/scripts/akm-migrate.js +120 -112
- package/dist/storage/repositories/proposals-repository.js +32 -6
- package/dist/tasks/scheduler-binding.js +15 -5
- package/dist/tasks/scheduler-sync-preview.js +4 -2
- package/dist/tasks/scheduler-sync.js +77 -42
- package/dist/tasks/source/task-to-v3.js +24 -12
- package/dist/tasks/source/task-to-v4.js +0 -10
- package/docs/reference/cli.md +25 -3
- package/docs/reference/configuration.md +27 -6
- package/docs/reference/tasks.md +10 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import { WorkflowSourceCollisionError, WorkflowSourceNameError, WorkflowSourceRe
|
|
|
17
17
|
import { compileWorkflowSource } from "../workflows/source-ir/compile.js";
|
|
18
18
|
import { prepareTaskV3Execution } from "./prepare/prepare.js";
|
|
19
19
|
import { parseSchedule } from "./schedule.js";
|
|
20
|
-
import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeBindingId, } from "./scheduler-binding.js";
|
|
20
|
+
import { assertSchedulerNativeArtifactCardinality, compileTaskSchedulerBindings, compileWorkflowSchedulerBindings, schedulerBindingNativeId, schedulerBindingOrdinal, schedulerNativeArtifactKey, schedulerNativeArtifactOwner, schedulerNativeBindingId, } from "./scheduler-binding.js";
|
|
21
21
|
import { parseTaskSource } from "./source/parse-task-source.js";
|
|
22
22
|
import { projectTaskSourceV4 } from "./source/project-v4.js";
|
|
23
23
|
import { taskSourceErrorDetail } from "./source-v3.js";
|
|
@@ -47,6 +47,7 @@ export async function prepareSchedulerSyncSourceSet(input) {
|
|
|
47
47
|
desired: compiled.desired,
|
|
48
48
|
sourceSnapshot,
|
|
49
49
|
executableWorkflows: compiled.executableWorkflows,
|
|
50
|
+
failures: compiled.failures,
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
export function finalizeSchedulerSyncPlan(input, prepared) {
|
|
@@ -108,35 +109,7 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
|
|
|
108
109
|
.sort(compareCodePoints);
|
|
109
110
|
for (const id of removed) {
|
|
110
111
|
const current = present.get(id);
|
|
111
|
-
|
|
112
|
-
throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
|
|
113
|
-
}
|
|
114
|
-
const nativeId = exactInstalledNativeId(id, current, inspection.artifacts);
|
|
115
|
-
const artifact = inspection.artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
|
|
116
|
-
const priorFingerprint = current.signature ?? artifact?.fingerprint;
|
|
117
|
-
if (!artifact || priorFingerprint === undefined) {
|
|
118
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
119
|
-
}
|
|
120
|
-
const logicalSource = installedLogicalSource(current.invocation, coherentInput);
|
|
121
|
-
const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
|
|
122
|
-
if (ordinal === undefined) {
|
|
123
|
-
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
124
|
-
}
|
|
125
|
-
operations.push(Object.freeze({
|
|
126
|
-
kind: "remove",
|
|
127
|
-
id,
|
|
128
|
-
nativeId,
|
|
129
|
-
expected: freezeRemovalExpectation({
|
|
130
|
-
state: "present",
|
|
131
|
-
bindingId: id,
|
|
132
|
-
nativeId,
|
|
133
|
-
logicalSource,
|
|
134
|
-
ordinal,
|
|
135
|
-
invocation: current.invocation,
|
|
136
|
-
fingerprint: priorFingerprint,
|
|
137
|
-
}),
|
|
138
|
-
...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
|
|
139
|
-
}));
|
|
112
|
+
operations.push(buildSchedulerRemoveOperation(id, current, inspection.artifacts, coherentInput));
|
|
140
113
|
}
|
|
141
114
|
return Object.freeze({
|
|
142
115
|
desired,
|
|
@@ -146,6 +119,49 @@ export function finalizeSchedulerSyncPlan(input, prepared) {
|
|
|
146
119
|
unchanged: Object.freeze(unchanged),
|
|
147
120
|
operations: Object.freeze(operations),
|
|
148
121
|
sourceSnapshot: prepared.sourceSnapshot,
|
|
122
|
+
failures: prepared.failures,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the exact removal operation for one installed binding: same
|
|
127
|
+
* exact-native-fingerprint / ordinal-attribution safety checks
|
|
128
|
+
* `finalizeSchedulerSyncPlan`'s remove loop always applied, factored out so
|
|
129
|
+
* `akm task prune` (#851) can build removal operations for entries
|
|
130
|
+
* `belongsToBundle` structurally can't see (unresolvable ownership) without
|
|
131
|
+
* re-deriving — or weakening — this logic. Throws the same `UsageError`s a
|
|
132
|
+
* sync removal would on an inexact match; callers computing prune candidates
|
|
133
|
+
* should only pass entries they've already independently confirmed are safe
|
|
134
|
+
* to remove.
|
|
135
|
+
*/
|
|
136
|
+
export function buildSchedulerRemoveOperation(id, current, artifacts, input) {
|
|
137
|
+
if (!current?.invocation) {
|
|
138
|
+
throw nativeArtifactCollision({ nativeId: current?.nativeId ?? schedulerNativeBindingId(id), bindingId: id }, { nativeId: current?.nativeId ?? schedulerNativeBindingId(id) });
|
|
139
|
+
}
|
|
140
|
+
const nativeId = exactInstalledNativeId(id, current, artifacts);
|
|
141
|
+
const artifact = artifacts.find((candidate) => candidate.nativeId === nativeId && candidate.bindingId === id);
|
|
142
|
+
const priorFingerprint = current.signature ?? artifact?.fingerprint;
|
|
143
|
+
if (!artifact || priorFingerprint === undefined) {
|
|
144
|
+
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} has no exact native fingerprint; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
145
|
+
}
|
|
146
|
+
const logicalSource = installedLogicalSource(current.invocation, input);
|
|
147
|
+
const ordinal = schedulerBindingOrdinal(id, logicalSource, current.invocation);
|
|
148
|
+
if (ordinal === undefined) {
|
|
149
|
+
throw new UsageError(`Installed scheduler binding ${JSON.stringify(id)} cannot be attributed to an exact schedule ordinal; refusing removal.`, "RESOURCE_ALREADY_EXISTS");
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
kind: "remove",
|
|
153
|
+
id,
|
|
154
|
+
nativeId,
|
|
155
|
+
expected: freezeRemovalExpectation({
|
|
156
|
+
state: "present",
|
|
157
|
+
bindingId: id,
|
|
158
|
+
nativeId,
|
|
159
|
+
logicalSource,
|
|
160
|
+
ordinal,
|
|
161
|
+
invocation: current.invocation,
|
|
162
|
+
fingerprint: priorFingerprint,
|
|
163
|
+
}),
|
|
164
|
+
...(current.ownerBundlePath !== undefined ? { ownerBundlePath: current.ownerBundlePath } : {}),
|
|
149
165
|
});
|
|
150
166
|
}
|
|
151
167
|
function exactInstalledNativeId(logicalId, current, artifacts) {
|
|
@@ -190,10 +206,17 @@ export function assertSchedulerNativeArtifactOwnership(desired, installed) {
|
|
|
190
206
|
const wanted = desiredByKey.get(key);
|
|
191
207
|
if (!wanted)
|
|
192
208
|
continue;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
// Re-derive ownership from the artifact's own invocation content (not the
|
|
210
|
+
// caller-supplied `bindingId` label) so a proven owner whose invocation
|
|
211
|
+
// no longer matches the desired shape is an UPDATE, not a refusal — that
|
|
212
|
+
// reconciliation happens below in finalizeSchedulerSyncPlan. An artifact
|
|
213
|
+
// whose invocation content does not actually prove it belongs to
|
|
214
|
+
// `wanted` (unproven, malformed, or a different logical owner) is still
|
|
215
|
+
// a genuine collision.
|
|
216
|
+
const provenBindingId = artifact.invocation !== undefined
|
|
217
|
+
? schedulerNativeArtifactOwner(artifact.nativeId, artifact.invocation)?.logicalId
|
|
218
|
+
: undefined;
|
|
219
|
+
if (artifact.nativeId !== schedulerBindingNativeId(wanted) || provenBindingId !== wanted.id) {
|
|
197
220
|
throw nativeArtifactCollision(desiredArtifact(wanted), artifact);
|
|
198
221
|
}
|
|
199
222
|
}
|
|
@@ -252,12 +275,17 @@ async function compileDesiredSourceSet(input, collector) {
|
|
|
252
275
|
const failures = [];
|
|
253
276
|
await compileTaskSources(input, collector, bindings, failures);
|
|
254
277
|
await compileWorkflowSources(input, collector, bindings, executableWorkflows, failures);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
278
|
+
// Degrade, don't reject (#867): one source that fails to parse/prepare no
|
|
279
|
+
// longer poisons the whole desired set — it is dropped from `desired` and
|
|
280
|
+
// reported here instead, so every OTHER task/workflow still reconciles.
|
|
281
|
+
// Genuinely cross-cutting integrity violations (duplicate ids, native
|
|
282
|
+
// artifact ownership conflicts, an incoherent backend inspection) are
|
|
283
|
+
// asserted separately in `finalizeSchedulerSyncPlan` and still hard-fail
|
|
284
|
+
// the whole sync — this only relaxes the per-source parse/prepare gate.
|
|
258
285
|
return Object.freeze({
|
|
259
286
|
desired: Object.freeze(bindings),
|
|
260
287
|
executableWorkflows: Object.freeze(executableWorkflows.sort((left, right) => compareCodePoints(left.ref, right.ref))),
|
|
288
|
+
failures: Object.freeze(failures.sort((left, right) => compareCodePoints(left.path, right.path))),
|
|
261
289
|
});
|
|
262
290
|
}
|
|
263
291
|
async function compileTaskSources(input, collector, out, failures) {
|
|
@@ -269,6 +297,7 @@ async function compileTaskSources(input, collector, out, failures) {
|
|
|
269
297
|
const relative = guarded.relativePath;
|
|
270
298
|
const conceptId = relative.slice(0, -4);
|
|
271
299
|
const id = input.adapterId === "akm-task" ? conceptId : path.basename(sourcePath, ".yml");
|
|
300
|
+
const qualifiedRefForFailure = makeBundleRef(input.bundleName, conceptId);
|
|
272
301
|
try {
|
|
273
302
|
const physicalIdentity = guarded.physicalIdentity;
|
|
274
303
|
const priorOwner = physicalOwners.get(physicalIdentity);
|
|
@@ -356,7 +385,7 @@ async function compileTaskSources(input, collector, out, failures) {
|
|
|
356
385
|
}
|
|
357
386
|
}
|
|
358
387
|
catch (cause) {
|
|
359
|
-
failures.push(taskFailure(sourcePath, cause));
|
|
388
|
+
failures.push(taskFailure(sourcePath, qualifiedRefForFailure, cause));
|
|
360
389
|
}
|
|
361
390
|
}
|
|
362
391
|
}
|
|
@@ -365,6 +394,8 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
|
|
|
365
394
|
return;
|
|
366
395
|
const lookups = enumerateWorkflowLookups(input, collector, failures);
|
|
367
396
|
for (const [canonicalName, sources] of lookups) {
|
|
397
|
+
const failurePath = sources[0]?.sourcePath ?? canonicalName;
|
|
398
|
+
const failureRef = makeBundleRef(input.bundleName, input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName);
|
|
368
399
|
try {
|
|
369
400
|
if (sources.length > 1) {
|
|
370
401
|
throw new WorkflowSourceCollisionError(input.adapterId === "akm" ? `workflows/${canonicalName}` : canonicalName, sources.map((source) => source.relativePath));
|
|
@@ -427,7 +458,7 @@ async function compileWorkflowSources(input, collector, out, evidence, failures)
|
|
|
427
458
|
}
|
|
428
459
|
}
|
|
429
460
|
catch (cause) {
|
|
430
|
-
failures.push(
|
|
461
|
+
failures.push(workflowFailure(failurePath, failureRef, cause));
|
|
431
462
|
}
|
|
432
463
|
}
|
|
433
464
|
}
|
|
@@ -456,7 +487,7 @@ function enumerateWorkflowLookups(input, collector, failures) {
|
|
|
456
487
|
const stem = authoredName.slice(0, -extension.length).toLowerCase();
|
|
457
488
|
const nestedSuffix = WORKFLOW_EXTENSIONS.find((suffix) => stem.endsWith(suffix));
|
|
458
489
|
if (nestedSuffix) {
|
|
459
|
-
failures.push(
|
|
490
|
+
failures.push(workflowFailure(sourcePath, undefined, new WorkflowSourceNameError(guarded.relativePath, nestedSuffix)));
|
|
460
491
|
continue;
|
|
461
492
|
}
|
|
462
493
|
const canonicalName = canonicalizeWorkflowName(authoredName);
|
|
@@ -529,9 +560,13 @@ function assertUniqueInstalledIds(installed) {
|
|
|
529
560
|
seen.add(binding.id);
|
|
530
561
|
}
|
|
531
562
|
}
|
|
532
|
-
function taskFailure(file, cause) {
|
|
563
|
+
function taskFailure(file, ref, cause) {
|
|
533
564
|
const detail = taskSourceErrorDetail(cause);
|
|
534
|
-
|
|
565
|
+
const reason = detail === errorMessage(cause) ? `${file}: ${detail}` : detail;
|
|
566
|
+
return Object.freeze({ path: file, ref, reason });
|
|
567
|
+
}
|
|
568
|
+
function workflowFailure(file, ref, cause) {
|
|
569
|
+
return Object.freeze({ path: file, ...(ref ? { ref } : {}), reason: errorMessage(cause) });
|
|
535
570
|
}
|
|
536
571
|
function errorMessage(cause) {
|
|
537
572
|
return cause instanceof Error ? cause.message : String(cause);
|
|
@@ -61,6 +61,27 @@ const SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
|
61
61
|
function shellStableV2Executable(executable) {
|
|
62
62
|
return executable === "akm" || executable.includes("/");
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* `env NAME=value... cmd args...` is env(1) itself resolving and exec'ing
|
|
66
|
+
* `cmd` via its own PATH search — that lookup happens inside env's execvp()
|
|
67
|
+
* regardless of whether env was launched by direct execve (v2) or by a host
|
|
68
|
+
* shell (v3 `run:`). The shell-vs-argv divergence `shellStableV2Executable`
|
|
69
|
+
* guards against (bare names shadowed by shell aliases/builtins/functions)
|
|
70
|
+
* therefore does not apply to whatever env ultimately invokes, so skip past
|
|
71
|
+
* a leading `env` and its `NAME=value` assignments to find the real target.
|
|
72
|
+
* Returns the original tokens, unchanged, when there is no such target
|
|
73
|
+
* (e.g. `env` with nothing after its assignments).
|
|
74
|
+
*/
|
|
75
|
+
function skipEnvAssignmentPrefix(tokens) {
|
|
76
|
+
if (tokens[0] !== "env")
|
|
77
|
+
return { tokens, envWrapped: false };
|
|
78
|
+
let index = 1;
|
|
79
|
+
while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
|
|
80
|
+
index += 1;
|
|
81
|
+
if (index >= tokens.length)
|
|
82
|
+
return { tokens, envWrapped: false };
|
|
83
|
+
return { tokens: tokens.slice(index), envWrapped: true };
|
|
84
|
+
}
|
|
64
85
|
const KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
65
86
|
"agents",
|
|
66
87
|
"commands",
|
|
@@ -81,15 +102,6 @@ function hash(bytes) {
|
|
|
81
102
|
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
82
103
|
}
|
|
83
104
|
function base(input) {
|
|
84
|
-
const inspectionIdentity = input.inspectionIdentity
|
|
85
|
-
? Object.freeze({
|
|
86
|
-
file: Object.freeze({ ...input.inspectionIdentity.file }),
|
|
87
|
-
root: Object.freeze({ ...input.inspectionIdentity.root }),
|
|
88
|
-
...(input.inspectionIdentity.bundleRoot
|
|
89
|
-
? { bundleRoot: Object.freeze({ ...input.inspectionIdentity.bundleRoot }) }
|
|
90
|
-
: {}),
|
|
91
|
-
})
|
|
92
|
-
: undefined;
|
|
93
105
|
return {
|
|
94
106
|
filePath: input.filePath,
|
|
95
107
|
before: Buffer.from(input.bytes),
|
|
@@ -98,7 +110,6 @@ function base(input) {
|
|
|
98
110
|
writable: input.writable,
|
|
99
111
|
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
100
112
|
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
101
|
-
...(inspectionIdentity ? { inspectionIdentity } : {}),
|
|
102
113
|
};
|
|
103
114
|
}
|
|
104
115
|
function blocked(input, reason, detail) {
|
|
@@ -348,8 +359,9 @@ function migratedObject(data) {
|
|
|
348
359
|
if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
349
360
|
return "shell-operators-change-v2-literal-argv-semantics";
|
|
350
361
|
}
|
|
351
|
-
const
|
|
352
|
-
|
|
362
|
+
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
|
|
363
|
+
const executable = targetTokens[0];
|
|
364
|
+
if (SHELL_ASSIGNMENT_WORD.test(executable) || (!envWrapped && !shellStableV2Executable(executable))) {
|
|
353
365
|
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
354
366
|
}
|
|
355
367
|
output.run = tokens.join(" ");
|
|
@@ -76,15 +76,6 @@ function causeMessage(cause) {
|
|
|
76
76
|
return cause instanceof Error ? cause.message : String(cause);
|
|
77
77
|
}
|
|
78
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
79
|
return {
|
|
89
80
|
filePath: input.filePath,
|
|
90
81
|
before: Buffer.from(input.bytes),
|
|
@@ -93,7 +84,6 @@ function base(input) {
|
|
|
93
84
|
writable: input.writable,
|
|
94
85
|
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
95
86
|
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
96
|
-
...(inspectionIdentity ? { inspectionIdentity } : {}),
|
|
97
87
|
};
|
|
98
88
|
}
|
|
99
89
|
function blocked(input, reason, detail) {
|
package/docs/reference/cli.md
CHANGED
|
@@ -2423,9 +2423,10 @@ shell commands. It manages on-disk task definitions under
|
|
|
2423
2423
|
(cron / launchd / schtasks). Task source v4 YAML (`version: 4`) is the only
|
|
2424
2424
|
executable source contract this release accepts; `akm task add` writes v4 —
|
|
2425
2425
|
see the canonical [Tasks reference](tasks.md). The
|
|
2426
|
-
group is `add | run | explain | sync | doctor | history` — there is
|
|
2427
|
-
or `remove`; use `akm search --type task` / `akm show tasks/<id>`
|
|
2428
|
-
and edit the file + `akm task sync` to change or remove a
|
|
2426
|
+
group is `add | run | explain | sync | doctor | history | prune` — there is
|
|
2427
|
+
no `list` or `remove`; use `akm search --type task` / `akm show tasks/<id>`
|
|
2428
|
+
to inspect, and edit the file + `akm task sync` to change or remove a
|
|
2429
|
+
schedule.
|
|
2429
2430
|
|
|
2430
2431
|
```sh
|
|
2431
2432
|
akm search --type task # List tasks (cross-bundle)
|
|
@@ -2439,8 +2440,12 @@ akm task run <id> # Execute now (what the scheduler ca
|
|
|
2439
2440
|
akm task explain <ref> # Read-only: declared inputs, target, schedule — spawns nothing
|
|
2440
2441
|
akm task history [--id <id>] [--limit <n>] # Recent runs from state.db
|
|
2441
2442
|
akm task sync # Reconcile on-disk YAML with scheduler
|
|
2443
|
+
akm task sync --dry-run # Preview the reconcile — zero scheduler writes
|
|
2442
2444
|
akm task sync --rebind # Also capture the current installed runtime
|
|
2443
2445
|
akm task doctor # Report scheduler backend + paths
|
|
2446
|
+
akm task prune # Preview orphaned scheduler entries — zero writes
|
|
2447
|
+
akm task prune --yes # Remove every currently-computed orphan
|
|
2448
|
+
akm task prune --id ghost,stale --yes # Remove only the named orphan ids
|
|
2444
2449
|
```
|
|
2445
2450
|
|
|
2446
2451
|
`task add` also accepts `--disabled` (register but leave off in the OS
|
|
@@ -2466,6 +2471,23 @@ schedule-binding) and run `akm task sync`. To remove one, delete its file
|
|
|
2466
2471
|
(`<bundle>/tasks/<id>.yml`) and run `akm task sync` — sync uninstalls the
|
|
2467
2472
|
orphaned scheduler entry.
|
|
2468
2473
|
|
|
2474
|
+
`akm task sync --dry-run` prints the planned adds/updates/removes (removals
|
|
2475
|
+
carry their owning bundle) without touching the scheduler — zero writes.
|
|
2476
|
+
Exits non-zero when removals are pending, so it can gate a CI/health check
|
|
2477
|
+
on "sync would change something."
|
|
2478
|
+
|
|
2479
|
+
`akm task prune` reclaims installed scheduler entries that `sync` can never
|
|
2480
|
+
clean up on its own: entries whose own `--scheduler-context` descriptor no
|
|
2481
|
+
longer resolves to a live bundle (a corrupt/missing descriptor, or the
|
|
2482
|
+
bundle directory it pointed at is gone). It never touches an entry that
|
|
2483
|
+
still resolves to a live bundle — that's `sync`'s job. Like `sync
|
|
2484
|
+
--dry-run`, the default is a dry-run preview (zero scheduler writes) that
|
|
2485
|
+
exits non-zero when there are candidates to remove; `--yes` executes the
|
|
2486
|
+
printed plan, and `--id <id1,id2,...>` narrows a `--yes` run (or a preview)
|
|
2487
|
+
to specific binding ids — naming an id that isn't a current orphan
|
|
2488
|
+
candidate (not installed, or it still resolves to a live bundle) is
|
|
2489
|
+
refused with a usage error and removes nothing.
|
|
2490
|
+
|
|
2469
2491
|
Scheduler activation captures the installed akm runtime. Ordinary `task sync`
|
|
2470
2492
|
reconciles definitions, schedules, and enabled state while preserving that
|
|
2471
2493
|
runtime binding. Use `task sync --rebind` only after intentionally moving or
|
|
@@ -7,12 +7,33 @@ directory. Project `.akm/config.json` files are not merged.
|
|
|
7
7
|
|
|
8
8
|
## Version 0.9
|
|
9
9
|
|
|
10
|
-
A present configuration file must set `configVersion` to
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
A present configuration file must set `configVersion` to a version this
|
|
11
|
+
binary knows: the current `"0.9.0"`, or a known older version it can
|
|
12
|
+
auto-upgrade in memory (see "Version read shim" below). Missing, newer,
|
|
13
|
+
numeric, and any other unrecognized version are rejected by ordinary
|
|
14
|
+
commands without rewriting the file — an older binary never guesses at a
|
|
15
|
+
newer, unknown shape. Pre-0.9 config and database layouts are not runtime
|
|
16
|
+
inputs and are not migrated by `akm upgrade`. Configure the current schema
|
|
17
|
+
directly. The standalone migrator exists only for explicit task migration:
|
|
18
|
+
task v2 to task v3, then task v3 to task source v4, in one pass.
|
|
19
|
+
|
|
20
|
+
### Version read shim
|
|
21
|
+
|
|
22
|
+
Like the task-source v2/v3 auto-shim (`akm migrate apply`'s in-memory
|
|
23
|
+
counterpart, documented under Migration below), a known older `configVersion`
|
|
24
|
+
is converted to the current shape in memory on load — with a one-line stderr
|
|
25
|
+
deprecation warning — rather than hard-failing every command. Nothing is
|
|
26
|
+
written back to disk by the shim itself; the very next config-mutating
|
|
27
|
+
command (`akm config set`, etc.) persists the upgrade for free, since every
|
|
28
|
+
config write already forces `configVersion` to the current value, which
|
|
29
|
+
silences the warning. A `configVersion` this binary does not recognize at
|
|
30
|
+
all — including anything newer than current — still fails closed with
|
|
31
|
+
`UNSUPPORTED_CONFIG_VERSION`.
|
|
32
|
+
|
|
33
|
+
As of this writing `"0.9.0"` is the only `configVersion` akm has ever
|
|
34
|
+
shipped, so there is no real older shape for the shim to convert yet; the
|
|
35
|
+
mechanism (`src/core/config/config-version-shim.ts`) is established ahead of
|
|
36
|
+
the first bump that will need it, per #863.
|
|
16
37
|
|
|
17
38
|
```jsonc
|
|
18
39
|
{
|
package/docs/reference/tasks.md
CHANGED
|
@@ -389,6 +389,16 @@ for full before/after examples and recovery guidance.
|
|
|
389
389
|
- Disable a binding by editing the source and syncing: set that schedule
|
|
390
390
|
entry's own `enabled: false`.
|
|
391
391
|
- Delete the `.yml` source and sync to remove its derived binding(s).
|
|
392
|
+
- `akm task sync --dry-run` previews the reconcile (adds/updates/removes,
|
|
393
|
+
removals annotated with their owning bundle) without writing to the
|
|
394
|
+
scheduler; exits non-zero when removals are pending.
|
|
395
|
+
- `akm task prune` removes installed scheduler entries `sync` cannot reach
|
|
396
|
+
because their own descriptor no longer resolves to a live bundle
|
|
397
|
+
(corrupt/missing `--scheduler-context`, or the owning bundle directory is
|
|
398
|
+
gone). It never touches an entry that still resolves to a live bundle.
|
|
399
|
+
Defaults to a dry-run preview (zero writes); `--yes` executes it; `--id
|
|
400
|
+
<id1,id2,...>` scopes to specific ids and refuses any id that isn't a
|
|
401
|
+
current orphan candidate.
|
|
392
402
|
- Use `akm task sync --rebind` only when deliberately changing the captured
|
|
393
403
|
AKM runtime, then verify with `akm task doctor`.
|
|
394
404
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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": [
|