akm-cli 0.9.6 → 0.9.8-beta.1
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 +408 -0
- package/dist/assets/hints/cli-hints-full.md +3 -3
- package/dist/assets/improve-strategies/catchup.json +40 -11
- package/dist/assets/improve-strategies/thorough.json +45 -7
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +2 -2
- package/dist/commands/agent/contribute-cli.js +11 -0
- package/dist/commands/env/env.js +2 -21
- package/dist/commands/health/checks.js +0 -25
- package/dist/commands/health/improve-metrics.js +8 -34
- package/dist/commands/health/windows.js +0 -4
- package/dist/commands/health.js +1 -35
- package/dist/commands/improve/consolidate/eligibility.js +11 -5
- package/dist/commands/improve/extract.js +36 -32
- package/dist/commands/improve/improve-cli.js +1 -1
- package/dist/commands/improve/improve-strategies.js +0 -4
- package/dist/commands/improve/memory/memory-belief.js +15 -5
- package/dist/commands/improve/memory/memory-contradiction-detect.js +16 -25
- package/dist/commands/improve/memory/memory-improve.js +9 -20
- package/dist/commands/improve/preparation.js +53 -37
- package/dist/commands/improve/reflect.js +14 -18
- package/dist/commands/lint/base-linter.js +182 -19
- package/dist/commands/lint/index.js +21 -9
- package/dist/commands/migrate/config-extra-params.js +61 -0
- package/dist/commands/migrate/dead-residue.js +113 -0
- package/dist/commands/migrate/stale-txn.js +49 -0
- package/dist/commands/migrate-cli.js +42 -1
- package/dist/commands/proposal/proposal.js +1 -21
- package/dist/commands/proposal/repository.js +0 -4
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -6
- package/dist/commands/read/curate.js +51 -18
- package/dist/commands/read/search-cli.js +24 -1
- package/dist/commands/read/show.js +2 -1
- package/dist/commands/sources/installed-stashes.js +5 -1
- package/dist/commands/sources/self-update.js +38 -1
- package/dist/commands/sources/sources-cli.js +17 -1
- package/dist/commands/tasks/tasks.js +0 -8
- package/dist/commands/url-checker.js +20 -28
- package/dist/core/adapter/adapters/agent-skills-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-adapter.js +2 -3
- package/dist/core/adapter/adapters/akm-lint.js +2 -20
- package/dist/core/adapter/adapters/akm-task-adapter.js +2 -6
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +2 -6
- package/dist/core/adapter/adapters/dotenv-adapter.js +2 -22
- package/dist/core/adapter/adapters/generic-files-adapter.js +3 -9
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +2 -6
- package/dist/core/adapter/adapters/okf-adapter.js +7 -18
- package/dist/core/adapter/adapters/shared.js +2 -0
- package/dist/core/adapter/adapters/tool-dir-shared.js +3 -9
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +2 -6
- package/dist/core/adapter/validate-context.js +1 -3
- package/dist/core/asset/asset-placement.js +14 -5
- package/dist/core/asset/frontmatter.js +212 -0
- package/dist/core/asset/memory-archive.js +97 -0
- package/dist/core/common.js +66 -2
- package/dist/core/config/config-walker.js +6 -10
- package/dist/core/config/config.js +11 -8
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/extra-params.js +17 -13
- package/dist/core/improve-result.js +1 -3
- package/dist/core/json-schema.js +9 -11
- package/dist/core/state/migrations.js +52 -2
- package/dist/core/state-db.js +2 -1
- package/dist/execution/executable-identity.js +1 -3
- package/dist/execution/guarded-source.js +1 -6
- package/dist/indexer/bundle-identity-guard.js +6 -1
- package/dist/indexer/db/graph-db.js +139 -154
- package/dist/indexer/ensure-index.js +11 -19
- package/dist/indexer/graph/graph-boost.js +23 -34
- package/dist/indexer/graph/graph-extraction.js +12 -2
- package/dist/indexer/indexer.js +1 -1
- package/dist/indexer/lookup/adapter-concept-owner.js +12 -9
- package/dist/indexer/passes/memory-inference.js +7 -2
- package/dist/indexer/scan/drain-dir.js +2 -4
- package/dist/indexer/search/db-search.js +3 -3
- package/dist/indexer/search/fts-query.js +10 -15
- package/dist/indexer/search/search-source.js +0 -13
- package/dist/indexer/usage/usage-events.js +9 -1
- package/dist/indexer/walk/walker.js +11 -6
- package/dist/integrations/harnesses/claude/result-extractor.js +30 -15
- package/dist/integrations/harnesses/codex/result-extractor.js +43 -6
- package/dist/integrations/harnesses/copilot/result-extractor.js +39 -12
- package/dist/integrations/harnesses/gemini/result-extractor.js +40 -12
- package/dist/integrations/harnesses/index.js +0 -4
- package/dist/integrations/harnesses/openhands/result-extractor.js +51 -19
- package/dist/integrations/harnesses/pi/result-extractor.js +45 -12
- package/dist/integrations/lockfile.js +0 -14
- package/dist/integrations/session-logs/index.js +0 -81
- package/dist/llm/client.js +0 -12
- package/dist/llm/memory-infer.js +1 -3
- package/dist/llm/usage-telemetry.js +1 -3
- package/dist/registry/create-provider-registry.js +4 -0
- package/dist/registry/factory.js +4 -0
- package/dist/registry/resolve.js +23 -8
- package/dist/runtime.js +0 -12
- package/dist/scripts/akm-migrate-node.js +155 -251
- package/dist/scripts/akm-migrate.js +155 -251
- package/dist/setup/setup.js +6 -12
- package/dist/sources/providers/git-install.js +7 -2
- package/dist/sources/providers/tar-utils.js +1 -7
- package/dist/sources/snapshot-fetchers/content-extract.js +0 -17
- package/dist/sources/snapshot-fetchers/website-ingest.js +126 -0
- package/dist/storage/managed-db.js +20 -7
- package/dist/storage/repositories/events-repository.js +0 -81
- package/dist/storage/repositories/index-connection.js +45 -3
- package/dist/storage/repositories/index-entries-repository.js +0 -17
- package/dist/storage/repositories/index-fts-repository.js +24 -30
- package/dist/storage/repositories/index-utility-repository.js +0 -57
- package/dist/storage/repositories/index-vec-repository.js +25 -27
- package/dist/storage/repositories/task-history-repository.js +9 -3
- package/dist/tasks/backends/cron.js +49 -9
- package/dist/tasks/backends/launchd.js +8 -18
- package/dist/tasks/resolve-akm-bin.js +17 -2
- package/dist/tasks/run/task-history.js +21 -31
- package/dist/tasks/scheduler-invocation.js +8 -1
- package/dist/tasks/scheduler-sync.js +1 -6
- package/dist/tasks/source/bounded-document.js +1 -14
- package/dist/tasks/source/parse-task-source.js +23 -9
- package/dist/workflows/exec/child-workflow.js +1 -1
- package/dist/workflows/exec/native-executor.js +2 -2
- package/dist/workflows/exec/step-work.js +5 -17
- package/dist/workflows/exec/worktree.js +40 -6
- package/dist/workflows/freeze/task-bindings.js +2 -4
- package/dist/workflows/ir/compile.js +3 -14
- package/dist/workflows/ir/schema-v4.js +4 -6
- package/dist/workflows/ir/schema.js +2 -5
- package/dist/workflows/parser.js +23 -26
- package/dist/workflows/source-files.js +8 -13
- package/dist/workflows/source-ir/schema.js +1 -19
- package/docs/migration/v0.9.0-troubleshooting.md +12 -2
- package/docs/reference/cli.md +7 -1
- package/docs/reference/workflow-schema.md +24 -0
- package/package.json +2 -5
- package/dist/assets/improve-strategies/frequent.json +0 -15
- package/dist/assets/improve-strategies/memory-focus.json +0 -15
- package/dist/workflows/source-ir/compare.js +0 -17
|
@@ -229,9 +229,14 @@ export function searchVec(db, queryEmbedding, k) {
|
|
|
229
229
|
.all(buf, k);
|
|
230
230
|
}
|
|
231
231
|
catch (err) {
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
232
|
+
// A dimension mismatch (e.g. the embedding provider/model changed since
|
|
233
|
+
// the fast-path table was built) is a real, expected reason this query
|
|
234
|
+
// specifically cannot use the vec table — the complete BLOB table below
|
|
235
|
+
// is unaffected, so fall back to it rather than either silently
|
|
236
|
+
// returning [] (masking a genuinely corrupt index) or failing the whole
|
|
237
|
+
// search over one degraded index.
|
|
238
|
+
warn("[db] searchVec (sqlite-vec path) failed, falling back to JS-cosine scan:", err instanceof Error ? err.message : String(err));
|
|
239
|
+
return searchBlobVec(db, queryEmbedding, k);
|
|
235
240
|
}
|
|
236
241
|
}
|
|
237
242
|
// Fallback: JS-based cosine similarity over BLOB table
|
|
@@ -281,32 +286,25 @@ function bufferToFloat32(buf, expectedDim) {
|
|
|
281
286
|
return Array.from(f32);
|
|
282
287
|
}
|
|
283
288
|
function searchBlobVec(db, queryEmbedding, k) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (rows.length === 0)
|
|
287
|
-
return [];
|
|
288
|
-
const expectedDim = queryEmbedding.length;
|
|
289
|
-
const scored = [];
|
|
290
|
-
for (const row of rows) {
|
|
291
|
-
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
292
|
-
if (embedding === null)
|
|
293
|
-
continue;
|
|
294
|
-
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
295
|
-
scored.push({ id: row.id, similarity });
|
|
296
|
-
}
|
|
297
|
-
scored.sort((a, b) => b.similarity - a.similarity);
|
|
298
|
-
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
299
|
-
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
300
|
-
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
301
|
-
id,
|
|
302
|
-
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
303
|
-
}));
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
// MD-5: Log the failure so it's visible in diagnostics
|
|
307
|
-
warn("[db] searchBlobVec (JS fallback) failed:", err instanceof Error ? err.message : String(err));
|
|
289
|
+
const rows = db.prepare("SELECT id, embedding FROM embeddings").all();
|
|
290
|
+
if (rows.length === 0)
|
|
308
291
|
return [];
|
|
292
|
+
const expectedDim = queryEmbedding.length;
|
|
293
|
+
const scored = [];
|
|
294
|
+
for (const row of rows) {
|
|
295
|
+
const embedding = bufferToFloat32(row.embedding, expectedDim);
|
|
296
|
+
if (embedding === null)
|
|
297
|
+
continue;
|
|
298
|
+
const similarity = cosineSimilarity(queryEmbedding, embedding);
|
|
299
|
+
scored.push({ id: row.id, similarity });
|
|
309
300
|
}
|
|
301
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
302
|
+
// Convert cosine similarity to L2 distance for compatibility with sqlite-vec interface
|
|
303
|
+
// For normalized vectors: L2² = 2(1 - cos_sim)
|
|
304
|
+
return scored.slice(0, k).map(({ id, similarity }) => ({
|
|
305
|
+
id,
|
|
306
|
+
distance: Math.sqrt(2 * Math.max(0, 1 - similarity)),
|
|
307
|
+
}));
|
|
310
308
|
}
|
|
311
309
|
/**
|
|
312
310
|
* Return all entries that do not yet have an embedding row.
|
|
@@ -1,12 +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
|
+
/**
|
|
5
|
+
* Repository for the state.db `task_history` table (per-task run ledger,
|
|
6
|
+
* replaces the old per-task JSONL files). Extracted verbatim from
|
|
7
|
+
* core/state-db.ts — queries unchanged, only relocated behind the repository
|
|
8
|
+
* boundary. Re-exported by core/state-db.ts so existing importers resolve.
|
|
9
|
+
*
|
|
10
|
+
* @module task-history-repository
|
|
11
|
+
*/
|
|
12
|
+
import { isRecord } from "../../core/common.js";
|
|
4
13
|
function metadataError(message) {
|
|
5
14
|
throw new Error(`invalid task_history metadata_json: ${message}`);
|
|
6
15
|
}
|
|
7
|
-
function isRecord(value) {
|
|
8
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
-
}
|
|
10
16
|
function validateDetail(value) {
|
|
11
17
|
if (value === undefined || value === null)
|
|
12
18
|
return;
|
|
@@ -31,7 +31,7 @@ import { getTaskLogDir } from "../../core/paths.js";
|
|
|
31
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
32
32
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
33
33
|
import { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
|
|
34
|
-
import { buildScheduledBindingInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
34
|
+
import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
35
35
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
36
36
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
37
37
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
@@ -105,7 +105,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
105
105
|
replaceCrontab(exec, existing, next);
|
|
106
106
|
},
|
|
107
107
|
list() {
|
|
108
|
-
return [...inspectCronState(readCrontab(exec)).installed];
|
|
108
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
|
|
109
109
|
},
|
|
110
110
|
listForRebind() {
|
|
111
111
|
const existing = readCrontab(exec);
|
|
@@ -123,14 +123,14 @@ export function CRON_BACKEND(options = {}) {
|
|
|
123
123
|
});
|
|
124
124
|
},
|
|
125
125
|
listNativeArtifacts() {
|
|
126
|
-
return [...inspectCronState(readCrontab(exec)).artifacts];
|
|
126
|
+
return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
|
|
127
127
|
},
|
|
128
128
|
inspectBindings() {
|
|
129
|
-
return inspectCronState(readCrontab(exec));
|
|
129
|
+
return inspectCronState(readCrontab(exec), defaultContextPath);
|
|
130
130
|
},
|
|
131
131
|
snapshotBindings(ids) {
|
|
132
132
|
const crontab = readCrontab(exec);
|
|
133
|
-
const inspection = inspectCronState(crontab);
|
|
133
|
+
const inspection = inspectCronState(crontab, defaultContextPath);
|
|
134
134
|
const keys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
135
135
|
return Object.freeze({
|
|
136
136
|
kind: CRON_SNAPSHOT,
|
|
@@ -144,7 +144,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
144
144
|
throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
145
145
|
}
|
|
146
146
|
const existing = readCrontab(exec);
|
|
147
|
-
const current = inspectCronState(existing);
|
|
147
|
+
const current = inspectCronState(existing, defaultContextPath);
|
|
148
148
|
const safeNativeIds = [];
|
|
149
149
|
const errors = [];
|
|
150
150
|
if (expectedCurrent) {
|
|
@@ -192,7 +192,7 @@ export function CRON_BACKEND(options = {}) {
|
|
|
192
192
|
},
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
-
function inspectCronState(crontab) {
|
|
195
|
+
function inspectCronState(crontab, fallbackContextPath) {
|
|
196
196
|
const installed = [];
|
|
197
197
|
const artifacts = [];
|
|
198
198
|
for (const { id, body } of listBlocks(crontab)) {
|
|
@@ -207,7 +207,14 @@ function inspectCronState(crontab) {
|
|
|
207
207
|
signature: fingerprint,
|
|
208
208
|
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
209
209
|
binding: parsed.binding,
|
|
210
|
-
|
|
210
|
+
// A legacy (pre-`--scheduler-context`) row has no real descriptor
|
|
211
|
+
// path to report — `extractLegacyCronInvocation` leaves it "". Fall
|
|
212
|
+
// back to the current default so downstream consumers (context
|
|
213
|
+
// validation in `akm task prune`/`explain`, `sync`'s reuse of an
|
|
214
|
+
// existing binding's contextPath) see a real, resolvable descriptor
|
|
215
|
+
// rather than an empty path, since the row is about to be reconciled
|
|
216
|
+
// to a current one anyway (#881).
|
|
217
|
+
contextPath: parsed.contextPath || fallbackContextPath,
|
|
211
218
|
};
|
|
212
219
|
Object.defineProperty(ref, "nativeId", { value: id });
|
|
213
220
|
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
@@ -323,7 +330,40 @@ export function extractCronInvocation(body) {
|
|
|
323
330
|
const redirectIndex = fields.indexOf(">>", commandStart);
|
|
324
331
|
if (redirectIndex === -1)
|
|
325
332
|
return undefined;
|
|
326
|
-
|
|
333
|
+
const tail = fields.slice(commandStart, redirectIndex);
|
|
334
|
+
const parsed = parseScheduledBindingArgv(tail);
|
|
335
|
+
if (parsed)
|
|
336
|
+
return parsed;
|
|
337
|
+
// Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
|
|
338
|
+
// no context argument at all — just the akm argv immediately followed by
|
|
339
|
+
// the public `task run …` / `workflow run …` tail. `extractCronInvocation`
|
|
340
|
+
// only ever runs on a body already isolated between this backend's own
|
|
341
|
+
// `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
|
|
342
|
+
// this older shape here doesn't extend trust to any unmarked crontab
|
|
343
|
+
// line — it only lets sync see and reconcile a row akm already owns
|
|
344
|
+
// instead of treating it as absent and colliding with the still-present
|
|
345
|
+
// artifact (#881). Guarded on the marker's absence so a row that DOES
|
|
346
|
+
// carry `--scheduler-context` but fails to parse for some other reason
|
|
347
|
+
// is never silently reinterpreted as legacy.
|
|
348
|
+
if (tail.includes(SCHEDULER_CONTEXT_ARG))
|
|
349
|
+
return undefined;
|
|
350
|
+
return extractLegacyCronInvocation(tail);
|
|
351
|
+
}
|
|
352
|
+
function extractLegacyCronInvocation(tail) {
|
|
353
|
+
for (let index = 0; index < tail.length - 1; index += 1) {
|
|
354
|
+
if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
|
|
355
|
+
const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
|
|
356
|
+
if (!publicInvocation)
|
|
357
|
+
return undefined;
|
|
358
|
+
return {
|
|
359
|
+
binding: tail.slice(0, index),
|
|
360
|
+
contextPath: "",
|
|
361
|
+
invocation: publicInvocation.invocation,
|
|
362
|
+
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return undefined;
|
|
327
367
|
}
|
|
328
368
|
/** Reverse {@link quoteForCron} for a single whitespace-free token. */
|
|
329
369
|
function splitCronShellWords(value) {
|
|
@@ -25,6 +25,7 @@ import fs from "node:fs";
|
|
|
25
25
|
import os from "node:os";
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import launchdTemplate from "../../assets/backends/launchd-template.xml" with { type: "text" };
|
|
28
|
+
import { hasErrnoCode } from "../../core/common.js";
|
|
28
29
|
import { ConfigError } from "../../core/errors.js";
|
|
29
30
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
30
31
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
@@ -851,22 +852,6 @@ export function parseLaunchdLoadedLabels(output) {
|
|
|
851
852
|
}
|
|
852
853
|
return labels;
|
|
853
854
|
}
|
|
854
|
-
function countCharacter(value, needle) {
|
|
855
|
-
let count = 0;
|
|
856
|
-
for (const character of value)
|
|
857
|
-
if (character === needle)
|
|
858
|
-
count += 1;
|
|
859
|
-
return count;
|
|
860
|
-
}
|
|
861
|
-
function hasUnsafeLaunchdControlCharacter(output) {
|
|
862
|
-
for (let index = 0; index < output.length; index += 1) {
|
|
863
|
-
const code = output.charCodeAt(index);
|
|
864
|
-
if (code <= 8 || code === 11 || code === 12 || (code >= 14 && code <= 31) || (code >= 127 && code <= 159)) {
|
|
865
|
-
return true;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
855
|
function readDisabledLabels(exec) {
|
|
871
856
|
try {
|
|
872
857
|
const result = exec.run(["launchctl", "print-disabled", `gui/${exec.uid()}`]);
|
|
@@ -933,8 +918,13 @@ function defaultLaunchdFs() {
|
|
|
933
918
|
try {
|
|
934
919
|
return fs.readdirSync(dir);
|
|
935
920
|
}
|
|
936
|
-
catch {
|
|
937
|
-
|
|
921
|
+
catch (error) {
|
|
922
|
+
// Genuinely absent is an empty listing; anything else (e.g. EACCES) is
|
|
923
|
+
// not "no plists" — surfacing it as such could make inspectBindings
|
|
924
|
+
// think there is nothing to reconcile.
|
|
925
|
+
if (hasErrnoCode(error, "ENOENT"))
|
|
926
|
+
return [];
|
|
927
|
+
throw new ConfigError(`Unable to read LaunchAgents directory at "${dir}".`, "INVALID_CONFIG_FILE");
|
|
938
928
|
}
|
|
939
929
|
},
|
|
940
930
|
exists(file) {
|
|
@@ -10,7 +10,11 @@
|
|
|
10
10
|
* Resolution order:
|
|
11
11
|
*
|
|
12
12
|
* 1. `process.execPath` alone for a Bun standalone executable.
|
|
13
|
-
* 2. Absolute Node plus the public `dist/akm` package launcher
|
|
13
|
+
* 2. Absolute Node plus the public `dist/akm` package launcher — eligible
|
|
14
|
+
* when it is the active npm global install, or when this process
|
|
15
|
+
* cannot write to the directory containing it (a read-only mount, e.g.
|
|
16
|
+
* an image-baked install, gives the same "won't change out from under
|
|
17
|
+
* the scheduler" guarantee npm-global ownership does).
|
|
14
18
|
* 3. Absolute runtime plus the source/build CLI entry, classified as a
|
|
15
19
|
* checkout that requires explicit `--rebind` for scheduler writes.
|
|
16
20
|
*
|
|
@@ -51,12 +55,13 @@ export function resolveAkmInvocation(options = {}) {
|
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
const npmGlobal = !checkout && packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot);
|
|
58
|
+
const readOnlyInstall = !checkout && !npmGlobal && !(options.isPathWritable ?? isPathWritable)(path.dirname(launcherPath));
|
|
54
59
|
const kind = checkout ? "checkout" : npmGlobal ? "npm" : "package-local";
|
|
55
60
|
return {
|
|
56
61
|
argv: [absoluteInvocationPath(nodePath), absoluteInvocationPath(launcherPath)],
|
|
57
62
|
via: kind,
|
|
58
63
|
kind,
|
|
59
|
-
eligible: npmGlobal,
|
|
64
|
+
eligible: npmGlobal || readOnlyInstall,
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
const checkoutEntry = resolveCheckoutEntry(options.cliEntryUrl ?? import.meta.url, runtime, mainPath);
|
|
@@ -117,6 +122,16 @@ function isCheckoutLauncher(file) {
|
|
|
117
122
|
const packageRoot = path.dirname(path.dirname(launcher));
|
|
118
123
|
return fs.existsSync(path.join(packageRoot, ".git"));
|
|
119
124
|
}
|
|
125
|
+
/** Whether this process can write to `dir` — false also covers a read-only mount. */
|
|
126
|
+
function isPathWritable(dir) {
|
|
127
|
+
try {
|
|
128
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
120
135
|
function packageBelongsToNpmGlobalRoot(launcherPath, npmGlobalRoot) {
|
|
121
136
|
if (!npmGlobalRoot)
|
|
122
137
|
return false;
|
|
@@ -14,20 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* D8 result-vocabulary re-code (why, and the WRITE side's exact shape): see
|
|
16
16
|
* docs/architecture/decisions/0005-task-result-vocabulary-and-legacy-read-mapping.md.
|
|
17
|
-
* The READ side
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* "shell"/"script"/"prompt" written WITHOUT a marker, which no production
|
|
27
|
-
* writer ever does) -> `"unknown"`. The P0-pinned null fallbacks survive:
|
|
28
|
-
* workflow `ref` falls back to `""`, the command/prompt arm's `engine`
|
|
29
|
-
* falls back to `null`. A row carrying `targetVocab: 2` reads `target_kind`
|
|
30
|
-
* directly in the current vocabulary — no mapping needed.
|
|
17
|
+
* The READ side used to carry a permanent legacy-vocabulary mapping here
|
|
18
|
+
* (row B-51 of docs/plans/specs/p4-deletions-closeout.md called deleting it
|
|
19
|
+
* "review-blocking") — SUPERSEDED: `task_history` is DB-owned data, so the
|
|
20
|
+
* remap is now a one-time schema migration
|
|
21
|
+
* (`025-task-history-vocabulary-backfill` in src/core/state/migrations.ts)
|
|
22
|
+
* instead of a read-side shim run forever. Every row this function sees now
|
|
23
|
+
* carries the current vocabulary's `target_kind` strings; the P0-pinned null
|
|
24
|
+
* fallbacks still apply (workflow `ref` falls back to `""`, the command
|
|
25
|
+
* arm's `engine` falls back to `null`).
|
|
31
26
|
*
|
|
32
27
|
* A DAG leaf with respect to the rest of src/tasks/run/**: this module
|
|
33
28
|
* imports TaskRunResult/TaskRunStatus's TYPE from ./task-result but no VALUE
|
|
@@ -37,6 +32,7 @@
|
|
|
37
32
|
*/
|
|
38
33
|
import { rethrowIfTestIsolationError } from "../../core/errors.js";
|
|
39
34
|
import { withStateDb } from "../../core/state-db.js";
|
|
35
|
+
import { warn } from "../../core/warn.js";
|
|
40
36
|
import { decodeTaskHistoryMetadata, finalizeTaskHistoryAttempt, getTaskHistory, getTaskHistoryRuns, queryTaskHistory, upsertTaskHistory, } from "../../storage/repositories/task-history-repository.js";
|
|
41
37
|
/** Append (or finalize a reserved attempt into) one task_history row. */
|
|
42
38
|
export function appendHistory(result, historyReserved = false) {
|
|
@@ -67,7 +63,11 @@ export function appendHistory(result, historyReserved = false) {
|
|
|
67
63
|
}
|
|
68
64
|
catch (error) {
|
|
69
65
|
rethrowIfTestIsolationError(error);
|
|
70
|
-
// History recording
|
|
66
|
+
// History recording must not alter the task's own result/exit status —
|
|
67
|
+
// the task already ran and its outcome is independent of whether we can
|
|
68
|
+
// persist a history row. But silence would make a failed write
|
|
69
|
+
// indistinguishable from the task never having run, so warn.
|
|
70
|
+
warn(`task history: failed to record history for task ${result.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
export function readTaskHistory(options = {}) {
|
|
@@ -111,32 +111,22 @@ function decodeTaskHistoryRows(rows) {
|
|
|
111
111
|
* Convert a `TaskHistoryRow` from state.db back to a `TaskRunResult` shape
|
|
112
112
|
* that callers of `readTaskHistory()` expect.
|
|
113
113
|
*
|
|
114
|
-
*
|
|
115
|
-
* `
|
|
114
|
+
* Reads `target_kind` directly in the current (post-D8) vocabulary — the
|
|
115
|
+
* `025-task-history-vocabulary-backfill` state migration rewrites every
|
|
116
|
+
* legacy-vocabulary row before this ever runs against it.
|
|
116
117
|
*/
|
|
117
118
|
function taskHistoryRowToResult(row) {
|
|
118
119
|
const meta = decodeTaskHistoryMetadata(row.metadata_json);
|
|
119
|
-
const marked = meta.targetVocab === 2;
|
|
120
120
|
const target = (() => {
|
|
121
121
|
switch (row.target_kind) {
|
|
122
122
|
case "workflow":
|
|
123
|
-
// PRESERVED for both vintages (incl. the null-ref fallback).
|
|
124
123
|
return { kind: "workflow", ref: row.target_ref ?? "" };
|
|
125
124
|
case "command":
|
|
126
|
-
|
|
127
|
-
// LEGACY vocabulary: the native shell/script arm's shared string.
|
|
128
|
-
return marked ? { kind: "command", engine: meta.engine ?? null } : { kind: "shell" };
|
|
125
|
+
return { kind: "command", engine: meta.engine ?? null };
|
|
129
126
|
case "shell":
|
|
130
|
-
|
|
131
|
-
// "shell" row is unreachable from any production writer.
|
|
132
|
-
return marked ? { kind: "shell" } : { kind: "unknown" };
|
|
127
|
+
return { kind: "shell" };
|
|
133
128
|
case "script":
|
|
134
|
-
|
|
135
|
-
// "script" row is unreachable from any production writer.
|
|
136
|
-
return marked ? { kind: "script" } : { kind: "unknown" };
|
|
137
|
-
case "prompt":
|
|
138
|
-
// Only LEGACY rows (pre-P1b) ever wrote this string.
|
|
139
|
-
return marked ? { kind: "unknown" } : { kind: "command", engine: meta.engine ?? null };
|
|
129
|
+
return { kind: "script" };
|
|
140
130
|
default:
|
|
141
131
|
return { kind: "unknown" };
|
|
142
132
|
}
|
|
@@ -189,7 +189,14 @@ export function parseScheduledBindingArgv(argv) {
|
|
|
189
189
|
...(publicInvocation.target !== undefined ? { target: publicInvocation.target } : {}),
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Parse just the public `task run …` / `workflow run …` tail, with no
|
|
194
|
+
* `--scheduler-context` wrapper. Exported so a backend can recognize a
|
|
195
|
+
* pre-`--scheduler-context` invocation still sitting inside akm's own
|
|
196
|
+
* ownership-marked block (see `extractCronInvocation` in
|
|
197
|
+
* `src/tasks/backends/cron.ts`) without re-implementing this grammar.
|
|
198
|
+
*/
|
|
199
|
+
export function parsePublicSchedulerInvocation(invocation) {
|
|
193
200
|
if (invocation[0] === "task" && invocation[1] === "run" && invocation[2]) {
|
|
194
201
|
try {
|
|
195
202
|
if (normaliseTaskConceptId(invocation[2]) !== invocation[2])
|
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { loadAdapterExecutionSource } from "../commands/command/execution-source-loader.js";
|
|
8
8
|
import { makeBundleRef } from "../core/asset/asset-ref.js";
|
|
9
|
+
import { compareCodePoints, toPosix } from "../core/common.js";
|
|
9
10
|
import { UsageError } from "../core/errors.js";
|
|
10
11
|
import { canonicalizeWorkflowName, WORKFLOW_EXTENSIONS } from "../core/recognition-util.js";
|
|
11
12
|
import { captureGuardedDirectoryManifest, captureGuardedExecutionSource, GuardedExecutionSourceCollector, } from "../execution/guarded-source.js";
|
|
@@ -742,12 +743,6 @@ function freezeOperation(operation) {
|
|
|
742
743
|
function sameInvocation(left, right) {
|
|
743
744
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
744
745
|
}
|
|
745
|
-
function compareCodePoints(left, right) {
|
|
746
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
747
|
-
}
|
|
748
|
-
function toPosix(value) {
|
|
749
|
-
return value.replaceAll("\\", "/");
|
|
750
|
-
}
|
|
751
746
|
// Retain the concrete error in this module's public dependency graph so callers
|
|
752
747
|
// can continue to identify ownership failures without importing an adapter.
|
|
753
748
|
export { WorkflowSourceRejectionError };
|
|
@@ -47,6 +47,7 @@ import fs from "node:fs";
|
|
|
47
47
|
import path from "node:path";
|
|
48
48
|
import { types as utilTypes } from "node:util";
|
|
49
49
|
import { isAlias, isMap, isScalar, isSeq, LineCounter, parseDocument } from "yaml";
|
|
50
|
+
import { wellFormedUnicode } from "../../core/common.js";
|
|
50
51
|
import { UsageError } from "../../core/errors.js";
|
|
51
52
|
import { DURATION_UNITS, parseDuration } from "../../core/time.js";
|
|
52
53
|
import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
|
|
@@ -67,20 +68,6 @@ export function own(value, key) {
|
|
|
67
68
|
export function utf8Bytes(value) {
|
|
68
69
|
return new TextEncoder().encode(value).byteLength;
|
|
69
70
|
}
|
|
70
|
-
export function wellFormedUnicode(value) {
|
|
71
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
72
|
-
const code = value.charCodeAt(index);
|
|
73
|
-
if (code >= 0xd800 && code <= 0xdbff) {
|
|
74
|
-
const next = value.charCodeAt(index + 1);
|
|
75
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
76
|
-
return false;
|
|
77
|
-
index += 1;
|
|
78
|
-
}
|
|
79
|
-
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
71
|
/** The one per-field error funnel both grammars render through, distinguished only by `ctx.sourceLabel` (D2-N4). */
|
|
85
72
|
export function sourceError(ctx, fieldPath, detail) {
|
|
86
73
|
const dotted = fieldPath.length === 0
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* | root `version` | outcome |
|
|
18
18
|
* |------------------------|-----------------------------------------------------------------|
|
|
19
19
|
* | `4` | `parseTaskSourceV4Document` — the new grammar (row B-13) |
|
|
20
|
-
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED
|
|
20
|
+
* | `2` or `3` | in-memory read shim (below): the SAME pure planners `akm migrate apply` uses (`./task-to-v3.ts`, `./task-to-v4.ts`) convert the bytes already in hand to v4 in memory; the result is parsed and returned with a one-line stderr deprecation warning. If the deterministic conversion itself fails (an unmigratable shape — the file needs a human decision, not a re-run), falls back to `TASK_SCHEMA_VERSION_UNSUPPORTED` naming the specific blocked reason (issue #869) — the shim removes friction for the deterministic case, it never hides a real problem |
|
|
21
21
|
* | any other number | `TASK_SCHEMA_VERSION_UNSUPPORTED`, naming the migrator (B-14/B-15) |
|
|
22
22
|
* | absent / not a number | `parseTaskSourceV4Document` — its own `TASK_SOURCE_INVALID` "version is required and must be 4" / "must be exactly 4" wording (row B-16) |
|
|
23
23
|
*
|
|
@@ -57,6 +57,18 @@ export function peekTaskSourceVersion(root) {
|
|
|
57
57
|
return typeof value === "number" ? value : undefined;
|
|
58
58
|
}
|
|
59
59
|
const TASK_MIGRATE_HINT = "Run `akm migrate apply --dry-run` to preview the task-v3 to task-source-v4 conversion, then run `akm migrate apply`.";
|
|
60
|
+
/**
|
|
61
|
+
* Thrown only when the deterministic conversion itself could not produce a
|
|
62
|
+
* task source v4 document — a case where a person must decide the intended
|
|
63
|
+
* behavior (e.g. an ambiguous shell command), not one the migrator can just
|
|
64
|
+
* be re-run to fix. `reason`/`detail` are the SAME blocked outcome
|
|
65
|
+
* `akm migrate status`/`apply` reports for this file, so the message names
|
|
66
|
+
* the actual decision instead of pointing at a command that will report the
|
|
67
|
+
* identical block.
|
|
68
|
+
*/
|
|
69
|
+
function unmigratableVersionError(filePath, version, reason, detail) {
|
|
70
|
+
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version} and needs a human decision before it can run — the deterministic migrator cannot convert it automatically (${reason}${detail ? `: ${detail}` : ""}).`, "TASK_SCHEMA_VERSION_UNSUPPORTED", "Review the file and resolve the ambiguity by hand, then it will convert normally; `akm migrate status` reports the same reason.");
|
|
71
|
+
}
|
|
60
72
|
function unsupportedVersionError(filePath, version) {
|
|
61
73
|
return new UsageError(`TASK_SCHEMA_VERSION_UNSUPPORTED: Task at ${filePath} uses task schema version ${version}, which this release does not accept.`, "TASK_SCHEMA_VERSION_UNSUPPORTED", TASK_MIGRATE_HINT);
|
|
62
74
|
}
|
|
@@ -64,9 +76,10 @@ function unsupportedVersionError(filePath, version) {
|
|
|
64
76
|
* Plan the SAME bytes already in hand through the pure v3->v4 (and, for v2,
|
|
65
77
|
* chained v2->v3->v4) migration planner(s) — never touches disk, never
|
|
66
78
|
* writes the file, never re-reads it from disk. Returns the produced v4
|
|
67
|
-
* YAML text, or
|
|
68
|
-
* proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
69
|
-
* hard error this gate threw before the shim existed
|
|
79
|
+
* YAML text, or the blocked reason/detail when the deterministic conversion
|
|
80
|
+
* cannot proceed (an unmigratable v2/v3 shape) — the caller falls back to
|
|
81
|
+
* the same hard error this gate threw before the shim existed, now naming
|
|
82
|
+
* that reason.
|
|
70
83
|
*/
|
|
71
84
|
function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
72
85
|
const bytes = Buffer.from(yaml, "utf8");
|
|
@@ -89,12 +102,12 @@ function planInMemoryV4Bytes(version, yaml, filePath, workspaceRoot) {
|
|
|
89
102
|
else {
|
|
90
103
|
const v3Outcome = planTaskToV3File(baseInput);
|
|
91
104
|
if (v3Outcome.status !== "changed")
|
|
92
|
-
return
|
|
105
|
+
return { reason: v3Outcome.reason, detail: v3Outcome.detail };
|
|
93
106
|
v3Bytes = v3Outcome.after;
|
|
94
107
|
}
|
|
95
108
|
const v4Outcome = planTaskToV4File({ ...baseInput, bytes: v3Bytes });
|
|
96
109
|
if (v4Outcome.status !== "changed")
|
|
97
|
-
return
|
|
110
|
+
return { reason: v4Outcome.reason, detail: v4Outcome.detail };
|
|
98
111
|
return v4Outcome.after.toString("utf8");
|
|
99
112
|
}
|
|
100
113
|
/** Parse task source YAML, routing per the terminal table above. */
|
|
@@ -103,16 +116,17 @@ export function parseTaskSource(input) {
|
|
|
103
116
|
const version = peekTaskSourceVersion(root);
|
|
104
117
|
if (version !== undefined && version !== TASK_SOURCE_V4_VERSION) {
|
|
105
118
|
if (version === 2 || version === 3) {
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
119
|
+
const shimmed = planInMemoryV4Bytes(version, input.yaml, input.filePath, input.workspaceRoot);
|
|
120
|
+
if (typeof shimmed === "string") {
|
|
108
121
|
const v4 = parseTaskSourceV4({
|
|
109
|
-
yaml:
|
|
122
|
+
yaml: shimmed,
|
|
110
123
|
filePath: input.filePath,
|
|
111
124
|
...(input.workspaceRoot ? { workspaceRoot: input.workspaceRoot } : {}),
|
|
112
125
|
});
|
|
113
126
|
warn(`akm: task ${input.filePath} uses schema v${version} — auto-read as v4; run \`akm migrate apply\` to rewrite it and silence this`);
|
|
114
127
|
return Object.freeze({ version: 4, v4 });
|
|
115
128
|
}
|
|
129
|
+
throw unmigratableVersionError(input.filePath, version, shimmed.reason, shimmed.detail);
|
|
116
130
|
}
|
|
117
131
|
throw unsupportedVersionError(input.filePath, version);
|
|
118
132
|
}
|
|
@@ -284,7 +284,7 @@ async function driveChildRun(input, childRow) {
|
|
|
284
284
|
* before step 6 (publication) produces `child_workflow_publish_failed`.
|
|
285
285
|
*/
|
|
286
286
|
export async function driveChildWorkflowUnit(input) {
|
|
287
|
-
const { request,
|
|
287
|
+
const { request, ctx } = input;
|
|
288
288
|
const precheck = precheckAndDeriveInvocationKey(input);
|
|
289
289
|
if (!precheck.ok) {
|
|
290
290
|
return precheck.outcome;
|
|
@@ -708,7 +708,7 @@ async function prepareAttemptWorktree(input) {
|
|
|
708
708
|
}
|
|
709
709
|
return { ok: true, request: { ...input.request, cwd: created.path }, worktreePath: created.path };
|
|
710
710
|
}
|
|
711
|
-
async function reserveJournaledDispatch(input,
|
|
711
|
+
async function reserveJournaledDispatch(input, worktreePath, startedAt) {
|
|
712
712
|
const { plan, workUnit, ctx, attemptId, inputHash } = input;
|
|
713
713
|
let durableAttempt;
|
|
714
714
|
await enqueueUnitWrite(async () => {
|
|
@@ -799,7 +799,7 @@ async function dispatchJournaledAttempt(input) {
|
|
|
799
799
|
const startedAt = new Date().toISOString();
|
|
800
800
|
let durableAttempt;
|
|
801
801
|
try {
|
|
802
|
-
durableAttempt = await reserveJournaledDispatch(input,
|
|
802
|
+
durableAttempt = await reserveJournaledDispatch(input, worktreePath, startedAt);
|
|
803
803
|
}
|
|
804
804
|
catch (err) {
|
|
805
805
|
// A failed dispatch-row insert means NOTHING dispatched (the row is the
|
|
@@ -20,7 +20,7 @@ import { UsageError } from "../../core/errors.js";
|
|
|
20
20
|
import { validateJsonSchemaSubset } from "../../core/json-schema.js";
|
|
21
21
|
import { canonicalInputJson, validateInputs } from "../../execution/input-contract.js";
|
|
22
22
|
import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
|
|
23
|
-
import { canonicalJson
|
|
23
|
+
import { canonicalJson } from "../ir/plan-hash.js";
|
|
24
24
|
import { parseReference, resolveReferenceString, } from "../program/expressions.js";
|
|
25
25
|
import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
|
|
26
26
|
import { completeWorkflowStep, isTruncatedEvidence, } from "../runtime/runs.js";
|
|
@@ -463,7 +463,7 @@ function buildExecContextEnv(args) {
|
|
|
463
463
|
function computeUnitInputHash(ctx, item) {
|
|
464
464
|
return createHash("sha256")
|
|
465
465
|
.update("akm.workflow.unit\0v7\0")
|
|
466
|
-
.update(
|
|
466
|
+
.update(canonicalJson({
|
|
467
467
|
hashVersion: 7,
|
|
468
468
|
role: "unit",
|
|
469
469
|
stepId: ctx.plan.stepId,
|
|
@@ -855,20 +855,8 @@ export function unitOutcomeFromRow(unitId, row, hasSchema) {
|
|
|
855
855
|
...(row.tokens !== null ? { tokens: row.tokens } : {}),
|
|
856
856
|
};
|
|
857
857
|
}
|
|
858
|
-
/**
|
|
859
|
-
export
|
|
860
|
-
return JSON.stringify(sortKeys(value));
|
|
861
|
-
}
|
|
862
|
-
function sortKeys(value) {
|
|
863
|
-
if (Array.isArray(value))
|
|
864
|
-
return value.map(sortKeys);
|
|
865
|
-
if (value && typeof value === "object") {
|
|
866
|
-
return Object.fromEntries(Object.entries(value)
|
|
867
|
-
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
868
|
-
.map(([k, v]) => [k, sortKeys(v)]));
|
|
869
|
-
}
|
|
870
|
-
return value;
|
|
871
|
-
}
|
|
858
|
+
/** Re-exported so existing importers (`tests/workflows/fuzz/*`) keep resolving; canonical impl lives in `../ir/plan-hash`. */
|
|
859
|
+
export { canonicalJson };
|
|
872
860
|
// ── Gate-feedback recovery (PURE) ────────────────────────────────────────────
|
|
873
861
|
//
|
|
874
862
|
// A gate rejection is journaled as `<stepId>.gate:l<loop>` with result_json
|
|
@@ -1431,7 +1419,7 @@ export async function finalizeExecutedStep(input) {
|
|
|
1431
1419
|
// fields are unchanged).
|
|
1432
1420
|
inputHash: createHash("sha256")
|
|
1433
1421
|
.update("akm.workflow.gate\0v7\0")
|
|
1434
|
-
.update(
|
|
1422
|
+
.update(canonicalJson({
|
|
1435
1423
|
hashVersion: 7,
|
|
1436
1424
|
dispatch: gateTarget,
|
|
1437
1425
|
invocation: null,
|