akm-cli 0.9.17-alpha.7 → 0.9.17-alpha.9
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 +473 -0
- package/STABILITY.md +9 -8
- package/dist/akm +55 -22
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/commands/health/archive-usage.js +98 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/html-report.js +1 -4
- package/dist/commands/health/improve-metrics.js +0 -25
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/report-view-model.js +4 -14
- package/dist/commands/health/windows.js +0 -1
- package/dist/commands/health.js +13 -0
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate.js +38 -63
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +23 -5
- package/dist/commands/improve/improve.js +19 -30
- package/dist/commands/improve/ledger.js +3 -2
- package/dist/commands/improve/loop-stages.js +5 -84
- package/dist/commands/improve/memory/memory-belief.js +3 -1
- package/dist/commands/improve/memory/memory-improve.js +269 -11
- package/dist/commands/improve/planner.js +0 -5
- package/dist/commands/improve/preparation.js +20 -135
- package/dist/commands/improve/retrieval-scope.js +19 -4
- package/dist/commands/improve/salience.js +1 -14
- package/dist/commands/improve/stage.js +0 -1
- package/dist/commands/lint/base-linter.js +19 -11
- package/dist/commands/proposal/drain.js +8 -1
- package/dist/commands/proposal/proposal-cli.js +16 -2
- package/dist/commands/proposal/proposal-types.js +7 -0
- package/dist/commands/proposal/proposal.js +37 -6
- package/dist/commands/proposal/repository.js +613 -4
- package/dist/commands/proposal/validators/proposals.js +9 -0
- package/dist/commands/read/curate.js +40 -13
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/show.js +55 -16
- package/dist/commands/sources/info.js +3 -0
- package/dist/commands/sources/stash-cli.js +2 -2
- package/dist/core/adapter/adapters/akm-adapter.js +2 -0
- package/dist/core/adapter/adapters/akm-metadata.js +31 -0
- package/dist/core/bundle-rename.js +1 -7
- package/dist/core/config/config-schema.js +8 -1
- package/dist/core/config/config.js +23 -48
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/schema/improve-processes.js +17 -42
- package/dist/core/config/schema/index-config.js +5 -25
- package/dist/core/file-change.js +13 -5
- package/dist/core/improve-result.js +16 -5
- package/dist/core/improve-types.js +0 -1
- package/dist/core/loopback.js +7 -12
- package/dist/core/parse.js +13 -16
- package/dist/core/state/migrations.js +15 -0
- package/dist/core/time.js +0 -20
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +2 -2
- package/dist/indexer/index-written-assets.js +2 -3
- package/dist/indexer/indexer.js +18 -418
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/passes/metadata.js +0 -19
- package/dist/indexer/scan/doc-to-entry.js +1 -0
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/llm/client.js +8 -10
- package/dist/llm/embedders/remote.js +1 -2
- package/dist/llm/feature-gate.js +0 -5
- package/dist/output/shapes/helpers.js +23 -4
- package/dist/output/text/command-format.js +0 -8
- package/dist/output/text/proposal-format.js +47 -1
- package/dist/output/text/show-format.js +13 -17
- package/dist/scripts/akm-migrate-node.js +2754 -2836
- package/dist/scripts/akm-migrate.js +2754 -2836
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/sources/providers/git-stash.js +55 -4
- package/dist/storage/repositories/improve-ledger-repository.js +48 -7
- package/dist/storage/repositories/index-entries-repository.js +16 -13
- package/dist/storage/repositories/index-entry-schema.js +22 -3
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -26
- package/dist/storage/repositories/index-schema.js +82 -104
- package/dist/storage/repositories/proposals-repository.js +61 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/tasks/source/task-to-v4.js +462 -74
- package/docs/migration/release-notes/0.9.17.md +7 -5
- package/docs/reference/cli.md +33 -21
- package/docs/reference/configuration.md +21 -12
- package/docs/reference/data-and-telemetry.md +0 -1
- package/package.json +1 -1
- package/schemas/akm-config.json +0 -342
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/indexer/db/graph-db.js +0 -431
- package/dist/indexer/graph/graph-extraction.js +0 -807
- package/dist/indexer/graph/graph-related.js +0 -131
- package/dist/indexer/graph/graph-types.js +0 -4
- package/dist/llm/graph-extract.js +0 -903
- package/dist/llm/metadata-enhance.js +0 -95
- package/dist/tasks/source/task-to-v3.js +0 -453
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* LLM-driven metadata enhancement for stash entries.
|
|
6
|
-
*
|
|
7
|
-
* Keeps the higher-level workflow for improving descriptions, tags, and search
|
|
8
|
-
* hints separate from the low-level transport client.
|
|
9
|
-
*/
|
|
10
|
-
import metadataEnhanceSystemPrompt from "../assets/prompts/metadata-enhance-system.md" with { type: "text" };
|
|
11
|
-
import { parseJsonResponse } from "../core/parse.js";
|
|
12
|
-
import { callStructured } from "./structured-call.js";
|
|
13
|
-
const SYSTEM_PROMPT = metadataEnhanceSystemPrompt;
|
|
14
|
-
/**
|
|
15
|
-
* Use an LLM to enhance a stash entry's metadata: improve description,
|
|
16
|
-
* generate searchHints, and suggest tags.
|
|
17
|
-
*
|
|
18
|
-
* When `akmConfig` is provided, routes through
|
|
19
|
-
* `tryLlmFeature("metadata_enhance", ...)` so the feature gate is honoured: a
|
|
20
|
-
* closed gate yields `{ status: "skipped" }` and a thrown/timed-out call yields
|
|
21
|
-
* `{ status: "failed" }` — neither is reported as an enrichment. When
|
|
22
|
-
* `akmConfig` is `undefined` the gate is bypassed entirely: the LLM call runs
|
|
23
|
-
* unconditionally and errors propagate to direct callers such as tests.
|
|
24
|
-
*/
|
|
25
|
-
export async function enhanceMetadata(runner, entry, fileContent, signal, akmConfig, onNotices) {
|
|
26
|
-
const contextParts = [`Name: ${entry.name}`, `Type: ${entry.type}`];
|
|
27
|
-
if (entry.description)
|
|
28
|
-
contextParts.push(`Current description: ${entry.description}`);
|
|
29
|
-
if (entry.tags?.length)
|
|
30
|
-
contextParts.push(`Current tags: ${entry.tags.join(", ")}`);
|
|
31
|
-
if (fileContent) {
|
|
32
|
-
// Limit content to first 4000 chars to stay within token limits (matches other modules)
|
|
33
|
-
const truncated = fileContent.length > 4000 ? `${fileContent.slice(0, 4000)}\n... (truncated)` : fileContent;
|
|
34
|
-
contextParts.push(`File content:\n${truncated}`);
|
|
35
|
-
}
|
|
36
|
-
const userPrompt = `${contextParts.join("\n")}
|
|
37
|
-
|
|
38
|
-
Generate improved metadata for this ${entry.type}. Return JSON with these fields:
|
|
39
|
-
- "description": a clear, concise one-sentence description of what this does
|
|
40
|
-
- "searchHints": an array of 3-6 natural language task phrases an agent might use to find this (e.g. "deploy a docker container", "run database migrations")
|
|
41
|
-
- "tags": an array of 3-8 relevant keyword tags
|
|
42
|
-
|
|
43
|
-
Return ONLY the JSON object, no explanation.`;
|
|
44
|
-
// `parse` owns the raw response: the `!raw`/unparseable case ⇒ empty metadata
|
|
45
|
-
// (still a genuine success), plus the description/searchHints/tags shaping.
|
|
46
|
-
// `enhanceMetadata` never warns and never bumps telemetry — `onError` (gated
|
|
47
|
-
// path only) reports `failed` WITHOUT warning, carrying the message so the
|
|
48
|
-
// caller can surface it. The ungated path (akmConfig === undefined) propagates
|
|
49
|
-
// errors via callStructured.
|
|
50
|
-
let timedOut = false;
|
|
51
|
-
const outcome = await callStructured({
|
|
52
|
-
feature: "metadata_enhance",
|
|
53
|
-
akmConfig,
|
|
54
|
-
runner,
|
|
55
|
-
messages: [
|
|
56
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
57
|
-
{ role: "user", content: userPrompt },
|
|
58
|
-
],
|
|
59
|
-
request: { signal, timeoutMs: runner.timeoutMs },
|
|
60
|
-
onNotices,
|
|
61
|
-
parse: (raw) => {
|
|
62
|
-
const parsed = raw ? parseJsonResponse(raw) : undefined;
|
|
63
|
-
const metadata = {};
|
|
64
|
-
if (parsed) {
|
|
65
|
-
if (typeof parsed.description === "string" && parsed.description) {
|
|
66
|
-
metadata.description = parsed.description;
|
|
67
|
-
}
|
|
68
|
-
if (Array.isArray(parsed.searchHints)) {
|
|
69
|
-
metadata.searchHints = parsed.searchHints
|
|
70
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
71
|
-
.slice(0, 8);
|
|
72
|
-
}
|
|
73
|
-
if (Array.isArray(parsed.tags)) {
|
|
74
|
-
metadata.tags = parsed.tags
|
|
75
|
-
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
76
|
-
.slice(0, 10);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return { status: "enriched", metadata };
|
|
80
|
-
},
|
|
81
|
-
onError: (_cls, err) => ({ status: "failed", error: err instanceof Error ? err.message : String(err) }),
|
|
82
|
-
fallback: { status: "skipped" },
|
|
83
|
-
// The seam returns the `skipped` fallback for a closed gate AND for a
|
|
84
|
-
// wrapper-level timeout. Record the timeout so we can reclassify it below —
|
|
85
|
-
// a timed-out call is a failure, not a silent skip.
|
|
86
|
-
onFallback: (event) => {
|
|
87
|
-
if (event.reason === "timeout")
|
|
88
|
-
timedOut = true;
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
if (outcome.status === "skipped" && timedOut) {
|
|
92
|
-
return { status: "failed", error: "metadata_enhance timed out" };
|
|
93
|
-
}
|
|
94
|
-
return outcome;
|
|
95
|
-
}
|
|
@@ -1,453 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/** Pure, byte-producing legacy-task to task-v3 migration planner. */
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
|
|
8
|
-
import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
9
|
-
import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
|
|
10
|
-
import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../workflows/resource-limits.js";
|
|
11
|
-
import { validateTaskId } from "../task-id.js";
|
|
12
|
-
import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
|
|
13
|
-
import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
|
|
14
|
-
const V2_KEYS = new Set([
|
|
15
|
-
"version",
|
|
16
|
-
"name",
|
|
17
|
-
"description",
|
|
18
|
-
"when_to_use",
|
|
19
|
-
"tags",
|
|
20
|
-
"schedule",
|
|
21
|
-
"enabled",
|
|
22
|
-
"workflow",
|
|
23
|
-
"prompt",
|
|
24
|
-
"command",
|
|
25
|
-
"params",
|
|
26
|
-
"engine",
|
|
27
|
-
"model",
|
|
28
|
-
"timeoutMs",
|
|
29
|
-
"maxSteps",
|
|
30
|
-
"maxRetries",
|
|
31
|
-
"llm",
|
|
32
|
-
"redact",
|
|
33
|
-
]);
|
|
34
|
-
const V2_SHARED_KEYS = new Set([
|
|
35
|
-
"version",
|
|
36
|
-
"name",
|
|
37
|
-
"description",
|
|
38
|
-
"when_to_use",
|
|
39
|
-
"tags",
|
|
40
|
-
"schedule",
|
|
41
|
-
"enabled",
|
|
42
|
-
"redact",
|
|
43
|
-
]);
|
|
44
|
-
const V2_LLM_KEYS = new Set([
|
|
45
|
-
"temperature",
|
|
46
|
-
"maxTokens",
|
|
47
|
-
"supportsJsonSchema",
|
|
48
|
-
"extraParams",
|
|
49
|
-
"contextLength",
|
|
50
|
-
"enableThinking",
|
|
51
|
-
"reasoningEffort",
|
|
52
|
-
]);
|
|
53
|
-
const SAFE_V2_COMMAND_TOKEN = /^[A-Za-z0-9_./:=+,-]+$/;
|
|
54
|
-
const SHELL_ASSIGNMENT_WORD = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
55
|
-
/**
|
|
56
|
-
* V2 executed argv directly, while v3 `run:` enters a host shell. An explicit
|
|
57
|
-
* path bypasses shell aliases/builtins; `akm` is the one bare executable whose
|
|
58
|
-
* v3 runtime resolution is contractually pinned to the current installation.
|
|
59
|
-
*/
|
|
60
|
-
function shellStableV2Executable(executable) {
|
|
61
|
-
return executable === "akm" || executable.includes("/");
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* `env NAME=value... cmd args...` is env(1) itself resolving and exec'ing
|
|
65
|
-
* `cmd` via its own PATH search — that lookup happens inside env's execvp()
|
|
66
|
-
* regardless of whether env was launched by direct execve (v2) or by a host
|
|
67
|
-
* shell (v3 `run:`). The shell-vs-argv divergence `shellStableV2Executable`
|
|
68
|
-
* guards against (bare names shadowed by shell aliases/builtins/functions)
|
|
69
|
-
* therefore does not apply to whatever env ultimately invokes, so skip past
|
|
70
|
-
* a leading `env` and its `NAME=value` assignments to find the real target.
|
|
71
|
-
* Returns the original tokens, unchanged, when there is no such target
|
|
72
|
-
* (e.g. `env` with nothing after its assignments).
|
|
73
|
-
*/
|
|
74
|
-
function skipEnvAssignmentPrefix(tokens) {
|
|
75
|
-
if (tokens[0] !== "env")
|
|
76
|
-
return { tokens, envWrapped: false };
|
|
77
|
-
let index = 1;
|
|
78
|
-
while (index < tokens.length && SHELL_ASSIGNMENT_WORD.test(tokens[index]))
|
|
79
|
-
index += 1;
|
|
80
|
-
if (index >= tokens.length)
|
|
81
|
-
return { tokens, envWrapped: false };
|
|
82
|
-
return { tokens: tokens.slice(index), envWrapped: true };
|
|
83
|
-
}
|
|
84
|
-
const KNOWN_PROMPT_REF_FAMILIES = new Set([
|
|
85
|
-
"agents",
|
|
86
|
-
"commands",
|
|
87
|
-
"env",
|
|
88
|
-
"facts",
|
|
89
|
-
"instructions",
|
|
90
|
-
"knowledge",
|
|
91
|
-
"lessons",
|
|
92
|
-
"memories",
|
|
93
|
-
"scripts",
|
|
94
|
-
"secrets",
|
|
95
|
-
"sessions",
|
|
96
|
-
"skills",
|
|
97
|
-
"tasks",
|
|
98
|
-
"workflows",
|
|
99
|
-
]);
|
|
100
|
-
function hash(bytes) {
|
|
101
|
-
return crypto.createHash("sha256").update(bytes).digest("hex");
|
|
102
|
-
}
|
|
103
|
-
function base(input) {
|
|
104
|
-
return {
|
|
105
|
-
filePath: input.filePath,
|
|
106
|
-
before: Buffer.from(input.bytes),
|
|
107
|
-
beforeHash: hash(input.bytes),
|
|
108
|
-
mode: input.mode,
|
|
109
|
-
writable: input.writable,
|
|
110
|
-
...(input.onDiskWritable !== undefined ? { onDiskWritable: input.onDiskWritable } : {}),
|
|
111
|
-
...(input.containmentRoot ? { containmentRoot: input.containmentRoot } : {}),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* #902: the one blocker with an unambiguous remedy. The sibling shell-safety
|
|
116
|
-
* reasons need a case-by-case judgement and stay reason-only.
|
|
117
|
-
*/
|
|
118
|
-
const ARGV_ARRAY_BLOCK_DETAIL = "Manual conversion required: an array `command:` has no safe v3 `run:` string. Rewrite it by hand as " +
|
|
119
|
-
"`run:` (string) plus `shell:` — see docs/migration/v0.9.1-to-v0.9.2.md for the full v2 to v4 field mapping.";
|
|
120
|
-
function blocked(input, reason, detail) {
|
|
121
|
-
return Object.freeze({ status: "blocked", ...base(input), reason, ...(detail ? { detail } : {}) });
|
|
122
|
-
}
|
|
123
|
-
function plainRecord(value, label) {
|
|
124
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
125
|
-
throw new Error(`${label} must be a mapping`);
|
|
126
|
-
const prototype = Object.getPrototypeOf(value);
|
|
127
|
-
if (prototype !== Object.prototype && prototype !== null)
|
|
128
|
-
throw new Error(`${label} must use a plain or null prototype`);
|
|
129
|
-
return value;
|
|
130
|
-
}
|
|
131
|
-
function exactString(value, label, nonempty = false) {
|
|
132
|
-
if (typeof value !== "string" || (nonempty && value.trim().length === 0)) {
|
|
133
|
-
throw new Error(`${label} must be ${nonempty ? "a non-empty " : "a "}string`);
|
|
134
|
-
}
|
|
135
|
-
return value;
|
|
136
|
-
}
|
|
137
|
-
function optionalString(value, label) {
|
|
138
|
-
if (value === undefined || value === null)
|
|
139
|
-
return undefined;
|
|
140
|
-
return exactString(value, label);
|
|
141
|
-
}
|
|
142
|
-
function parseLegacyTaskYaml(input) {
|
|
143
|
-
let source;
|
|
144
|
-
try {
|
|
145
|
-
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
throw new Error("task YAML contains invalid UTF-8 bytes");
|
|
149
|
-
}
|
|
150
|
-
const lineCounter = new LineCounter();
|
|
151
|
-
let document;
|
|
152
|
-
try {
|
|
153
|
-
document = parseDocument(source, { lineCounter, uniqueKeys: true });
|
|
154
|
-
}
|
|
155
|
-
catch (cause) {
|
|
156
|
-
throw new Error(`invalid YAML: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
157
|
-
}
|
|
158
|
-
const [parseError] = document.errors;
|
|
159
|
-
if (parseError)
|
|
160
|
-
throw new Error(`invalid YAML: ${parseError.message.split("\n")[0]}`);
|
|
161
|
-
const [parseWarning] = document.warnings;
|
|
162
|
-
if (parseWarning)
|
|
163
|
-
throw new Error(`unsupported YAML construct: ${parseWarning.message}`);
|
|
164
|
-
assertBoundedTaskYamlDocument(document, {
|
|
165
|
-
filePath: input.filePath,
|
|
166
|
-
sourceLabel: "task v2 migration source",
|
|
167
|
-
lineCounter,
|
|
168
|
-
});
|
|
169
|
-
return { data: plainRecord(document.toJS({ maxAliasCount: 0 }), "task YAML"), source };
|
|
170
|
-
}
|
|
171
|
-
function validateCommonV2(data) {
|
|
172
|
-
const unknown = Object.keys(data).filter((key) => !V2_KEYS.has(key));
|
|
173
|
-
if (unknown.length > 0)
|
|
174
|
-
throw new Error(`unknown v2 field(s): ${unknown.join(", ")}`);
|
|
175
|
-
exactString(data.schedule, "schedule", true);
|
|
176
|
-
if (data.enabled !== undefined && typeof data.enabled !== "boolean")
|
|
177
|
-
throw new Error("enabled must be a boolean");
|
|
178
|
-
for (const key of ["name", "description", "when_to_use"])
|
|
179
|
-
optionalString(data[key], key);
|
|
180
|
-
if (data.tags !== undefined && data.tags !== null) {
|
|
181
|
-
if (!Array.isArray(data.tags) || data.tags.some((entry) => typeof entry !== "string" || entry.length === 0)) {
|
|
182
|
-
throw new Error("tags must be an array of non-empty strings");
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (data.timeoutMs !== undefined && data.timeoutMs !== null) {
|
|
186
|
-
if (!Number.isInteger(data.timeoutMs) ||
|
|
187
|
-
data.timeoutMs < 1 ||
|
|
188
|
-
data.timeoutMs > WORKFLOW_MAX_TIMEOUT_MS) {
|
|
189
|
-
throw new Error(`timeoutMs must be null or an integer from 1 through ${WORKFLOW_MAX_TIMEOUT_MS}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
193
|
-
if (!Array.isArray(data.redact) ||
|
|
194
|
-
data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
|
|
195
|
-
data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
|
|
196
|
-
throw new Error("redact must contain only bounded environment variable names");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function validateTargetFields(data, allowed) {
|
|
201
|
-
const targetFields = new Set([...allowed, "workflow", "prompt", "command"]);
|
|
202
|
-
const invalid = Object.keys(data).filter((key) => !V2_SHARED_KEYS.has(key) && !targetFields.has(key));
|
|
203
|
-
if (invalid.length > 0)
|
|
204
|
-
throw new Error(`field(s) not valid for this target: ${invalid.join(", ")}`);
|
|
205
|
-
}
|
|
206
|
-
function validateV2Llm(value) {
|
|
207
|
-
if (value === undefined)
|
|
208
|
-
return undefined;
|
|
209
|
-
const llm = plainRecord(value, "llm");
|
|
210
|
-
const unknown = Object.keys(llm).filter((key) => !V2_LLM_KEYS.has(key));
|
|
211
|
-
if (unknown.length > 0)
|
|
212
|
-
throw new Error(`llm has unknown field(s): ${unknown.join(", ")}`);
|
|
213
|
-
if (llm.temperature !== undefined && (typeof llm.temperature !== "number" || !Number.isFinite(llm.temperature))) {
|
|
214
|
-
throw new Error("llm.temperature must be a finite number");
|
|
215
|
-
}
|
|
216
|
-
for (const key of ["maxTokens", "contextLength"]) {
|
|
217
|
-
if (llm[key] !== undefined && (!Number.isInteger(llm[key]) || llm[key] <= 0)) {
|
|
218
|
-
throw new Error(`llm.${key} must be a positive integer`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
for (const key of ["supportsJsonSchema", "enableThinking"]) {
|
|
222
|
-
if (llm[key] !== undefined && typeof llm[key] !== "boolean")
|
|
223
|
-
throw new Error(`llm.${key} must be a boolean`);
|
|
224
|
-
}
|
|
225
|
-
if (llm.reasoningEffort !== undefined && (typeof llm.reasoningEffort !== "string" || !llm.reasoningEffort.trim())) {
|
|
226
|
-
throw new Error("llm.reasoningEffort must be a non-empty string");
|
|
227
|
-
}
|
|
228
|
-
if (llm.extraParams !== undefined) {
|
|
229
|
-
const issue = validateExtraParams(llm.extraParams)[0];
|
|
230
|
-
if (issue)
|
|
231
|
-
throw new Error(formatExtraParamsIssue("llm.extraParams", issue));
|
|
232
|
-
}
|
|
233
|
-
return llm;
|
|
234
|
-
}
|
|
235
|
-
function commonAkm(data) {
|
|
236
|
-
const akm = {
|
|
237
|
-
schedule: exactString(data.schedule, "schedule", true),
|
|
238
|
-
enabled: data.enabled === undefined ? true : data.enabled,
|
|
239
|
-
};
|
|
240
|
-
for (const key of ["description", "when_to_use", "tags"]) {
|
|
241
|
-
if (data[key] !== undefined && data[key] !== null)
|
|
242
|
-
akm[key] = data[key];
|
|
243
|
-
}
|
|
244
|
-
return akm;
|
|
245
|
-
}
|
|
246
|
-
function addRuntimeOverrides(data, akm) {
|
|
247
|
-
for (const key of ["engine", "model"]) {
|
|
248
|
-
const value = optionalString(data[key], key);
|
|
249
|
-
if (value)
|
|
250
|
-
akm[key] = value;
|
|
251
|
-
}
|
|
252
|
-
const llm = validateV2Llm(data.llm);
|
|
253
|
-
if (llm !== undefined)
|
|
254
|
-
akm.inference = llm;
|
|
255
|
-
if (data.timeoutMs !== undefined)
|
|
256
|
-
akm.timeout = data.timeoutMs;
|
|
257
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
258
|
-
akm.redact = [...new Set(data.redact)];
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function addSharedNonPromptOverrides(data, akm) {
|
|
262
|
-
if (data.timeoutMs !== undefined)
|
|
263
|
-
akm.timeout = data.timeoutMs;
|
|
264
|
-
if (data.redact !== undefined && data.redact !== null) {
|
|
265
|
-
akm.redact = [...new Set(data.redact)];
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
function promptSourceKind(raw) {
|
|
269
|
-
const trimmed = raw.trim();
|
|
270
|
-
if (trimmed.startsWith("./") ||
|
|
271
|
-
trimmed.startsWith("../") ||
|
|
272
|
-
path.isAbsolute(trimmed) ||
|
|
273
|
-
/^[A-Za-z]:[\\/]/.test(trimmed)) {
|
|
274
|
-
return "file";
|
|
275
|
-
}
|
|
276
|
-
try {
|
|
277
|
-
const parsed = parseBundleRef(trimmed);
|
|
278
|
-
const family = parsed.conceptId.split("/", 1)[0] ?? "";
|
|
279
|
-
if (bundleRefToString(parsed) !== trimmed || !parsed.conceptId.includes("/")) {
|
|
280
|
-
return "inline";
|
|
281
|
-
}
|
|
282
|
-
if (!KNOWN_PROMPT_REF_FAMILIES.has(family))
|
|
283
|
-
return parsed.bundle === undefined ? "inline" : "other-ref";
|
|
284
|
-
if (family === "agents")
|
|
285
|
-
return "agent";
|
|
286
|
-
if (family === "commands")
|
|
287
|
-
return "command";
|
|
288
|
-
return "other-ref";
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
return "inline";
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
function migratedObject(data) {
|
|
295
|
-
validateCommonV2(data);
|
|
296
|
-
const targets = ["workflow", "prompt", "command"].filter((key) => Object.hasOwn(data, key) && data[key] !== null && data[key] !== "");
|
|
297
|
-
if (targets.length !== 1)
|
|
298
|
-
throw new Error("v2 task must declare exactly one of workflow, prompt, or command");
|
|
299
|
-
const output = { version: 3 };
|
|
300
|
-
if (data.name !== undefined && data.name !== null)
|
|
301
|
-
output.name = data.name;
|
|
302
|
-
const akm = commonAkm(data);
|
|
303
|
-
if (targets[0] === "workflow") {
|
|
304
|
-
validateTargetFields(data, ["params", "timeoutMs", "maxSteps", "maxRetries"]);
|
|
305
|
-
const ref = exactString(data.workflow, "workflow", true).trim();
|
|
306
|
-
let target;
|
|
307
|
-
try {
|
|
308
|
-
target = classifyTaskV3Uses(ref);
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
throw new Error("workflow is not a canonical v3 asset ref");
|
|
312
|
-
}
|
|
313
|
-
if (target.kind !== "workflow")
|
|
314
|
-
throw new Error("workflow target is not a workflows/ ref");
|
|
315
|
-
output.uses = ref;
|
|
316
|
-
if (data.params !== undefined && data.params !== null) {
|
|
317
|
-
const params = plainRecord(data.params, "params");
|
|
318
|
-
output.with = params;
|
|
319
|
-
}
|
|
320
|
-
if (data.maxSteps !== undefined && data.maxSteps !== null) {
|
|
321
|
-
if (!Number.isSafeInteger(data.maxSteps) || data.maxSteps < 1)
|
|
322
|
-
throw new Error("maxSteps must be positive");
|
|
323
|
-
akm.maxSteps = data.maxSteps;
|
|
324
|
-
}
|
|
325
|
-
if (data.maxRetries !== undefined && data.maxRetries !== null) {
|
|
326
|
-
if (!Number.isSafeInteger(data.maxRetries) || data.maxRetries < 0) {
|
|
327
|
-
throw new Error("maxRetries must be a non-negative integer");
|
|
328
|
-
}
|
|
329
|
-
akm.maxRetries = data.maxRetries;
|
|
330
|
-
}
|
|
331
|
-
addSharedNonPromptOverrides(data, akm);
|
|
332
|
-
}
|
|
333
|
-
else if (targets[0] === "prompt") {
|
|
334
|
-
validateTargetFields(data, ["engine", "model", "timeoutMs", "llm"]);
|
|
335
|
-
const prompt = exactString(data.prompt, "prompt", true).trim();
|
|
336
|
-
const kind = promptSourceKind(prompt);
|
|
337
|
-
if (kind === "file")
|
|
338
|
-
return "dynamic-file-read-cannot-be-inlined-without-changing-semantics";
|
|
339
|
-
if (kind === "agent")
|
|
340
|
-
return "agent-ref-has-persona-but-no-command-work";
|
|
341
|
-
if (kind === "other-ref")
|
|
342
|
-
return "non-command-asset-has-no-v3-command-ref-equivalent";
|
|
343
|
-
if (kind === "command")
|
|
344
|
-
output.uses = prompt;
|
|
345
|
-
else {
|
|
346
|
-
output.uses = "akm/command";
|
|
347
|
-
output.with = { content: prompt };
|
|
348
|
-
}
|
|
349
|
-
addRuntimeOverrides(data, akm);
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
validateTargetFields(data, ["timeoutMs"]);
|
|
353
|
-
if (Array.isArray(data.command))
|
|
354
|
-
return "argv-array-has-no-portable-shell-string";
|
|
355
|
-
const command = exactString(data.command, "command", true).trim();
|
|
356
|
-
if (/['"\\]/.test(command))
|
|
357
|
-
return "shell-quoting-changes-v2-whitespace-split-semantics";
|
|
358
|
-
const tokens = command.split(/\s+/).filter(Boolean);
|
|
359
|
-
if (tokens.length === 0 || tokens.some((token) => !SAFE_V2_COMMAND_TOKEN.test(token))) {
|
|
360
|
-
return "shell-operators-change-v2-literal-argv-semantics";
|
|
361
|
-
}
|
|
362
|
-
const { tokens: targetTokens, envWrapped } = skipEnvAssignmentPrefix(tokens);
|
|
363
|
-
const executable = targetTokens[0];
|
|
364
|
-
if (SHELL_ASSIGNMENT_WORD.test(executable) || (!envWrapped && !shellStableV2Executable(executable))) {
|
|
365
|
-
return "shell-command-resolution-changes-v2-literal-argv-semantics";
|
|
366
|
-
}
|
|
367
|
-
output.run = tokens.join(" ");
|
|
368
|
-
addSharedNonPromptOverrides(data, akm);
|
|
369
|
-
}
|
|
370
|
-
output.akm = akm;
|
|
371
|
-
return output;
|
|
372
|
-
}
|
|
373
|
-
function isReason(value) {
|
|
374
|
-
return typeof value === "string";
|
|
375
|
-
}
|
|
376
|
-
/** Convert one already-normalized legacy record directly to final task v3. */
|
|
377
|
-
function planLegacyTaskDataToV3(input, data) {
|
|
378
|
-
if (data.version !== 2) {
|
|
379
|
-
return blocked(input, "unsupported-task-version", `expected normalized legacy version 2, got ${String(data.version)}`);
|
|
380
|
-
}
|
|
381
|
-
if (!input.writable || input.onDiskWritable === false) {
|
|
382
|
-
return blocked(input, "read-only-source", !input.writable ? "the owning source is not writable" : "the source file or publication directory is read-only");
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
validateTaskId(path.basename(input.filePath, ".yml"));
|
|
386
|
-
}
|
|
387
|
-
catch (cause) {
|
|
388
|
-
return blocked(input, "invalid-v2-task", cause instanceof Error ? cause.message : String(cause));
|
|
389
|
-
}
|
|
390
|
-
let migrated;
|
|
391
|
-
try {
|
|
392
|
-
migrated = migratedObject(data);
|
|
393
|
-
}
|
|
394
|
-
catch (cause) {
|
|
395
|
-
return blocked(input, "invalid-v2-task", cause instanceof Error ? cause.message : String(cause));
|
|
396
|
-
}
|
|
397
|
-
if (isReason(migrated)) {
|
|
398
|
-
const detail = migrated === "argv-array-has-no-portable-shell-string" ? ARGV_ARRAY_BLOCK_DETAIL : undefined;
|
|
399
|
-
return blocked(input, migrated, detail);
|
|
400
|
-
}
|
|
401
|
-
const after = Buffer.from(stringifyYaml(migrated), "utf8");
|
|
402
|
-
try {
|
|
403
|
-
parseTaskV3Yaml({
|
|
404
|
-
yaml: after.toString("utf8"),
|
|
405
|
-
filePath: input.filePath,
|
|
406
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
catch (cause) {
|
|
410
|
-
return blocked(input, "generated-v3-validation-failed", cause instanceof Error ? cause.message : String(cause));
|
|
411
|
-
}
|
|
412
|
-
return Object.freeze({
|
|
413
|
-
status: "changed",
|
|
414
|
-
...base(input),
|
|
415
|
-
reason: "task-converted",
|
|
416
|
-
after,
|
|
417
|
-
afterHash: hash(after),
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
/** Plan exactly one source file without touching disk. */
|
|
421
|
-
export function planTaskToV3File(input) {
|
|
422
|
-
let data;
|
|
423
|
-
let source;
|
|
424
|
-
try {
|
|
425
|
-
({ data, source } = parseLegacyTaskYaml(input));
|
|
426
|
-
}
|
|
427
|
-
catch (cause) {
|
|
428
|
-
return blocked(input, "invalid-task-yaml", cause instanceof Error ? cause.message : String(cause));
|
|
429
|
-
}
|
|
430
|
-
if (data.version === 3) {
|
|
431
|
-
try {
|
|
432
|
-
parseTaskV3Yaml({
|
|
433
|
-
yaml: source,
|
|
434
|
-
filePath: input.filePath,
|
|
435
|
-
...(input.containmentRoot ? { workspaceRoot: input.containmentRoot } : {}),
|
|
436
|
-
});
|
|
437
|
-
return Object.freeze({ status: "skipped", ...base(input), reason: "already-v3" });
|
|
438
|
-
}
|
|
439
|
-
catch (cause) {
|
|
440
|
-
return blocked(input, "invalid-v3-task", cause instanceof Error ? cause.message : String(cause));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
if (data.version === 4) {
|
|
444
|
-
// Generation 1 owns v2 only. Do not validate v4 here: generation 2 must
|
|
445
|
-
// be allowed to recognize and remove the retired schedule[].enabled field
|
|
446
|
-
// before the current runtime parser sees those bytes.
|
|
447
|
-
return Object.freeze({ status: "skipped", ...base(input), reason: "already-v4" });
|
|
448
|
-
}
|
|
449
|
-
if (data.version !== 2) {
|
|
450
|
-
return blocked(input, "unsupported-task-version", `expected version 2, 3, or 4, got ${String(data.version)}`);
|
|
451
|
-
}
|
|
452
|
-
return planLegacyTaskDataToV3(input, data);
|
|
453
|
-
}
|