akm-cli 0.9.1-beta.1 → 0.9.1-beta.2
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 +18 -1
- package/dist/cli/parse-args.js +7 -1
- package/dist/commands/env/child-env.js +14 -0
- package/dist/commands/improve/eval-cases.js +2 -0
- package/dist/commands/improve/memory/memory-improve.js +1 -0
- package/dist/commands/lint/index.js +5 -1
- package/dist/commands/sources/add-cli.js +8 -2
- package/dist/commands/sources/migration-help.js +12 -3
- package/dist/commands/sources/self-update.js +9 -1
- package/dist/core/adapter/adapters/agent-skills-adapter.js +32 -16
- package/dist/core/adapter/adapters/akm-lint.js +6 -2
- package/dist/core/adapter/adapters/akm-task-adapter.js +4 -2
- package/dist/core/adapter/adapters/dotenv-adapter.js +21 -0
- package/dist/core/asset/frontmatter.js +6 -1
- package/dist/core/common.js +81 -3
- package/dist/core/config/config-io.js +5 -45
- package/dist/core/config/schema/engines.js +14 -3
- package/dist/core/extra-params.js +11 -0
- package/dist/core/fs-txn.js +15 -2
- package/dist/core/json-schema.js +19 -2
- package/dist/core/paths.js +16 -2
- package/dist/core/redaction.js +22 -1
- package/dist/core/state-db.js +1 -0
- package/dist/core/write-source.js +26 -2
- package/dist/indexer/indexer.js +31 -6
- package/dist/indexer/search/db-search.js +17 -2
- package/dist/indexer/walk/walker.js +6 -1
- package/dist/integrations/agent/detect.js +13 -1
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +21 -0
- package/dist/integrations/lockfile.js +10 -0
- package/dist/llm/client.js +14 -19
- package/dist/llm/embedder.js +23 -3
- package/dist/llm/embedders/remote.js +27 -2
- package/dist/output/html-render.js +40 -1
- package/dist/runtime.js +23 -1
- package/dist/scripts/akm-migrate-node.js +303 -107
- package/dist/scripts/akm-migrate.js +303 -107
- package/dist/setup/setup.js +22 -7
- package/dist/sources/providers/git-install.js +25 -2
- package/dist/sources/snapshot-fetchers/content-extract.js +63 -1
- package/dist/storage/database.js +71 -12
- package/dist/storage/engines/sqlite-migrations.js +61 -2
- package/dist/storage/repositories/index-connection.js +11 -1
- package/dist/storage/repositories/index-meta-repository.js +11 -0
- package/dist/storage/repositories/index-schema.js +17 -2
- package/dist/storage/repositories/index-vec-repository.js +43 -5
- package/dist/storage/sqlite-pragmas.js +12 -1
- package/dist/tasks/runner.js +84 -7
- package/dist/tasks/scheduler-invocation.js +19 -0
- package/dist/tasks/schema.js +21 -1
- package/dist/text-import-hook.mjs +1 -1
- package/dist/workflows/exec/native-executor.js +8 -0
- package/dist/workflows/exec/step-work.js +10 -2
- package/dist/workflows/parser.js +26 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +10 -5
- package/schemas/akm-workflow.json +7 -3
package/dist/tasks/runner.js
CHANGED
|
@@ -49,12 +49,13 @@ import { resolveModel } from "../integrations/agent/model-aliases.js";
|
|
|
49
49
|
import { executeRunner } from "../integrations/agent/runner-dispatch.js";
|
|
50
50
|
import { chatCompletion } from "../llm/client.js";
|
|
51
51
|
import { resolveAssetPath } from "../sources/resolve.js";
|
|
52
|
-
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, queryTaskHistory, reserveTaskHistoryAttempt, upsertTaskHistory, } from "../storage/repositories/task-history-repository.js";
|
|
52
|
+
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, reserveTaskHistoryAttempt, upsertTaskHistory, } from "../storage/repositories/task-history-repository.js";
|
|
53
53
|
import { runWorkflowSteps } from "../workflows/exec/run-workflow.js";
|
|
54
54
|
import { findBareAkmExecutableIndex } from "./command-executable.js";
|
|
55
55
|
import { collectTaskLogSensitiveValues } from "./log-redaction.js";
|
|
56
56
|
import { parseTaskDocument } from "./parser.js";
|
|
57
57
|
import { resolveAkmInvocation } from "./resolve-akm-bin.js";
|
|
58
|
+
import { scheduledTaskContextEnv } from "./scheduler-invocation.js";
|
|
58
59
|
import { validateTaskId } from "./task-id.js";
|
|
59
60
|
export const INVALID_TASK_ATTEMPT_ID = "_invalid-task-id";
|
|
60
61
|
export async function runTask(id, options) {
|
|
@@ -175,7 +176,8 @@ async function runCommandTask(input) {
|
|
|
175
176
|
throw new Error("invariant: command target");
|
|
176
177
|
const { cmd } = task.target;
|
|
177
178
|
const spawnCmd = resolveNestedAkmCommand(cmd);
|
|
178
|
-
|
|
179
|
+
// Unset → the unattended default; `null` → the explicit no-timeout opt-out.
|
|
180
|
+
const timeoutMs = task.timeoutMs !== undefined ? task.timeoutMs : DEFAULT_SCHEDULED_TASK_TIMEOUT_MS;
|
|
179
181
|
const header = `[akm task] task=${task.id} kind=command cmd=${cmd.join(" ")}`;
|
|
180
182
|
const logLines = [header];
|
|
181
183
|
const dbLines = [{ line: header }];
|
|
@@ -280,6 +282,18 @@ function resolveNestedAkmCommand(cmd) {
|
|
|
280
282
|
* the explicit opt-out back to unbounded.
|
|
281
283
|
*/
|
|
282
284
|
export const DEFAULT_WORKFLOW_TASK_TIMEOUT_MS = 6 * 60 * 60 * 1000;
|
|
285
|
+
/**
|
|
286
|
+
* The same unattended default for command and prompt tasks.
|
|
287
|
+
*
|
|
288
|
+
* The reasoning above is about SCHEDULED runs, not about workflows: nobody is
|
|
289
|
+
* watching, and one wedged run silently stops the schedule. Command tasks
|
|
290
|
+
* defaulted to `null` (no kill timer) and prompt tasks inherited
|
|
291
|
+
* DEFAULT_AGENT_TIMEOUT_MS, also null — so a hung `curl`, a prompting agent
|
|
292
|
+
* waiting on stdin, or a stuck engine wedged the task forever while the
|
|
293
|
+
* workflow arm was protected. Same value, same opt-out: an explicit
|
|
294
|
+
* `timeoutMs:` wins, and `timeoutMs: null` restores unbounded.
|
|
295
|
+
*/
|
|
296
|
+
export const DEFAULT_SCHEDULED_TASK_TIMEOUT_MS = DEFAULT_WORKFLOW_TASK_TIMEOUT_MS;
|
|
283
297
|
async function runWorkflowTask(input) {
|
|
284
298
|
const { task, logPath, startedAt, now, runWorkflowStepsImpl, historyReserved } = input;
|
|
285
299
|
if (task.target.kind !== "workflow")
|
|
@@ -310,6 +324,13 @@ async function runWorkflowTask(input) {
|
|
|
310
324
|
// The prompt path logs the engine-fallback announcement; a workflow-backed
|
|
311
325
|
// task must leave the same trace rather than silently using a chosen engine.
|
|
312
326
|
let runWarnings = [];
|
|
327
|
+
// Stamp task-runner provenance for the duration of the run (DRIFT-6), as the
|
|
328
|
+
// command and prompt arms do. This arm executes IN-PROCESS, so the stamp goes
|
|
329
|
+
// on process.env — child akm invocations made by workflow steps inherit it.
|
|
330
|
+
// Without it, workflow-task traffic was recorded as user demand. A more
|
|
331
|
+
// specific stamp already present wins, matching the command arm.
|
|
332
|
+
const priorEventSource = process.env.AKM_EVENT_SOURCE;
|
|
333
|
+
process.env.AKM_EVENT_SOURCE = priorEventSource ?? "task";
|
|
313
334
|
try {
|
|
314
335
|
const execution = await runWorkflowStepsImpl({
|
|
315
336
|
target: workflowTarget.ref,
|
|
@@ -331,6 +352,10 @@ async function runWorkflowTask(input) {
|
|
|
331
352
|
}
|
|
332
353
|
finally {
|
|
333
354
|
deadline.disarm();
|
|
355
|
+
if (priorEventSource === undefined)
|
|
356
|
+
delete process.env.AKM_EVENT_SOURCE;
|
|
357
|
+
else
|
|
358
|
+
process.env.AKM_EVENT_SOURCE = priorEventSource;
|
|
334
359
|
}
|
|
335
360
|
// A timeout is a failed ATTEMPT even though the engine stopped cleanly: the
|
|
336
361
|
// aborted run comes back `active` (resumable), which on its own would map to
|
|
@@ -483,7 +508,10 @@ async function runPromptTask(input) {
|
|
|
483
508
|
runner = {
|
|
484
509
|
...runner,
|
|
485
510
|
profile: { ...runner.profile, ...(model ? { model, modelIsExact: true } : {}) },
|
|
486
|
-
|
|
511
|
+
// Unset → the unattended default (DEFAULT_AGENT_TIMEOUT_MS is null, which
|
|
512
|
+
// let a prompting or wedged agent CLI hang the schedule); `null` → the
|
|
513
|
+
// explicit no-timeout opt-out.
|
|
514
|
+
timeoutMs: promptTarget.timeoutMs !== undefined ? promptTarget.timeoutMs : DEFAULT_SCHEDULED_TASK_TIMEOUT_MS,
|
|
487
515
|
};
|
|
488
516
|
}
|
|
489
517
|
const promptText = await resolvePromptText(task, stashDir);
|
|
@@ -494,7 +522,13 @@ async function runPromptTask(input) {
|
|
|
494
522
|
// Stamp task-runner provenance for any akm invocation the agent makes
|
|
495
523
|
// (DRIFT-6: agent-task traffic must not be recorded as user demand).
|
|
496
524
|
// Caller-supplied env still wins on conflicts.
|
|
497
|
-
|
|
525
|
+
//
|
|
526
|
+
// The agent child env is built from an allowlist, not inherited, so the
|
|
527
|
+
// scheduler's AKM_* directory context was dropped here — an agent's `akm`
|
|
528
|
+
// sub-commands then targeted the DEFAULT stash and DB rather than the
|
|
529
|
+
// ones the scheduled run was configured for. The command arm keeps this
|
|
530
|
+
// context because it inherits process.env; forward it explicitly.
|
|
531
|
+
env: { AKM_EVENT_SOURCE: "task", ...scheduledTaskContextEnv(), ...agentOptions?.env },
|
|
498
532
|
}, {
|
|
499
533
|
...(input.runAgentImpl ? { runAgent: input.runAgentImpl } : {}),
|
|
500
534
|
llm: async (spec, prompt, options) => {
|
|
@@ -598,12 +632,49 @@ function resolveTaskLogPath(logDir, taskId, startedAtIso) {
|
|
|
598
632
|
return "";
|
|
599
633
|
}
|
|
600
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* Redact logs.db rows against the SAME contiguous text the file sink sees.
|
|
637
|
+
*
|
|
638
|
+
* The rows arrive already split on "\n" (see {@link streamLines}), but the
|
|
639
|
+
* redaction needles are whole env values — and a needle containing a newline
|
|
640
|
+
* can never match inside a single line. Scrubbing row-by-row therefore left
|
|
641
|
+
* multi-line secrets (PEM keys, multi-line service-account credentials) intact
|
|
642
|
+
* in logs.db while the flat .log was correctly scrubbed, defeating all three
|
|
643
|
+
* tiers including the explicit `redact:` opt-in.
|
|
644
|
+
*
|
|
645
|
+
* Consecutive rows sharing a stream and level are rejoined, scrubbed as one
|
|
646
|
+
* string, and re-split, so a needle spanning lines matches. Collapsing a
|
|
647
|
+
* multi-line secret into a single [REDACTED] row is the intended outcome.
|
|
648
|
+
*/
|
|
649
|
+
export function scrubDbLines(dbLines, scrub) {
|
|
650
|
+
const out = [];
|
|
651
|
+
for (let i = 0; i < dbLines.length;) {
|
|
652
|
+
const { stream, level } = dbLines[i];
|
|
653
|
+
let end = i;
|
|
654
|
+
while (end < dbLines.length && dbLines[end].stream === stream && dbLines[end].level === level)
|
|
655
|
+
end++;
|
|
656
|
+
const joined = dbLines
|
|
657
|
+
.slice(i, end)
|
|
658
|
+
.map((entry) => entry.line)
|
|
659
|
+
.join("\n");
|
|
660
|
+
for (const line of scrub(joined).split("\n")) {
|
|
661
|
+
if (line.length > 0)
|
|
662
|
+
out.push({ stream, level, line });
|
|
663
|
+
}
|
|
664
|
+
i = end;
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
601
668
|
/** Split captured pipe output into per-line logs.db rows (blank lines dropped). */
|
|
602
669
|
function streamLines(text, stream, level) {
|
|
603
|
-
return text
|
|
670
|
+
return (text
|
|
604
671
|
.split("\n")
|
|
672
|
+
// Windows child output is CRLF-terminated. Splitting on "\n" alone left a
|
|
673
|
+
// trailing "\r" on every row and turned blank CRLF lines into phantom
|
|
674
|
+
// rows containing just "\r" (length 1 passes the filter below).
|
|
675
|
+
.map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line))
|
|
605
676
|
.filter((line) => line.length > 0)
|
|
606
|
-
.map((line) => ({ stream, level, line }));
|
|
677
|
+
.map((line) => ({ stream, level, line })));
|
|
607
678
|
}
|
|
608
679
|
/**
|
|
609
680
|
* Persist a finished run's log: the flat text file (so `log_path` in
|
|
@@ -661,7 +732,7 @@ function persistRunLog(input) {
|
|
|
661
732
|
? redactSensitiveText(redactCredentialPatterns(text), sensitive)
|
|
662
733
|
: redactCredentialPatterns(text);
|
|
663
734
|
const fileText = scrub(input.fileText);
|
|
664
|
-
const dbLines = input.dbLines
|
|
735
|
+
const dbLines = scrubDbLines(input.dbLines, scrub);
|
|
665
736
|
if (input.logPath) {
|
|
666
737
|
try {
|
|
667
738
|
// Written at the process umask. #756 pinned 0600/0700 here; that went out
|
|
@@ -820,6 +891,12 @@ export function readTaskHistory(options = {}) {
|
|
|
820
891
|
if (options.limit === 0)
|
|
821
892
|
return [];
|
|
822
893
|
if (options.id) {
|
|
894
|
+
// An id-scoped query used the single-row helper, so `--limit` was silently
|
|
895
|
+
// discarded and `akm task history --id X --limit 20` always returned one
|
|
896
|
+
// run. The CLI documents --limit as "Maximum rows to return"; honour it.
|
|
897
|
+
if (options.limit !== undefined && options.limit > 0) {
|
|
898
|
+
return getTaskHistoryRuns(db, options.id, options.limit).map(taskHistoryRowToResult);
|
|
899
|
+
}
|
|
823
900
|
const row = getTaskHistory(db, options.id);
|
|
824
901
|
return row ? [taskHistoryRowToResult(row)] : [];
|
|
825
902
|
}
|
|
@@ -14,6 +14,25 @@ export const SCHEDULED_TASK_CONTEXT_KEYS = [
|
|
|
14
14
|
"AKM_CACHE_DIR",
|
|
15
15
|
"AKM_STATE_DIR",
|
|
16
16
|
];
|
|
17
|
+
/**
|
|
18
|
+
* The AKM_* directory context currently in effect, as a plain env fragment.
|
|
19
|
+
*
|
|
20
|
+
* A scheduled run restores these into `process.env` from its
|
|
21
|
+
* `--scheduler-context` descriptor precisely because such installs have
|
|
22
|
+
* non-default directories. Paths that build a child environment from an
|
|
23
|
+
* allowlist rather than inheriting (the agent spawn) must forward this
|
|
24
|
+
* explicitly, or the child's `akm` sub-commands silently target the default
|
|
25
|
+
* stash and DB.
|
|
26
|
+
*/
|
|
27
|
+
export function scheduledTaskContextEnv(env = process.env) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const key of SCHEDULED_TASK_CONTEXT_KEYS) {
|
|
30
|
+
const value = env[key];
|
|
31
|
+
if (value)
|
|
32
|
+
out[key] = value;
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
17
36
|
export const SCHEDULER_CONTEXT_ARG = "--scheduler-context";
|
|
18
37
|
/** Resolve the complete non-secret AKM directory context captured by schedulers. */
|
|
19
38
|
export function resolveScheduledTaskContext(env = process.env, platform = process.platform) {
|
package/dist/tasks/schema.js
CHANGED
|
@@ -52,8 +52,28 @@ export const TASK_MAX_REDACT_NAMES = WORKFLOW_MAX_EXEC_PASS_ENV;
|
|
|
52
52
|
* at runtime with TASK_SCHEMA_VERSION_UNSUPPORTED). `schemas/akm-task.json`
|
|
53
53
|
* agrees with the parser: `required: [version, schedule]`, `version:
|
|
54
54
|
* {const: 2}`, `enabled` optional but boolean. Target-arity rules stay with
|
|
55
|
-
* each caller (they legitimately differ: at-least-one vs exactly-one)
|
|
55
|
+
* each caller (they legitimately differ: at-least-one vs exactly-one), but
|
|
56
|
+
* what COUNTS as a target is shared — see {@link isPresentTarget}.
|
|
56
57
|
*/
|
|
58
|
+
/**
|
|
59
|
+
* Whether a task-target field counts as declared.
|
|
60
|
+
*
|
|
61
|
+
* The arity rules differ between the two linters, but the presence test must
|
|
62
|
+
* not: the runtime parser treats `""` as absent, so a key-existence check let
|
|
63
|
+
* `workflow: ""` lint clean and then die with MISSING_REQUIRED_ARGUMENT. An
|
|
64
|
+
* array target (`command`) counts only when it has entries, for the same
|
|
65
|
+
* reason. Kept beside {@link taskFieldProblems} so all three definitions of
|
|
66
|
+
* "valid task" stay in one file.
|
|
67
|
+
*/
|
|
68
|
+
export function isPresentTarget(value) {
|
|
69
|
+
if (value === undefined || value === null)
|
|
70
|
+
return false;
|
|
71
|
+
if (typeof value === "string")
|
|
72
|
+
return value.trim() !== "";
|
|
73
|
+
if (Array.isArray(value))
|
|
74
|
+
return value.length > 0;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
57
77
|
export function taskFieldProblems(data) {
|
|
58
78
|
const problems = [];
|
|
59
79
|
if (data.version !== TASK_SCHEMA_VERSION)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readFile } from "node:fs/promises";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
-
const TEXT_EXTENSIONS = new Set([".md", ".xml", ".txt", ".sql", ".yaml", ".yml"]);
|
|
19
|
+
const TEXT_EXTENSIONS = new Set([".md", ".xml", ".txt", ".sql", ".yaml", ".yml", ".html"]);
|
|
20
20
|
|
|
21
21
|
function isTextImport(url, importAttributes) {
|
|
22
22
|
if (importAttributes && importAttributes.type === "text") return true;
|
|
@@ -948,6 +948,14 @@ async function dispatchUnit(request, dispatcher) {
|
|
|
948
948
|
return {
|
|
949
949
|
unitId: request.unitId,
|
|
950
950
|
ok: false,
|
|
951
|
+
// NOTE: `validation_error` is deliberately outside PROGRAM_RETRY_REASONS,
|
|
952
|
+
// so no `retry.on:` can name it and a schema-violating unit is not
|
|
953
|
+
// re-run — see "fails with `validation_error` and is NOT re-run" in
|
|
954
|
+
// tests/integration/workflows/exec-unit.test.ts. A sweep finding
|
|
955
|
+
// proposed mapping it onto `llm_invalid_json` (which the parser accepts
|
|
956
|
+
// but this path never emits) to make such failures retryable; that is a
|
|
957
|
+
// behaviour change against an intentional design, not a bug fix, so it
|
|
958
|
+
// is left alone. Reconciling the vocabulary is a 0.9.2 decision.
|
|
951
959
|
failureReason: structured.reason,
|
|
952
960
|
error: structured.errors.join("; "),
|
|
953
961
|
text: structured.raw,
|
|
@@ -667,8 +667,16 @@ export function buildEvidence(units, reducer, isFanOut) {
|
|
|
667
667
|
evidence.voteError = `Vote reducer tied at ${ranked[0].count} vote(s) — no majority.`;
|
|
668
668
|
}
|
|
669
669
|
else {
|
|
670
|
-
|
|
671
|
-
evidence.
|
|
670
|
+
const winner = ranked[0].value;
|
|
671
|
+
evidence.vote = { winner, votes: ranked[0].count, total: units.length };
|
|
672
|
+
// An empty free-text unit normalizes to absent text, so its vote value is
|
|
673
|
+
// `undefined`. Assigning that to `evidence.output` made the key vanish
|
|
674
|
+
// under JSON serialization: a LIVE run then saw `output` absent (and fell
|
|
675
|
+
// back to the whole evidence envelope), while a RESUMED run rehydrated the
|
|
676
|
+
// same step from the journal and produced a different artifact — with the
|
|
677
|
+
// raw envelope exposed as `steps.<id>.output`. Normalize to an explicit
|
|
678
|
+
// empty string so both paths promote the same value.
|
|
679
|
+
evidence.output = winner === undefined ? "" : winner;
|
|
672
680
|
}
|
|
673
681
|
}
|
|
674
682
|
return evidence;
|
package/dist/workflows/parser.js
CHANGED
|
@@ -655,7 +655,16 @@ function parseUnit(ctx, raw, path, stepLabel) {
|
|
|
655
655
|
unit.output = output;
|
|
656
656
|
if (raw.env !== undefined) {
|
|
657
657
|
if (Array.isArray(raw.env) && raw.env.every((entry) => typeof entry === "string" && entry.trim() !== "")) {
|
|
658
|
-
|
|
658
|
+
const envRefs = raw.env.map((entry) => entry.trim());
|
|
659
|
+
// Same decoder uniqueness requirement as `inputs` above: duplicates linted
|
|
660
|
+
// clean and then failed the run with an unlocated frozen-plan error.
|
|
661
|
+
const duplicate = envRefs.find((ref, i) => envRefs.indexOf(ref) !== i);
|
|
662
|
+
if (duplicate !== undefined) {
|
|
663
|
+
ctx.err([...path, "env"], `${stepLabel} "env" contains a duplicate entry: "${duplicate}".`);
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
unit.env = envRefs;
|
|
667
|
+
}
|
|
659
668
|
}
|
|
660
669
|
else {
|
|
661
670
|
ctx.err([...path, "env"], `${stepLabel} "env" must be a list of non-empty env asset refs.`);
|
|
@@ -859,6 +868,13 @@ function parseRoute(ctx, raw, path, stepLabel, stepIndex, routeChecks) {
|
|
|
859
868
|
return;
|
|
860
869
|
}
|
|
861
870
|
const match = String(branch.match);
|
|
871
|
+
// The frozen-plan decoder requires every `when` key to be non-empty, so an
|
|
872
|
+
// empty match parsed and linted clean and then failed the run with an
|
|
873
|
+
// unlocated "Invalid frozen workflow plan". Reject it here, at the line.
|
|
874
|
+
if (match === "") {
|
|
875
|
+
ctx.errAtLine(matchLine, `${stepLabel} "when[${i}].match" must not be empty.`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
862
878
|
if (typeof branch.step !== "string" || branch.step.trim() === "") {
|
|
863
879
|
ctx.err([...branchPath, "step"], `${stepLabel} "when[${i}].step" must be a step id string.`);
|
|
864
880
|
return;
|
|
@@ -901,12 +917,21 @@ function parseInputs(ctx, raw, path, stepLabel) {
|
|
|
901
917
|
ctx.err(path, `${stepLabel} "inputs" must contain at most ${WORKFLOW_MAX_INPUTS} entries.`);
|
|
902
918
|
}
|
|
903
919
|
const out = [];
|
|
920
|
+
// The frozen-plan decoder requires uniqueness (validateStringArray(..., true)),
|
|
921
|
+
// so duplicates parsed and linted clean and then failed the run with an
|
|
922
|
+
// unlocated "Invalid frozen workflow plan". Reject them here, on the entry.
|
|
923
|
+
const seen = new Set();
|
|
904
924
|
raw.forEach((entry, i) => {
|
|
905
925
|
if (typeof entry !== "string" || entry.trim() === "") {
|
|
906
926
|
ctx.err([...path, i], `${stepLabel} "inputs[${i}]" must be a non-empty reference string.`);
|
|
907
927
|
return;
|
|
908
928
|
}
|
|
909
929
|
const value = entry.trim();
|
|
930
|
+
if (seen.has(value)) {
|
|
931
|
+
ctx.err([...path, i], `${stepLabel} "inputs[${i}]" duplicates an earlier entry: "${value}".`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
seen.add(value);
|
|
910
935
|
checkReferenceSyntax(ctx, value, [...path, i], `${stepLabel} "inputs[${i}]"`);
|
|
911
936
|
out.push(value);
|
|
912
937
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.9.1-beta.
|
|
3
|
+
"version": "0.9.1-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "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 Cursor.",
|
|
6
6
|
"keywords": [
|
package/schemas/akm-config.json
CHANGED
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"anyOf": [
|
|
46
46
|
{
|
|
47
47
|
"type": "integer",
|
|
48
|
-
"exclusiveMinimum": 0
|
|
48
|
+
"exclusiveMinimum": 0,
|
|
49
|
+
"maximum": 2147483647
|
|
49
50
|
},
|
|
50
51
|
{
|
|
51
52
|
"type": "null"
|
|
@@ -122,7 +123,8 @@
|
|
|
122
123
|
"anyOf": [
|
|
123
124
|
{
|
|
124
125
|
"type": "integer",
|
|
125
|
-
"exclusiveMinimum": 0
|
|
126
|
+
"exclusiveMinimum": 0,
|
|
127
|
+
"maximum": 2147483647
|
|
126
128
|
},
|
|
127
129
|
{
|
|
128
130
|
"type": "null"
|
|
@@ -1697,7 +1699,8 @@
|
|
|
1697
1699
|
"anyOf": [
|
|
1698
1700
|
{
|
|
1699
1701
|
"type": "integer",
|
|
1700
|
-
"exclusiveMinimum": 0
|
|
1702
|
+
"exclusiveMinimum": 0,
|
|
1703
|
+
"maximum": 2147483647
|
|
1701
1704
|
},
|
|
1702
1705
|
{
|
|
1703
1706
|
"type": "null"
|
|
@@ -1774,7 +1777,8 @@
|
|
|
1774
1777
|
"anyOf": [
|
|
1775
1778
|
{
|
|
1776
1779
|
"type": "integer",
|
|
1777
|
-
"exclusiveMinimum": 0
|
|
1780
|
+
"exclusiveMinimum": 0,
|
|
1781
|
+
"maximum": 2147483647
|
|
1778
1782
|
},
|
|
1779
1783
|
{
|
|
1780
1784
|
"type": "null"
|
|
@@ -3332,7 +3336,8 @@
|
|
|
3332
3336
|
"anyOf": [
|
|
3333
3337
|
{
|
|
3334
3338
|
"type": "integer",
|
|
3335
|
-
"exclusiveMinimum": 0
|
|
3339
|
+
"exclusiveMinimum": 0,
|
|
3340
|
+
"maximum": 2147483647
|
|
3336
3341
|
},
|
|
3337
3342
|
{
|
|
3338
3343
|
"type": "null"
|
|
@@ -309,11 +309,12 @@
|
|
|
309
309
|
},
|
|
310
310
|
"env": {
|
|
311
311
|
"type": "array",
|
|
312
|
+
"uniqueItems": true,
|
|
312
313
|
"items": {
|
|
313
314
|
"type": "string",
|
|
314
315
|
"minLength": 1
|
|
315
316
|
},
|
|
316
|
-
"description": "Env asset refs injected into the dispatched unit env."
|
|
317
|
+
"description": "Env asset refs injected into the dispatched unit env. Entries must be unique: the frozen-plan decoder rejects duplicates at run start."
|
|
317
318
|
},
|
|
318
319
|
"isolation": {
|
|
319
320
|
"$ref": "#/definitions/isolation"
|
|
@@ -350,7 +351,9 @@
|
|
|
350
351
|
"additionalProperties": false,
|
|
351
352
|
"properties": {
|
|
352
353
|
"match": {
|
|
353
|
-
"type": ["string", "number", "boolean"]
|
|
354
|
+
"type": ["string", "number", "boolean"],
|
|
355
|
+
"minLength": 1,
|
|
356
|
+
"description": "Branch key. An empty string is rejected: the frozen-plan decoder requires every `when` key to be non-empty."
|
|
354
357
|
},
|
|
355
358
|
"step": {
|
|
356
359
|
"$ref": "#/definitions/identifier"
|
|
@@ -397,10 +400,11 @@
|
|
|
397
400
|
"type": "array",
|
|
398
401
|
"minItems": 1,
|
|
399
402
|
"maxItems": 64,
|
|
403
|
+
"uniqueItems": true,
|
|
400
404
|
"items": {
|
|
401
405
|
"$ref": "#/definitions/reference"
|
|
402
406
|
},
|
|
403
|
-
"description": "Prior-step artifacts this unit/map step consumes, as reference strings (sub-paths legal). Attached to the dispatched unit as structured context; never spliced into instructions."
|
|
407
|
+
"description": "Prior-step artifacts this unit/map step consumes, as reference strings (sub-paths legal). Attached to the dispatched unit as structured context; never spliced into instructions. Entries must be unique: the frozen-plan decoder rejects duplicates at run start."
|
|
404
408
|
},
|
|
405
409
|
"step": {
|
|
406
410
|
"type": "object",
|