akm-cli 0.9.11 → 0.9.12
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 +166 -0
- package/STABILITY.md +6 -1
- package/dist/assets/hints/cli-hints-full.md +1 -1
- package/dist/assets/improve-strategies/consolidate.json +1 -1
- package/dist/assets/improve-strategies/default.json +1 -1
- package/dist/assets/improve-strategies/thorough.json +1 -2
- package/dist/cli/shared.js +16 -4
- package/dist/cli.js +15 -13
- package/dist/commands/agent/agent-dispatch.js +8 -0
- package/dist/commands/command/execution-source-loader.js +25 -22
- package/dist/commands/command/portable-template.js +4 -26
- package/dist/commands/config-cli.js +10 -4
- package/dist/commands/env/env-binding.js +10 -3
- package/dist/commands/env/env-cli.js +7 -0
- package/dist/commands/env/secret-cli.js +15 -4
- package/dist/commands/health/checks.js +186 -71
- package/dist/commands/health.js +16 -4
- package/dist/commands/improve/distill/quality-gate.js +2 -2
- package/dist/commands/improve/distill.js +28 -12
- package/dist/commands/improve/execution.js +1 -2
- package/dist/commands/improve/extract.js +82 -56
- package/dist/commands/improve/improve-strategies.js +26 -8
- package/dist/commands/improve/improve.js +13 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/index.js +3 -1
- package/dist/commands/migrate-cli.js +6 -4
- package/dist/commands/proposal/drain-policies.js +22 -2
- package/dist/commands/proposal/repository.js +4 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
- package/dist/commands/proposal/validators/proposals.js +10 -19
- package/dist/commands/read/show.js +42 -31
- package/dist/commands/registry-cli.js +4 -2
- package/dist/commands/sources/init.js +4 -8
- package/dist/commands/sources/self-update.js +2 -2
- package/dist/commands/sources/source-clone.js +5 -7
- package/dist/commands/sources/sources-cli.js +3 -5
- package/dist/commands/tasks/tasks-cli.js +4 -12
- package/dist/commands/tasks/tasks.js +38 -35
- package/dist/commands/workflow-cli.js +17 -15
- package/dist/core/activation-policy.js +31 -3
- package/dist/core/adapter/execution-source.js +39 -11
- package/dist/core/asset/stash-meta.js +7 -41
- package/dist/core/common.js +8 -17
- package/dist/core/config/config-schema.js +3 -23
- package/dist/core/config/config-walker.js +50 -4
- package/dist/core/config/config.js +21 -5
- package/dist/core/config/legacy-source-shape-shim.js +79 -0
- package/dist/core/config/schema/embedding.js +2 -2
- package/dist/core/config/schema/engines.js +2 -2
- package/dist/core/config/schema/index-config.js +19 -21
- package/dist/core/config/schema/primitives.js +21 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +2 -3
- package/dist/core/improve-types.js +17 -0
- package/dist/core/json-schema.js +1 -11
- package/dist/core/maintenance-barrier.js +17 -2
- package/dist/core/paths.js +12 -15
- package/dist/core/state/migrations.js +28 -0
- package/dist/core/state-db.js +28 -1
- package/dist/core/write-source.js +6 -6
- package/dist/indexer/bundle-identity-guard.js +3 -0
- package/dist/indexer/ensure-index.js +5 -0
- package/dist/indexer/indexer.js +11 -3
- package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
- package/dist/indexer/passes/metadata.js +16 -5
- package/dist/indexer/search/search-fields.js +1 -30
- package/dist/integrations/agent/engine-resolution.js +15 -1
- package/dist/integrations/agent/model-map.js +16 -10
- package/dist/integrations/agent/prompts.js +13 -6
- package/dist/integrations/lockfile.js +22 -7
- package/dist/llm/client.js +16 -0
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +5 -3
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1461 -1267
- package/dist/scripts/akm-migrate.js +1460 -1266
- package/dist/setup/setup.js +14 -21
- package/dist/sources/include.js +150 -20
- package/dist/sources/providers/git-install.js +14 -12
- package/dist/sources/providers/git-provider.js +3 -3
- package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
- package/dist/sources/website-url.js +12 -4
- package/dist/storage/engines/sqlite-migrations.js +40 -10
- package/dist/storage/like-pattern.js +7 -0
- package/dist/storage/repositories/extract-sessions-repository.js +23 -0
- package/dist/storage/repositories/index-connection.js +27 -10
- package/dist/storage/repositories/index-entry-schema.js +19 -2
- package/dist/storage/repositories/index-schema.js +30 -9
- package/dist/storage/repositories/proposals-repository.js +2 -1
- package/dist/storage/repositories/task-history-repository.js +14 -7
- package/dist/storage/repositories/workflow-runs-repository.js +15 -1
- package/dist/storage/sqlite-read-snapshot.js +11 -9
- package/dist/tasks/backends/cron.js +34 -5
- package/dist/tasks/backends/launchd.js +23 -26
- package/dist/tasks/backends/schtasks.js +50 -3
- package/dist/tasks/frozen-script.js +2 -0
- package/dist/tasks/prepare/prepare.js +2 -7
- package/dist/tasks/prepare/script-capture.js +38 -6
- package/dist/tasks/schedule.js +154 -13
- package/dist/tasks/source/task-source-v3-frozen.js +0 -1
- package/dist/tasks/source/task-source-v4.js +0 -1
- package/dist/workflows/exec/child-workflow.js +2 -3
- package/dist/workflows/exec/exec-unit.js +3 -4
- package/dist/workflows/exec/run-workflow.js +19 -10
- package/dist/workflows/exec/step-work.js +35 -56
- package/dist/workflows/freeze/resolve-steps.js +19 -11
- package/dist/workflows/freeze/source-freeze.js +7 -0
- package/dist/workflows/freeze/targets/child-workflow.js +12 -18
- package/dist/workflows/freeze/targets/command.js +14 -2
- package/dist/workflows/ir/environment-v4.js +4 -2
- package/dist/workflows/ir/freeze-v4.js +2 -5
- package/dist/workflows/ir/plan-hash.js +0 -3
- package/dist/workflows/ir/schema-v4.js +14 -9
- package/dist/workflows/ir/schema.js +1 -3
- package/dist/workflows/resource-limits.js +35 -48
- package/dist/workflows/runtime/plan-classifier.js +89 -41
- package/dist/workflows/runtime/run-outputs.js +1 -21
- package/dist/workflows/runtime/runs.js +76 -150
- package/dist/workflows/source-files.js +28 -54
- package/dist/workflows/source-ir/program.js +2 -2
- package/dist/workflows/source-ir/semantics.js +5 -23
- package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
- package/docs/reference/cli.md +74 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -15,12 +15,12 @@ import { createRequire } from "node:module";
|
|
|
15
15
|
import { ConfigError } from "../../core/errors.js";
|
|
16
16
|
import { classifyPathAccess, describeInaccessiblePath } from "../../core/path-access.js";
|
|
17
17
|
import { getDbPath } from "../../core/paths.js";
|
|
18
|
-
import { warn } from "../../core/warn.js";
|
|
18
|
+
import { warn, warnOnce } from "../../core/warn.js";
|
|
19
19
|
import { openDatabase } from "../database.js";
|
|
20
20
|
import { openManagedDatabase } from "../managed-db.js";
|
|
21
21
|
import { SQLITE_BUSY_TIMEOUT_MS } from "../sqlite-pragmas.js";
|
|
22
|
-
import { openSqliteReadSnapshot } from "../sqlite-read-snapshot.js";
|
|
23
|
-
import { isCanonicalIndexGeneration } from "./index-entry-schema.js";
|
|
22
|
+
import { openSqliteReadSnapshot, SqliteReadSnapshotUnavailableError } from "../sqlite-read-snapshot.js";
|
|
23
|
+
import { CANONICAL_INDEX_DB_VERSION, classifyIndexGeneration, isCanonicalIndexGeneration } from "./index-entry-schema.js";
|
|
24
24
|
import { ensureSchema } from "./index-schema.js";
|
|
25
25
|
import { loadVecExtension, warnIfVecMissing } from "./index-vec-repository.js";
|
|
26
26
|
/**
|
|
@@ -130,15 +130,19 @@ export function openExistingDatabase(dbPath) {
|
|
|
130
130
|
path: resolvedPath,
|
|
131
131
|
init: (db) => {
|
|
132
132
|
loadVecExtension(db);
|
|
133
|
-
|
|
133
|
+
warnIfNonCanonicalIndexGeneration(db, resolvedPath);
|
|
134
134
|
},
|
|
135
135
|
create: false,
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
|
-
function
|
|
138
|
+
function warnIfNonCanonicalIndexGeneration(db, resolvedPath) {
|
|
139
139
|
if (isCanonicalIndexGeneration(db))
|
|
140
140
|
return;
|
|
141
|
-
|
|
141
|
+
const classification = classifyIndexGeneration(db);
|
|
142
|
+
warnOnce(`index-read-noncanonical:${resolvedPath}`, `Index database at ${resolvedPath} does not match this akm's derived schema (stored generation ` +
|
|
143
|
+
`${classification.storedVersion ?? "unknown"}; this binary understands ${CANONICAL_INDEX_DB_VERSION}). ` +
|
|
144
|
+
"Reading it as-is; a query that needs a table or column this generation lacks will fail on its own. " +
|
|
145
|
+
"Run 'akm index' to rebuild it for this binary.");
|
|
142
146
|
}
|
|
143
147
|
/**
|
|
144
148
|
* Refuse to treat an UNREADABLE index as a missing one (#791).
|
|
@@ -160,6 +164,21 @@ export function assertIndexPathReadable(resolvedPath) {
|
|
|
160
164
|
return;
|
|
161
165
|
throw new ConfigError(`Index database exists but is not readable: ${describeInaccessiblePath(resolvedPath, code)}.`, "DATA_DIR_UNREADABLE");
|
|
162
166
|
}
|
|
167
|
+
function openPlainReadonly(resolvedPath) {
|
|
168
|
+
return openDatabase(resolvedPath, { readonly: true, create: false });
|
|
169
|
+
}
|
|
170
|
+
function openIsolatedSnapshotOrFallBack(resolvedPath) {
|
|
171
|
+
try {
|
|
172
|
+
return openSqliteReadSnapshot(resolvedPath);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (!(error instanceof SqliteReadSnapshotUnavailableError))
|
|
176
|
+
throw error;
|
|
177
|
+
warnOnce(`index-read-snapshot-unavailable:${resolvedPath}`, `Could not take a non-mutating snapshot of ${resolvedPath} (${error.message}) — falling back to a plain ` +
|
|
178
|
+
"read-only open of the index database.");
|
|
179
|
+
return openPlainReadonly(resolvedPath);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
163
182
|
/**
|
|
164
183
|
* Open an existing index for queries without changing the source database or
|
|
165
184
|
* running schema initialization. The default path attaches read-only to the
|
|
@@ -173,9 +192,7 @@ export function openReadonlyExistingDatabase(dbPath, options) {
|
|
|
173
192
|
assertIndexPathReadable(resolvedPath);
|
|
174
193
|
if (classifyPathAccess(resolvedPath).access === "absent")
|
|
175
194
|
return undefined;
|
|
176
|
-
const db = options?.isolatedSnapshot
|
|
177
|
-
? openSqliteReadSnapshot(resolvedPath)
|
|
178
|
-
: openDatabase(resolvedPath, { readonly: true, create: false });
|
|
195
|
+
const db = options?.isolatedSnapshot ? openIsolatedSnapshotOrFallBack(resolvedPath) : openPlainReadonly(resolvedPath);
|
|
179
196
|
if (!db)
|
|
180
197
|
return undefined;
|
|
181
198
|
// This opener bypasses openManagedDatabase/applyStandardPragmas by design (no
|
|
@@ -187,7 +204,7 @@ export function openReadonlyExistingDatabase(dbPath, options) {
|
|
|
187
204
|
// connection, so apply just that one.
|
|
188
205
|
try {
|
|
189
206
|
db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
190
|
-
|
|
207
|
+
warnIfNonCanonicalIndexGeneration(db, resolvedPath);
|
|
191
208
|
return db;
|
|
192
209
|
}
|
|
193
210
|
catch (error) {
|
|
@@ -244,10 +244,27 @@ export function hasCanonicalEntrySchema(db) {
|
|
|
244
244
|
return false;
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
export function
|
|
247
|
+
export function classifyIndexGeneration(db) {
|
|
248
|
+
let storedVersion;
|
|
248
249
|
try {
|
|
249
250
|
const row = db.prepare("SELECT value FROM index_meta WHERE key = 'version'").get();
|
|
250
|
-
|
|
251
|
+
storedVersion = row?.value;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
storedVersion = undefined;
|
|
255
|
+
}
|
|
256
|
+
if (storedVersion === String(CANONICAL_INDEX_DB_VERSION) && hasCanonicalEntrySchema(db)) {
|
|
257
|
+
return { status: "canonical", storedVersion };
|
|
258
|
+
}
|
|
259
|
+
const storedNumeric = storedVersion === undefined ? undefined : Number(storedVersion);
|
|
260
|
+
if (storedNumeric !== undefined && Number.isFinite(storedNumeric) && storedNumeric > CANONICAL_INDEX_DB_VERSION) {
|
|
261
|
+
return { status: "newer", storedVersion };
|
|
262
|
+
}
|
|
263
|
+
return { status: "older", storedVersion };
|
|
264
|
+
}
|
|
265
|
+
export function isCanonicalIndexGeneration(db) {
|
|
266
|
+
try {
|
|
267
|
+
return classifyIndexGeneration(db).status === "canonical";
|
|
251
268
|
}
|
|
252
269
|
catch {
|
|
253
270
|
return false;
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
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
|
+
* index.db schema and version stamps, kept in the
|
|
6
|
+
* storage layer. This isolates the one genuinely risky area (schema
|
|
7
|
+
* evolution) from the CRUD/FTS/vector queries.
|
|
8
|
+
*
|
|
9
|
+
* The meta accessors, embedding purge, and vec-availability probe that
|
|
10
|
+
* `ensureSchema` leans on live in the sibling `index-meta-repository` /
|
|
11
|
+
* `index-vec-repository` modules.
|
|
12
|
+
*/
|
|
13
|
+
import { ConfigError } from "../../core/errors.js";
|
|
14
|
+
import { warn } from "../../core/warn.js";
|
|
15
|
+
import { CANONICAL_ENTRY_SCHEMA_SQL, CANONICAL_INDEX_DB_VERSION, classifyIndexGeneration, isCanonicalIndexGeneration, } from "./index-entry-schema.js";
|
|
5
16
|
import { getMeta, setMeta } from "./index-meta-repository.js";
|
|
6
17
|
import { isVecAvailable, purgeEmbeddings } from "./index-vec-repository.js";
|
|
7
18
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -165,6 +176,15 @@ function rebuildIncompatibleIndexGeneration(db) {
|
|
|
165
176
|
return;
|
|
166
177
|
if (isCanonicalIndexGeneration(db))
|
|
167
178
|
return;
|
|
179
|
+
const classification = classifyIndexGeneration(db);
|
|
180
|
+
if (classification.status === "newer") {
|
|
181
|
+
throw new ConfigError(`Index database was built by a newer akm (stored generation ${classification.storedVersion ?? "unknown"}; ` +
|
|
182
|
+
`this binary understands generation ${CANONICAL_INDEX_DB_VERSION}). Refusing to modify it — upgrade akm to ` +
|
|
183
|
+
"write to this index, or delete index.db to rebuild it from scratch with this binary.", "INDEX_SCHEMA_INCOMPATIBLE");
|
|
184
|
+
}
|
|
185
|
+
warn(`Index database generation ${classification.storedVersion ?? "unknown"} is older than this akm's generation ` +
|
|
186
|
+
`${CANONICAL_INDEX_DB_VERSION} — rebuilding the derived index (entries, FTS, embeddings, graph tables, ` +
|
|
187
|
+
"utility scores, and the LLM enrichment cache). This re-walks and re-indexes every source on the next run.");
|
|
168
188
|
let vecResetPending = false;
|
|
169
189
|
try {
|
|
170
190
|
db.exec("DROP TABLE IF EXISTS entries_vec");
|
|
@@ -326,12 +346,16 @@ export function ensureSchema(db, embeddingDim) {
|
|
|
326
346
|
// - When `embeddingDim` is a number, the caller explicitly asked for
|
|
327
347
|
// that dim and owns the dim-change/backup/wipe semantics.
|
|
328
348
|
const dimExplicit = embeddingDim !== undefined;
|
|
329
|
-
const
|
|
349
|
+
const requestedDim = embeddingDim ?? (Number(getMeta(db, "embeddingDim")) || EMBEDDING_DIM);
|
|
350
|
+
const effectiveDim = Number.isInteger(requestedDim) && requestedDim > 0 ? requestedDim : EMBEDDING_DIM;
|
|
351
|
+
if (effectiveDim !== requestedDim) {
|
|
352
|
+
warn(`Invalid embedding dimension ${requestedDim} — falling back to the default (${EMBEDDING_DIM}).`);
|
|
353
|
+
}
|
|
330
354
|
if (isVecAvailable(db)) {
|
|
331
355
|
// Check if stored embedding dimension differs from configured one
|
|
332
356
|
if (dimExplicit) {
|
|
333
357
|
const storedDim = getMeta(db, "embeddingDim");
|
|
334
|
-
if (storedDim && storedDim !== String(
|
|
358
|
+
if (storedDim && storedDim !== String(effectiveDim)) {
|
|
335
359
|
// Stored vectors are incompatible with the new dimension. Drop the vec
|
|
336
360
|
// table so the block below recreates it at the new width; the BLOB rows
|
|
337
361
|
// go too. Regenerable from markdown — re-embedded by the next index.
|
|
@@ -340,9 +364,6 @@ export function ensureSchema(db, embeddingDim) {
|
|
|
340
364
|
}
|
|
341
365
|
const vecExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='entries_vec'").get();
|
|
342
366
|
if (!vecExists) {
|
|
343
|
-
if (!Number.isInteger(effectiveDim) || effectiveDim <= 0 || effectiveDim > 4096) {
|
|
344
|
-
throw new Error(`Invalid embedding dimension: ${effectiveDim}`);
|
|
345
|
-
}
|
|
346
367
|
db.exec(`
|
|
347
368
|
CREATE VIRTUAL TABLE entries_vec USING vec0(
|
|
348
369
|
id INTEGER PRIMARY KEY,
|
|
@@ -351,7 +372,7 @@ export function ensureSchema(db, embeddingDim) {
|
|
|
351
372
|
`);
|
|
352
373
|
}
|
|
353
374
|
if (dimExplicit) {
|
|
354
|
-
setMeta(db, "embeddingDim", String(
|
|
375
|
+
setMeta(db, "embeddingDim", String(effectiveDim));
|
|
355
376
|
}
|
|
356
377
|
}
|
|
357
378
|
else {
|
|
@@ -361,11 +382,11 @@ export function ensureSchema(db, embeddingDim) {
|
|
|
361
382
|
// changes, those stored BLOBs become silently incompatible.
|
|
362
383
|
if (dimExplicit) {
|
|
363
384
|
const storedDim = getMeta(db, "embeddingDim");
|
|
364
|
-
if (storedDim && storedDim !== String(
|
|
385
|
+
if (storedDim && storedDim !== String(effectiveDim)) {
|
|
365
386
|
// JS-fallback path: no vec table, just clear the stale BLOB vectors.
|
|
366
387
|
purgeEmbeddings(db);
|
|
367
388
|
}
|
|
368
|
-
setMeta(db, "embeddingDim", String(
|
|
389
|
+
setMeta(db, "embeddingDim", String(effectiveDim));
|
|
369
390
|
}
|
|
370
391
|
}
|
|
371
392
|
// Usage telemetry (usage_events) lives in state.db since Chunk-8 WI-8.3 —
|
|
@@ -13,6 +13,7 @@ import path from "node:path";
|
|
|
13
13
|
import { stashDirFor } from "../../core/asset/asset-placement.js";
|
|
14
14
|
import { bundleRefToString, isBundleSlug, parseBundleRef } from "../../core/asset/asset-ref.js";
|
|
15
15
|
import { warnOnce } from "../../core/warn.js";
|
|
16
|
+
import { escapeLikePattern } from "../like-pattern.js";
|
|
16
17
|
/** Serialize `Proposal.changes` for `metadata_json` (see {@link StoredFileChange}). */
|
|
17
18
|
function changesToStored(changes) {
|
|
18
19
|
return changes.map((c, i) => ({
|
|
@@ -390,7 +391,7 @@ export function getStateProposal(db, id, stashDir) {
|
|
|
390
391
|
* `%` / `_` / `\` in the prefix are escaped so the LIKE pattern is literal.
|
|
391
392
|
*/
|
|
392
393
|
export function listStateProposalIdsByPrefix(db, stashDir, idPrefix) {
|
|
393
|
-
const escaped = idPrefix
|
|
394
|
+
const escaped = escapeLikePattern(idPrefix);
|
|
394
395
|
const rows = db
|
|
395
396
|
.prepare(`SELECT id FROM proposals
|
|
396
397
|
WHERE stash_dir = ? AND status = 'pending' AND id LIKE ? ESCAPE '\\'
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* @module task-history-repository
|
|
11
11
|
*/
|
|
12
12
|
import { isRecord } from "../../core/common.js";
|
|
13
|
+
import { warnOnce } from "../../core/warn.js";
|
|
13
14
|
function metadataError(message) {
|
|
14
15
|
throw new Error(`invalid task_history metadata_json: ${message}`);
|
|
15
16
|
}
|
|
@@ -18,10 +19,6 @@ function validateDetail(value) {
|
|
|
18
19
|
return;
|
|
19
20
|
if (!isRecord(value))
|
|
20
21
|
metadataError("detail must be an object or null");
|
|
21
|
-
const allowed = new Set(["runId", "reason", "error", "exitCode"]);
|
|
22
|
-
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
23
|
-
if (unknown.length > 0)
|
|
24
|
-
metadataError(`unknown detail fields: ${unknown.sort().join(", ")}`);
|
|
25
22
|
for (const field of ["runId", "reason", "error"]) {
|
|
26
23
|
if (value[field] !== undefined && typeof value[field] !== "string")
|
|
27
24
|
metadataError(`detail.${field} must be a string`);
|
|
@@ -60,7 +57,8 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
60
57
|
if (!isRecord(parsed))
|
|
61
58
|
metadataError("root must be an object");
|
|
62
59
|
if (parsed.metadataVersion !== undefined && parsed.metadataVersion !== 2) {
|
|
63
|
-
|
|
60
|
+
warnOnce(`task-history-metadata-version:${String(parsed.metadataVersion)}`, `task_history row has metadataVersion ${String(parsed.metadataVersion)}, newer than this akm's 2 — ` +
|
|
61
|
+
"decoding it best-effort as version 2 rather than rejecting the row.");
|
|
64
62
|
}
|
|
65
63
|
if (typeof parsed.durationMs !== "number")
|
|
66
64
|
metadataError("durationMs must be a number");
|
|
@@ -69,13 +67,22 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
69
67
|
metadataError("engine must be a string or null");
|
|
70
68
|
}
|
|
71
69
|
if (parsed.targetVocab !== undefined && parsed.targetVocab !== 2) {
|
|
72
|
-
|
|
70
|
+
warnOnce(`task-history-target-vocab:${String(parsed.targetVocab)}`, `task_history row has targetVocab ${String(parsed.targetVocab)}, newer than this akm's 2 — falling back to ` +
|
|
71
|
+
"the legacy target_kind mapping rather than rejecting the row.");
|
|
73
72
|
}
|
|
74
73
|
validateDetail(detail);
|
|
74
|
+
const cleanDetail = detail
|
|
75
|
+
? {
|
|
76
|
+
...(detail.runId !== undefined ? { runId: detail.runId } : {}),
|
|
77
|
+
...(detail.reason !== undefined ? { reason: detail.reason } : {}),
|
|
78
|
+
...(detail.error !== undefined ? { error: detail.error } : {}),
|
|
79
|
+
...(detail.exitCode !== undefined ? { exitCode: detail.exitCode } : {}),
|
|
80
|
+
}
|
|
81
|
+
: null;
|
|
75
82
|
return {
|
|
76
83
|
metadataVersion: 2,
|
|
77
84
|
durationMs: parsed.durationMs,
|
|
78
|
-
detail:
|
|
85
|
+
detail: cleanDetail,
|
|
79
86
|
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
80
87
|
...(parsed.targetVocab === 2 ? { targetVocab: 2 } : {}),
|
|
81
88
|
};
|
|
@@ -2,9 +2,10 @@
|
|
|
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 { randomUUID } from "node:crypto";
|
|
5
|
-
import { UsageError } from "../../core/errors.js";
|
|
5
|
+
import { NotFoundError, UsageError } from "../../core/errors.js";
|
|
6
6
|
import { openStateDatabase, withImmediateTransaction } from "../../core/state-db.js";
|
|
7
7
|
import { borrowScopedStateDb, withStateDbScope } from "../../core/state-db-scope.js";
|
|
8
|
+
import { escapeLikePattern } from "../like-pattern.js";
|
|
8
9
|
import { resolveStorageLocations } from "../locations.js";
|
|
9
10
|
import { insertEventOnce, insertEventStrict } from "./events-repository.js";
|
|
10
11
|
function assertAttemptReservationLease(input, run) {
|
|
@@ -96,6 +97,19 @@ export class WorkflowRunsRepository {
|
|
|
96
97
|
const row = this.db.prepare("SELECT 1 FROM workflow_runs WHERE id = ? LIMIT 1").get(runId);
|
|
97
98
|
return !!row;
|
|
98
99
|
}
|
|
100
|
+
/** The one run id starting with `prefix` (#919); `UsageError` on several, `NotFoundError` on none. */
|
|
101
|
+
resolveRunIdPrefix(prefix) {
|
|
102
|
+
const escaped = escapeLikePattern(prefix);
|
|
103
|
+
const rows = this.db
|
|
104
|
+
.prepare("SELECT id FROM workflow_runs WHERE id LIKE ? ESCAPE '\\' ORDER BY id ASC")
|
|
105
|
+
.all(`${escaped}%`);
|
|
106
|
+
if (rows.length === 1)
|
|
107
|
+
return rows[0].id;
|
|
108
|
+
if (rows.length > 1) {
|
|
109
|
+
throw new UsageError(`Ambiguous workflow run id prefix "${prefix}" — matches: ${rows.map((r) => r.id).join(", ")}`, "INVALID_FLAG_VALUE");
|
|
110
|
+
}
|
|
111
|
+
throw new NotFoundError(`Workflow run "${prefix}" not found.`, "WORKFLOW_NOT_FOUND");
|
|
112
|
+
}
|
|
99
113
|
listRuns(filter) {
|
|
100
114
|
const filters = [];
|
|
101
115
|
const params = [];
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import os from "node:os";
|
|
15
15
|
import path from "node:path";
|
|
16
|
+
import { sleepSync } from "../runtime.js";
|
|
16
17
|
import { openDatabaseFinalizing } from "./database.js";
|
|
18
|
+
const SNAPSHOT_MAX_ATTEMPTS = 8;
|
|
19
|
+
const SNAPSHOT_BACKOFF_MS = 25;
|
|
17
20
|
export class SqliteReadSnapshotUnavailableError extends Error {
|
|
18
21
|
constructor(message) {
|
|
19
22
|
super(message);
|
|
@@ -67,19 +70,17 @@ function fingerprintsEqual(left, right) {
|
|
|
67
70
|
export function openSqliteReadSnapshot(dbPath) {
|
|
68
71
|
if (!pathExists(dbPath))
|
|
69
72
|
return undefined;
|
|
70
|
-
if (pathExists(`${dbPath}-journal`)) {
|
|
71
|
-
throw new SqliteReadSnapshotUnavailableError("an active SQLite rollback journal is present; a non-mutating point-in-time snapshot is unavailable");
|
|
72
|
-
}
|
|
73
73
|
const snapshotDir = fs.mkdtempSync(path.join(os.tmpdir(), "akm-sqlite-read-"));
|
|
74
74
|
const snapshotPath = path.join(snapshotDir, "snapshot.db");
|
|
75
75
|
let db;
|
|
76
76
|
try {
|
|
77
77
|
let copied = false;
|
|
78
|
-
for (let attempt = 0; attempt <
|
|
78
|
+
for (let attempt = 0; attempt < SNAPSHOT_MAX_ATTEMPTS; attempt++) {
|
|
79
|
+
if (attempt > 0)
|
|
80
|
+
sleepSync(SNAPSHOT_BACKOFF_MS * attempt);
|
|
79
81
|
try {
|
|
80
|
-
if (pathExists(`${dbPath}-journal`))
|
|
81
|
-
|
|
82
|
-
}
|
|
82
|
+
if (pathExists(`${dbPath}-journal`))
|
|
83
|
+
continue;
|
|
83
84
|
const before = databaseFingerprint(dbPath);
|
|
84
85
|
fs.copyFileSync(dbPath, snapshotPath);
|
|
85
86
|
if (before.wal)
|
|
@@ -87,7 +88,7 @@ export function openSqliteReadSnapshot(dbPath) {
|
|
|
87
88
|
else
|
|
88
89
|
fs.rmSync(`${snapshotPath}-wal`, { force: true });
|
|
89
90
|
const after = databaseFingerprint(dbPath);
|
|
90
|
-
if (fingerprintsEqual(before, after)) {
|
|
91
|
+
if (fingerprintsEqual(before, after) && !pathExists(`${dbPath}-journal`)) {
|
|
91
92
|
copied = true;
|
|
92
93
|
break;
|
|
93
94
|
}
|
|
@@ -99,7 +100,8 @@ export function openSqliteReadSnapshot(dbPath) {
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
if (!copied) {
|
|
102
|
-
throw new SqliteReadSnapshotUnavailableError(
|
|
103
|
+
throw new SqliteReadSnapshotUnavailableError(`SQLite main/WAL files did not settle after ${SNAPSHOT_MAX_ATTEMPTS} attempts with backoff — ` +
|
|
104
|
+
"a writer may be continuously active, or a hot rollback journal never cleared");
|
|
103
105
|
}
|
|
104
106
|
db = openDatabaseFinalizing(snapshotPath, { readonly: true, create: false });
|
|
105
107
|
const closeSnapshot = db.close.bind(db);
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
//
|
|
30
30
|
// Tests inject a fake exec so unit tests don't touch the real crontab.
|
|
31
31
|
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { createHash } from "node:crypto";
|
|
32
33
|
import path from "node:path";
|
|
33
34
|
import { ConfigError } from "../../core/errors.js";
|
|
34
35
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
@@ -60,9 +61,16 @@ export function CRON_BACKEND(options = {}) {
|
|
|
60
61
|
// Create the log directory before writing the crontab line — cron
|
|
61
62
|
// appends with `>>` and the surrounding shell will fail the entire
|
|
62
63
|
// entry if the parent directory doesn't exist.
|
|
63
|
-
const
|
|
64
|
+
const cronLineParts = buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
|
|
65
|
+
const cronLine = cronLineParts.line;
|
|
64
66
|
assertPortableCronLine(cronLine);
|
|
65
67
|
fsLike.ensureDir(logDir);
|
|
68
|
+
if (cronLineParts.wrapper) {
|
|
69
|
+
if (!fsLike.writeFile) {
|
|
70
|
+
throw new ConfigError("Cron backend needs to write a wrapper script for this task's long invocation, but the configured filesystem cannot write files.", "INVALID_CONFIG_FILE");
|
|
71
|
+
}
|
|
72
|
+
fsLike.writeFile(cronLineParts.wrapper.path, cronLineParts.wrapper.content);
|
|
73
|
+
}
|
|
66
74
|
const existing = readCrontab(exec);
|
|
67
75
|
const nativeId = schedulerBindingNativeId(task);
|
|
68
76
|
const blocks = listBlocks(existing);
|
|
@@ -245,14 +253,35 @@ function isCronBindingSnapshot(value) {
|
|
|
245
253
|
value.kind === CRON_SNAPSHOT &&
|
|
246
254
|
typeof value.crontab === "string");
|
|
247
255
|
}
|
|
248
|
-
|
|
249
|
-
export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
|
|
256
|
+
function buildCronLineParts(task, akmArgv, logDir, contextPath, _target) {
|
|
250
257
|
const spec = parseSchedule(task.cron, "cron");
|
|
251
258
|
const cronExpr = translateToCron(spec);
|
|
252
|
-
const
|
|
259
|
+
const nativeId = schedulerBindingNativeId(task);
|
|
260
|
+
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
253
261
|
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
254
262
|
const cmd = invocation.argv.map((part) => quoteForCron(part)).join(" ");
|
|
255
|
-
|
|
263
|
+
const directLine = `${cronExpr} ${cmd} >> ${quoteForCron(logPath)} 2>&1`;
|
|
264
|
+
if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT) {
|
|
265
|
+
return { line: directLine };
|
|
266
|
+
}
|
|
267
|
+
const content = cronWrapperScriptContent(invocation.argv);
|
|
268
|
+
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
269
|
+
const wrapperPath = path.join(logDir, `${CRON_WRAPPER_PREFIX}${nativeId}-${contentHash}.sh`);
|
|
270
|
+
const line = `${cronExpr} sh ${quoteForCron(wrapperPath)} >> ${quoteForCron(logPath)} 2>&1`;
|
|
271
|
+
return { line, wrapper: { path: wrapperPath, content } };
|
|
272
|
+
}
|
|
273
|
+
const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
|
|
274
|
+
function quoteForShellScript(part) {
|
|
275
|
+
if (/^[A-Za-z0-9_\-./@:=+,]+$/.test(part))
|
|
276
|
+
return part;
|
|
277
|
+
return `'${part.replace(/'/g, `'\\''`)}'`;
|
|
278
|
+
}
|
|
279
|
+
function cronWrapperScriptContent(argv) {
|
|
280
|
+
const cmd = argv.map(quoteForShellScript).join(" ");
|
|
281
|
+
return `#!/bin/sh\nexec ${cmd}\n`;
|
|
282
|
+
}
|
|
283
|
+
export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
|
|
284
|
+
return buildCronLineParts(task, akmArgv, logDir, contextPath, _target).line;
|
|
256
285
|
}
|
|
257
286
|
/** The crontab line as it appears inside a block — commented when disabled. */
|
|
258
287
|
export function cronBlockBody(cronLine, enabled, executionEvidenceDigest) {
|
|
@@ -28,6 +28,7 @@ import launchdTemplate from "../../assets/backends/launchd-template.xml" with {
|
|
|
28
28
|
import { hasErrnoCode } from "../../core/common.js";
|
|
29
29
|
import { ConfigError } from "../../core/errors.js";
|
|
30
30
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
31
|
+
import { warn } from "../../core/warn.js";
|
|
31
32
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
32
33
|
import { parseSchedule, translateToLaunchd } from "../schedule.js";
|
|
33
34
|
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
@@ -514,13 +515,7 @@ function inspectStableLaunchdNamespace(seedIds, context) {
|
|
|
514
515
|
throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
|
|
515
516
|
}
|
|
516
517
|
const loadedLabels = parseLaunchdLoadedLabels(domain.stdout);
|
|
517
|
-
if (loadedLabels === undefined) {
|
|
518
|
-
throw new ConfigError("launchctl returned an unsafe, unsupported, or oversized loaded-service inventory during scheduler state inspection.", "INVALID_CONFIG_FILE");
|
|
519
|
-
}
|
|
520
518
|
const disabledLabels = readDisabledLabels(context.exec);
|
|
521
|
-
if (disabledLabels === undefined) {
|
|
522
|
-
throw new ConfigError("launchctl print-disabled failed during scheduler state inspection.", "INVALID_CONFIG_FILE");
|
|
523
|
-
}
|
|
524
519
|
const akmDisabledLabels = [...disabledLabels].filter((label) => label.startsWith(LAUNCHD_LABEL_PREFIX)).sort();
|
|
525
520
|
const plistEntries = [];
|
|
526
521
|
if (context.fsLike.exists(context.agentsDir)) {
|
|
@@ -834,48 +829,50 @@ function normalizeSignature(xml) {
|
|
|
834
829
|
* labels we will track. Exceeding either still returns `undefined`.
|
|
835
830
|
*/
|
|
836
831
|
export function parseLaunchdLoadedLabels(output) {
|
|
837
|
-
if (Buffer.byteLength(output, "utf8") > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES)
|
|
838
|
-
return undefined;
|
|
839
832
|
const labels = new Set();
|
|
840
833
|
// Our own namespace is the only thing we look for. `[^\s"{}=,()]` stops the
|
|
841
834
|
// token at whatever punctuation the surrounding launchctl syntax uses, so a
|
|
842
835
|
// label works whether it appears as a bare table cell, a quoted string, or a
|
|
843
836
|
// dictionary key.
|
|
844
837
|
const labelPattern = /com\.akm\.task\.[^\s"{}=,()]+/gu;
|
|
845
|
-
for (const match of output.matchAll(labelPattern)) {
|
|
838
|
+
for (const match of boundLaunchdOutput(output).matchAll(labelPattern)) {
|
|
846
839
|
const label = match[0];
|
|
847
840
|
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
848
841
|
continue;
|
|
849
842
|
labels.add(label);
|
|
850
|
-
if (labels.size
|
|
851
|
-
|
|
843
|
+
if (labels.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
|
|
844
|
+
break;
|
|
852
845
|
}
|
|
853
846
|
return labels;
|
|
854
847
|
}
|
|
848
|
+
function boundLaunchdOutput(output) {
|
|
849
|
+
return output.length > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES ? output.slice(0, MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES) : output;
|
|
850
|
+
}
|
|
855
851
|
function readDisabledLabels(exec) {
|
|
856
852
|
try {
|
|
857
853
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|
|
858
|
-
if (result.status !== 0)
|
|
859
|
-
|
|
854
|
+
if (result.status !== 0) {
|
|
855
|
+
warn("[akm] launchctl print-disabled exited %d; assuming no akm task is disabled.", result.status);
|
|
856
|
+
return new Set();
|
|
857
|
+
}
|
|
860
858
|
return parseDisabledLabels(result.stdout);
|
|
861
859
|
}
|
|
862
|
-
catch {
|
|
863
|
-
|
|
860
|
+
catch (error) {
|
|
861
|
+
warn("[akm] launchctl print-disabled could not be run; assuming no akm task is disabled: %s", error instanceof Error ? error.message : String(error));
|
|
862
|
+
return new Set();
|
|
864
863
|
}
|
|
865
864
|
}
|
|
866
865
|
function parseDisabledLabels(output) {
|
|
867
|
-
const envelope = /^\s*disabled services\s*=\s*\{([\s\S]*)\}\s*$/.exec(output);
|
|
868
|
-
if (!envelope)
|
|
869
|
-
return undefined;
|
|
870
866
|
const disabled = new Set();
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
if (!
|
|
875
|
-
|
|
876
|
-
if (
|
|
877
|
-
disabled.add(
|
|
878
|
-
|
|
867
|
+
const entryPattern = /"(com\.akm\.task\.[^"\r\n]+)"\s*=>\s*(true|false|enabled|disabled)/gu;
|
|
868
|
+
for (const match of boundLaunchdOutput(output).matchAll(entryPattern)) {
|
|
869
|
+
const label = match[1];
|
|
870
|
+
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
871
|
+
continue;
|
|
872
|
+
if (match[2] === "true" || match[2] === "disabled")
|
|
873
|
+
disabled.add(label);
|
|
874
|
+
if (disabled.size >= MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
|
|
875
|
+
break;
|
|
879
876
|
}
|
|
880
877
|
return disabled;
|
|
881
878
|
}
|
|
@@ -552,6 +552,8 @@ function expandNativeTriggers(trigger) {
|
|
|
552
552
|
return [{ kind: "daily", atHour: trigger.atHour, atMinute: trigger.atMinute }];
|
|
553
553
|
case "weekly":
|
|
554
554
|
return [trigger];
|
|
555
|
+
case "monthly":
|
|
556
|
+
return [trigger];
|
|
555
557
|
}
|
|
556
558
|
}
|
|
557
559
|
function renderNativeTrigger(trigger, startBoundary) {
|
|
@@ -570,9 +572,10 @@ ${repetition} <StartBoundary>${startBoundary}</StartBoundary>
|
|
|
570
572
|
<ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay>
|
|
571
573
|
</CalendarTrigger>`;
|
|
572
574
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
575
|
+
if (trigger.kind === "weekly") {
|
|
576
|
+
const dayMap = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
577
|
+
const days = trigger.daysOfWeek.map((d) => ` <${dayMap[d]} />`).join("\n");
|
|
578
|
+
return ` <CalendarTrigger>
|
|
576
579
|
<StartBoundary>${startBoundary}</StartBoundary>
|
|
577
580
|
<Enabled>true</Enabled>
|
|
578
581
|
<ScheduleByWeek>
|
|
@@ -582,6 +585,35 @@ ${days}
|
|
|
582
585
|
<WeeksInterval>1</WeeksInterval>
|
|
583
586
|
</ScheduleByWeek>
|
|
584
587
|
</CalendarTrigger>`;
|
|
588
|
+
}
|
|
589
|
+
const monthMap = [
|
|
590
|
+
"January",
|
|
591
|
+
"February",
|
|
592
|
+
"March",
|
|
593
|
+
"April",
|
|
594
|
+
"May",
|
|
595
|
+
"June",
|
|
596
|
+
"July",
|
|
597
|
+
"August",
|
|
598
|
+
"September",
|
|
599
|
+
"October",
|
|
600
|
+
"November",
|
|
601
|
+
"December",
|
|
602
|
+
];
|
|
603
|
+
const days = trigger.daysOfMonth.map((d) => ` <Day>${d}</Day>`).join("\n");
|
|
604
|
+
const months = trigger.months.map((m) => ` <${monthMap[m - 1]} />`).join("\n");
|
|
605
|
+
return ` <CalendarTrigger>
|
|
606
|
+
<StartBoundary>${startBoundary}</StartBoundary>
|
|
607
|
+
<Enabled>true</Enabled>
|
|
608
|
+
<ScheduleByMonth>
|
|
609
|
+
<DaysOfMonth>
|
|
610
|
+
${days}
|
|
611
|
+
</DaysOfMonth>
|
|
612
|
+
<Months>
|
|
613
|
+
${months}
|
|
614
|
+
</Months>
|
|
615
|
+
</ScheduleByMonth>
|
|
616
|
+
</CalendarTrigger>`;
|
|
585
617
|
}
|
|
586
618
|
function formatRepetitionInterval(minutes) {
|
|
587
619
|
return formatMinuteDuration(minutes);
|
|
@@ -617,6 +649,21 @@ function nextStartBoundary(trigger, now) {
|
|
|
617
649
|
boundary.setHours(trigger.atHour, trigger.atMinute, 0, 0);
|
|
618
650
|
}
|
|
619
651
|
return boundary;
|
|
652
|
+
case "monthly": {
|
|
653
|
+
boundary.setHours(trigger.atHour, trigger.atMinute, 0, 0);
|
|
654
|
+
const daysOfMonth = new Set(trigger.daysOfMonth);
|
|
655
|
+
const months = new Set(trigger.months);
|
|
656
|
+
for (let guard = 0; guard < 4000; guard++) {
|
|
657
|
+
if (daysOfMonth.has(boundary.getDate()) &&
|
|
658
|
+
months.has(boundary.getMonth() + 1) &&
|
|
659
|
+
boundary.getTime() > now.getTime()) {
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
boundary.setDate(boundary.getDate() + 1);
|
|
663
|
+
boundary.setHours(trigger.atHour, trigger.atMinute, 0, 0);
|
|
664
|
+
}
|
|
665
|
+
return boundary;
|
|
666
|
+
}
|
|
620
667
|
}
|
|
621
668
|
}
|
|
622
669
|
function formatStartBoundary(d) {
|
|
@@ -24,6 +24,8 @@ export function frozenScriptCommand(script, materializedPath) {
|
|
|
24
24
|
return [process.execPath, materializedPath];
|
|
25
25
|
case "bun-standalone":
|
|
26
26
|
return [process.execPath, STANDALONE_FROZEN_SCRIPT_ARG, materializedPath];
|
|
27
|
+
case "node":
|
|
28
|
+
return ["node", materializedPath];
|
|
27
29
|
case "powershell":
|
|
28
30
|
return ["powershell", "-NoProfile", "-NonInteractive", "-File", materializedPath];
|
|
29
31
|
case "cmd":
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import fs from "node:fs";
|
|
22
22
|
import { prepareCommandInvocation } from "../../commands/command/command-execution.js";
|
|
23
23
|
import { UsageError } from "../../core/errors.js";
|
|
24
|
+
import { warn } from "../../core/warn.js";
|
|
24
25
|
import { base, commandEnvironmentSnapshot, currentExecutionValues, defaultTaskShell, environmentSnapshot, qualifyOwnedRef, resolvedOwnedAsset, validatePreparedCommand, validateWorkflowRuntimeSource, } from "./prepare-support.js";
|
|
25
26
|
import { captureDirectoryIdentity, captureScriptTarget } from "./script-capture.js";
|
|
26
27
|
/** Project one canonical task-v3 source into immutable executable work. */
|
|
@@ -79,13 +80,7 @@ export async function prepareTaskV3Execution(document, context) {
|
|
|
79
80
|
if (target.kind === "workflow") {
|
|
80
81
|
// Stays reachable — task source v4 still has a top-level env: (P4-N4).
|
|
81
82
|
if (Object.keys(environment).length > 0) {
|
|
82
|
-
|
|
83
|
-
// not re-coded — tests/integration/tasks-with-classification-characterization.test.ts's
|
|
84
|
-
// P-04 block pins this exact code (CONVERT, not FLIP, per §7.2 F-A2.8:
|
|
85
|
-
// "the P-04 block ... stays reachable and stays pinned"). §5.2's target
|
|
86
|
-
// table predicted all 3 of this file's remaining sites → COMPOSITION_INVALID;
|
|
87
|
-
// this is the recorded deviation for the one site a preservation gate blocks.
|
|
88
|
-
throw new UsageError("Task workflow env cannot be consumed by the durable workflow runtime in 0.9.2; remove env or use a command target.", "INVALID_FLAG_VALUE");
|
|
83
|
+
warn("[akm] Task %s: env: is not translated for a workflow target and will be ignored; the durable workflow runtime does not consume it. Use a command target, or drop env:.", context.taskRef);
|
|
89
84
|
}
|
|
90
85
|
const resolved = await resolvedOwnedAsset(qualified, "workflow", context);
|
|
91
86
|
validateWorkflowRuntimeSource(resolved.file, resolved.bundleRoot, context.readFile ?? ((targetPath) => fs.readFileSync(targetPath)));
|