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
340 lines (339 loc) • 16.7 kB
JavaScript
// 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/.
/**
* Plain-text renderers for `akm workflow *` commands.
*
* Split out of `helpers.ts` (formerly 1418 lines / 59 fns) as its own
* sibling module: the workflow list/status/run/create/resume renderers form a
* cohesive, self-contained cluster (they call only each other, never
* formatters from other domains).
*/
export function formatWorkflowListPlain(result) {
const runs = Array.isArray(result.runs) ? result.runs : [];
if (runs.length === 0) {
return "No workflow runs in the current working scope. Start one with `akm workflow run workflows/<name>` or author one with `akm workflow create <name>`.";
}
return runs
.map((run) => {
const id = typeof run.id === "string" ? run.id : "unknown";
// Fallback matches the `id`/`status` convention just below (plain
// "unknown", not a `type:name`-shaped placeholder — that shape reads as
// a ref in the dead colon grammar, which this fallback must not model).
const ref = typeof run.workflowRef === "string" ? run.workflowRef : "unknown";
const status = typeof run.status === "string" ? run.status : "unknown";
const currentStep = typeof run.currentStepId === "string" ? ` (current: ${run.currentStepId})` : "";
return `${id} ${ref} [${status}]${currentStep}`;
})
.join("\n");
}
/** §4.5's glyph table (P3b) — drawn from the repo's existing vocabulary (status-list.ts's ✗/✓, proposal-format.ts's →). */
const CHILD_STATUS_GLYPHS = {
completed: "✓",
active: "→",
blocked: "⚠",
failed: "✗",
};
/**
* Render one `children:` tree node (P3b, spec §4.5) plus its own children,
* recursively, indenting two spaces per level. Order is SPAWN order (the
* envelope's own array order) — the tree is structural, never severity-sorted.
*/
function renderChildNode(child, depth, lines) {
const indent = " ".repeat(depth + 1);
const runId = typeof child.runId === "string" ? child.runId : "unknown";
const ref = typeof child.workflowRef === "string" ? child.workflowRef : "unknown";
const status = typeof child.status === "string" ? child.status : "unknown";
const glyph = CHILD_STATUS_GLYPHS[status] ?? "?";
const stepId = typeof child.stepId === "string" ? ` (step "${child.stepId}")` : "";
lines.push(`${indent}- ${glyph} ${runId} ${ref} [${status}]${stepId}`);
const resume = typeof child.resume === "object" && child.resume !== null ? child.resume : undefined;
if (resume) {
lines.push(`${indent} resume: ${typeof resume.command === "string" ? resume.command : ""}`);
lines.push(`${indent} then: ${typeof resume.then === "string" ? resume.then : ""}`);
}
const nested = Array.isArray(child.children) ? child.children : [];
for (const grandchild of nested)
renderChildNode(grandchild, depth + 1, lines);
}
export function formatWorkflowStatusPlain(result) {
const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
const workflow = typeof result.workflow === "object" && result.workflow !== null
? result.workflow
: undefined;
if (!run || !workflow)
return null;
const lines = [
`workflow: ${String(workflow.ref ?? "unknown")}`,
`run: ${String(run.id ?? "unknown")}`,
`title: ${String(run.workflowTitle ?? workflow.title ?? "Workflow")}`,
`status: ${String(run.status ?? "unknown")}`,
];
if (run.currentStepId)
lines.push(`currentStep: ${String(run.currentStepId)}`);
const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
if (steps.length > 0) {
lines.push("steps:");
for (const step of steps) {
const title = typeof step.title === "string" ? step.title : "Untitled step";
const id = typeof step.id === "string" ? step.id : "unknown";
const status = typeof step.status === "string" ? step.status : "unknown";
lines.push(` - ${title} [${id}] (${status})`);
if (typeof step.notes === "string" && step.notes.trim()) {
lines.push(` notes: ${step.notes}`);
}
}
}
// The parent-child status tree (P3b, spec §4.5): renders ONLY when
// `children` is a non-empty array, immediately after `steps:` and before
// `units:`. Absent for a childless run — byte-identical to pre-P3b text
// (row B-33, PRESERVE).
const children = Array.isArray(result.children) ? result.children : undefined;
if (children && children.length > 0) {
lines.push("children:");
for (const child of children)
renderChildNode(child, 0, lines);
}
// `workflow status --units` (#22): the honest per-unit diagnostic surface —
// failure_reason plus any journaled result/error text. Diagnostics only; the
// deterministic step evidence above is unaffected.
const units = Array.isArray(result.units) ? result.units : undefined;
if (units) {
lines.push("");
lines.push(units.length > 0 ? "units:" : "units: (none journaled)");
for (const unit of units) {
const id = typeof unit.unitId === "string" ? unit.unitId : "unknown";
const status = typeof unit.status === "string" ? unit.status : "unknown";
const node = typeof unit.nodeId === "string" ? unit.nodeId : "";
const attempts = typeof unit.attempts === "number" ? unit.attempts : undefined;
const suffix = attempts !== undefined && attempts > 1 ? `, attempt ${attempts}` : "";
lines.push(` - ${id} [${node}] (${status}${suffix})`);
if (typeof unit.failureReason === "string" && unit.failureReason.trim()) {
lines.push(` failure_reason: ${unit.failureReason}`);
}
if (typeof unit.diagnostic === "string" && unit.diagnostic.trim()) {
const diagLines = unit.diagnostic.split("\n");
lines.push(` diagnostic: ${diagLines[0]}`);
for (const diagLine of diagLines.slice(1))
lines.push(` ${diagLine}`);
}
}
}
return lines.join("\n");
}
/**
* One `! lowering[<severity>] <code> (<adapter>[; <field>]): <message>` line
* from a `{code, severity, adapter, field?, message}` lowering notice, or
* `null` when `value` does not carry that shape. Shared by
* `formatWorkflowRunPlain` (dispatch-time notices) and `formatWorkflowPlanPlain`
* (freeze-time notices, P3b §4.6, B-57) — ONE projection, rendered the same
* way regardless of which invocation computed the notice.
*/
function renderLoweringNoticeLine(value) {
if (typeof value !== "object" || value === null)
return null;
const notice = value;
if (typeof notice.code !== "string" ||
typeof notice.severity !== "string" ||
typeof notice.adapter !== "string" ||
typeof notice.message !== "string") {
return null;
}
const field = typeof notice.field === "string" && notice.field ? `; ${notice.field}` : "";
return `! lowering[${notice.severity}] ${notice.code} (${notice.adapter}${field}): ${notice.message}`;
}
export function formatWorkflowRunPlain(result) {
const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
if (!run)
return null;
const lines = [];
// #919: `workflow run <ref>` silently resuming an existing active run (the
// #485 concurrency guard) is now announced up front, with the escape hatch.
if (result.resumed === true) {
lines.push(`resuming existing run ${String(run.id ?? "unknown")} for ${String(run.workflowRef ?? "this ref")}; ` +
`pass --new to start a fresh run`);
}
lines.push(`run: ${String(run.id ?? "unknown")}`, `status: ${String(run.status ?? "unknown")}`);
// Creation-time notices (e.g. the implicit engine fallback) must survive the
// text renderer: JSON/YAML pass them through, and dropping them here would
// hide the announcement from the DEFAULT output mode — where it matters most.
for (const warning of Array.isArray(result.warnings) ? result.warnings : [])
lines.push(`! ${String(warning)}`);
// Live-only common-lowerer diagnostics are already sanitized and deduped by
// the workflow engine. Render their typed headline (not opaque details) so
// default text output has the same observability as JSON without widening a
// durable workflow schema.
for (const value of Array.isArray(result.notices) ? result.notices : []) {
const line = renderLoweringNoticeLine(value);
if (line)
lines.push(line);
}
const executed = Array.isArray(result.executed) ? result.executed : [];
if (executed.length === 0) {
lines.push("executed: (no steps — run was already done or blocked)");
}
else {
lines.push("executed:");
for (const step of executed) {
const marker = step.ok === true ? "ok" : "FAILED";
lines.push(` - ${String(step.stepId ?? "?")} [${marker}] units: ${String(step.unitCount ?? 0)}` +
(Number(step.failedUnits ?? 0) > 0 ? ` (${String(step.failedUnits)} failed)` : ""));
if (typeof step.summary === "string" && step.summary.trim()) {
lines.push(` ${step.summary}`);
}
}
}
const gate = typeof result.gateRejection === "object" && result.gateRejection !== null
? result.gateRejection
: undefined;
if (gate) {
lines.push(`gate rejected step ${String(gate.stepId ?? "?")}: ${String(gate.feedback ?? "")}`);
const missing = Array.isArray(gate.missing) ? gate.missing : [];
for (const item of missing)
lines.push(` missing: ${String(item)}`);
}
if (result.aborted === true) {
lines.push(result.timedOut === true
? "workflow timed out; the run remains resumable."
: "workflow interrupted; the run remains resumable.");
}
if (result.done === true)
lines.push("workflow completed.");
return lines.join("\n");
}
export function formatWorkflowCreatePlain(r) {
// `workflow create --print`: no file is written, just the template text
// (dropped `workflow template`'s output, moved onto this shape).
if (typeof r.template === "string")
return r.template;
if (r.ref && r.path) {
return `Created ${String(r.ref)} at ${String(r.path)}`;
}
return null;
}
export function formatWorkflowResumePlain(r) {
return formatWorkflowStatusPlain(r) ?? `Resumed workflow run ${String(r.id ?? r.runId ?? "?")}`;
}
/**
* A step's `inputBindings[]` (P3b §4.6's `with:` line) — a literal shows its
* value, a reference shows only its unresolved `from` (never a resolved
* value; B-52/B-53's closed print list explicitly allows a literal *task/
* child* input binding's `.value`, unlike a literal **environment** binding's).
*/
function formatInputBindingsList(bindings) {
return bindings
.map((binding) => {
const name = String(binding.name ?? "");
return binding.kind === "literal"
? `${name}=${JSON.stringify(binding.value)} (literal)`
: `${name} <- ${String(binding.from ?? "")} (reference)`;
})
.join(", ");
}
/**
* One `akm workflow plan` step line (P3b, spec §4.6), plus its child-workflow
* expansion's own steps, recursively, indented one level per composition
* boundary. A child step also carries the child's `planHash` on the step
* line itself, and (when present) a `with:` line for the step's own
* `inputBindings[]` and an `exports:` line for the child's declared output
* names — §4.6's worked example.
*/
function renderPlanStepLines(step, ordinal, lines, prefix = " ") {
const stepId = typeof step.stepId === "string" ? step.stepId : "unknown";
const targetKind = typeof step.targetKind === "string" ? `[${step.targetKind}]` : "";
const expansion = typeof step.expansion === "object" && step.expansion !== null ? step.expansion : {};
const via = typeof expansion.via === "string" ? expansion.via : "direct";
const childPlanHash = typeof expansion.childPlanHash === "string" ? expansion.childPlanHash : undefined;
const viaText = via === "task"
? `via ${String(expansion.taskRef ?? "")}`
: via === "child"
? `-> ${String(expansion.childRef ?? "")}${childPlanHash ? ` (plan ${childPlanHash})` : ""}`
: "direct";
lines.push(`${prefix}${ordinal}. ${stepId} ${targetKind} ${viaText}`.replace(/ +/g, " ").trimEnd());
const detailPrefix = `${prefix} `;
const inputBindings = Array.isArray(step.inputBindings) ? step.inputBindings : [];
if (inputBindings.length > 0) {
lines.push(`${detailPrefix}with: ${formatInputBindingsList(inputBindings)}`);
}
if (via === "child") {
const childOutputs = Array.isArray(expansion.childOutputs) ? expansion.childOutputs : [];
if (childOutputs.length > 0) {
lines.push(`${detailPrefix}exports: ${childOutputs.map(String).join(", ")}`);
}
const childSteps = Array.isArray(expansion.steps) ? expansion.steps : [];
for (const [index, childStep] of childSteps.entries()) {
renderPlanStepLines(childStep, index + 1, lines, `${prefix} ${ordinal}.`);
}
}
}
/**
* `akm workflow plan <ref>` text mode (P3b, spec §4.6): a human summary of
* the same compile+freeze data `--format json` returns — never a resolved
* env/secret value (B-52, B-53).
*/
export function formatWorkflowPlanPlain(result) {
if (typeof result.ref !== "string")
return null;
const sourceFormat = typeof result.sourceFormat === "string" ? result.sourceFormat : "unknown";
const irVersion = typeof result.irVersion === "number" ? result.irVersion : "unknown";
const planHash = typeof result.planHash === "string" ? result.planHash : "unknown";
const execution = typeof result.execution === "object" && result.execution !== null
? result.execution
: {};
const budget = typeof result.budget === "object" && result.budget !== null
? result.budget
: undefined;
const lines = [
`workflow: ${result.ref} (${sourceFormat})`,
`source: ${typeof result.sourcePath === "string" ? result.sourcePath : "unknown"}`,
`plan: irVersion ${irVersion}, hash ${planHash} (not published)`,
];
const limitsParts = [
`maxConcurrency ${typeof execution.maxConcurrency === "number" ? execution.maxConcurrency : "?"}`,
];
if (budget) {
const budgetParts = [];
if (typeof budget.maxUnits === "number")
budgetParts.push(`max_units ${budget.maxUnits}`);
if (typeof budget.maxTokens === "number")
budgetParts.push(`max_tokens ${budget.maxTokens}`);
if (budgetParts.length > 0)
limitsParts.push(`budget ${budgetParts.join(", ")}`);
}
lines.push(`limits: ${limitsParts.join("; ")}`);
const params = Array.isArray(result.params) ? result.params : undefined;
if (params && params.length > 0)
lines.push(`params: ${params.join(", ")}`);
const outputs = typeof result.outputs === "object" && result.outputs !== null
? result.outputs
: undefined;
if (outputs) {
for (const [name, declaration] of Object.entries(outputs)) {
const from = typeof declaration === "object" && declaration !== null
? String(declaration.from ?? "")
: "";
lines.push(`outputs: ${name} <- ${from}`);
}
}
const steps = Array.isArray(result.steps) ? result.steps : [];
lines.push("steps:");
for (const [index, step] of steps.entries()) {
renderPlanStepLines(step, index + 1, lines);
}
const notices = Array.isArray(result.notices) ? result.notices : [];
if (notices.length > 0) {
lines.push("notices:");
for (const value of notices) {
const line = renderLoweringNoticeLine(value);
if (line)
lines.push(` ${line}`);
}
}
const warnings = Array.isArray(result.warnings) ? result.warnings : [];
if (warnings.length > 0) {
lines.push("warnings:");
for (const warning of warnings)
lines.push(` ! ${String(warning)}`);
}
return lines.join("\n");
}