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
125 lines (124 loc) • 6.07 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/.
import fs from "node:fs";
import path from "node:path";
import { parseBundleRef } from "../../../core/asset/asset-ref.js";
import { UsageError } from "../../../core/errors.js";
import { prepareTaskV3Execution } from "../../../tasks/prepare/prepare.js";
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
import { projectTaskSourceV4 } from "../../../tasks/source/project-v4.js";
import { workflowShellCommand } from "../../source-semantics.js";
import { freezeEnvironment, resolveOwnedAsset, workflowExecutionSource } from "../environment.js";
import { gitIdentity } from "../identity.js";
import { declaredParamNames, earlierStepIds, freezeExecSpec, } from "../step-values.js";
import { freezeTaskInputBindings } from "../task-bindings.js";
import { childWorkflowDispatch } from "./child-workflow.js";
import { commandResult } from "./command.js";
import { scriptResult } from "./script.js";
/** The rejection for a `with:` on a task that declares no inputs (or does not resolve). */
function noDeclaredInputsError(stepId, ref) {
return new UsageError(`Workflow step ${stepId} cannot pass with: to task target ${ref}; ${ref} declares no inputs.`, "COMPOSITION_INVALID");
}
/**
* Resolve and parse the composed task. An authored `with:` whose target does
* not even resolve cannot be proven a valid binding surface, so it is refused
* the same no-declared-inputs way; a genuine defect in the resolved task's own
* source propagates unchanged.
*/
async function resolveTaskForComposition(source, refInput, context) {
let owned;
let yaml;
try {
owned = await resolveOwnedAsset(refInput, "task", context);
yaml = fs.readFileSync(owned.file, "utf8");
}
catch (cause) {
if (source.with !== undefined)
throw noDeclaredInputsError(source.id, refInput);
throw cause;
}
const parsed = parseTaskSource({ yaml, filePath: owned.file, workspaceRoot: owned.root });
return { owned, task: projectTaskSourceV4(parsed.v4), contract: parsed.v4.inputs };
}
export async function taskDispatch(source, baseUnit, refInput, context) {
const { owned, task, contract } = await resolveTaskForComposition(source, refInput, context);
// Any authored `with:` (even `{}`) on a task that declares no inputs is COMPOSITION_INVALID.
if (source.with !== undefined && contract === undefined) {
throw noDeclaredInputsError(source.id, refInput);
}
// This step's own `with:` against this task's own declared inputs (none → no bindings).
const bindings = freezeTaskInputBindings({
stepId: source.id,
targetRef: refInput,
with: source.with,
contract: contract ?? {},
earlierStepIds: earlierStepIds(context.plan, source.id),
declaredParamNames: declaredParamNames(context.plan),
});
const prepared = await prepareTaskV3Execution(task, {
taskId: parseBundleRef(owned.ref).conceptId.slice("tasks/".length),
taskRef: owned.ref,
bundleName: owned.bundle,
bundleRoot: owned.root,
config: context.config,
commandSourceLoader: (ref, kind) => workflowExecutionSource(ref, kind, context),
resolveAsset: async ({ ref, type }) => {
const target = await resolveOwnedAsset(ref, type, context);
return { file: target.file, bundleRoot: target.root };
},
});
if (prepared.kind === "workflow") {
// A task whose own target is a workflow composes it as a child. This
// task's effective inputs, already classified against its own contract,
// are re-bound against the child's `params:` — never round-tripped through
// the `with:` grammar, which would reinterpret a literal shaped like
// `{from: ...}` as a reference.
return childWorkflowDispatch({
source,
baseUnit,
childRefInput: prepared.ref,
context,
via: "task",
taskRef: prepared.taskRef,
authoredInputs: { kind: "bindings", value: bindings },
});
}
const taskLiterals = Object.entries(prepared.environment).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
if (prepared.kind === "command") {
return withInputBindings(commandResult(source, baseUnit, prepared.invocation, context, taskLiterals), bindings);
}
if (prepared.kind === "shell") {
const authoredExec = {
command: workflowShellCommand(prepared.shell, prepared.command),
...(prepared.cwdIdentity.realCwd !== prepared.cwdIdentity.realRoot
? { cwd: path.relative(prepared.cwdIdentity.realRoot, prepared.cwdIdentity.realCwd) }
: {}),
};
const exec = freezeExecSpec(source, authoredExec, context);
const environment = Object.freeze([...taskLiterals, ...freezeEnvironment(source, authoredExec, context)]);
const target = Object.freeze({
kind: "shell",
contentHash: "",
exec,
cwdIdentity: prepared.cwdIdentity,
...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
});
return withInputBindings({
target,
environment,
unit: { ...baseUnit, exec: authoredExec },
instructions: source.instructions ?? `Run task ${owned.ref}.`,
}, bindings);
}
return withInputBindings(scriptResult(source, baseUnit, prepared, context, taskLiterals), bindings);
}
/** Attach the frozen inputBindings to whichever target shape taskDispatch produced. Absent, never [], when empty. */
function withInputBindings(resolved, bindings) {
if (bindings.length === 0)
return resolved;
return {
...resolved,
target: Object.freeze({ ...resolved.target, inputBindings: bindings }),
};
}