akm-cli 0.9.0-rc.3 → 0.9.0-rc.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +6 -4
  2. package/README.md +7 -8
  3. package/SECURITY.md +5 -3
  4. package/dist/akm +44 -33
  5. package/dist/akm-migrate-storage +44 -33
  6. package/dist/assets/backends/schtasks-template.xml +2 -1
  7. package/dist/cli.js +25 -10
  8. package/dist/commands/health/html-report.js +23 -2
  9. package/dist/commands/health/improve-metrics.js +45 -6
  10. package/dist/commands/health/md-report.js +10 -1
  11. package/dist/commands/health/windows.js +1 -1
  12. package/dist/commands/health.js +1 -1
  13. package/dist/commands/tasks/default-tasks.js +5 -5
  14. package/dist/commands/tasks/tasks-cli.js +7 -3
  15. package/dist/commands/tasks/tasks.js +261 -68
  16. package/dist/core/improve-result.js +87 -7
  17. package/dist/core/state-db.js +1 -0
  18. package/dist/scripts/migrate-storage.js +21 -3
  19. package/dist/scripts/migrations/import-fs-improve-runs-to-db.js +89 -8
  20. package/dist/storage/repositories/task-history-repository.js +30 -3
  21. package/dist/tasks/backends/cron.js +71 -38
  22. package/dist/tasks/backends/exec-utils.js +55 -3
  23. package/dist/tasks/backends/launchd.js +241 -50
  24. package/dist/tasks/backends/schtasks.js +434 -59
  25. package/dist/tasks/command-executable.js +93 -0
  26. package/dist/tasks/parser.js +142 -6
  27. package/dist/tasks/resolve-akm-bin.js +19 -4
  28. package/dist/tasks/runner.js +258 -93
  29. package/dist/tasks/schedule.js +108 -19
  30. package/dist/tasks/scheduler-invocation.js +86 -0
  31. package/dist/tasks/task-id.js +37 -0
  32. package/docs/README.md +103 -0
  33. package/docs/migration/release-notes/0.9.0.md +4 -2
  34. package/docs/migration/v0.8-to-v0.9.md +64 -9
  35. package/package.json +2 -3
@@ -15,9 +15,9 @@
15
15
  * • a launchd plist `<StartCalendarInterval>` / `<StartInterval>` (macOS),
16
16
  * • Task Scheduler XML triggers (Windows).
17
17
  *
18
- * The shared subset is `*`, single integers, `*\/N`, plus the `@hourly /
18
+ * The shared subset is `*`, single integers, `*\/N`, `A-B/N`, plus the `@hourly /
19
19
  * @daily / @weekly / @monthly` aliases. Patterns outside that — multi-value
20
- * lists, ranges, step values other than `*\/N`, day-of-month AND
20
+ * lists, plain ranges, day-of-month AND
21
21
  * day-of-week combinations — are rejected with a {@link UsageError}.
22
22
  *
23
23
  * Cron is the most permissive of the three backends; some patterns it
@@ -41,9 +41,9 @@ const FIELD_LIMITS = {
41
41
  month: { min: 1, max: 12 },
42
42
  dow: { min: 0, max: 6 },
43
43
  };
44
- const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), step-on-star (`*/N`), and comma lists (`7,37`). " +
44
+ const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), steps (`*/N`, `A-B/N`), and comma lists (`7,37`). " +
45
45
  "Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`. " +
46
- "Lists, ranges, and named days/months are not supported.";
46
+ "Plain ranges and named days/months are not supported.";
47
47
  export function parseSchedule(input, backend) {
48
48
  const cron = expandAlias(input);
49
49
  const fields = parseCronFields(cron, input);
@@ -99,6 +99,16 @@ function parseField(raw, name, limit, original) {
99
99
  }
100
100
  return { kind: "step", step };
101
101
  }
