akm-cli 0.9.11 → 0.9.13
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 +227 -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/assets/workflows/workflow-template.md +4 -0
- 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 +14 -0
- package/dist/commands/improve/preparation.js +9 -6
- package/dist/commands/improve/reflect.js +61 -77
- package/dist/commands/lint/base-linter.js +10 -0
- 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/drain.js +48 -6
- package/dist/commands/proposal/proposal-cli.js +1 -0
- 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 +56 -6
- package/dist/core/config/config.js +42 -17
- 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 +27 -10
- package/dist/core/config/schema/sources-bundles.js +1 -6
- package/dist/core/errors.js +4 -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 +28 -8
- package/dist/llm/embedders/remote.js +3 -2
- package/dist/llm/index-passes.js +3 -2
- package/dist/output/shapes/passthrough.js +9 -3
- package/dist/output/shapes.js +50 -3
- package/dist/output/text/proposal-format.js +5 -0
- package/dist/output/text/workflow-format.js +8 -1
- package/dist/scripts/akm-migrate-node.js +1737 -1392
- package/dist/scripts/akm-migrate.js +1736 -1391
- 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 +133 -11
- 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 +20 -11
- package/dist/workflows/exec/step-work.js +76 -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/parser.js +1 -1
- 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 +104 -154
- 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 +92 -17
- package/package.json +1 -1
- package/schemas/akm-config.json +5 -10
|
@@ -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)));
|
|
@@ -33,17 +33,49 @@ const SCRIPT_INTERPRETERS = Object.freeze({
|
|
|
33
33
|
".kt": "kotlin",
|
|
34
34
|
".kts": "kotlin",
|
|
35
35
|
});
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const SHEBANG_INTERPRETERS = [
|
|
37
|
+
[/^(bash|sh|zsh|dash|ksh)$/, "sh"],
|
|
38
|
+
[/^python[0-9.]*$/, "python"],
|
|
39
|
+
[/^ruby$/, "ruby"],
|
|
40
|
+
[/^perl$/, "perl"],
|
|
41
|
+
[/^php$/, "php"],
|
|
42
|
+
[/^lua$/, "lua"],
|
|
43
|
+
[/^node$/, "node"],
|
|
44
|
+
];
|
|
45
|
+
function interpreterFromShebang(bytes) {
|
|
46
|
+
if (!bytes || bytes.length < 2 || bytes[0] !== 0x23 || bytes[1] !== 0x21)
|
|
47
|
+
return undefined;
|
|
48
|
+
const newline = bytes.indexOf(0x0a);
|
|
49
|
+
const lineBytes = newline === -1 ? bytes.subarray(2) : bytes.subarray(2, newline);
|
|
50
|
+
const tokens = Buffer.from(lineBytes).toString("utf8").trim().split(/\s+/).filter(Boolean);
|
|
51
|
+
const [first, second] = tokens;
|
|
52
|
+
const named = first?.split("/").pop() === "env" ? second : first?.split("/").pop();
|
|
53
|
+
if (!named)
|
|
54
|
+
return undefined;
|
|
55
|
+
for (const [pattern, interpreter] of SHEBANG_INTERPRETERS) {
|
|
56
|
+
if (pattern.test(named))
|
|
57
|
+
return interpreter;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
function nodeStripsTypeScript() {
|
|
62
|
+
const features = process.features;
|
|
63
|
+
return Boolean(features?.typescript);
|
|
64
|
+
}
|
|
65
|
+
export function scriptInterpreter(extension, ref, bytes) {
|
|
66
|
+
const interpreter = SCRIPT_INTERPRETERS[extension] ?? (extension === "" ? interpreterFromShebang(bytes) : undefined);
|
|
38
67
|
if (!interpreter) {
|
|
39
68
|
throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} has no closed runtime interpreter for extension ${JSON.stringify(extension)}.`, "TASK_TARGET_UNSUPPORTED");
|
|
40
69
|
}
|
|
41
70
|
if (interpreter !== "bun")
|
|
42
71
|
return interpreter;
|
|
43
|
-
if (
|
|
44
|
-
|
|
72
|
+
if (process.versions.bun) {
|
|
73
|
+
return isBunStandaloneMain() ? "bun-standalone" : "bun";
|
|
74
|
+
}
|
|
75
|
+
if (extension === ".js" || (extension === ".ts" && nodeStripsTypeScript())) {
|
|
76
|
+
return "node";
|
|
45
77
|
}
|
|
46
|
-
|
|
78
|
+
throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} requires Bun for ${extension} execution, but this runtime cannot provide it.`, "TASK_TARGET_UNSUPPORTED");
|
|
47
79
|
}
|
|
48
80
|
export function captureDirectoryIdentity(bundleRoot, workingDirectory) {
|
|
49
81
|
try {
|
|
@@ -69,7 +101,7 @@ export function captureScriptTarget(ref, file, bundleRoot, readFile) {
|
|
|
69
101
|
const bytes = Uint8Array.from(raw);
|
|
70
102
|
const cwdIdentity = captureDirectoryIdentity(bundleRoot);
|
|
71
103
|
return Object.freeze({
|
|
72
|
-
interpreter: scriptInterpreter(extension, ref),
|
|
104
|
+
interpreter: scriptInterpreter(extension, ref, bytes),
|
|
73
105
|
extension,
|
|
74
106
|
bytesBase64: Buffer.from(bytes).toString("base64"),
|
|
75
107
|
byteLength: bytes.byteLength,
|
package/dist/tasks/schedule.js
CHANGED
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
* • a launchd plist `<StartCalendarInterval>` / `<StartInterval>` (macOS),
|
|
16
16
|
* • Task Scheduler XML triggers (Windows).
|
|
17
17
|
*
|
|
18
|
-
* The shared subset is `*`, single integers, `*\/N`, `A-B/N`,
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* day-of-week
|
|
18
|
+
* The shared subset is `*`, single integers, `*\/N`, `A-B`, `A-B/N`, comma
|
|
19
|
+
* lists, three-letter day/month names (`MON`, `JAN`) and name ranges
|
|
20
|
+
* (`MON-FRI`), plus the `@hourly / @daily / @weekly / @monthly / @yearly /
|
|
21
|
+
* @annually / @reboot` aliases. Day-of-month AND day-of-week combined in the
|
|
22
|
+
* same expression are rejected with a {@link UsageError} — cron gives that
|
|
23
|
+
* combination OR semantics no other backend can express portably, and it is
|
|
24
|
+
* genuinely ambiguous to a person reading the schedule back.
|
|
22
25
|
*
|
|
23
26
|
* Cron is the most permissive of the three backends; some patterns it
|
|
24
27
|
* accepts (e.g. `@hourly` = `0 * * * *`) have no clean schtasks primitive.
|
|
@@ -33,7 +36,58 @@ const ALIAS_TO_CRON = {
|
|
|
33
36
|
"@midnight": "0 0 * * *",
|
|
34
37
|
"@weekly": "0 0 * * 0",
|
|
35
38
|
"@monthly": "0 0 1 * *",
|
|
39
|
+
"@yearly": "0 0 1 1 *",
|
|
40
|
+
"@annually": "0 0 1 1 *",
|
|
36
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* vixie-cron's one true nickname with no 5-field equivalent — it fires once at
|
|
44
|
+
* daemon startup, not on a recurring calendar boundary. Passed straight
|
|
45
|
+
* through, unexpanded, on the cron backend (a real crontab line is just
|
|
46
|
+
* `@reboot <command>`); launchd and schtasks have no "run at boot" trigger in
|
|
47
|
+
* the vocabulary this module targets, so those backends reject it.
|
|
48
|
+
*/
|
|
49
|
+
const REBOOT_ALIAS = "@reboot";
|
|
50
|
+
/** Placeholder fields for the `@reboot` `ScheduleSpec` — never read: cron's translator emits `spec.cron` verbatim, and launchd/schtasks refuse `@reboot` before touching `fields`. */
|
|
51
|
+
const REBOOT_FIELDS = {
|
|
52
|
+
minute: { kind: "star" },
|
|
53
|
+
hour: { kind: "star" },
|
|
54
|
+
dom: { kind: "star" },
|
|
55
|
+
month: { kind: "star" },
|
|
56
|
+
dow: { kind: "star" },
|
|
57
|
+
};
|
|
58
|
+
const DOW_NAMES = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
|
|
59
|
+
const MONTH_NAMES = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
60
|
+
/**
|
|
61
|
+
* Substitute three-letter day-of-week / month names (and name ranges, e.g.
|
|
62
|
+
* `MON-FRI`) with their numeric cron equivalents, case-insensitively, so the
|
|
63
|
+
* existing numeric grammar below (value / range / list) parses the result
|
|
64
|
+
* unchanged. Tokens that are not recognized names pass through untouched —
|
|
65
|
+
* they are numbers already, or genuinely invalid and left for the numeric
|
|
66
|
+
* parsers to reject with their own message.
|
|
67
|
+
*/
|
|
68
|
+
function substituteNamedTokens(raw, fieldName) {
|
|
69
|
+
const names = fieldName === "month" ? MONTH_NAMES : DOW_NAMES;
|
|
70
|
+
const offset = fieldName === "month" ? 1 : 0;
|
|
71
|
+
const indexOf = (token) => {
|
|
72
|
+
const idx = names.indexOf(token.toLowerCase());
|
|
73
|
+
return idx === -1 ? undefined : idx + offset;
|
|
74
|
+
};
|
|
75
|
+
return raw
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((part) => {
|
|
78
|
+
const rangeMatch = part.match(/^([A-Za-z]{3})-([A-Za-z]{3})$/);
|
|
79
|
+
if (rangeMatch) {
|
|
80
|
+
const start = indexOf(rangeMatch[1]);
|
|
81
|
+
const end = indexOf(rangeMatch[2]);
|
|
82
|
+
if (start !== undefined && end !== undefined)
|
|
83
|
+
return `${start}-${end}`;
|
|
84
|
+
return part;
|
|
85
|
+
}
|
|
86
|
+
const single = /^[A-Za-z]{3}$/.test(part) ? indexOf(part) : undefined;
|
|
87
|
+
return single !== undefined ? String(single) : part;
|
|
88
|
+
})
|
|
89
|
+
.join(",");
|
|
90
|
+
}
|
|
37
91
|
const FIELD_LIMITS = {
|
|
38
92
|
minute: { min: 0, max: 59 },
|
|
39
93
|
hour: { min: 0, max: 23 },
|
|
@@ -41,10 +95,17 @@ const FIELD_LIMITS = {
|
|
|
41
95
|
month: { min: 1, max: 12 },
|
|
42
96
|
dow: { min: 0, max: 6 },
|
|
43
97
|
};
|
|
44
|
-
const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), steps (`*/N`, `A-B/N`), and comma lists
|
|
45
|
-
"
|
|
46
|
-
"
|
|
98
|
+
const SUPPORTED_HINT = "Supported subset: `*`, single integers (`5`), ranges (`A-B`), steps (`*/N`, `A-B/N`), and comma lists " +
|
|
99
|
+
"(`7,37`). Day-of-week and month fields also accept three-letter names and name ranges (`MON`, `MON-FRI`, " +
|
|
100
|
+
"`JAN`). Aliases: `@hourly`, `@daily`, `@weekly`, `@monthly`, `@yearly`/`@annually`, `@reboot` (cron only).";
|
|
47
101
|
export function parseSchedule(input, backend) {
|
|
102
|
+
const trimmed = input.trim();
|
|
103
|
+
if (trimmed.toLowerCase() === REBOOT_ALIAS) {
|
|
104
|
+
if (backend !== "cron") {
|
|
105
|
+
throw new UsageError(`Schedule "${input}" (@reboot, run once at startup) has no ${backend === "launchd" ? "macOS launchd" : "Windows Task Scheduler"} equivalent this release expresses. Use a cron-backed install, or rewrite the task with a recurring schedule.`, "INVALID_FLAG_VALUE");
|
|
106
|
+
}
|
|
107
|
+
return { raw: input, cron: REBOOT_ALIAS, fields: REBOOT_FIELDS };
|
|
108
|
+
}
|
|
48
109
|
const cron = expandAlias(input);
|
|
49
110
|
const fields = parseCronFields(cron, input);
|
|
50
111
|
const spec = { raw: input, cron, fields };
|
|
@@ -84,7 +145,8 @@ function parseCronFields(cron, original) {
|
|
|
84
145
|
dow: parseField(dow, "day-of-week", FIELD_LIMITS.dow, original),
|
|
85
146
|
};
|
|
86
147
|
}
|
|
87
|
-
function parseField(
|
|
148
|
+
function parseField(rawInput, name, limit, original) {
|
|
149
|
+
const raw = name === "month" || name === "day-of-week" ? substituteNamedTokens(rawInput, name) : rawInput;
|
|
88
150
|
if (raw === "*")
|
|
89
151
|
return { kind: "star" };
|
|
90
152
|
const stepMatch = raw.match(/^\*\/(\d+)$/);
|
|
@@ -109,6 +171,19 @@ function parseField(raw, name, limit, original) {
|
|
|
109
171
|
}
|
|
110
172
|
return { kind: "rangeStep", start, end, step };
|
|
111
173
|
}
|
|
174
|
+
// Plain range, no step: `A-B` (e.g. `1-5` for Mon-Fri, `1-5` for the first
|
|
175
|
+
// five days of the month). Represented as a `rangeStep` with `step: 1` so
|
|
176
|
+
// every downstream consumer (verbatim cron passthrough, launchd/schtasks
|
|
177
|
+
// expansion) reuses the exact same handling a stepped range already gets.
|
|
178
|
+
const rangeMatch = raw.match(/^(\d+)-(\d+)$/);
|
|
179
|
+
if (rangeMatch) {
|
|
180
|
+
const start = Number(rangeMatch[1]);
|
|
181
|
+
const end = Number(rangeMatch[2]);
|
|
182
|
+
if (start < limit.min || end > limit.max || start > end) {
|
|
183
|
+
throw new UsageError(`Invalid ${name} range "${raw}" in schedule "${original}" (allowed ${limit.min}-${limit.max}).`, "INVALID_FLAG_VALUE");
|
|
184
|
+
}
|
|
185
|
+
return { kind: "rangeStep", start, end, step: 1 };
|
|
186
|
+
}
|
|
112
187
|
if (/^\d+$/.test(raw)) {
|
|
113
188
|
const value = Number(raw);
|
|
114
189
|
if (value < limit.min || value > limit.max) {
|
|
@@ -161,6 +236,42 @@ export function translateToLaunchd(spec) {
|
|
|
161
236
|
})),
|
|
162
237
|
};
|
|
163
238
|
}
|
|
239
|
+
// A day-of-week or month range/list (`1-5` for Mon-Fri, `1,3,5`) has no
|
|
240
|
+
// single-dict launchd primitive either, but — unlike a step — it names a
|
|
241
|
+
// small, closed set of concrete values, so it genuinely IS expressible: one
|
|
242
|
+
// calendar dict per (month, weekday) combination, same trick as the
|
|
243
|
+
// minute/hour step expansion above. Only attempted when the remaining
|
|
244
|
+
// fields are already single values or `*`; anything else (e.g. a minute
|
|
245
|
+
// step combined with a weekday range) falls through to the generic path
|
|
246
|
+
// below, where `rejectStepInsideCalendar` reports the field it actually
|
|
247
|
+
// cannot express.
|
|
248
|
+
const dowValues = expandListLikeField(f.dow, FIELD_LIMITS.dow);
|
|
249
|
+
const monthValues = expandListLikeField(f.month, FIELD_LIMITS.month);
|
|
250
|
+
const remainingFieldsAreSimple = (f.minute.kind === "value" || f.minute.kind === "star") &&
|
|
251
|
+
(f.hour.kind === "value" || f.hour.kind === "star") &&
|
|
252
|
+
(f.dom.kind === "value" || f.dom.kind === "star");
|
|
253
|
+
if ((dowValues || monthValues) && remainingFieldsAreSimple) {
|
|
254
|
+
const base = {};
|
|
255
|
+
if (f.minute.kind === "value")
|
|
256
|
+
base.Minute = f.minute.value;
|
|
257
|
+
if (f.hour.kind === "value")
|
|
258
|
+
base.Hour = f.hour.value;
|
|
259
|
+
if (f.dom.kind === "value")
|
|
260
|
+
base.Day = f.dom.value;
|
|
261
|
+
const months = monthValues ?? (f.month.kind === "value" ? [f.month.value] : [undefined]);
|
|
262
|
+
const weekdays = dowValues ?? (f.dow.kind === "value" ? [f.dow.value] : [undefined]);
|
|
263
|
+
const calendars = [];
|
|
264
|
+
for (const month of months) {
|
|
265
|
+
for (const weekday of weekdays) {
|
|
266
|
+
calendars.push({
|
|
267
|
+
...base,
|
|
268
|
+
...(month !== undefined ? { Month: month } : {}),
|
|
269
|
+
...(weekday !== undefined ? { Weekday: weekday } : {}),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { calendars };
|
|
274
|
+
}
|
|
164
275
|
// Otherwise build a calendar dict from concrete values. launchd treats any
|
|
165
276
|
// omitted key as "every value", so a `*` field translates to "no key".
|
|
166
277
|
// Exception: launchd does not support arbitrary step values inside a
|
|
@@ -199,12 +310,21 @@ function expandFieldValues(field, limit) {
|
|
|
199
310
|
values.push(value);
|
|
200
311
|
return values;
|
|
201
312
|
}
|
|
313
|
+
/** Discrete values named by a `list` or (stepless) `rangeStep` field; `null` for anything else (`*`, `value`, `step`). */
|
|
314
|
+
function expandListLikeField(field, limit) {
|
|
315
|
+
if (field.kind === "list")
|
|
316
|
+
return field.values;
|
|
317
|
+
if (field.kind === "rangeStep")
|
|
318
|
+
return expandFieldValues(field, limit);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
202
321
|
function rejectStepInsideCalendar(field, name, spec) {
|
|
203
322
|
if (field.kind === "step") {
|
|
204
323
|
throw new UsageError(`Schedule "${spec.raw}" uses step (${name} = */N) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either restrict the step to the minute or hour field only, or rewrite the schedule with concrete values.");
|
|
205
324
|
}
|
|
206
325
|
if (field.kind === "rangeStep") {
|
|
207
|
-
|
|
326
|
+
const shape = field.step === 1 ? "A-B" : "A-B/N";
|
|
327
|
+
throw new UsageError(`Schedule "${spec.raw}" uses a range (${name} = ${shape}) in a position macOS launchd cannot express. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Restrict the range to the minute, hour, day-of-week, or month field, or rewrite the schedule with a concrete value.");
|
|
208
328
|
}
|
|
209
329
|
if (field.kind === "list") {
|
|
210
330
|
throw new UsageError(`Schedule "${spec.raw}" uses comma list (${name} = a,b,...) which macOS launchd cannot express as a single trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Either install one task per list element, or rewrite the schedule with a step (`*/N`) or single value.");
|
|
@@ -286,20 +406,41 @@ export function translateToSchtasks(spec) {
|
|
|
286
406
|
f.dow.kind === "star") {
|
|
287
407
|
return { kind: "daily", atHour: f.hour.value, atMinute: f.minute.value };
|
|
288
408
|
}
|
|
289
|
-
// `M H * * D` → WEEKLY at H:M on day D
|
|
409
|
+
// `M H * * D` → WEEKLY at H:M on day(s) D — a single day, a plain range
|
|
410
|
+
// (`1-5`, e.g. from `MON-FRI`), or a comma list all name a small closed set
|
|
411
|
+
// of weekdays that Task Scheduler's native `<DaysOfWeek>` already takes as
|
|
412
|
+
// a set, so no expansion into multiple triggers is needed.
|
|
290
413
|
if (f.minute.kind === "value" &&
|
|
291
414
|
f.hour.kind === "value" &&
|
|
292
415
|
f.dom.kind === "star" &&
|
|
293
416
|
f.month.kind === "star" &&
|
|
294
|
-
f.dow.kind === "value") {
|
|
417
|
+
(f.dow.kind === "value" || f.dow.kind === "list" || f.dow.kind === "rangeStep")) {
|
|
418
|
+
const daysOfWeek = f.dow.kind === "value" ? [f.dow.value] : expandListLikeField(f.dow, FIELD_LIMITS.dow);
|
|
295
419
|
return {
|
|
296
420
|
kind: "weekly",
|
|
297
421
|
atHour: f.hour.value,
|
|
298
422
|
atMinute: f.minute.value,
|
|
299
|
-
daysOfWeek
|
|
423
|
+
daysOfWeek,
|
|
300
424
|
};
|
|
301
425
|
}
|
|
302
|
-
|
|
426
|
+
// `M H D * *` / `M H D m *` → MONTHLY at H:M on day(s)-of-month D, in the
|
|
427
|
+
// given month(s) m (or every month when `m` is `*`). Task Scheduler's
|
|
428
|
+
// `ScheduleByMonth` trigger takes both as native sets, same as `ScheduleByWeek`
|
|
429
|
+
// above.
|
|
430
|
+
if (f.minute.kind === "value" &&
|
|
431
|
+
f.hour.kind === "value" &&
|
|
432
|
+
(f.dom.kind === "value" || f.dom.kind === "list") &&
|
|
433
|
+
(f.month.kind === "star" || f.month.kind === "value" || f.month.kind === "list") &&
|
|
434
|
+
f.dow.kind === "star") {
|
|
435
|
+
const daysOfMonth = f.dom.kind === "value" ? [f.dom.value] : f.dom.values;
|
|
436
|
+
const months = f.month.kind === "star"
|
|
437
|
+
? Array.from({ length: 12 }, (_, i) => i + 1)
|
|
438
|
+
: f.month.kind === "value"
|
|
439
|
+
? [f.month.value]
|
|
440
|
+
: f.month.values;
|
|
441
|
+
return { kind: "monthly", atHour: f.hour.value, atMinute: f.minute.value, daysOfMonth, months };
|
|
442
|
+
}
|
|
443
|
+
throw new UsageError(`Schedule "${spec.raw}" cannot be expressed as a Windows Task Scheduler trigger. ${SUPPORTED_HINT}`, "INVALID_FLAG_VALUE", "Use one of: minute steps/range-steps, fixed-minute hour steps/range-steps, hourly, daily, weekly on one or more weekdays, or monthly on one or more days-of-month.");
|
|
303
444
|
}
|
|
304
445
|
function minuteValuesTrigger(minutes, spec) {
|
|
305
446
|
if (minutes.length > MAX_SCHTASKS_TRIGGERS) {
|
|
@@ -377,7 +377,6 @@ export function parseTaskV3Document(value, options) {
|
|
|
377
377
|
if (own(input, "with"))
|
|
378
378
|
sourceError(ctx, ["with"], "is legal only with uses.");
|
|
379
379
|
const run = stringField(input.run, ctx, ["run"], { nonempty: true });
|
|
380
|
-
noGithubExpression(run, ctx, ["run"]);
|
|
381
380
|
let shell;
|
|
382
381
|
if (own(input, "shell")) {
|
|
383
382
|
const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
|
|
@@ -259,7 +259,6 @@ function parseTarget(input, ctx) {
|
|
|
259
259
|
if (own(input, "with"))
|
|
260
260
|
sourceError(ctx, ["with"], "is legal only with uses: akm/command; declare typed inputs: instead.");
|
|
261
261
|
const run = stringField(input.run, ctx, ["run"], { nonempty: true });
|
|
262
|
-
noGithubExpression(run, ctx, ["run"]);
|
|
263
262
|
let shell;
|
|
264
263
|
if (own(input, "shell")) {
|
|
265
264
|
const rawShell = stringField(input.shell, ctx, ["shell"], { nonempty: true });
|
|
@@ -250,9 +250,8 @@ async function driveChildRun(input, childRow) {
|
|
|
250
250
|
// (dispatchJournaledAttempt awaits this call with no try of its own),
|
|
251
251
|
// so an uncaught throw here escaped all the way into the scheduler and
|
|
252
252
|
// was silently swallowed (R1, above). Reachable causes include the
|
|
253
|
-
// child's own LeaseHeartbeat.assertAlive() firing mid-drive,
|
|
254
|
-
//
|
|
255
|
-
// WORKFLOW_OUTPUT_INVALID), requireExecutableWorkflowPlan rejecting a
|
|
253
|
+
// child's own LeaseHeartbeat.assertAlive() firing mid-drive,
|
|
254
|
+
// requireExecutableWorkflowPlan rejecting a
|
|
256
255
|
// tampered child plan_json, and the child's status changing between
|
|
257
256
|
// this function's own step 5 read and the drive's internal
|
|
258
257
|
// getNextWorkflowStep re-read — none of which match
|
|
@@ -220,10 +220,9 @@ function truncationNote(read) {
|
|
|
220
220
|
* The captured stdout, with an unmistakable truncation block appended when the
|
|
221
221
|
* retention cap discarded part of it.
|
|
222
222
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* is missing rather than inferring it from a suspiciously round length.
|
|
223
|
+
* Truncated data must never be mistakable for complete data. The block names
|
|
224
|
+
* both byte counts, so a reader can see exactly how much is missing rather
|
|
225
|
+
* than inferring it from a suspiciously round length.
|
|
227
226
|
*/
|
|
228
227
|
function markTruncatedStdout(result) {
|
|
229
228
|
const read = result.stdoutRead;
|
|
@@ -36,6 +36,10 @@ export async function runWorkflowSteps(options) {
|
|
|
36
36
|
let target = options.target;
|
|
37
37
|
let params = options.params;
|
|
38
38
|
let parameterFlags = options.parameterFlags;
|
|
39
|
+
// `--new` only applies to the FIRST resolution of `target` (a ref); every
|
|
40
|
+
// retry re-targets the run id `startWorkflowRun` already created, so it is
|
|
41
|
+
// cleared alongside `params`/`parameterFlags` below (#919).
|
|
42
|
+
let newRun = options.newRun;
|
|
39
43
|
let remainingRetries = options.maxRetries ?? 0;
|
|
40
44
|
let remainingSteps = options.maxSteps;
|
|
41
45
|
const executed = [];
|
|
@@ -51,6 +55,7 @@ export async function runWorkflowSteps(options) {
|
|
|
51
55
|
target,
|
|
52
56
|
...(params !== undefined ? { params } : { params: undefined }),
|
|
53
57
|
...(parameterFlags !== undefined ? { parameterFlags } : { parameterFlags: undefined }),
|
|
58
|
+
newRun,
|
|
54
59
|
...(remainingSteps !== undefined ? { maxSteps: remainingSteps } : { maxSteps: undefined }),
|
|
55
60
|
}, liveEvidence);
|
|
56
61
|
executed.push(...result.executed);
|
|
@@ -72,12 +77,14 @@ export async function runWorkflowSteps(options) {
|
|
|
72
77
|
target = result.run.id;
|
|
73
78
|
params = undefined;
|
|
74
79
|
parameterFlags = undefined;
|
|
80
|
+
newRun = undefined;
|
|
75
81
|
remainingRetries -= 1;
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
async function runWorkflowAttempt(options, liveEvidence) {
|
|
79
85
|
const next = await getNextWorkflowStep(options.target, options.params, {
|
|
80
86
|
parameterFlags: options.parameterFlags,
|
|
87
|
+
newRun: options.newRun,
|
|
81
88
|
});
|
|
82
89
|
// Version/canonical/hash validation precedes every executable mutation,
|
|
83
90
|
// including lease acquisition. Historical rows remain inspectable/abandonable.
|
|
@@ -137,8 +144,14 @@ async function runWorkflowAttempt(options, liveEvidence) {
|
|
|
137
144
|
const result = await withWorkflowRunsConnection(() => driveRun(options, next, leaseHolder, heartbeat, liveEvidence));
|
|
138
145
|
// Creation-time notices reach the caller only here: the run row has no
|
|
139
146
|
// warnings column, and a later invocation of the same run must stay silent
|
|
140
|
-
// about a decision it did not make. `driveRun` never sets `warnings
|
|
141
|
-
|
|
147
|
+
// about a decision it did not make. `driveRun` never sets `warnings` or
|
|
148
|
+
// `resumed` — both are properties of THIS resolution of `target`, not of
|
|
149
|
+
// the run row (#919).
|
|
150
|
+
return {
|
|
151
|
+
...result,
|
|
152
|
+
...(next.resumed ? { resumed: true } : {}),
|
|
153
|
+
...(next.startWarnings?.length ? { warnings: next.startWarnings } : {}),
|
|
154
|
+
};
|
|
142
155
|
}
|
|
143
156
|
finally {
|
|
144
157
|
heartbeat?.stop();
|
|
@@ -181,7 +194,7 @@ async function acquireRunLease(runId, holder) {
|
|
|
181
194
|
const row = repo.getRunById(runId);
|
|
182
195
|
throw new UsageError(`Workflow run ${runId} is already being driven by engine ${row?.engine_lease_holder ?? "(unknown)"} ` +
|
|
183
196
|
`(run lease expires ${row?.engine_lease_until ?? "(unknown)"}). A second \`akm workflow run\` would race it — ` +
|
|
184
|
-
`wait for that invocation to finish or for the lease to expire
|
|
197
|
+
`wait for that invocation to finish or for the lease to expire.`, "RUN_LEASE_HELD");
|
|
185
198
|
}));
|
|
186
199
|
}
|
|
187
200
|
/**
|
|
@@ -639,14 +652,10 @@ async function driveRun(options, initial, leaseHolder, heartbeat,
|
|
|
639
652
|
/**
|
|
640
653
|
* The COMPLETE in-memory evidence of every step THIS call has completed,
|
|
641
654
|
* keyed by step id, preferred over the re-read row when the downstream scope
|
|
642
|
-
* is rebuilt below
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* retains 8 MiB). Preferring the live value keeps the persistence bound
|
|
647
|
-
* invisible to the run that produced it. A LATER `akm workflow run` starts
|
|
648
|
-
* with an empty map and reads the rows, where a reference into a truncated
|
|
649
|
-
* artifact fails loudly by name (`isTruncatedEvidence`).
|
|
655
|
+
* is rebuilt below — avoiding a re-parse of a row this same invocation just
|
|
656
|
+
* wrote (step artifacts are persisted whole, so the two values agree; this
|
|
657
|
+
* is purely an avoided round trip, not a correctness dependency). A LATER
|
|
658
|
+
* `akm workflow run` starts with an empty map and reads the rows directly.
|
|
650
659
|
*
|
|
651
660
|
* Only steps some OTHER step's references NAME are stored (`referencedStepIds`
|
|
652
661
|
* — the set-time filter): a step nothing downstream reads has no consumer to
|