akm-cli 0.9.2-alpha.4 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +493 -0
- package/STABILITY.md +23 -5
- package/dist/assets/hints/cli-hints-full.md +12 -7
- package/dist/assets/tasks/core/extract.yml +3 -5
- package/dist/assets/tasks/core/improve.yml +3 -5
- package/dist/assets/tasks/core/index-refresh.yml +3 -5
- package/dist/assets/tasks/core/sync.yml +3 -5
- package/dist/assets/tasks/core/version-check.yml +3 -5
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +6 -6
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +3 -5
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +3 -5
- package/dist/cli/unknown-flags.js +12 -1
- package/dist/cli.js +8 -1
- package/dist/commands/command/command-execution.js +23 -2
- package/dist/commands/health/improve-metrics.js +38 -0
- package/dist/commands/health/windows.js +8 -4
- package/dist/commands/health.js +8 -4
- package/dist/commands/lint/index.js +1 -1
- package/dist/commands/migrate-cli.js +130 -24
- package/dist/commands/proposal/validators/proposal-validators.js +7 -2
- package/dist/commands/tasks/explain.js +304 -0
- package/dist/commands/tasks/tasks-cli.js +185 -3
- package/dist/commands/tasks/tasks.js +265 -52
- package/dist/commands/workflow/plan.js +159 -0
- package/dist/commands/workflow-cli.js +94 -2
- package/dist/core/activation-policy.js +2 -12
- package/dist/core/adapter/adapters/akm-lint.js +7 -4
- package/dist/core/adapter/adapters/akm-metadata.js +26 -14
- package/dist/core/adapter/adapters/akm-task-adapter.js +13 -10
- package/dist/core/errors.js +45 -0
- package/dist/core/json-schema.js +15 -5
- package/dist/core/state/migrations.js +57 -0
- package/dist/core/state-db.js +16 -14
- package/dist/core/subprocess.js +47 -13
- package/dist/execution/guarded-source.js +44 -0
- package/dist/execution/input-contract.js +250 -0
- package/dist/execution/target-ref.js +63 -0
- package/dist/indexer/usage/usage-events.js +14 -3
- package/dist/integrations/agent/execution-lowering.js +12 -1
- package/dist/output/shapes/passthrough.js +2 -0
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +12 -3
- package/dist/output/text/workflow-format.js +192 -10
- package/dist/output/text/workflow.js +2 -1
- package/dist/runtime.js +1 -0
- package/dist/scripts/akm-migrate-node.js +11838 -10118
- package/dist/scripts/akm-migrate.js +11828 -10117
- package/dist/setup/steps/tasks.js +34 -17
- package/dist/storage/repositories/task-history-repository.js +5 -1
- package/dist/storage/repositories/workflow-runs-repository.js +144 -6
- package/dist/tasks/backends/launchd.js +31 -84
- package/dist/tasks/embedded.js +13 -7
- package/dist/tasks/model/invocation.js +4 -0
- package/dist/tasks/prepare/prepare-script-target.js +9 -0
- package/dist/tasks/prepare/prepare-support.js +154 -0
- package/dist/tasks/prepare/prepare.js +117 -0
- package/dist/tasks/prepare/prepared-execution.js +4 -0
- package/dist/tasks/prepare/script-capture.js +80 -0
- package/dist/tasks/run/attempt-lifecycle.js +165 -0
- package/dist/tasks/run/load-task.js +117 -0
- package/dist/tasks/run/provenance.js +20 -0
- package/dist/tasks/run/run-command-task.js +92 -0
- package/dist/tasks/run/run-native-task.js +222 -0
- package/dist/tasks/run/run-task.js +99 -0
- package/dist/tasks/run/run-workflow-task.js +222 -0
- package/dist/tasks/run/task-history.js +134 -0
- package/dist/tasks/run/task-log.js +179 -0
- package/dist/tasks/run/task-result.js +19 -0
- package/dist/tasks/scheduler-binding.js +66 -2
- package/dist/tasks/scheduler-invocation.js +63 -3
- package/dist/tasks/scheduler-sync.js +77 -14
- package/dist/tasks/source/bounded-document.js +455 -0
- package/dist/tasks/source/parse-task-source.js +59 -0
- package/dist/tasks/source/project-v4.js +62 -0
- package/dist/tasks/source/task-input-diagnostics.js +36 -0
- package/dist/tasks/source/task-source-v4.js +626 -0
- package/dist/tasks/source-v3.js +10 -733
- package/dist/tasks/task-run-reserved-flags.js +79 -0
- package/dist/workflows/authoring/authoring.js +17 -8
- package/dist/workflows/exec/child-invocation.js +34 -0
- package/dist/workflows/exec/child-workflow.js +370 -0
- package/dist/workflows/exec/exec-unit.js +50 -170
- package/dist/workflows/exec/frozen-judge.js +19 -2
- package/dist/workflows/exec/native-executor.js +49 -27
- package/dist/workflows/exec/param-secrets.js +12 -0
- package/dist/workflows/exec/run-workflow.js +48 -59
- package/dist/workflows/exec/step-work.js +222 -80
- package/dist/workflows/exec/unit-dispatch.js +72 -0
- package/dist/workflows/freeze/child-output-references.js +94 -0
- package/dist/workflows/freeze/environment.js +174 -0
- package/dist/workflows/freeze/identity.js +22 -0
- package/dist/workflows/freeze/resolve-steps.js +78 -0
- package/dist/workflows/freeze/source-freeze.js +57 -0
- package/dist/workflows/freeze/step-values.js +68 -0
- package/dist/workflows/freeze/targets/child-workflow.js +206 -0
- package/dist/workflows/freeze/targets/command.js +81 -0
- package/dist/workflows/freeze/targets/script.js +57 -0
- package/dist/workflows/freeze/targets/shell.js +31 -0
- package/dist/workflows/freeze/targets/task.js +179 -0
- package/dist/workflows/freeze/task-bindings.js +180 -0
- package/dist/workflows/ir/compile.js +59 -11
- package/dist/workflows/ir/environment-v4.js +3 -3
- package/dist/workflows/ir/freeze-v4.js +41 -7
- package/dist/workflows/ir/params.js +58 -131
- package/dist/workflows/ir/plan-hash.js +3 -3
- package/dist/workflows/ir/schema-v4.js +246 -17
- package/dist/workflows/parser.js +74 -2
- package/dist/workflows/program/schema.js +5 -2
- package/dist/workflows/resource-limits.js +20 -0
- package/dist/workflows/runtime/plan-classifier.js +24 -7
- package/dist/workflows/runtime/run-outputs.js +103 -0
- package/dist/workflows/runtime/runs.js +114 -9
- package/dist/workflows/runtime/workflow-asset-loader.js +14 -6
- package/dist/workflows/source-files.js +5 -5
- package/dist/workflows/source-ir/compare.js +17 -0
- package/dist/workflows/source-ir/compile.js +7 -3
- package/dist/workflows/source-ir/github-yaml.js +64 -17
- package/dist/workflows/source-ir/schema.js +69 -21
- package/dist/workflows/source-ir/semantics.js +7 -25
- package/dist/workflows/source-ir/triggers.js +79 -0
- package/dist/workflows/source-ir/uses.js +33 -7
- package/docs/migration/README.md +1 -1
- package/docs/migration/release-notes/0.9.2.md +87 -11
- package/docs/migration/release-notes/README.md +3 -2
- package/docs/migration/v0.8-to-v0.9.md +13 -11
- package/docs/migration/v0.9.0-troubleshooting.md +20 -13
- package/docs/migration/v0.9.1-to-v0.9.2.md +598 -49
- package/docs/reference/README.md +1 -1
- package/docs/reference/cli.md +140 -46
- package/docs/reference/configuration.md +6 -5
- package/docs/reference/supported-formats.md +9 -5
- package/docs/reference/tasks.md +338 -75
- package/docs/reference/workflow-schema.md +290 -16
- package/docs/reference/workflows.md +57 -7
- package/package.json +1 -1
- package/schemas/akm-task.json +173 -118
- package/schemas/akm-workflow.json +28 -0
- package/dist/tasks/runner.js +0 -941
- package/dist/tasks/runtime-v3.js +0 -281
- package/dist/workflows/ir/source-freeze-v4.js +0 -506
- package/dist/workflows/source-ir/ordering.js +0 -38
|
@@ -14,7 +14,7 @@ import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetFor
|
|
|
14
14
|
import { backendNameForPlatform } from "../../tasks/backends/index.js";
|
|
15
15
|
import { listEmbeddedTasks } from "../../tasks/embedded.js";
|
|
16
16
|
import { parseSchedule } from "../../tasks/schedule.js";
|
|
17
|
-
import {
|
|
17
|
+
import { parseTaskSource } from "../../tasks/source/parse-task-source.js";
|
|
18
18
|
import { prompt } from "../prompt.js";
|
|
19
19
|
/**
|
|
20
20
|
* A scheduled server-only nightly full sweep exists among the embedded
|
|
@@ -71,13 +71,28 @@ export function detectServerDefault() {
|
|
|
71
71
|
function normaliseTaskIdForMatch(raw) {
|
|
72
72
|
return raw.trim().replace(/\.(yml|md)$/, "");
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Toggle a task source v4 file's enabled state via a full parse/render
|
|
76
|
+
* round-trip (setup's own edits are infrequent and not comment-preservation-
|
|
77
|
+
* sensitive, unlike `commands/tasks/tasks.ts`'s `setEnabledInYaml` line
|
|
78
|
+
* splice). Broadcasts `enabled` across every `schedule[]` entry — the
|
|
79
|
+
* closest v4 equivalent of v3's single document-level flag. `src` no longer
|
|
80
|
+
* accepts a task v3 file at all (P4 §3.2) — `listSetupTaskDefinitions` below
|
|
81
|
+
* already fails closed on one before this function is ever reached, so
|
|
82
|
+
* there is no legacy `akm.enabled` shape left to handle here.
|
|
83
|
+
*/
|
|
84
|
+
function setTaskEnabledInYaml(yaml, enabled) {
|
|
75
85
|
const document = yamlParse(yaml);
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
|
|
86
|
+
const schedule = document.schedule;
|
|
87
|
+
if (typeof schedule === "string") {
|
|
88
|
+
document.schedule = [{ cron: schedule, enabled }];
|
|
89
|
+
}
|
|
90
|
+
else if (Array.isArray(schedule) && schedule.length > 0) {
|
|
91
|
+
document.schedule = schedule.map((entry) => entry && typeof entry === "object" && !Array.isArray(entry) ? { ...entry, enabled } : entry);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw new UsageError("Task source v4 must declare a schedule before setup can change enabled state.");
|
|
79
95
|
}
|
|
80
|
-
akm.enabled = enabled;
|
|
81
96
|
return yamlStringify(document);
|
|
82
97
|
}
|
|
83
98
|
export function listSetupTaskDefinitions() {
|
|
@@ -93,20 +108,24 @@ export function listSetupTaskDefinitions() {
|
|
|
93
108
|
const id = file.slice(0, -4);
|
|
94
109
|
const filePath = path.join(taskDir, file);
|
|
95
110
|
try {
|
|
96
|
-
const
|
|
111
|
+
const parsed = parseTaskSource({
|
|
97
112
|
yaml: fs.readFileSync(filePath, "utf8"),
|
|
98
113
|
filePath,
|
|
99
114
|
workspaceRoot: target.source.path,
|
|
100
115
|
});
|
|
101
|
-
|
|
116
|
+
const document = parsed.v4;
|
|
117
|
+
if (document.schedule.length === 0)
|
|
102
118
|
continue;
|
|
103
|
-
const schedules =
|
|
119
|
+
const schedules = document.schedule.map((entry) => entry.cron);
|
|
104
120
|
tasks.push({
|
|
105
121
|
id,
|
|
106
122
|
schedule: schedules[0],
|
|
107
123
|
schedules: Object.freeze(schedules),
|
|
108
|
-
|
|
109
|
-
|
|
124
|
+
// task source v4 has no document-level enabled (P4-N6) — a task is
|
|
125
|
+
// considered enabled for review purposes when at least one of its
|
|
126
|
+
// schedule bindings will actually fire.
|
|
127
|
+
enabled: document.schedule.some((entry) => entry.enabled),
|
|
128
|
+
...(document.description !== undefined ? { description: document.description } : {}),
|
|
110
129
|
});
|
|
111
130
|
}
|
|
112
131
|
catch (error) {
|
|
@@ -128,17 +147,15 @@ export async function prepareSetupTaskDefinitions(tasks, deps = {}) {
|
|
|
128
147
|
const original = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : undefined;
|
|
129
148
|
let yaml;
|
|
130
149
|
if (original !== undefined) {
|
|
131
|
-
yaml =
|
|
150
|
+
yaml = setTaskEnabledInYaml(original, plan.enabled);
|
|
132
151
|
}
|
|
133
152
|
else {
|
|
134
153
|
const document = yamlParse(plan.task.yaml);
|
|
135
|
-
|
|
136
|
-
akm.schedule = plan.schedule;
|
|
137
|
-
akm.enabled = plan.enabled;
|
|
154
|
+
document.schedule = plan.enabled ? plan.schedule : [{ cron: plan.schedule, enabled: false }];
|
|
138
155
|
yaml = yamlStringify(document);
|
|
139
156
|
}
|
|
140
|
-
const parsed =
|
|
141
|
-
for (const schedule of parsed.
|
|
157
|
+
const parsed = parseTaskSource({ yaml, filePath, workspaceRoot: target.source.path });
|
|
158
|
+
for (const schedule of parsed.v4.schedule) {
|
|
142
159
|
parseSchedule(schedule.cron, backendNameForPlatform());
|
|
143
160
|
}
|
|
144
161
|
return { filePath, original, yaml, ref: { type: "task", name: plan.task.id } };
|
|
@@ -39,7 +39,7 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
39
39
|
metadataError("root must be an object");
|
|
40
40
|
if (parsed.metadataVersion !== 2)
|
|
41
41
|
metadataError(`unsupported metadataVersion: ${String(parsed.metadataVersion)}`);
|
|
42
|
-
const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine"]);
|
|
42
|
+
const allowed = new Set(["metadataVersion", "durationMs", "detail", "engine", "targetVocab"]);
|
|
43
43
|
const unknown = Object.keys(parsed).filter((key) => !allowed.has(key));
|
|
44
44
|
if (unknown.length > 0)
|
|
45
45
|
metadataError(`unknown fields: ${unknown.sort().join(", ")}`);
|
|
@@ -50,12 +50,16 @@ export function decodeTaskHistoryMetadata(input) {
|
|
|
50
50
|
if (parsed.engine !== undefined && parsed.engine !== null && typeof parsed.engine !== "string") {
|
|
51
51
|
metadataError("engine must be a string or null");
|
|
52
52
|
}
|
|
53
|
+
if (parsed.targetVocab !== undefined && parsed.targetVocab !== 2) {
|
|
54
|
+
metadataError("targetVocab must be 2 when present");
|
|
55
|
+
}
|
|
53
56
|
validateDetail(parsed.detail);
|
|
54
57
|
return {
|
|
55
58
|
metadataVersion: 2,
|
|
56
59
|
durationMs: parsed.durationMs,
|
|
57
60
|
detail: parsed.detail ?? null,
|
|
58
61
|
...(parsed.engine !== undefined ? { engine: parsed.engine } : {}),
|
|
62
|
+
...(parsed.targetVocab === 2 ? { targetVocab: 2 } : {}),
|
|
59
63
|
};
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
@@ -49,25 +49,48 @@ export class WorkflowRunsRepository {
|
|
|
49
49
|
return withImmediateTransaction(this.db, () => fn(this.db));
|
|
50
50
|
}
|
|
51
51
|
// ── reads (fully materialised) ─────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* The top-level start guard `publishWorkflowRunV4` uses to refuse starting
|
|
54
|
+
* a SECOND active run of the same ref in the same scope. `AND
|
|
55
|
+
* parent_run_id IS NULL` (B-N10's FOURTH site, code-review round 4 finding
|
|
56
|
+
* 2 / Review log R2): a child run carries the PARENT's `scope_key` (P3a
|
|
57
|
+
* §5.2), so once a parent publishes a child of some ref, starting a
|
|
58
|
+
* TOP-LEVEL run of that same ref in that scope must never resolve to the
|
|
59
|
+
* child — the child is not "already an active run in this scope" from a
|
|
60
|
+
* fresh top-level invocation's point of view; the PARENT, if anything, is.
|
|
61
|
+
* Before this filter, `publishWorkflowRunV4` refused with
|
|
62
|
+
* `RESOURCE_ALREADY_EXISTS` naming the CHILD's own run id, instructing the
|
|
63
|
+
* operator to `akm workflow abandon` a child a parent is actively driving.
|
|
64
|
+
* For any database with no child rows the result is byte-identical, same
|
|
65
|
+
* as the other three B-N10 sites below.
|
|
66
|
+
*/
|
|
52
67
|
findActiveRunForScope(workflowRefs, scopeKey) {
|
|
53
68
|
const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
|
|
54
69
|
if (refs.length === 0)
|
|
55
70
|
return undefined;
|
|
56
71
|
return this.db
|
|
57
|
-
.prepare(`SELECT id, current_step_id FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
|
|
72
|
+
.prepare(`SELECT id, current_step_id FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' AND parent_run_id IS NULL ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
|
|
58
73
|
.get(...refs, scopeKey);
|
|
59
74
|
}
|
|
60
75
|
getRunById(runId) {
|
|
61
76
|
return (this.db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId) ??
|
|
62
77
|
undefined);
|
|
63
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The scope-attach lookup `akm workflow run <ref>` uses to find an
|
|
81
|
+
* in-progress top-level run to resume instead of starting a new one.
|
|
82
|
+
* `AND parent_run_id IS NULL` (B-N10): a child run must never be attached
|
|
83
|
+
* to directly through this path — a parent-driven child would then have
|
|
84
|
+
* TWO drivers. For any database with no child rows the result is
|
|
85
|
+
* byte-identical.
|
|
86
|
+
*/
|
|
64
87
|
getActiveRunRowForScope(workflowRefs, scopeKey) {
|
|
65
88
|
const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
|
|
66
89
|
if (refs.length === 0)
|
|
67
90
|
return undefined;
|
|
68
|
-
return this.db
|
|
69
|
-
.prepare(`SELECT * FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
|
|
70
|
-
.get(...refs, scopeKey);
|
|
91
|
+
return (this.db
|
|
92
|
+
.prepare(`SELECT * FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' AND parent_run_id IS NULL ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
|
|
93
|
+
.get(...refs, scopeKey) ?? undefined);
|
|
71
94
|
}
|
|
72
95
|
hasRun(runId) {
|
|
73
96
|
const row = this.db.prepare("SELECT 1 FROM workflow_runs WHERE id = ? LIMIT 1").get(runId);
|
|
@@ -99,6 +122,12 @@ export class WorkflowRunsRepository {
|
|
|
99
122
|
// into this shared list filter.
|
|
100
123
|
filters.push("status = 'active'");
|
|
101
124
|
}
|
|
125
|
+
// B-N10: a child run is invisible to this scope query unless the caller
|
|
126
|
+
// explicitly opts in (`akm workflow list --children`). For any database
|
|
127
|
+
// with no child rows the result set is byte-identical either way.
|
|
128
|
+
if (!filter.includeChildren) {
|
|
129
|
+
filters.push("parent_run_id IS NULL");
|
|
130
|
+
}
|
|
102
131
|
const where = filters.length > 0 ? `WHERE ${filters.join(" AND ")}` : "";
|
|
103
132
|
return this.db
|
|
104
133
|
.prepare(`SELECT * FROM workflow_runs ${where} ORDER BY updated_at DESC, created_at DESC`)
|
|
@@ -112,13 +141,26 @@ export class WorkflowRunsRepository {
|
|
|
112
141
|
getStep(runId, stepId) {
|
|
113
142
|
return this.db.prepare("SELECT * FROM workflow_run_steps WHERE run_id = ? AND step_id = ?").get(runId, stepId);
|
|
114
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The `akm show` active-run guard: which run (if any) currently occupies
|
|
146
|
+
* this scope. `AND parent_run_id IS NULL` (B-N10) — a child a parent is
|
|
147
|
+
* driving must never be reported as "the" active run; the PARENT is. For
|
|
148
|
+
* any database with no child rows the result is byte-identical.
|
|
149
|
+
*/
|
|
115
150
|
findActiveOrBlockedRunForScope(scopeKey) {
|
|
116
151
|
return (this.db
|
|
117
|
-
.prepare("SELECT id, current_step_id, workflow_ref FROM workflow_runs WHERE scope_key = ? AND status IN ('active', 'blocked') ORDER BY updated_at DESC LIMIT 1")
|
|
152
|
+
.prepare("SELECT id, current_step_id, workflow_ref FROM workflow_runs WHERE scope_key = ? AND status IN ('active', 'blocked') AND parent_run_id IS NULL ORDER BY updated_at DESC LIMIT 1")
|
|
118
153
|
.get(scopeKey) ?? null);
|
|
119
154
|
}
|
|
120
155
|
// ── writes ─────────────────────────────────────────────────────────────────
|
|
121
156
|
insertRun(input) {
|
|
157
|
+
// R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
|
|
158
|
+
// this 13-column list is hand-duplicated by publishChildWorkflowRun's own
|
|
159
|
+
// INSERT below, which extends it with parent_run_id/parent_unit_id/
|
|
160
|
+
// invocation_key. A signature refactor to share one INSERT builder was
|
|
161
|
+
// considered and deliberately deferred — see
|
|
162
|
+
// docs/architecture/decisions/0009-child-run-publication-column-parity.md.
|
|
163
|
+
// Keep both column lists in sync by hand.
|
|
122
164
|
this.db
|
|
123
165
|
.prepare(`INSERT INTO workflow_runs (
|
|
124
166
|
id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
|
|
@@ -190,7 +232,7 @@ export class WorkflowRunsRepository {
|
|
|
190
232
|
}
|
|
191
233
|
this.insertRun(input.run);
|
|
192
234
|
this.insertSteps(input.steps);
|
|
193
|
-
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version =
|
|
235
|
+
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = 5 WHERE id = ?").run(input.planJson, input.planHash, input.run.id);
|
|
194
236
|
insertEventOnce(db, {
|
|
195
237
|
eventType: "workflow_started",
|
|
196
238
|
ts: input.run.createdAt,
|
|
@@ -201,6 +243,102 @@ export class WorkflowRunsRepository {
|
|
|
201
243
|
});
|
|
202
244
|
});
|
|
203
245
|
}
|
|
246
|
+
// ── child workflow run publication (migration 023, P3a §5.2-§5.3) ─────────
|
|
247
|
+
//
|
|
248
|
+
// No production caller exists in P3a (§5.5) — dispatch is P3b's. These
|
|
249
|
+
// three methods are reachable only from tests until then.
|
|
250
|
+
/**
|
|
251
|
+
* Idempotently publish a child workflow run underneath the parent unit
|
|
252
|
+
* that spawned it. ONE `immediateTransaction`: SELECT by
|
|
253
|
+
* `(parent_run_id, invocation_key)` and return the existing child if
|
|
254
|
+
* present; otherwise INSERT the child run row (parentage columns +
|
|
255
|
+
* `invocation_key`), its step rows, attach the embedded frozen child plan
|
|
256
|
+
* (`plan_ir_version = 5`), and append its `workflow_started` event — then
|
|
257
|
+
* return the freshly-inserted row.
|
|
258
|
+
*
|
|
259
|
+
* Deliberately does NOT: call {@link findActiveRunForScope} or raise
|
|
260
|
+
* `RESOURCE_ALREADY_EXISTS` (top-level scope-conflict rules do not apply to
|
|
261
|
+
* a child, C-10); call `revalidateSources` or read the filesystem in any
|
|
262
|
+
* way (the child plan was frozen and CAS'd into the PARENT's read set at
|
|
263
|
+
* parent freeze, C-11); or touch {@link publishWorkflowRunV4} /
|
|
264
|
+
* `startWorkflowRun` (untouched, C-12).
|
|
265
|
+
*
|
|
266
|
+
* This method's serialization guarantee holds only when it is the
|
|
267
|
+
* OUTERMOST transaction on the connection (spec Review log R10,
|
|
268
|
+
* docs/plans/specs/p3a-plan-v5-child-freeze.md). `withImmediateTransaction`
|
|
269
|
+
* (src/core/state-db.ts) has a re-entrancy guard: if a transaction is
|
|
270
|
+
* already open on the connection, it SILENTLY JOINS that transaction
|
|
271
|
+
* instead of issuing its own `BEGIN IMMEDIATE`.
|
|
272
|
+
* `WorkflowRunsRepository.transaction()` is DEFERRED (`db.transaction(fn)()`)
|
|
273
|
+
* and is already used in production at `resumeWorkflowRun`
|
|
274
|
+
* (src/workflows/runtime/runs.ts:506) and `completeWorkflowStep` (:782) —
|
|
275
|
+
* a caller that wires this call inside one of those outer transactions
|
|
276
|
+
* loses the guarantee below: the SELECT can read a stale snapshot, both
|
|
277
|
+
* publishers can miss the existing row, and the loser's INSERT hits
|
|
278
|
+
* `idx_workflow_runs_invocation_key` with a raw `SQLiteError` instead of
|
|
279
|
+
* returning the winner's row.
|
|
280
|
+
*
|
|
281
|
+
* As the outermost transaction, the whole SELECT-else-INSERT sequence runs
|
|
282
|
+
* inside one `BEGIN IMMEDIATE`, so two concurrent callers racing on the
|
|
283
|
+
* same `(parentRunId, invocationKey)` serialize on SQLite's write lock: the
|
|
284
|
+
* first to acquire it inserts and commits, and the second's own SELECT —
|
|
285
|
+
* which can only run once it has acquired the lock in turn — finds and
|
|
286
|
+
* returns the first's row rather than inserting a duplicate or throwing
|
|
287
|
+
* (C-09). Calling this twice with the same key, including across a crash
|
|
288
|
+
* between publish and parent-side recording, is therefore safe and returns
|
|
289
|
+
* the same child both times, with exactly one event and one step set
|
|
290
|
+
* (C-08).
|
|
291
|
+
*/
|
|
292
|
+
publishChildWorkflowRun(input) {
|
|
293
|
+
return this.immediateTransaction((db) => {
|
|
294
|
+
const existing = db
|
|
295
|
+
.prepare("SELECT * FROM workflow_runs WHERE parent_run_id = ? AND invocation_key = ?")
|
|
296
|
+
.get(input.parentRunId, input.invocationKey);
|
|
297
|
+
if (existing)
|
|
298
|
+
return existing;
|
|
299
|
+
// R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
|
|
300
|
+
// the first 13 columns here must stay byte-identical to insertRun's own
|
|
301
|
+
// column list above (hand-duplicated, not shared, by deliberate choice
|
|
302
|
+
// — see docs/architecture/decisions/0009-child-run-publication-column-parity.md).
|
|
303
|
+
// Keep both column lists in sync by hand.
|
|
304
|
+
db.prepare(`INSERT INTO workflow_runs (
|
|
305
|
+
id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
|
|
306
|
+
agent_harness, agent_session_id, checkin_armed_at, parent_run_id, parent_unit_id, invocation_key
|
|
307
|
+
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.scopeKey, input.run.workflowEntryId, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.run.checkinArmedAt, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
|
|
308
|
+
this.insertSteps(input.steps);
|
|
309
|
+
db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = 5 WHERE id = ?").run(input.planJson, input.planHash, input.run.id);
|
|
310
|
+
insertEventOnce(db, {
|
|
311
|
+
eventType: "workflow_started",
|
|
312
|
+
ts: input.run.createdAt,
|
|
313
|
+
ref: input.run.workflowRef,
|
|
314
|
+
metadata: { runId: input.run.id, status: "active" },
|
|
315
|
+
idempotencyKey: input.run.id,
|
|
316
|
+
idempotencyMetadataKey: "runId",
|
|
317
|
+
});
|
|
318
|
+
return db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(input.run.id);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Persist a run's resolved declared `outputs:` (migration 024, P3b §4.3).
|
|
323
|
+
* Called from INSIDE `completeWorkflowStep`'s own write transaction — this
|
|
324
|
+
* method opens none of its own, matching `updateStepCompletion` /
|
|
325
|
+
* `updateRunState` immediately above.
|
|
326
|
+
*/
|
|
327
|
+
setRunOutputs(runId, outputsJson) {
|
|
328
|
+
this.db.prepare("UPDATE workflow_runs SET outputs_json = ? WHERE id = ?").run(outputsJson, runId);
|
|
329
|
+
}
|
|
330
|
+
/** Every child run published under `parentRunId`, oldest first. `[]` when none. */
|
|
331
|
+
childRunsOf(parentRunId) {
|
|
332
|
+
return this.db
|
|
333
|
+
.prepare("SELECT * FROM workflow_runs WHERE parent_run_id = ? ORDER BY created_at ASC, id ASC")
|
|
334
|
+
.all(parentRunId);
|
|
335
|
+
}
|
|
336
|
+
/** The child run published under `(parentRunId, key)`, or undefined if none has been published yet. */
|
|
337
|
+
getRunByInvocationKey(parentRunId, key) {
|
|
338
|
+
return (this.db
|
|
339
|
+
.prepare("SELECT * FROM workflow_runs WHERE parent_run_id = ? AND invocation_key = ?")
|
|
340
|
+
.get(parentRunId, key) ?? undefined);
|
|
341
|
+
}
|
|
204
342
|
// ── engine run lease (migration 006 columns, R2 enforcement) ──────────────
|
|
205
343
|
//
|
|
206
344
|
// Single-driver invariant: at most one `akm workflow run` invocation drives
|
|
@@ -817,97 +817,44 @@ function renderCalendar(calendar, indent) {
|
|
|
817
817
|
function normalizeSignature(xml) {
|
|
818
818
|
return xml.replace(/\r\n/g, "\n").trim();
|
|
819
819
|
}
|
|
820
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* Collect the akm-owned service labels present in `launchctl` output.
|
|
822
|
+
*
|
|
823
|
+
* This reads the CURRENT USER'S OWN scheduler inventory — the tasks they asked
|
|
824
|
+
* akm to schedule, on their own machine. It is not an attacker-controlled
|
|
825
|
+
* document, so it is parsed permissively: scan for labels in our own
|
|
826
|
+
* `com.akm.task.` namespace and ignore everything else.
|
|
827
|
+
*
|
|
828
|
+
* It previously enforced a rigid grammar over the whole document and returned
|
|
829
|
+
* `undefined` — surfacing as a hard INVALID_CONFIG_FILE that refused to inspect
|
|
830
|
+
* scheduler state at all — if ANY line failed to match. Real
|
|
831
|
+
* `launchctl print gui/<uid>` output on macOS is far richer than that grammar
|
|
832
|
+
* allowed, so on a real Mac this rejected the user's own inventory and broke
|
|
833
|
+
* every launchd operation. Caught by the gated native-scheduler suite, which
|
|
834
|
+
* had never been dispatched before.
|
|
835
|
+
*
|
|
836
|
+
* The two bounds that remain are real resource bounds, not structural ones: a
|
|
837
|
+
* cap on how much output we will read, and a cap on how many distinct akm
|
|
838
|
+
* labels we will track. Exceeding either still returns `undefined`.
|
|
839
|
+
*/
|
|
821
840
|
export function parseLaunchdLoadedLabels(output) {
|
|
822
|
-
if (Buffer.byteLength(output, "utf8") > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES
|
|
841
|
+
if (Buffer.byteLength(output, "utf8") > MAX_LAUNCHD_DOMAIN_OUTPUT_BYTES)
|
|
823
842
|
return undefined;
|
|
824
|
-
}
|
|
825
|
-
const lines = output.replace(/\r\n?/gu, "\n").split("\n");
|
|
826
843
|
const labels = new Set();
|
|
827
|
-
|
|
828
|
-
|
|
844
|
+
// Our own namespace is the only thing we look for. `[^\s"{}=,()]` stops the
|
|
845
|
+
// token at whatever punctuation the surrounding launchctl syntax uses, so a
|
|
846
|
+
// label works whether it appears as a bare table cell, a quoted string, or a
|
|
847
|
+
// dictionary key.
|
|
848
|
+
const labelPattern = /com\.akm\.task\.[^\s"{}=,()]+/gu;
|
|
849
|
+
for (const match of output.matchAll(labelPattern)) {
|
|
850
|
+
const label = match[0];
|
|
829
851
|
if (!LAUNCHD_AKM_LABEL_RE.test(label))
|
|
830
|
-
return false;
|
|
831
|
-
akmEntryCount += 1;
|
|
832
|
-
if (akmEntryCount > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES || labels.has(label))
|
|
833
|
-
return false;
|
|
834
|
-
labels.add(label);
|
|
835
|
-
return true;
|
|
836
|
-
};
|
|
837
|
-
const firstContent = lines.findIndex((line) => line.trim() !== "");
|
|
838
|
-
if (firstContent >= 0 && /^PID\s+Status\s+Label$/iu.test(lines[firstContent].trim())) {
|
|
839
|
-
for (const line of lines.slice(firstContent + 1)) {
|
|
840
|
-
if (!line.trim())
|
|
841
|
-
continue;
|
|
842
|
-
const row = /^\s*(?:-|\d+)\s+-?\d+\s+(\S+)\s*$/u.exec(line);
|
|
843
|
-
if (!row?.[1])
|
|
844
|
-
return undefined;
|
|
845
|
-
if (row[1].startsWith(LAUNCHD_LABEL_PREFIX) && !add(row[1]))
|
|
846
|
-
return undefined;
|
|
847
|
-
}
|
|
848
|
-
return labels;
|
|
849
|
-
}
|
|
850
|
-
const firstLine = firstContent < 0 ? undefined : lines[firstContent];
|
|
851
|
-
if (firstLine === undefined || !/^\s*(?:gui|user)\/\d+\s*=\s*\{\s*$/u.test(firstLine))
|
|
852
|
-
return undefined;
|
|
853
|
-
let lastContent = lines.length - 1;
|
|
854
|
-
while (lastContent >= 0 && !lines[lastContent]?.trim())
|
|
855
|
-
lastContent -= 1;
|
|
856
|
-
const lastLine = lines[lastContent];
|
|
857
|
-
if (lastContent <= firstContent || lastLine === undefined || !/^\s*\}\s*$/u.test(lastLine))
|
|
858
|
-
return undefined;
|
|
859
|
-
let depth = 1;
|
|
860
|
-
let servicesSeen = false;
|
|
861
|
-
let insideServices = false;
|
|
862
|
-
for (let index = firstContent + 1; index <= lastContent; index += 1) {
|
|
863
|
-
const line = lines[index];
|
|
864
|
-
if (index === lastContent) {
|
|
865
|
-
if (insideServices || depth !== 1)
|
|
866
|
-
return undefined;
|
|
867
|
-
depth = 0;
|
|
868
852
|
continue;
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const emptyServicesBlock = /^\s*services\s*=\s*\{\s*\}\s*$/u.test(line);
|
|
872
|
-
if (servicesBlock || emptyServicesBlock) {
|
|
873
|
-
if (servicesSeen || insideServices || depth !== 1)
|
|
874
|
-
return undefined;
|
|
875
|
-
servicesSeen = true;
|
|
876
|
-
if (servicesBlock) {
|
|
877
|
-
insideServices = true;
|
|
878
|
-
depth += 1;
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
if (/^\s*services\s*=/u.test(line))
|
|
883
|
-
return undefined;
|
|
884
|
-
if (insideServices && depth === 2) {
|
|
885
|
-
if (!line.trim())
|
|
886
|
-
continue;
|
|
887
|
-
if (/^\s*\}\s*$/u.test(line)) {
|
|
888
|
-
insideServices = false;
|
|
889
|
-
depth -= 1;
|
|
890
|
-
continue;
|
|
891
|
-
}
|
|
892
|
-
const assignment = /^\s*-?\d+\s*=\s*"?([^"\s{}=]+)"?\s*$/u.exec(line);
|
|
893
|
-
const domainTable = /^\s*(?:-|\d+)\s+(?:-|-?\d+)\s+(\S+)\s*$/u.exec(line);
|
|
894
|
-
const dictionary = /^\s*"?([^"\s{}=]+)"?\s*=\s*\{\s*$/u.exec(line);
|
|
895
|
-
const candidate = assignment?.[1] ?? domainTable?.[1] ?? dictionary?.[1];
|
|
896
|
-
if (!candidate)
|
|
897
|
-
return undefined;
|
|
898
|
-
if (candidate.startsWith(LAUNCHD_LABEL_PREFIX) && !add(candidate))
|
|
899
|
-
return undefined;
|
|
900
|
-
if (dictionary)
|
|
901
|
-
depth += 1;
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
if (line.includes(LAUNCHD_LABEL_PREFIX))
|
|
905
|
-
return undefined;
|
|
906
|
-
depth += countCharacter(line, "{") - countCharacter(line, "}");
|
|
907
|
-
if (depth < 1 || (insideServices && depth < 2))
|
|
853
|
+
labels.add(label);
|
|
854
|
+
if (labels.size > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES)
|
|
908
855
|
return undefined;
|
|
909
856
|
}
|
|
910
|
-
return
|
|
857
|
+
return labels;
|
|
911
858
|
}
|
|
912
859
|
function countCharacter(value, needle) {
|
|
913
860
|
let count = 0;
|
package/dist/tasks/embedded.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import fs from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import { getDirname } from "../runtime.js";
|
|
25
|
-
import {
|
|
25
|
+
import { parseTaskSource } from "./source/parse-task-source.js";
|
|
26
26
|
/** Directory holding the bundled task template categories. */
|
|
27
27
|
const TASKS_ASSETS_DIR = path.join(getDirname(import.meta.url), "../assets/tasks");
|
|
28
28
|
/**
|
|
@@ -65,22 +65,28 @@ export function listEmbeddedTasks() {
|
|
|
65
65
|
catch {
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
|
-
let
|
|
68
|
+
let parsed;
|
|
69
69
|
try {
|
|
70
|
-
|
|
70
|
+
parsed = parseTaskSource({ yaml, filePath, workspaceRoot: TASKS_ASSETS_DIR });
|
|
71
71
|
}
|
|
72
72
|
catch {
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
// Shipped templates are task source v4 (spec docs/plans/specs/p4-deletions-closeout.md
|
|
76
|
+
// §3.2.6, row B-24): a template's `enabled` is per schedule-binding, so
|
|
77
|
+
// the single-cron display shape here reads the FIRST schedule entry —
|
|
78
|
+
// every shipped template authors exactly one.
|
|
79
|
+
const task = parsed.v4;
|
|
80
|
+
const [firstSchedule] = task.schedule;
|
|
81
|
+
if (task.target.kind !== "run" || !firstSchedule)
|
|
76
82
|
continue;
|
|
77
83
|
tasks.push({
|
|
78
84
|
id,
|
|
79
85
|
label: `${category}/${id}`,
|
|
80
86
|
command: task.target.run,
|
|
81
|
-
schedule:
|
|
82
|
-
description: task.
|
|
83
|
-
enabled:
|
|
87
|
+
schedule: firstSchedule.cron,
|
|
88
|
+
description: task.description ?? "",
|
|
89
|
+
enabled: firstSchedule.enabled,
|
|
84
90
|
yaml,
|
|
85
91
|
});
|
|
86
92
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
import { captureScriptTarget } from "./script-capture.js";
|
|
5
|
+
/** Project a script asset's own identity into the frozen shape a workflow script step dispatches. */
|
|
6
|
+
export function prepareScriptTarget(input) {
|
|
7
|
+
const captured = captureScriptTarget(input.ref, input.file, input.bundleRoot, input.readFile);
|
|
8
|
+
return Object.freeze({ ref: input.ref, ...captured });
|
|
9
|
+
}
|