102
+ const rangeStepMatch = raw.match(/^(\d+)-(\d+)\/(\d+)$/);
103
+ if (rangeStepMatch) {
104
+ const start = Number(rangeStepMatch[1]);
105
+ const end = Number(rangeStepMatch[2]);
106
+ const step = Number(rangeStepMatch[3]);
107
+ if (start < limit.min || end > limit.max || start > end || step <= 0) {
108
+ throw new UsageError(`Invalid ${name} range-step "${raw}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
109
+ }
110
+ return { kind: "rangeStep", start, end, step };
111
+ }
102
112
  if (/^\d+$/.test(raw)) {
103
113
  const value = Number(raw);
104
114
  if (value < limit.min || value > limit.max) {
@@ -128,22 +138,28 @@ export function translateToCron(spec) {
128
138
  }
129
139
  export function translateToLaunchd(spec) {
130
140
  const f = spec.fields;
131
- // `*/N` minute (everything else `*`) → StartInterval = N*60 seconds.
132
- if (f.minute.kind === "step" &&
141
+ rejectDomDowCombination(spec, "macOS launchd");
142
+ // Expand minute steps into clock-minute anchors. StartInterval would drift
143
+ // from cron whenever the agent is loaded away from a matching boundary.
144
+ if ((f.minute.kind === "step" || f.minute.kind === "rangeStep") &&
133
145
  f.hour.kind === "star" &&
134
146
  f.dom.kind === "star" &&
135
147
  f.month.kind === "star" &&
136
148
  f.dow.kind === "star") {
137
- return { intervalSeconds: f.minute.step * 60 };
149
+ return { calendars: expandFieldValues(f.minute, FIELD_LIMITS.minute).map((Minute) => ({ Minute })) };
138
150
  }
139
- // `*/N` hour StartInterval = N*3600.
151
+ // Likewise, anchor hour steps to the same hours cron selects each day.
140
152
  if (f.minute.kind === "value" &&
141
- f.minute.value === 0 &&
142
- f.hour.kind === "step" &&
153
+ (f.hour.kind === "step" || f.hour.kind === "rangeStep") &&
143
154
  f.dom.kind === "star" &&
144
155
  f.month.kind === "star" &&
145
156
  f.dow.kind === "star") {
146
- return { intervalSeconds: f.hour.step * 3600 };
157
+ return {
158
+ calendars: expandFieldValues(f.hour, FIELD_LIMITS.hour).map((Hour) => ({
159
+ Minute: f.minute.kind === "value" ? f.minute.value : 0,
160
+ Hour,
161
+ })),
162
+ };
147
163
  }
148
164
  // Otherwise build a calendar dict from concrete values. launchd treats any
149
165
  // omitted key as "every value", so a `*` field translates to "no key".
@@ -166,39 +182,101 @@ export function translateToLaunchd(spec) {
166
182
  if (f.dow.kind === "value")
167
183
  calendar.Weekday = f.dow.value;
168
184
  // launchd's CalendarInterval requires at least one specific key. If every
169
- // field is `*` the schedule has no anchor and we'd need a StartInterval
170
- // instead treat this as "every minute".
185
+ // Expand every-minute to explicit minute boundaries rather than a
186
+ // load-relative 60-second interval.
171
187
  if (Object.keys(calendar).length === 0) {
172
- return { intervalSeconds: 60 };
188
+ return {
189
+ calendars: expandFieldValues({ kind: "step", step: 1 }, FIELD_LIMITS.minute).map((Minute) => ({ Minute })),
190
+ };
173
191
  }
174
192
  return { calendar };
175
193
  }
194
+ function expandFieldValues(field, limit) {
195
+ const start = field.kind === "rangeStep" ? field.start : limit.min;
196
+ const end = field.kind === "rangeStep" ? field.end : limit.max;
197
+ const values = [];
198
+ for (let value = start; value <= end; value += field.step)
199
+ values.push(value);
200
+ return values;
201
+ }
176
202
  function rejectStepInsideCalendar(field, name, spec) {
177
203
  if (field.kind === "step") {
178
204
  throw new UsageError(`Schedule "${spec.raw}" uses step (${name} = */N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either restrict the step to the minute or hour field only, or rewrite the schedule with concrete values.");
179
205
  }
206
+ if (field.kind === "rangeStep") {
207
+ throw new UsageError(`Schedule "${spec.raw}" uses range-step (${name} = A-B/N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Restrict the range-step to the minute or hour field, or rewrite the schedule with a concrete value.");
208
+ }
180
209
  if (field.kind === "list") {
181
210
  throw new UsageError(`Schedule "${spec.raw}" uses comma list (${name} = a,b,...) which macOS launchd cannot express as a single trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either install one task per list element, or rewrite the schedule with a step (`*/N`) or single value.");
182
211
  }
183
212
  }
213
+ const MAX_SCHTASKS_TRIGGERS = 48;
184
214
  export function translateToSchtasks(spec) {
185
215
  const f = spec.fields;
186
- // `*/N` minute → MINUTE, every N.
216
+ rejectDomDowCombination(spec, "Windows Task Scheduler");
217
+ // A repetition interval is cron-equivalent indefinitely only when it divides
218
+ // the field's wall-clock cycle. Other steps must reset at every hour.
187
219
  if (f.minute.kind === "step" &&
188
220
  f.hour.kind === "star" &&
189
221
  f.dom.kind === "star" &&
190
222
  f.month.kind === "star" &&
191
223
  f.dow.kind === "star") {
224
+ if (60 % f.minute.step !== 0) {
225
+ return minuteValuesTrigger(expandFieldValues(f.minute, FIELD_LIMITS.minute), spec);
226
+ }
192
227
  return { kind: "minute", everyMinutes: f.minute.step };
193
228
  }
194
- // `0 */N * * *` → HOURLY, every N.
229
+ if (f.minute.kind === "rangeStep" &&
230
+ f.hour.kind === "star" &&
231
+ f.dom.kind === "star" &&
232
+ f.month.kind === "star" &&
233
+ f.dow.kind === "star") {
234
+ if (f.minute.start === FIELD_LIMITS.minute.min && f.minute.end === FIELD_LIMITS.minute.max) {
235
+ if (60 % f.minute.step === 0)
236
+ return { kind: "minute", everyMinutes: f.minute.step };
237
+ }
238
+ return minuteValuesTrigger(expandFieldValues(f.minute, FIELD_LIMITS.minute), spec);
239
+ }
240
+ // Fixed-minute hour schedules can use one daily-reset repetition only when
241
+ // the interval divides 24 hours. Non-divisors become explicit daily times.
195
242
  if (f.minute.kind === "value" &&
196
- f.minute.value === 0 &&
197
243
  f.hour.kind === "step" &&
198
244
  f.dom.kind === "star" &&
199
245
  f.month.kind === "star" &&
200
246
  f.dow.kind === "star") {
201
- return { kind: "hour", everyHours: f.hour.step };
247
+ if (24 % f.hour.step === 0 && f.hour.step < 24) {
248
+ return { kind: "hour", everyHours: f.hour.step, atMinute: f.minute.value };
249
+ }
250
+ return {
251
+ kind: "hourValues",
252
+ hours: expandFieldValues(f.hour, FIELD_LIMITS.hour),
253
+ atMinute: f.minute.value,
254
+ };
255
+ }
256
+ if (f.minute.kind === "value" &&
257
+ f.hour.kind === "rangeStep" &&
258
+ f.dom.kind === "star" &&
259
+ f.month.kind === "star" &&
260
+ f.dow.kind === "star") {
261
+ if (f.hour.start === FIELD_LIMITS.hour.min && f.hour.end === FIELD_LIMITS.hour.max) {
262
+ if (24 % f.hour.step === 0 && f.hour.step < 24) {
263
+ return { kind: "hour", everyHours: f.hour.step, atMinute: f.minute.value };
264
+ }
265
+ }
266
+ return {
267
+ kind: "hourValues",
268
+ hours: expandFieldValues(f.hour, FIELD_LIMITS.hour),
269
+ atMinute: f.minute.value,
270
+ };
271
+ }
272
+ // `M * * * *` includes the shipped top-of-hour task and arbitrary fixed
273
+ // minutes. A daily calendar trigger resets the hourly repetition each day.
274
+ if (f.minute.kind === "value" &&
275
+ f.hour.kind === "star" &&
276
+ f.dom.kind === "star" &&
277
+ f.month.kind === "star" &&
278
+ f.dow.kind === "star") {
279
+ return { kind: "hour", everyHours: 1, atMinute: f.minute.value };
202
280
  }
203
281
  // `M H * * *` → DAILY at H:M.
204
282
  if (f.minute.kind === "value" &&
@@ -221,7 +299,18 @@ export function translateToSchtasks(spec) {
221
299
  daysOfWeek: [f.dow.value],
222
300
  };
223
301
  }
224
- throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: */N minutes, every N hours (0 */N * * *), daily at HH:MM, or weekly on a single weekday.");
302
+ throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: minute steps/range-steps, fixed-minute hour steps/range-steps, hourly, daily, or weekly on a single weekday.");
303
+ }
304
+ function minuteValuesTrigger(minutes, spec) {
305
+ if (minutes.length > MAX_SCHTASKS_TRIGGERS) {
306
+ throw new UsageError(`Schedule "${spec.raw}" requires ${minutes.length} native triggers; Windows Task Scheduler allows at most ${MAX_SCHTASKS_TRIGGERS}.`, "INVALID_FLAG_VALUE", "Use a full-field step whose interval divides 60, or reduce the minute range.");
307
+ }
308
+ return { kind: "minuteValues", minutes };
309
+ }
310
+ function rejectDomDowCombination(spec, backend) {
311
+ if (spec.fields.dom.kind !== "star" && spec.fields.dow.kind !== "star") {
312
+ throw new UsageError(`Schedule "${spec.raw}": day-of-month and day-of-week use OR semantics in cron, which ${backend} cannot express portably.`, "INVALID_FLAG_VALUE", "Use either day-of-month or day-of-week, but not both.");
313
+ }
225
314
  }
226
315
  /** Human-readable summary used by `tasks doctor`. */
227
316
  export const SCHEDULE_SUPPORTED_SUBSET_HINT = SUPPORTED_HINT;
@@ -0,0 +1,86 @@
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
+ import path from "node:path";
5
+ import { resolveStashDir } from "../core/common.js";
6
+ import { ConfigError } from "../core/errors.js";
7
+ import { getCacheDir, getConfigDir, getDataDir } from "../core/paths.js";
8
+ export const SCHEDULED_TASK_CONTEXT_KEYS = [
9
+ "AKM_STASH_DIR",
10
+ "AKM_CONFIG_DIR",
11
+ "AKM_DATA_DIR",
12
+ "AKM_CACHE_DIR",
13
+ "AKM_STATE_DIR",
14
+ ];
15
+ /** Resolve the complete non-secret AKM directory context captured by schedulers. */
16
+ export function resolveScheduledTaskContext(env = process.env, platform = process.platform) {
17
+ return canonicalContext({
18
+ AKM_STASH_DIR: path.resolve(resolveStashDir(undefined, env)),
19
+ AKM_CONFIG_DIR: path.resolve(getConfigDir(env, platform)),
20
+ AKM_DATA_DIR: path.resolve(getDataDir(env, platform)),
21
+ AKM_CACHE_DIR: path.resolve(getCacheDir(env)),
22
+ // Retain the legacy state root for scheduled commands and upgrade tooling
23
+ // that still honor it even though current durable state lives under DATA.
24
+ AKM_STATE_DIR: path.resolve(resolveStateDir(env, platform)),
25
+ });
26
+ }
27
+ /** Build the one scheduler-generated argv shape consumed by all backends. */
28
+ export function buildScheduledTaskInvocation(akmArgv, id, context) {
29
+ const environment = canonicalContext(context);
30
+ return {
31
+ argv: [...akmArgv, "tasks", "run", id, "--scheduled"],
32
+ environment,
33
+ };
34
+ }
35
+ function canonicalContext(input) {
36
+ const inputKeys = Object.keys(input);
37
+ if (inputKeys.length !== SCHEDULED_TASK_CONTEXT_KEYS.length ||
38
+ inputKeys.some((key) => !SCHEDULED_TASK_CONTEXT_KEYS.includes(key))) {
39
+ throw invalidSchedulerContext();
40
+ }
41
+ const context = {};
42
+ for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
43
+ const value = input[key];
44
+ if (typeof value !== "string" ||
45
+ value.trim().length === 0 ||
46
+ containsControlCharacter(value) ||
47
+ (!path.posix.isAbsolute(value) && !path.win32.isAbsolute(value))) {
48
+ throw invalidSchedulerContext();
49
+ }
50
+ context[key] = value;
51
+ }
52
+ return context;
53
+ }
54
+ function containsControlCharacter(value) {
55
+ for (const char of value) {
56
+ const code = char.codePointAt(0) ?? 0;
57
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
58
+ return true;
59
+ }
60
+ return false;
61
+ }
62
+ function resolveStateDir(env, platform) {
63
+ const override = env.AKM_STATE_DIR?.trim();
64
+ if (override)
65
+ return override;
66
+ if (platform === "win32") {
67
+ const localAppData = env.LOCALAPPDATA?.trim();
68
+ if (localAppData)
69
+ return path.join(localAppData, "akm", "state");
70
+ const userProfile = env.USERPROFILE?.trim();
71
+ if (userProfile)
72
+ return path.join(userProfile, "AppData", "Local", "akm", "state");
73
+ const appData = env.APPDATA?.trim();
74
+ if (appData)
75
+ return path.join(appData, "..", "Local", "akm", "state");
76
+ throw new ConfigError("Unable to determine state directory. Set LOCALAPPDATA, USERPROFILE, or APPDATA.", "CONFIG_DIR_UNRESOLVABLE");
77
+ }
78
+ const xdgStateHome = env.XDG_STATE_HOME?.trim();
79
+ if (xdgStateHome)
80
+ return path.join(xdgStateHome, "akm");
81
+ const home = env.HOME?.trim();
82
+ return home ? path.join(home, ".local", "state", "akm") : path.join("/tmp", "akm-state");
83
+ }
84
+ function invalidSchedulerContext() {
85
+ return new ConfigError(`Invalid scheduler context; expected exactly ${SCHEDULED_TASK_CONTEXT_KEYS.join(", ")} as absolute paths.`, "INVALID_CONFIG_FILE");
86
+ }
@@ -0,0 +1,37 @@
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
+ import { UsageError } from "../core/errors.js";
5
+ const VALID_TASK_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
6
+ const TASK_FILE_SUFFIX_RE = /\.(?:yml|yaml)$/i;
7
+ const WINDOWS_RESERVED_DEVICE_RE = /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i;
8
+ const WINDOWS_TASK_PATH_MAX_LENGTH = 238;
9
+ const WINDOWS_TASK_FOLDER_PREFIX_LENGTH = "\\akm\\".length;
10
+ const PORTABLE_FILENAME_COMPONENT_MAX_LENGTH = 255;
11
+ const LAUNCHD_FILENAME_OVERHEAD = "com.akm.task.".length + ".plist".length;
12
+ const TASK_FILENAME_OVERHEAD = ".yml".length;
13
+ const SCHTASKS_TEMP_FILENAME_OVERHEAD = "akm-task-".length + "-".length + 13 + ".xml".length;
14
+ export const MAX_PORTABLE_TASK_ID_LENGTH = Math.min(WINDOWS_TASK_PATH_MAX_LENGTH - WINDOWS_TASK_FOLDER_PREFIX_LENGTH, PORTABLE_FILENAME_COMPONENT_MAX_LENGTH - LAUNCHD_FILENAME_OVERHEAD, PORTABLE_FILENAME_COMPONENT_MAX_LENGTH - TASK_FILENAME_OVERHEAD, PORTABLE_FILENAME_COMPONENT_MAX_LENGTH - SCHTASKS_TEMP_FILENAME_OVERHEAD);
15
+ export function validateTaskId(id) {
16
+ if (!id) {
17
+ throw new UsageError("Task id must be non-empty.", "MISSING_REQUIRED_ARGUMENT");
18
+ }
19
+ if (!VALID_TASK_ID_RE.test(id)) {
20
+ throw new UsageError(`Task id "${id}" is invalid. Use letters, digits, dots, underscores, and dashes only.`, "INVALID_FLAG_VALUE");
21
+ }
22
+ if (id.length > MAX_PORTABLE_TASK_ID_LENGTH) {
23
+ throw new UsageError(`Task id "${id}" is invalid. Use at most ${MAX_PORTABLE_TASK_ID_LENGTH} characters for all supported schedulers.`, "INVALID_FLAG_VALUE");
24
+ }
25
+ if (TASK_FILE_SUFFIX_RE.test(id)) {
26
+ throw new UsageError(`Task id "${id}" is invalid. Use the bare task id without a .yml or .yaml suffix.`, "INVALID_FLAG_VALUE");
27
+ }
28
+ if (WINDOWS_RESERVED_DEVICE_RE.test(id)) {
29
+ throw new UsageError(`Task id "${id}" uses a reserved Windows device name. Choose a different task id.`, "INVALID_FLAG_VALUE");
30
+ }
31
+ return id;
32
+ }
33
+ export function normaliseTaskId(raw) {
34
+ // Keep accepting old task-file suffixes at the CLI boundary, but never
35
+ // normalize filesystem-derived ids into a different filename.
36
+ return validateTaskId(raw.trim().replace(/\.(yml|md)$/, ""));
37
+ }
package/docs/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Documentation
2
+
3
+ ## Getting Started
4
+
5
+ - [Concepts](concepts.md) -- Stashes, registries, asset types, and refs
6
+ - [Getting Started](getting-started.md) -- Quick setup guide
7
+ - [Local Development](local-development.md) -- Dogfooding akm while editing its own source
8
+ - [Agent Install Guide](agents/agent-install.md) -- Step-by-step automated install for agents
9
+ - [Stash Maker's Guide](stash-makers.md) -- Build and share a stash on GitHub, npm, or a network directory
10
+ - [Wikis](wikis.md) -- Multi-wiki knowledge bases (Karpathy-style)
11
+
12
+ ## Configuration & Data
13
+
14
+ - [Configuration](configuration.md) -- Providers, settings, and Ollama setup
15
+ - [Configuration](configuration.md#engines) -- Named LLM and agent engines
16
+ - [Data & Telemetry](data-and-telemetry.md) -- Exactly what akm reads and writes on your machine (no remote telemetry)
17
+
18
+ ## Features
19
+
20
+ - [Workflows](features/workflows.md) -- Structured, resumable multi-step procedures
21
+ - [Search & Discovery](features/search-discovery.md) -- Finding assets without knowing their exact name
22
+ - [Knowledge Management](features/knowledge-management.md) -- Lessons, incidents, and research as first-class assets
23
+ - [Sources & Registries](features/sources-registries.md) -- Where assets come from and how to find more
24
+ - [Wiki Snapshot Fetchers](features/wiki-snapshot-fetchers.md) -- Pluggable fetchers for URL-based knowledge reads
25
+ - [Agent Integration](features/agent-integration.md) -- Wiring akm into any shell-capable coding agent
26
+ - [The Improvement Loop](features/improvement-loop.md) -- How the stash adapts from usage and feedback
27
+
28
+ ## Upgrading
29
+
30
+ - [Roadmap](roadmap.md) -- High-level focus for the 0.9 and 1.0 releases
31
+ - [v0.8 -> v0.9 migration guide](migration/v0.8-to-v0.9.md) -- Current-cycle breaking changes
32
+ - [Release notes (latest: 0.9.0)](migration/release-notes/0.9.0.md) -- Per-release notes; see the [release-notes index](migration/release-notes/README.md) for every version
33
+ - [v0.5 -> v0.6 migration guide](migration/v0.5-to-v0.6.md) -- Every breaking change with before/after code, publisher checklist, and troubleshooting
34
+
35
+ ## Reference
36
+
37
+ - [CLI](cli.md) -- All `akm` commands and flags
38
+ - [Registry](registry.md) -- Registries, search, hosting, and managing sources
39
+ - [akm-eval](akm-eval.md) -- Standalone toolkit for measuring whether `akm improve` is working
40
+
41
+ ## Agents
42
+
43
+ - [AGENTS.md](agents/AGENTS.md) -- The system-prompt reference agents load to use akm
44
+ - [Curate Workmap](agents/curate-workmap.md) -- Read before changing `akm curate` ranking or output
45
+
46
+ ## Architecture
47
+
48
+ - [Architecture](technical/architecture.md) -- How akm's sources, cache, index, and registries fit together
49
+ - [Runtime Boundary Design](architecture/runtime-boundary-design.md) -- Isolating `bun:sqlite`/`Bun.*` from the core
50
+ - [Brain Workflow (diagram)](architecture/brain-workflow.html) -- Visual map of the improve/self-learning loop
51
+
52
+ ## Example Stash
53
+
54
+ - [Example Stash](example-stash/README.md) -- A documentation-backed example stash showing how asset types fit together
55
+
56
+ ## Analysis
57
+
58
+ - [Indexer Vertical Slice Refactor Plan](analysis/indexer-vertical-slice-refactor-plan.md)
59
+ - [Indexer Refactor Review (Expert Options)](analysis/indexer-refactor-expert-options.md)
60
+
61
+ ## Design (unshipped work)
62
+
63
+ - [Self-Improvement, Self-Learning & Memory Reference Index](design/self-improvement-learning-memory-reference-index.md) -- Master index for every unshipped improve/self-learning/memory design doc, with a subsystem-to-doc status table
64
+
65
+ Every doc under `docs/design/` must carry a `Status` / `Supersedes` / `Date` header (pre-existing docs are grandfathered until next touched). When a design ships, the shipping PR moves it to `docs/archive/` in the same PR.
66
+
67
+ ## Archive
68
+
69
+ - [Archive](archive/README.md) -- Design and implementation plans whose work has already shipped, retained as ADR-style records
70
+
71
+ ## Internals (technical/)
72
+
73
+ - [Filesystem](technical/filesystem.md) -- Directory layout plus `.stash.json` deprecation and migration notes
74
+ - [Search](technical/search.md) -- Hybrid search architecture and scoring
75
+ - [Indexing](technical/indexing.md) -- How the search index is built
76
+ - [Classification](technical/classification.md) -- Matcher and renderer behavior
77
+ - [Storage Locations](technical/storage-locations.md) -- Authoritative inventory of every on-disk read/write path
78
+ - [Improve Workflow](technical/improve-workflow.md) -- `akm improve` command surface and pipeline reference
79
+ - [Health Advisories](technical/health-advisories.md) -- `akm health` advisory-to-action map for operators
80
+ - [Fresh-Host Rebuild Runbook](technical/fresh-host-rebuild-runbook.md) -- Rebuild an akm install on a new machine
81
+ - [Ranking Ablation & Saturation Analysis](technical/ranking-ablation-and-saturation-analysis.md) -- Reproducible contributor-ablation measurement and the score-saturation trap
82
+ - [Functional Contract Patterns](technical/functional-contract-patterns.md) -- Quick reference for contributor pipelines and small process contracts
83
+ - [Test Coverage Guide](technical/test-coverage-guide.md) -- High-value testing areas
84
+ - [Testing Workflow](technical/testing-workflow.md) -- End-to-end, Docker, deployment, and upgrade validation
85
+ - [Ref Format](technical/ref.md) -- Wire format for asset references
86
+ - [Core Principles](technical/akm-core-principles.md) -- Design principles and constraints
87
+ - [Claude Code workflows vs. akm workflows](technical/claude-code-vs-akm-workflows.md) -- Comparing the two things that share a name
88
+ - [Workflow source and IR](features/workflows.md) -- Current workflow execution model
89
+
90
+ ## Official Ecosystem Repositories
91
+
92
+ - [itlackey/akm-stash](https://github.com/itlackey/akm-stash) -- the official onboarding stash with ready-made assets you can install with `akm add`
93
+ - [itlackey/akm-registry](https://github.com/itlackey/akm-registry) -- the official registry index that powers built-in discovery
94
+ - [itlackey/akm-plugins](https://github.com/itlackey/akm-plugins) -- optional integrations for tools like OpenCode
95
+ - [itlackey/akm-bench](https://github.com/itlackey/akm-bench) -- the standalone benchmark and evaluation repo for akm
96
+
97
+ ## Posts
98
+
99
+ - [Blog posts](posts/) -- Articles and posts about akm
100
+
101
+ ---
102
+
103
+ New docs, in five lines: keep one current-truth doc per subsystem, don't fork a second one. Unshipped designs live in `docs/design/` with a mandatory `Status` / `Supersedes` / `Date` header. The PR that ships the design moves its doc to `docs/archive/` in that same PR. Cite code by symbol and memories by search-terms -- not line numbers or exact refs, both rot. Nothing in `docs/` may reference `.plans/` (it's scratch -- promote the content or drop the link); no new improve-analysis docs until the 30-clean-day gate.
@@ -7,8 +7,10 @@ normal use.
7
7
 
8
8
  Key operator-facing changes:
9
9
 
10
- - Node.js is supported again for the published CLI. Install with Bun, Node.js
11
- >= 20.12, or the prebuilt binary.
10
+ - The published npm package requires Node.js >= 20.12 as its cross-platform
11
+ bootstrap. When a working Bun >= 1.0 is also on `PATH`, the launcher prefers
12
+ Bun after bootstrap; old, unusable, or absent Bun installations fall back to
13
+ Node.js. The standalone binaries are runtime-free.
12
14
  - The legacy `vault` asset type and `akm vault ...` command family are gone.
13
15
  Use `env:` for whole `.env` groups and `secret:` for a single sensitive
14
16
  value.
@@ -25,25 +25,34 @@ Use this package-manager/manual boundary procedure instead:
25
25
  package; a standalone operator should retain the old executable. Keep the
26
26
  independent data backup in either case.
27
27
  5. Invoke the newly installed or staged 0.9 binary, whose migration startup
28
- bypass can read the old installation without loading its config normally:
28
+ bypass can read the old installation without loading its config normally.
29
+ 6. After apply succeeds, run `akm tasks sync` with that same 0.9 binary and
30
+ inspect `skipped` before restarting schedulers. Sync registers the migrated
31
+ task view and quarantines the obsolete published backup task described below.
29
32
 
30
33
  Package-manager installation examples for step 4:
31
34
 
35
+ Package-manager installs require Node.js >= 20.12. If Bun >= 1.0 is also on
36
+ `PATH`, the installed launcher prefers Bun after Node.js bootstraps it.
37
+
32
38
  ```sh
33
39
  npm install -g akm-cli@0.9.0
34
- # or: bun install -g akm-cli@0.9.0
35
40
  # or: pnpm add -g akm-cli@0.9.0
36
41
  ```
37
42
 
43
+ Commands for steps 5 and 6:
44
+
38
45
  ```sh
39
46
  # Package-manager install: this `akm` is now the 0.9 binary.
40
47
  akm migrate status --config ./prepared-0.9.json
41
48
  akm migrate apply --config ./prepared-0.9.json --dry-run
42
49
  akm migrate apply --config ./prepared-0.9.json
50
+ akm tasks sync
43
51
 
44
52
  # Or invoke a checksummed staged standalone binary explicitly.
45
53
  ./akm-0.9 migrate status --config ./prepared-0.9.json
46
54
  ./akm-0.9 migrate apply --config ./prepared-0.9.json
55
+ ./akm-0.9 tasks sync
47
56
  ```
48
57
 
49
58
  Status and dry-run perform the same read-only eligibility checks and report the
@@ -113,8 +122,10 @@ Replace `profiles.llm.<name>` and `profiles.agent.<name>` with one
113
122
  Do not reuse a colliding LLM and agent profile name without deciding which new
114
123
  engine names make the distinction clear. AKM cannot safely infer that choice.
115
124
 
116
- Task files are strict YAML v2. Add `version: 2`, replace prompt `profile:` with
117
- `engine:`, and retain only fields valid for the target:
125
+ New task files use strict YAML v2. Existing valid 0.8 task files continue to
126
+ load without being rewritten: AKM normalizes their missing/`1` version,
127
+ prompt `profile:` field, permissive scalar forms, and `akm improve --profile`
128
+ commands in memory. Update user-authored files to `version: 2` when editing them:
118
129
 
119
130
  ```yaml
120
131
  version: 2
@@ -128,11 +139,55 @@ enabled: true
128
139
 
129
140
  Prompt tasks may use `engine`, `model`, `timeoutMs`, and `llm`; command tasks
130
141
  may use `timeoutMs`; workflow tasks may use `params`. Unknown and wrong-target
131
- keys are errors. `akm tasks list`, `sync`, and `doctor` report stale v1 files;
132
- `show`, `run`, `enable`, and `disable` reject them without mutation. Regenerate
133
- the known default improve tasks or replace `akm improve --profile <name>` with
134
- `akm improve --strategy <name>` yourself. AKM never rewrites arbitrary shell
135
- commands.
142
+ keys are errors in v2. Unsupported future versions are reported by `akm tasks
143
+ list`, `sync`, and `doctor`. The 0.8 reader normalization changes only the
144
+ removed AKM spellings; arbitrary shell commands and task files are never
145
+ rewritten.
146
+
147
+ For 0.8 command tasks, syntax migration and self-invocation routing are separate.
148
+ `--profile` is lowered only for a PATH-selected bare `akm`/`akm.exe`, including
149
+ when it follows supported `env` options and assignments. The scanner recognizes
150
+ citty-valid global forms before `improve`, including `--no-quiet`,
151
+ `--no-verbose`, `--quiet=false`, `--verbose=false`, and value options such as
152
+ `--format json`. An explicit `./akm`, `/opt/vendor/akm`, or other executable path
153
+ is operator-owned: it keeps selecting that exact binary and its command argv is
154
+ retained exactly. In particular, AKM does not change syntax sent to a retained
155
+ 0.8 binary. Version-2 commands receive no compatibility rewriting.
156
+
157
+ The published 0.8 core `backup.yml` is a special unsafe legacy definition. It
158
+ was enabled and ran `akm db backups`, but that command only listed snapshots; it
159
+ did not create a recurring backup. In 0.9, `tasks add` refuses the enabled exact
160
+ bare-self command before changing its source file or scheduler. `tasks sync`
161
+ reports it in `skipped`, does not install a missing entry, and disables an
162
+ existing scheduler entry while preserving the task file byte-for-byte. `tasks
163
+ enable` also refuses to re-enable it. This quarantine applies only when bare
164
+ `akm`/`akm.exe` is followed by exactly `db backups`; an explicit executable path
165
+ is operator-owned and is not quarantined. Use `akm backup create --for 0.9.0`
166
+ for an explicit migration recovery snapshot. Existing 0.8 data-directory backup
167
+ folders are left untouched by migration.
168
+
169
+ Task `enabled` state controls scheduler-originated execution, not explicit
170
+ operator invocation. `akm tasks run <id>` intentionally runs a disabled task so
171
+ manual catch-up definitions remain useful. Backend-generated invocations carry
172
+ the internal `--scheduled` marker and record a `disabled` result without running
173
+ the target. Do not use the manual command as a scheduler replacement.
174
+
175
+ Canonical task IDs contain only letters, digits, dots, underscores, and dashes,
176
+ start with a letter or digit, are at most 228 characters, omit `.yml`/`.yaml`,
177
+ and cannot use Windows device aliases such as `CON`, `NUL`, `COM1`, or `LPT1`
178
+ (including aliases followed by a dot). The 228-character limit is the final
179
+ portable bound after scheduler and filename overhead. These portability checks
180
+ apply on every platform. For command-line compatibility only, a trailing
181
+ lowercase `.yml` or legacy `.md` is stripped from an ID; a filename discovered
182
+ under `tasks/` must already be canonical and is never renamed. Sync skips a
183
+ non-portable file and disables any matching installed entry rather than guessing
184
+ a replacement ID.
185
+
186
+ Canonical migration preserves 0.8 `state.db` task-history rows and their log
187
+ paths. One historical detail cannot be recovered: published 0.8.14 stored
188
+ command-task history with `target_kind=prompt`. Because the durable row contains
189
+ no command marker, 0.9 preserves and exposes it as legacy prompt history rather
190
+ than inventing a command classification. New runs use the correct target kind.
136
191
 
137
192
  ## CLI rename table (old → new, removed 0.9.0)
138
193
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akm-cli",
3
- "version": "0.9.0-rc.3",
3
+ "version": "0.9.0-rc.4",
4
4
  "type": "module",
5
5
  "description": "akm (Agent Knowledge Management) — A package manager for AI agent skills, commands, tools, and knowledge. Works with Claude Code, OpenCode, Cursor, and any AI coding assistant.",
6
6
  "keywords": [
@@ -52,7 +52,7 @@
52
52
  "akm-migrate-storage": "dist/akm-migrate-storage"
53
53
  },
54
54
  "scripts": {
55
- "preinstall": "node -e \"var ua=process.env.npm_config_user_agent||'';var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>20||(v[0]===20&&(v[1]>12||(v[1]===12&&v[2]>=0)));if(process.versions.bun||ua.startsWith('bun/')||process.env.BUN_INSTALL||ok){process.exit(0)}console.error('\\n ERROR: akm-cli requires the Bun runtime (https://bun.sh), Node.js >= 20.12, or the prebuilt binary.\\n Install options:\\n 1. Bun: curl -fsSL https://bun.sh/install | bash && bun install -g akm-cli\\n 2. Node: upgrade to Node.js 20.12 or newer (https://nodejs.org)\\n 3. Binary: curl -fsSL https://github.com/itlackey/akm/releases/latest/download/install.sh | bash\\n');process.exit(1)\"",
55
+ "preinstall": "node -e \"var v=(process.versions.node||'0').split('.').map(function(n){return parseInt(n,10)||0});var ok=v[0]>20||(v[0]===20&&(v[1]>12||(v[1]===12&&v[2]>=0)));if(ok){process.exit(0)}console.error('\\n ERROR: the akm-cli npm package requires Node.js >= 20.12.\\n A working Bun >= 1.0 on PATH is optional and preferred for execution.\\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)\"",
56
56
  "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",
57
57
  "check": "bun run lint && bunx tsc --noEmit && bun run test:unit && bun run test:integration",
58
58
  "check:fast": "bun run lint && bunx tsc --noEmit && bun run test:unit",
@@ -94,7 +94,6 @@
94
94
  "sqlite-vec": "^0.1.9"
95
95
  },
96
96
  "engines": {
97
- "bun": ">=1.0.0",
98
97
  "node": ">=20.12.0"
99
98
  },
100
99
  "dependencies": {