UNPKG

akm-cli

Version:

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 Cur

177 lines (176 loc) • 8.87 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * `akm task validate <path>` (#907) — parse ONE task file by filesystem * path, not a concept ref/id, and report the same diagnostic `akm task * sync` would produce for it. The file need not live in any configured * bundle: unlike every other `akm task` subcommand, this one never resolves * a bundle/adapter/concept id at all — it reads exactly the path it was * given and classifies it. * * Reuses the exact version router `parseTaskSource` * (`src/tasks/source/parse-task-source.ts`) every other task-source reader * uses (`akm task sync`'s `compileTaskSources` included) — this module never * forks a second parser. `readBoundedTaskSourceYaml` / * `peekTaskSourceVersion` / `own` are the SAME front-end helpers the router * calls first; they are used here only to recover the file's DECLARED schema * version for the report and to tell `blocked` from `invalid`. * * Beyond parsing, this module also runs the SAME two per-source gates * `akm task sync`'s `compileTaskSources` runs before it ever installs a * schedule — `assertTaskScheduleInputsSatisfyContract` and * `assertTaskScheduleCronValid` (both extracted from `scheduler-sync.ts` for * exactly this reuse) — so a file `sync` would reject can never * be reported `valid` here. Cron dialect is checked against * `backendNameForPlatform()`, the same platform default `sync` falls back * to whenever it has no injected/native-inspected backend to hand (see * `akmTasksAdd`, `src/commands/tasks/tasks.ts`); a bare file was never * installed anywhere; there is no native scheduler state to inspect for it. * * Deliberately DOES NOT call `prepareTaskV3Execution` (the function * `compileTaskSources` calls between those two gates) or resolve an * execution engine: that path resolves a composed command/persona ref * against the local index and lowers the task's cascade-composed * engine/model — both of which assume a real, indexed bundle and a * configured engine. A bare file passed to `validate` has neither, so * running that step would make an otherwise-valid command-kind task report * `invalid` on any machine with no engine configured. `resolved` * is therefore the compiled task shape `sync` itself would build a * scheduler binding from — id, the compiled v4 `version`, `target` * (`uses`/`run`), the declared `inputs` contract, and `schedule` bindings — * never an execution-lowered plan. * * Outcome classification (mirrors `parse-task-source.ts`'s own routing * table in its header, extended for the two gates above): * - `valid` — parses as task source v4 and passes both sync gates. * - `blocked` — declared `version: 2` or `3`, or a `version: 4` * document still carrying the retired * `schedule[].enabled`: what `akm migrate apply` * rewrites. The runtime reads only v4 (#987), so the * reason names that command. * - `invalid` — the document declares SOME version (`4`, or anything * other than 2/3/4) but fails to parse/validate, OR it * parsed but fails one of the two sync gates above, OR * the YAML itself does not parse at all (a genuine * syntax error, not merely a non-task shape) — reported * with the parser's own reason. * - `not-a-task` — the document parses as YAML but never declares a * `version:` field at all (or isn't a YAML mapping) — * the strongest signal available that the file was * never intended as a task source in the first place. */ import fs from "node:fs"; import path from "node:path"; import { UsageError } from "../../core/errors.js"; import { backendNameForPlatform } from "../../tasks/backends/index.js"; import { assertTaskScheduleCronValid, assertTaskScheduleInputsSatisfyContract } from "../../tasks/scheduler-sync.js"; import { own, readBoundedTaskSourceYaml } from "../../tasks/source/bounded-document.js"; import { parseTaskSource, peekTaskSourceVersion, v4ScheduleHasRetiredEnabledKey, } from "../../tasks/source/parse-task-source.js"; /** True when `root` is a YAML mapping that itself declares a `version:` key, regardless of that key's type/value. */ function declaresVersionKey(root) { return root !== null && typeof root === "object" && !Array.isArray(root) && own(root, "version"); } function buildResolved(id, v4) { return { id, version: v4.version, ...(v4.name !== undefined ? { name: v4.name } : {}), ...(v4.description !== undefined ? { description: v4.description } : {}), target: v4.target, inputs: v4.inputs ?? {}, schedule: v4.schedule, }; } export async function akmTaskValidate(filePath) { const resolvedPath = path.resolve(filePath); let stat; try { stat = fs.statSync(resolvedPath); } catch { throw new UsageError(`Task file not found: ${JSON.stringify(filePath)}.`, "INVALID_FLAG_VALUE"); } if (!stat.isFile()) { throw new UsageError(`${JSON.stringify(filePath)} is not a regular file.`, "INVALID_FLAG_VALUE"); } let yaml; try { yaml = fs.readFileSync(resolvedPath, "utf8"); } catch (cause) { throw new UsageError(`Task file ${JSON.stringify(filePath)} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`, "INVALID_FLAG_VALUE"); } // Peek the declared version BEFORE the real parse, using the identical // bounded YAML front end `parseTaskSource` calls internally — never a // second/looser YAML reader. A front-end failure here (unparseable YAML, // not a mapping, exceeds a resource bound) means the document itself does // not parse at all — tracked as `peekFailed` so that case reports // `invalid`, never `not-a-task` (`not-a-task` is reserved for // YAML that DOES parse but never declared a task shape). The real parse // below throws the identical error either way, so nothing is lost by // swallowing it here. let root; let peekFailed = false; try { root = readBoundedTaskSourceYaml({ yaml, filePath: resolvedPath }, { sourceLabel: "task source" }).root; } catch { peekFailed = true; } const declaredVersion = peekFailed ? undefined : peekTaskSourceVersion(root); const hasVersionKey = !peekFailed && declaresVersionKey(root); const workspaceRoot = path.dirname(resolvedPath); const backend = backendNameForPlatform(); let parsed; try { parsed = parseTaskSource({ yaml, filePath: resolvedPath, workspaceRoot }); } catch (cause) { if (!(cause instanceof UsageError)) throw cause; const reason = cause.message; // What `akm migrate apply` rewrites is the only refusal for these // shapes (`parse-task-source.ts`), so no message-text sniffing is needed. if (declaredVersion === 2 || declaredVersion === 3 || (declaredVersion === 4 && v4ScheduleHasRetiredEnabledKey(root))) { return { ok: false, path: resolvedPath, sourceVersion: declaredVersion, outcome: "blocked", reason }; } if (peekFailed) { return { ok: false, path: resolvedPath, outcome: "invalid", reason }; } if (!hasVersionKey) { return { ok: false, path: resolvedPath, outcome: "not-a-task", reason }; } return { ok: false, path: resolvedPath, ...(declaredVersion !== undefined ? { sourceVersion: declaredVersion } : {}), outcome: "invalid", reason, }; } // Success is unreachable from any path that leaves `declaredVersion` // undefined — the router requires a numeric version 4 to reach here. const sourceVersion = declaredVersion ?? 4; // The document itself parsed — now the two gates `compileTaskSources` // runs before accepting it. A violation here is `invalid`. try { assertTaskScheduleInputsSatisfyContract(parsed.v4, resolvedPath); assertTaskScheduleCronValid(parsed.v4, backend); } catch (cause) { if (!(cause instanceof UsageError)) throw cause; return { ok: false, path: resolvedPath, sourceVersion, outcome: "invalid", reason: cause.message }; } const id = path.parse(resolvedPath).name; return { ok: true, path: resolvedPath, sourceVersion, outcome: "valid", resolved: buildResolved(id, parsed.v4), }; }