akm-cli 0.9.0-rc.13 → 0.9.0-rc.14
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 +117 -23
- package/dist/akm-migrate +3 -1
- package/dist/assets/hints/cli-hints-full.md +3 -4
- package/dist/assets/hints/cli-hints-short.md +5 -5
- package/dist/assets/workflows/workflow-template.md +4 -3
- package/dist/cli/invocation.js +3 -2
- package/dist/cli/retired-commands.js +3 -0
- package/dist/cli/unknown-flags.js +226 -0
- package/dist/cli.js +19 -35
- package/dist/commands/agent/contribute-cli.js +15 -3
- package/dist/commands/feedback-cli.js +6 -3
- package/dist/commands/improve/collapse-detector.js +2 -3
- package/dist/commands/lint/base-linter.js +4 -16
- package/dist/commands/lint/index.js +13 -13
- package/dist/commands/log.js +6 -1
- package/dist/commands/migration-tool.js +4 -5
- package/dist/commands/observability-cli.js +1 -1
- package/dist/commands/proposal/repository.js +5 -5
- package/dist/commands/read/knowledge.js +2 -0
- package/dist/commands/registry-cli.js +5 -3
- package/dist/commands/sources/add-cli.js +6 -6
- package/dist/commands/sources/self-update.js +30 -7
- package/dist/commands/sources/source-add.js +17 -2
- package/dist/commands/tasks/tasks.js +8 -3
- package/dist/commands/workflow-cli.js +142 -119
- package/dist/core/adapter/adapters/akm-lint.js +17 -13
- package/dist/core/adapter/adapters/akm-task-adapter.js +14 -11
- package/dist/core/asset/akm-markdown.js +41 -8
- package/dist/core/asset/frontmatter.js +22 -0
- package/dist/core/asset/resolve-ref.js +23 -3
- package/dist/core/common.js +45 -2
- package/dist/core/config/config-schema.js +8 -0
- package/dist/core/config/schema/experimental.js +5 -13
- package/dist/core/config/schema/sources-bundles.js +11 -0
- package/dist/core/config/schema/workflow.js +3 -1
- package/dist/core/errors.js +5 -0
- package/dist/core/logs-db.js +2 -1
- package/dist/core/parse.js +4 -1
- package/dist/core/state/migrations.js +11 -14
- package/dist/core/state-db.js +4 -6
- package/dist/core/subprocess.js +6 -4
- package/dist/core/type-presentation.js +1 -1
- package/dist/indexer/indexer.js +16 -1
- package/dist/indexer/search/search-source.js +1 -1
- package/dist/indexer/walk/matchers.js +3 -1
- package/dist/integrations/agent/config.js +2 -2
- package/dist/integrations/agent/detect.js +49 -19
- package/dist/integrations/agent/profiles.js +10 -0
- package/dist/integrations/agent/spawn.js +1 -2
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +7 -3
- package/dist/integrations/lockfile.js +11 -56
- package/dist/output/shapes/passthrough.js +0 -4
- package/dist/output/text/command-format.js +3 -3
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/show-directives.js +4 -6
- package/dist/output/text/workflow-format.js +7 -62
- package/dist/output/text/workflow.js +1 -5
- package/dist/scripts/akm-migrate-node.js +58773 -0
- package/dist/scripts/akm-migrate.js +33976 -11391
- package/dist/setup/detect.js +40 -15
- package/dist/setup/setup.js +1 -1
- package/dist/sources/providers/git-stash.js +4 -2
- package/dist/sources/providers/website.js +5 -0
- package/dist/sources/snapshot-fetchers/bluesky.js +146 -0
- package/dist/sources/snapshot-fetchers/content-extract.js +370 -0
- package/dist/sources/snapshot-fetchers/fetcher-util.js +40 -0
- package/dist/sources/snapshot-fetchers/host-guard.js +199 -0
- package/dist/sources/snapshot-fetchers/registry.js +10 -1
- package/dist/sources/snapshot-fetchers/robots.js +348 -0
- package/dist/sources/snapshot-fetchers/rss.js +279 -0
- package/dist/sources/snapshot-fetchers/secret-seam.js +42 -0
- package/dist/sources/snapshot-fetchers/website-ingest.js +488 -257
- package/dist/sources/snapshot-fetchers/x.js +193 -0
- package/dist/storage/engines/sqlite-migrations.js +22 -107
- package/dist/tasks/runner.js +19 -16
- package/dist/tasks/schema.js +24 -1
- package/dist/workflows/exec/brief.js +1 -1
- package/dist/workflows/exec/frozen-judge.js +28 -2
- package/dist/workflows/exec/native-executor.js +18 -1
- package/dist/workflows/exec/report.js +11 -4
- package/dist/workflows/exec/run-workflow.js +103 -44
- package/dist/workflows/exec/step-work.js +19 -18
- package/dist/workflows/exec/unit-dispatch.js +4 -0
- package/dist/workflows/exec/workflow-engine-gate.js +11 -13
- package/dist/workflows/ir/compile.js +2 -2
- package/dist/workflows/ir/freeze.js +16 -8
- package/dist/workflows/ir/params.js +134 -10
- package/dist/workflows/ir/plan-hash.js +1 -1
- package/dist/workflows/ir/schema.js +6 -2
- package/dist/workflows/renderer.js +2 -2
- package/dist/workflows/runtime/checkin.js +3 -3
- package/dist/workflows/runtime/runs.js +50 -29
- package/dist/workflows/validate-summary.js +30 -14
- package/docs/migration/release-notes/0.9.0.md +31 -4
- package/docs/migration/v0.8-to-v0.9.md +69 -100
- package/docs/reference/data-and-telemetry.md +5 -5
- package/package.json +6 -2
- package/schemas/akm-config.json +24 -0
- package/schemas/akm-workflow.json +1 -1
- package/dist/workflows/cli.js +0 -33
|
@@ -2,111 +2,22 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
/**
|
|
5
|
-
* `akm workflow` command family.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* form.
|
|
11
|
-
*
|
|
12
|
-
* 0.9.0 CLI overhaul (S5): `workflow template` is dropped — `workflow create
|
|
13
|
-
* --print` prints the same content without writing. `workflow validate` is
|
|
14
|
-
* dropped — `akm lint --type workflows` covers structural validation.
|
|
15
|
-
* `workflow watch` is dropped — poll `akm log --since '@offset:<id>' --run
|
|
16
|
-
* <run-id>` instead. Workflows are markdown-only (workflow-format-
|
|
17
|
-
* unification) — `workflow create <name>.yaml` is a usage error.
|
|
5
|
+
* `akm workflow` command family. `run` is the canonical start/resume/execute
|
|
6
|
+
* surface; the former public `start`, `next`, and `complete` lifecycle is gone.
|
|
7
|
+
* `brief`/`report` retain the experimental harness-neutral driver protocol.
|
|
8
|
+
* Workflows are markdown-only; authoring uses `create --print` and validation
|
|
9
|
+
* uses `akm lint --type workflows`.
|
|
18
10
|
*/
|
|
19
|
-
import { getParsedInvocation } from "../cli/invocation.js";
|
|
20
11
|
import { getStringArg } from "../cli/parse-args.js";
|
|
21
|
-
import { defineGroupCommand, defineJsonCommand, output } from "../cli/shared.js";
|
|
12
|
+
import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
|
|
22
13
|
import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
|
|
23
14
|
import { loadConfig } from "../core/config/config.js";
|
|
24
15
|
import { NotFoundError, UsageError } from "../core/errors.js";
|
|
25
16
|
import { akmIndex } from "../indexer/indexer.js";
|
|
26
17
|
import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
|
|
27
|
-
import { parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES } from "../workflows/cli.js";
|
|
28
18
|
import { requireWorkflowEngineEnabled } from "../workflows/exec/workflow-engine-gate.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
meta: {
|
|
32
|
-
name: "start",
|
|
33
|
-
description: "Start a new workflow run in the current working scope",
|
|
34
|
-
},
|
|
35
|
-
args: {
|
|
36
|
-
ref: { type: "positional", description: "Workflow ref (workflows/<name>)", required: true },
|
|
37
|
-
params: { type: "string", description: "Workflow parameters as a JSON object" },
|
|
38
|
-
force: {
|
|
39
|
-
type: "boolean",
|
|
40
|
-
description: "Allow a parallel run when an active run already exists in this scope",
|
|
41
|
-
default: false,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
async run({ args }) {
|
|
45
|
-
const result = await startWorkflowRun(args.ref, parseWorkflowJsonObject(args.params, "--params"), {
|
|
46
|
-
force: args.force === true,
|
|
47
|
-
});
|
|
48
|
-
output("workflow-start", result);
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
const workflowNextCommand = defineJsonCommand({
|
|
52
|
-
meta: {
|
|
53
|
-
name: "next",
|
|
54
|
-
description: "Show the next actionable workflow step in the current scope, auto-starting a run when passed a workflow ref",
|
|
55
|
-
},
|
|
56
|
-
args: {
|
|
57
|
-
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
58
|
-
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
59
|
-
},
|
|
60
|
-
async run({ args }) {
|
|
61
|
-
// `--dry-run` is intentionally NOT a declared arg (so it stays out of
|
|
62
|
-
// --help). The guard reads it straight from the invocation singleton so
|
|
63
|
-
// existing callers still get a clear, actionable error instead of a
|
|
64
|
-
// generic "unknown flag" from citty.
|
|
65
|
-
if (getParsedInvocation().hasFlag("--dry-run")) {
|
|
66
|
-
throw new UsageError("`akm workflow next` does not support --dry-run. Remove the flag to start or resume a run.", "INVALID_FLAG_VALUE");
|
|
67
|
-
}
|
|
68
|
-
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
69
|
-
const result = await getNextWorkflowStep(args.target, parsedParams);
|
|
70
|
-
output("workflow-next", result);
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
const workflowCompleteCommand = defineJsonCommand({
|
|
74
|
-
meta: {
|
|
75
|
-
name: "complete",
|
|
76
|
-
description: "Update a workflow step state and persist notes/evidence",
|
|
77
|
-
},
|
|
78
|
-
args: {
|
|
79
|
-
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
80
|
-
step: { type: "string", description: "Workflow step id", required: true },
|
|
81
|
-
state: {
|
|
82
|
-
type: "string",
|
|
83
|
-
description: `Step state (default: completed). One of: ${WORKFLOW_STEP_STATES.join(", ")}.`,
|
|
84
|
-
},
|
|
85
|
-
notes: { type: "string", description: "Notes for the completed step" },
|
|
86
|
-
summary: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "Summary of work done (required when completing a step); validated against completion criteria",
|
|
89
|
-
},
|
|
90
|
-
evidence: { type: "string", description: "Evidence JSON object for the step" },
|
|
91
|
-
},
|
|
92
|
-
async run({ args }) {
|
|
93
|
-
const result = await completeWorkflowStep({
|
|
94
|
-
runId: args.runId,
|
|
95
|
-
stepId: args.step,
|
|
96
|
-
status: parseWorkflowStepState(args.state),
|
|
97
|
-
notes: args.notes,
|
|
98
|
-
summary: args.summary,
|
|
99
|
-
evidence: args.evidence ? parseWorkflowJsonObject(args.evidence, "--evidence") : undefined,
|
|
100
|
-
});
|
|
101
|
-
if ("ok" in result && result.ok === false) {
|
|
102
|
-
// Summary failed the completion-criteria validation gate (#506): the
|
|
103
|
-
// step stays pending and the agent receives corrective feedback.
|
|
104
|
-
output("workflow-complete-rejected", result);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
output("workflow-complete", result);
|
|
108
|
-
},
|
|
109
|
-
});
|
|
19
|
+
import { WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
|
|
20
|
+
import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
|
|
110
21
|
const workflowStatusCommand = defineJsonCommand({
|
|
111
22
|
meta: {
|
|
112
23
|
name: "status",
|
|
@@ -219,7 +130,7 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
219
130
|
from: args.from,
|
|
220
131
|
force: args.force,
|
|
221
132
|
});
|
|
222
|
-
// Index the newly-written workflow so `akm workflow
|
|
133
|
+
// Index the newly-written workflow so `akm workflow run` can resolve
|
|
223
134
|
// a workflowEntryId without requiring an explicit `akm index` call
|
|
224
135
|
// first. Uses the same incremental index path that `akm add` uses.
|
|
225
136
|
await akmIndex({ stashDir: result.stashDir });
|
|
@@ -229,34 +140,149 @@ const workflowCreateCommand = defineJsonCommand({
|
|
|
229
140
|
const workflowRunCommand = defineJsonCommand({
|
|
230
141
|
meta: {
|
|
231
142
|
name: "run",
|
|
232
|
-
description: "
|
|
233
|
-
"engine — akm dispatches each step's units (fan-out, schema output) to the configured runner and advances " +
|
|
234
|
-
"the run through the normal completion gates",
|
|
143
|
+
description: "Start or resume a workflow and execute it through completion, failure, a verification gate, or an explicit limit",
|
|
235
144
|
},
|
|
236
145
|
args: {
|
|
237
146
|
target: { type: "positional", description: "Workflow run id or workflow ref (auto-starts a run)", required: true },
|
|
238
|
-
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
239
147
|
"max-steps": { type: "string", description: "Stop after executing this many steps" },
|
|
148
|
+
"max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
|
|
149
|
+
timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
|
|
240
150
|
},
|
|
241
|
-
async run({ args }) {
|
|
242
|
-
requireWorkflowEngineEnabled(loadConfig(), "run");
|
|
151
|
+
async run({ args, rawArgs }) {
|
|
243
152
|
const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
153
|
+
const parameterFlags = parseWorkflowParameterFlags(rawArgs, args.target);
|
|
154
|
+
const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
|
|
155
|
+
const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0, WORKFLOW_MAX_RETRIES);
|
|
156
|
+
const timeoutMs = parseWorkflowTimeout(getStringArg(args, "timeout"));
|
|
157
|
+
const controller = new AbortController();
|
|
158
|
+
let timedOut = false;
|
|
159
|
+
let signalExitCode;
|
|
160
|
+
const interrupt = (signal) => {
|
|
161
|
+
signalExitCode = signal === "SIGINT" ? 130 : 143;
|
|
162
|
+
controller.abort(new Error(`Workflow run interrupted by ${signal}.`));
|
|
163
|
+
};
|
|
164
|
+
const onSigint = () => interrupt("SIGINT");
|
|
165
|
+
const onSigterm = () => interrupt("SIGTERM");
|
|
166
|
+
process.once("SIGINT", onSigint);
|
|
167
|
+
process.once("SIGTERM", onSigterm);
|
|
168
|
+
const timer = timeoutMs === undefined
|
|
169
|
+
? undefined
|
|
170
|
+
: setTimeout(() => {
|
|
171
|
+
timedOut = true;
|
|
172
|
+
controller.abort(new Error(`Workflow run timed out after ${timeoutMs}ms.`));
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
timer?.unref?.();
|
|
175
|
+
try {
|
|
176
|
+
const result = await runWorkflowSteps({
|
|
177
|
+
target: args.target,
|
|
178
|
+
parameterFlags,
|
|
179
|
+
...(maxSteps !== undefined ? { maxSteps } : {}),
|
|
180
|
+
...(maxRetries !== undefined ? { maxRetries } : {}),
|
|
181
|
+
signal: controller.signal,
|
|
182
|
+
});
|
|
183
|
+
const rendered = { ...result, ...(timedOut ? { timedOut: true } : {}) };
|
|
184
|
+
output("workflow-run", rendered);
|
|
185
|
+
if (result.run.status === "failed" || result.gateRejection || result.aborted) {
|
|
186
|
+
process.exitCode = signalExitCode ?? EXIT_CODES.GENERAL;
|
|
250
187
|
}
|
|
251
188
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
189
|
+
finally {
|
|
190
|
+
if (timer)
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
process.off("SIGINT", onSigint);
|
|
193
|
+
process.off("SIGTERM", onSigterm);
|
|
194
|
+
}
|
|
258
195
|
},
|
|
259
196
|
});
|
|
197
|
+
const WORKFLOW_RUN_VALUE_FLAGS = new Set([
|
|
198
|
+
"max-steps",
|
|
199
|
+
"maxSteps",
|
|
200
|
+
"max-retries",
|
|
201
|
+
"maxRetries",
|
|
202
|
+
"timeout",
|
|
203
|
+
"format",
|
|
204
|
+
"detail",
|
|
205
|
+
"shape",
|
|
206
|
+
"output",
|
|
207
|
+
]);
|
|
208
|
+
const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose"]);
|
|
209
|
+
export function parseWorkflowParameterFlags(rawArgs, target) {
|
|
210
|
+
const flags = [];
|
|
211
|
+
let targetSeen = false;
|
|
212
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
213
|
+
const token = rawArgs[index];
|
|
214
|
+
if (token === "--") {
|
|
215
|
+
throw new UsageError("`akm workflow run` does not accept positional arguments after `--`.", "INVALID_FLAG_VALUE");
|
|
216
|
+
}
|
|
217
|
+
if (!token.startsWith("-") || token === "-" || /^-\d/.test(token)) {
|
|
218
|
+
if (!targetSeen) {
|
|
219
|
+
if (token !== target) {
|
|
220
|
+
throw new UsageError("Workflow parameter flags must come after the workflow ref or run id.", "INVALID_FLAG_VALUE");
|
|
221
|
+
}
|
|
222
|
+
targetSeen = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw new UsageError(`Unexpected positional workflow argument "${token}".`, "INVALID_FLAG_VALUE");
|
|
226
|
+
}
|
|
227
|
+
if (!token.startsWith("--"))
|
|
228
|
+
continue;
|
|
229
|
+
const body = token.slice(2);
|
|
230
|
+
const equalsAt = body.indexOf("=");
|
|
231
|
+
const name = equalsAt === -1 ? body : body.slice(0, equalsAt);
|
|
232
|
+
const inlineValue = equalsAt === -1 ? undefined : body.slice(equalsAt + 1);
|
|
233
|
+
if (name === "params") {
|
|
234
|
+
throw new UsageError("--params was removed. Pass each declared workflow parameter as its own flag, for example `--version=1.2.3`.", "INVALID_FLAG_VALUE");
|
|
235
|
+
}
|
|
236
|
+
if (WORKFLOW_RUN_VALUE_FLAGS.has(name)) {
|
|
237
|
+
if (inlineValue === undefined)
|
|
238
|
+
index += 1;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (WORKFLOW_RUN_BOOLEAN_FLAGS.has(name))
|
|
242
|
+
continue;
|
|
243
|
+
if (!targetSeen) {
|
|
244
|
+
throw new UsageError("Workflow parameter flags must come after the workflow ref or run id.", "INVALID_FLAG_VALUE");
|
|
245
|
+
}
|
|
246
|
+
if (inlineValue !== undefined) {
|
|
247
|
+
flags.push({ name, value: inlineValue });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const next = rawArgs[index + 1];
|
|
251
|
+
if (next !== undefined && (!next.startsWith("-") || /^-\d/.test(next))) {
|
|
252
|
+
flags.push({ name, value: next });
|
|
253
|
+
index += 1;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
flags.push({ name, value: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return flags;
|
|
260
|
+
}
|
|
261
|
+
function parseIntegerFlag(raw, name, minimum, maximum) {
|
|
262
|
+
if (raw === undefined)
|
|
263
|
+
return undefined;
|
|
264
|
+
const value = Number.parseInt(raw, 10);
|
|
265
|
+
if (!/^\d+$/.test(raw) || value < minimum || (maximum !== undefined && value > maximum)) {
|
|
266
|
+
const range = maximum === undefined ? `at least ${minimum}` : `from ${minimum} through ${maximum}`;
|
|
267
|
+
throw new UsageError(`${name} must be an integer ${range}, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
function parseWorkflowTimeout(raw) {
|
|
272
|
+
if (raw === undefined)
|
|
273
|
+
return undefined;
|
|
274
|
+
const match = /^(\d+)(ms|s|m)?$/.exec(raw);
|
|
275
|
+
if (!match) {
|
|
276
|
+
throw new UsageError(`--timeout must be N, Nms, Ns, or Nm, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
277
|
+
}
|
|
278
|
+
const amount = Number(match[1]);
|
|
279
|
+
const multiplier = match[2] === "m" ? 60_000 : match[2] === "s" ? 1_000 : 1;
|
|
280
|
+
const timeoutMs = amount * multiplier;
|
|
281
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > WORKFLOW_MAX_TIMEOUT_MS) {
|
|
282
|
+
throw new UsageError(`--timeout must resolve to 1 through ${WORKFLOW_MAX_TIMEOUT_MS} milliseconds, got "${raw}".`, "INVALID_FLAG_VALUE");
|
|
283
|
+
}
|
|
284
|
+
return timeoutMs;
|
|
285
|
+
}
|
|
260
286
|
const workflowBriefCommand = defineJsonCommand({
|
|
261
287
|
meta: {
|
|
262
288
|
name: "brief",
|
|
@@ -433,9 +459,6 @@ export const workflowCommand = defineGroupCommand({
|
|
|
433
459
|
description: "Author, inspect, and execute step-by-step workflow assets",
|
|
434
460
|
},
|
|
435
461
|
subCommands: {
|
|
436
|
-
start: workflowStartCommand,
|
|
437
|
-
next: workflowNextCommand,
|
|
438
|
-
complete: workflowCompleteCommand,
|
|
439
462
|
status: workflowStatusCommand,
|
|
440
463
|
list: workflowListCommand,
|
|
441
464
|
create: workflowCreateCommand,
|
|
@@ -52,8 +52,10 @@
|
|
|
52
52
|
*/
|
|
53
53
|
import path from "node:path";
|
|
54
54
|
import { isDangerousEnvKey } from "../../../commands/lint/env-key-rules.js";
|
|
55
|
+
import { taskFieldProblems } from "../../../tasks/schema.js";
|
|
55
56
|
import { compileWorkflowPlan } from "../../../workflows/ir/compile.js";
|
|
56
57
|
import { parseWorkflow } from "../../../workflows/parser.js";
|
|
58
|
+
import { conceptIdForStashFile } from "../../asset/resolve-ref.js";
|
|
57
59
|
/** Recommended `category` values for facts — `commands/lint/fact-linter.ts:9`. */
|
|
58
60
|
const KNOWN_CATEGORIES = new Set(["personal", "team", "project", "convention", "meta"]);
|
|
59
61
|
/** Placeholder markers a workflow stub carries — `commands/lint/workflow-linter.ts:10`. */
|
|
@@ -152,8 +154,12 @@ function collectSuppressedKeys(raw) {
|
|
|
152
154
|
/**
|
|
153
155
|
* env/secret dangerous-key scan (`lint/index.ts:191-218` + `env-key-rules.ts#checkEnvForDangerousKeys`),
|
|
154
156
|
* keyed on `type` and preserving the `.env`-suffix narrowness (see file header).
|
|
155
|
-
* Reads the overlay `raw`, not disk.
|
|
156
|
-
*
|
|
157
|
+
* Reads the overlay `raw`, not disk.
|
|
158
|
+
*
|
|
159
|
+
* The emitted `Ref:` comes from `conceptIdForStashFile` — the one place that
|
|
160
|
+
* spells a diagnostic ref the way `akm show` accepts it. It used to be
|
|
161
|
+
* hand-built as `env:<base>` / `secret:<base>`, a colon grammar the 0.9.0 ref
|
|
162
|
+
* parser rejects outright — a dead-end ref on a *security* finding.
|
|
157
163
|
*/
|
|
158
164
|
export function dangerousEnvKeyDiagnostics(type, relPath, raw) {
|
|
159
165
|
if (type !== "env" && type !== "secret")
|
|
@@ -161,9 +167,8 @@ export function dangerousEnvKeyDiagnostics(type, relPath, raw) {
|
|
|
161
167
|
const baseNameWithExt = path.basename(relPath);
|
|
162
168
|
if (!baseNameWithExt.endsWith(".env"))
|
|
163
169
|
return []; // NARROWNESS: collectEnvFiles only visits *.env
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const ref = baseName === "" ? `${refPrefix}:.env` : `${refPrefix}:${baseName}`;
|
|
170
|
+
// `relPath` is already stash-root-relative, so "." IS the stash root here.
|
|
171
|
+
const ref = conceptIdForStashFile(type, ".", relPath);
|
|
167
172
|
const keys = scanKeys(raw);
|
|
168
173
|
const suppressed = collectSuppressedKeys(raw);
|
|
169
174
|
const diagnostics = [];
|
|
@@ -259,17 +264,16 @@ export function factDiagnostics(relPath, data) {
|
|
|
259
264
|
}
|
|
260
265
|
return [];
|
|
261
266
|
}
|
|
262
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* TaskLinter extra check (`task-linter.ts:25-58`). `data` is the parsed YAML.
|
|
269
|
+
* Field rules come from the shared {@link taskFieldProblems} (see its doc for
|
|
270
|
+
* the lint-vs-parser reconciliation story); this sweep additionally requires
|
|
271
|
+
* at least one target.
|
|
272
|
+
*/
|
|
263
273
|
export function taskDiagnostics(relPath, data) {
|
|
264
274
|
if (data === null || Object.keys(data).length === 0)
|
|
265
275
|
return [];
|
|
266
|
-
const missing =
|
|
267
|
-
if (!("schedule" in data) || typeof data.schedule !== "string" || data.schedule.trim() === "") {
|
|
268
|
-
missing.push("schedule");
|
|
269
|
-
}
|
|
270
|
-
if (!("enabled" in data) || typeof data.enabled !== "boolean") {
|
|
271
|
-
missing.push("enabled (must be a boolean)");
|
|
272
|
-
}
|
|
276
|
+
const missing = taskFieldProblems(data);
|
|
273
277
|
const hasTarget = "prompt" in data || "workflow" in data || "command" in data;
|
|
274
278
|
if (!hasTarget)
|
|
275
279
|
missing.push("prompt, workflow, or command");
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
*
|
|
13
13
|
* ── validate (spec §6 task validation column) ──
|
|
14
14
|
*
|
|
15
|
-
* A task must declare a `schedule`,
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* A task must declare `version: 2`, a `schedule`, and EXACTLY ONE target
|
|
16
|
+
* (`prompt` XOR `workflow` XOR `command`). `enabled` is OPTIONAL — the parser
|
|
17
|
+
* defaults it to `true`, so an omitted field means an ACTIVE task — but must
|
|
18
|
+
* be a boolean when present. The akm adapter's `TaskLinter` port checks
|
|
19
|
+
* "at least one" target; the native task family here is STRICTER — declaring
|
|
20
|
+
* two targets is `invalid-task-yaml`. So this adapter owns a purpose-built
|
|
21
|
+
* one-target check rather than reusing that port.
|
|
20
22
|
*
|
|
21
23
|
* Conformance oracle (authored, DO NOT modify): fixture
|
|
22
24
|
* `tests/fixtures/bundles/akm-task/` + goldens
|
|
@@ -25,6 +27,7 @@
|
|
|
25
27
|
import fs from "node:fs";
|
|
26
28
|
import path from "node:path";
|
|
27
29
|
import { parse as parseYaml } from "yaml";
|
|
30
|
+
import { taskFieldProblems } from "../../../tasks/schema.js";
|
|
28
31
|
import { hashContent } from "./shared.js";
|
|
29
32
|
/** A native task bundle is single-component; its one component is `main`. */
|
|
30
33
|
const COMPONENT_ID = "main";
|
|
@@ -68,15 +71,15 @@ function parseTaskYaml(raw) {
|
|
|
68
71
|
}
|
|
69
72
|
return {};
|
|
70
73
|
}
|
|
71
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* The native `invalid-task-yaml` check: the shared field rules
|
|
76
|
+
* ({@link taskFieldProblems} — see its doc for the lint-vs-parser
|
|
77
|
+
* reconciliation story) plus this adapter's stricter EXACTLY-ONE-target rule.
|
|
78
|
+
*/
|
|
72
79
|
function taskDiagnostics(relPath, data) {
|
|
73
80
|
if (Object.keys(data).length === 0)
|
|
74
81
|
return [];
|
|
75
|
-
const problems =
|
|
76
|
-
if (typeof data.schedule !== "string" || data.schedule.trim() === "")
|
|
77
|
-
problems.push("schedule");
|
|
78
|
-
if (typeof data.enabled !== "boolean")
|
|
79
|
-
problems.push("enabled (must be a boolean)");
|
|
82
|
+
const problems = taskFieldProblems(data);
|
|
80
83
|
const targets = TARGET_KEYS.filter((k) => k in data && data[k] !== undefined && data[k] !== null);
|
|
81
84
|
if (targets.length === 0)
|
|
82
85
|
problems.push("exactly one target (prompt, workflow, or command)");
|
|
@@ -2,14 +2,36 @@
|
|
|
2
2
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
3
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { localDateStamp } from "../common.js";
|
|
5
6
|
import { UsageError } from "../errors.js";
|
|
6
7
|
import { serializeFrontmatter } from "./asset-serialize.js";
|
|
7
|
-
import { parseFrontmatterBlock } from "./frontmatter.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
import { parseFrontmatterBlock, spliceFrontmatterLine } from "./frontmatter.js";
|
|
9
|
+
/**
|
|
10
|
+
* Ensure an AKM-authored Markdown concept is also a conformant OKF concept.
|
|
11
|
+
*
|
|
12
|
+
* Stamps BOTH `type` and `updated`, because both are required of a conformant
|
|
13
|
+
* document and this is the one chokepoint every `.md` write passes through
|
|
14
|
+
* (`core/write-source.ts`). Without the `updated` stamp, every asset akm
|
|
15
|
+
* created for you — `akm remember`, `akm import`, accepted proposals,
|
|
16
|
+
* authored workflows — was immediately flagged `missing-updated` by akm's own
|
|
17
|
+
* `akm lint`, so the tool disagreed with itself about its own output.
|
|
18
|
+
*
|
|
19
|
+
* An existing `updated` is left alone: this fills a gap, it does not
|
|
20
|
+
* re-stamp on every write (which would churn timestamps and manufacture
|
|
21
|
+
* needless diffs in git-backed bundles).
|
|
22
|
+
*
|
|
23
|
+
* Source preservation: when the type already matches and the ONLY change is
|
|
24
|
+
* adding `updated`, the line is spliced into the original block textually —
|
|
25
|
+
* round-tripping through the YAML serializer would drop user-authored
|
|
26
|
+
* comments and normalize formatting just to contribute one field. Only a
|
|
27
|
+
* document whose `type` must actually be corrected takes the re-serialize
|
|
28
|
+
* path (as it always has).
|
|
29
|
+
*/
|
|
30
|
+
export function ensureAkmMarkdownType(content, type, now = new Date()) {
|
|
10
31
|
const block = parseFrontmatterBlock(content);
|
|
11
|
-
if (!block)
|
|
12
|
-
return `---\
|
|
32
|
+
if (!block) {
|
|
33
|
+
return `---\n${serializeFrontmatter({ type, updated: localDateStamp(now) })}\n---\n${content}`;
|
|
34
|
+
}
|
|
13
35
|
let parsed;
|
|
14
36
|
try {
|
|
15
37
|
parsed = block.frontmatter.trim() ? parseYaml(block.frontmatter) : {};
|
|
@@ -23,8 +45,19 @@ export function ensureAkmMarkdownType(content, type) {
|
|
|
23
45
|
throw new UsageError("AKM Markdown frontmatter must be a YAML mapping.", "INVALID_FLAG_VALUE");
|
|
24
46
|
}
|
|
25
47
|
const data = parsed;
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
const needsUpdated = !("updated" in data);
|
|
49
|
+
if (data.type === type) {
|
|
50
|
+
if (!needsUpdated)
|
|
51
|
+
return content;
|
|
52
|
+
const spliced = spliceFrontmatterLine(content, `updated: ${localDateStamp(now)}`);
|
|
53
|
+
if (spliced !== null)
|
|
54
|
+
return spliced;
|
|
55
|
+
// Unreachable in practice (parseFrontmatterBlock succeeded above), but a
|
|
56
|
+
// re-serialized document beats a non-conformant one.
|
|
57
|
+
}
|
|
28
58
|
const { type: _priorType, ...rest } = data;
|
|
29
|
-
|
|
59
|
+
const next = { type, ...rest };
|
|
60
|
+
if (needsUpdated)
|
|
61
|
+
next.updated = localDateStamp(now);
|
|
62
|
+
return `---\n${serializeFrontmatter(next)}\n---\n${block.content}`;
|
|
30
63
|
}
|
|
@@ -176,6 +176,28 @@ function countLines(text) {
|
|
|
176
176
|
return 0;
|
|
177
177
|
return text.split(/\r?\n/).length - 1;
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* Insert one `key: value` line just before the closing `---` of an existing
|
|
181
|
+
* frontmatter block, leaving every other byte — YAML comments, quoting, key
|
|
182
|
+
* order, line endings — untouched. Returns null when `raw` has no well-formed
|
|
183
|
+
* block, so callers can fall back to a parse-and-serialize path.
|
|
184
|
+
*
|
|
185
|
+
* This is the source-preserving way to ADD a field to user-authored
|
|
186
|
+
* frontmatter: round-tripping the mapping through the YAML serializer drops
|
|
187
|
+
* comments and normalizes formatting, which is unacceptable for a write that
|
|
188
|
+
* only needs to contribute one line. Shared by `ensureAkmMarkdownType`
|
|
189
|
+
* (stamping `updated:` on write) and lint's `--fix` for `missing-updated`.
|
|
190
|
+
*/
|
|
191
|
+
export function spliceFrontmatterLine(raw, line) {
|
|
192
|
+
const lines = raw.split(/\r?\n/);
|
|
193
|
+
if (lines[0]?.trim() !== "---")
|
|
194
|
+
return null;
|
|
195
|
+
const closeIdx = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
196
|
+
if (closeIdx === -1)
|
|
197
|
+
return null;
|
|
198
|
+
lines.splice(closeIdx, 0, line);
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
179
201
|
/**
|
|
180
202
|
* Parse a YAML scalar value (string, boolean, or number).
|
|
181
203
|
*
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* grammar to bridge any more.
|
|
35
35
|
*/
|
|
36
36
|
import { NotFoundError, UsageError } from "../errors.js";
|
|
37
|
-
import { placementSpecFor, stashDirFor, typeForStashDir } from "./asset-placement.js";
|
|
37
|
+
import { deriveCanonicalAssetNameFromStashRoot, placementSpecFor, stashDirFor, typeForStashDir, } from "./asset-placement.js";
|
|
38
38
|
import { isBundleSlug, parseBundleRef } from "./asset-ref.js";
|
|
39
39
|
/**
|
|
40
40
|
* Resolve a maybe-short input ref to a fully-qualified {@link ResolvedRef}
|
|
@@ -97,6 +97,19 @@ export function conceptIdFromTypeName(type, name) {
|
|
|
97
97
|
const stashDir = stashDirFor(type);
|
|
98
98
|
return stashDir !== undefined ? `${stashDir}/${name}` : name;
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* User-facing conceptId for a file on disk, derived through the placement
|
|
102
|
+
* spec's canonical-name rule — the ONE way a diagnostic should spell a ref it
|
|
103
|
+
* expects the user to paste into `akm show`. (The dangerous-env-key lint used
|
|
104
|
+
* to hand-build `env:<base>` colon refs the parser rejects; both its emission
|
|
105
|
+
* sites now route through here.) For a type with no placement spec — which no
|
|
106
|
+
* built-in caller passes — falls back to the raw name so the output is still
|
|
107
|
+
* informative rather than empty.
|
|
108
|
+
*/
|
|
109
|
+
export function conceptIdForStashFile(type, stashRoot, filePath) {
|
|
110
|
+
const name = deriveCanonicalAssetNameFromStashRoot(type, stashRoot, filePath);
|
|
111
|
+
return name === undefined ? filePath : conceptIdFromTypeName(type, name);
|
|
112
|
+
}
|
|
100
113
|
/**
|
|
101
114
|
* Build the USER-FACING / envelope ref string for an indexed item, applying the
|
|
102
115
|
* Chunk-5 flip F4b output-spelling rule (orchestrator decision; ref-grammar
|
|
@@ -116,8 +129,15 @@ export function conceptIdFromTypeName(type, name) {
|
|
|
116
129
|
* derived slug bundle id, never the retired `origin//type:name` spelling.
|
|
117
130
|
*/
|
|
118
131
|
export function displayRef(item, defaultBundleId) {
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
return displayRefForConceptId(item.conceptId ?? conceptIdFromTypeName(item.type, item.name), item.bundleId, defaultBundleId);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The F4b output-spelling flip itself, for a caller that already holds the
|
|
136
|
+
* conceptId (no `type`/`name` derivation needed — e.g. lint findings built
|
|
137
|
+
* from {@link conceptIdForStashFile}). {@link displayRef} delegates here, so
|
|
138
|
+
* the short-default / qualified-secondary rule still has exactly one home.
|
|
139
|
+
*/
|
|
140
|
+
export function displayRefForConceptId(conceptId, bundleId, defaultBundleId) {
|
|
121
141
|
// Default/primary bundle → SHORT conceptId (the flip).
|
|
122
142
|
if (bundleId === undefined || bundleId === defaultBundleId)
|
|
123
143
|
return conceptId;
|
package/dist/core/common.js
CHANGED
|
@@ -409,7 +409,7 @@ export async function fetchWithRetry(url, init, options) {
|
|
|
409
409
|
if (attempt < maxRetries && shouldRetry(response.status)) {
|
|
410
410
|
const retryAfter = parseRetryAfter(response);
|
|
411
411
|
const delay = retryAfter ?? baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
|
|
412
|
-
await
|
|
412
|
+
await abortableDelay(delay, init?.signal);
|
|
413
413
|
continue;
|
|
414
414
|
}
|
|
415
415
|
return response;
|
|
@@ -417,12 +417,41 @@ export async function fetchWithRetry(url, init, options) {
|
|
|
417
417
|
catch (err) {
|
|
418
418
|
if (attempt >= maxRetries)
|
|
419
419
|
throw err;
|
|
420
|
+
// A caller-supplied abort is terminal: never keep retrying past it.
|
|
421
|
+
if (init?.signal?.aborted)
|
|
422
|
+
throw err;
|
|
420
423
|
const delay = baseDelay * 2 ** attempt * (0.5 + Math.random() * 0.5);
|
|
421
|
-
await
|
|
424
|
+
await abortableDelay(delay, init?.signal);
|
|
422
425
|
}
|
|
423
426
|
}
|
|
424
427
|
throw new Error("fetchWithRetry: unreachable");
|
|
425
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Sleep, but wake immediately if `signal` aborts.
|
|
431
|
+
*
|
|
432
|
+
* A server-supplied `Retry-After` is honored verbatim and can be arbitrarily
|
|
433
|
+
* large. Sleeping it out with a bare `setTimeout` ignored the caller's abort
|
|
434
|
+
* signal entirely, so a single `429` could park an operation far past any
|
|
435
|
+
* deadline its caller believed it had imposed — the request timeout bounds
|
|
436
|
+
* only the request, never the wait between attempts.
|
|
437
|
+
*/
|
|
438
|
+
function abortableDelay(ms, signal) {
|
|
439
|
+
if (!signal)
|
|
440
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
441
|
+
if (signal.aborted)
|
|
442
|
+
return Promise.reject(signal.reason ?? new Error("Aborted"));
|
|
443
|
+
return new Promise((resolve, reject) => {
|
|
444
|
+
const onAbort = () => {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
447
|
+
};
|
|
448
|
+
const timer = setTimeout(() => {
|
|
449
|
+
signal.removeEventListener("abort", onAbort);
|
|
450
|
+
resolve();
|
|
451
|
+
}, ms);
|
|
452
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
453
|
+
});
|
|
454
|
+
}
|
|
426
455
|
function shouldRetry(status) {
|
|
427
456
|
return status === 429 || status >= 500;
|
|
428
457
|
}
|
|
@@ -628,6 +657,20 @@ export function toErrorMessage(error) {
|
|
|
628
657
|
export function todayIso() {
|
|
629
658
|
return new Date().toISOString().slice(0, 10);
|
|
630
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* `YYYY-MM-DD` in LOCAL time — deliberately not {@link todayIso}, which is
|
|
662
|
+
* UTC and can differ near midnight. This is the spelling the `updated:`
|
|
663
|
+
* frontmatter stampers share (`core/asset/akm-markdown.ts` on write,
|
|
664
|
+
* `commands/lint/base-linter.ts` on `--fix`), so the field's format has one
|
|
665
|
+
* definition even though the two stampers pick different instants (now vs
|
|
666
|
+
* file mtime).
|
|
667
|
+
*/
|
|
668
|
+
export function localDateStamp(d) {
|
|
669
|
+
const y = d.getFullYear();
|
|
670
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
671
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
672
|
+
return `${y}-${m}-${day}`;
|
|
673
|
+
}
|
|
631
674
|
/**
|
|
632
675
|
* Return a filesystem-safe timestamp string derived from the current instant.
|
|
633
676
|
* Colons and dots are replaced with hyphens so the result is safe as a
|
|
@@ -234,6 +234,14 @@ export const AkmConfigSchema = AkmConfigBaseSchema.superRefine((config, ctx) =>
|
|
|
234
234
|
message: "llmEngine must name an LLM engine",
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
|
+
const workflowJudge = config.workflow?.judgeEngine;
|
|
238
|
+
if (workflowJudge && !config.engines?.[workflowJudge]) {
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
path: ["workflow", "judgeEngine"],
|
|
242
|
+
message: "judgeEngine does not name a configured engine",
|
|
243
|
+
});
|
|
244
|
+
}
|
|
237
245
|
const defaultStrategy = config.defaults?.improveStrategy;
|
|
238
246
|
if (defaultStrategy &&
|
|
239
247
|
!BUILTIN_IMPROVE_STRATEGY_NAMES.includes(defaultStrategy) &&
|