akm-cli 0.9.31 → 0.10.261002-beta
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 +420 -0
- package/STABILITY.md +99 -57
- package/dist/api-node.mjs +20 -0
- package/dist/api.d.ts +23 -0
- package/dist/api.js +45 -0
- package/dist/assets/hints/cli-hints-full.md +15 -29
- package/dist/assets/hints/cli-hints-short.md +2 -2
- package/dist/assets/improve-strategies/default.json +3 -3
- package/dist/assets/prompts/consolidate-pair.md +9 -7
- package/dist/assets/stash-skeleton/README.md +1 -1
- package/dist/assets/tasks/improve/akm-improve-consolidate.yml +1 -1
- package/dist/cli/shared.js +31 -48
- package/dist/cli/unknown-flags.js +6 -43
- package/dist/cli.js +8 -34
- package/dist/commands/agent/contribute-cli.js +1 -1
- package/dist/commands/completions.js +1 -2
- package/dist/commands/config-cli.js +5 -28
- package/dist/commands/env/env-cli.js +5 -5
- package/dist/commands/env/secret-cli.js +2 -2
- package/dist/commands/feedback-cli.js +75 -49
- package/dist/commands/health/checks.js +41 -16
- package/dist/commands/health/plugin-staleness.js +52 -22
- package/dist/commands/improve/autonomy-gate.js +5 -3
- package/dist/commands/improve/consolidate/chunking.js +3 -10
- package/dist/commands/improve/consolidate/continuity-check.js +4 -4
- package/dist/commands/improve/consolidate/pair-pass.js +29 -10
- package/dist/commands/improve/consolidate.js +10 -65
- package/dist/commands/improve/distill/lesson-update.js +91 -0
- package/dist/commands/improve/distill-guards.js +1 -43
- package/dist/commands/improve/distill.js +148 -21
- package/dist/commands/improve/eligibility.js +14 -1
- package/dist/commands/improve/extract-cli.js +1 -1
- package/dist/commands/improve/extract-prompt.js +17 -7
- package/dist/commands/improve/extract.js +5 -7
- package/dist/commands/improve/improve-cli.js +92 -106
- package/dist/commands/improve/improve-strategies.js +8 -12
- package/dist/commands/improve/improve.js +134 -52
- package/dist/commands/improve/loop-stages.js +18 -5
- package/dist/commands/improve/planner.js +0 -1
- package/dist/commands/improve/preparation.js +40 -99
- package/dist/commands/improve/proactive-maintenance.js +2 -2
- package/dist/commands/improve/reflect-noise.js +3 -55
- package/dist/commands/improve/reflect.js +29 -34
- package/dist/commands/improve/retrieval-queries.js +37 -0
- package/dist/commands/improve/retrieval-scope.js +2 -2
- package/dist/commands/improve/salience.js +2 -9
- package/dist/commands/improve/session-asset.js +4 -1
- package/dist/commands/improve/stage.js +23 -12
- package/dist/commands/lint/index.js +5 -20
- package/dist/commands/observability-cli.js +3 -9
- package/dist/commands/proposal/drain.js +43 -6
- package/dist/commands/proposal/proposal-cli.js +11 -31
- package/dist/commands/proposal/repository.js +20 -14
- package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -3
- package/dist/commands/read/knowledge.js +3 -3
- package/dist/commands/read/remember-cli.js +0 -13
- package/dist/commands/read/search-cli.js +29 -97
- package/dist/commands/read/search.js +0 -11
- package/dist/commands/read/show.js +0 -19
- package/dist/commands/sources/plugin-upgrade.js +164 -101
- package/dist/commands/sources/sources-cli.js +1 -15
- package/dist/commands/sources/stash-cli.js +6 -14
- package/dist/commands/tasks/tasks-cli.js +4 -115
- package/dist/commands/workflow/plan.js +11 -33
- package/dist/commands/workflow-cli.js +2 -7
- package/dist/core/adapter/adapter-ids.js +1 -2
- package/dist/core/adapter/adapters/agent-skills-adapter.js +0 -4
- package/dist/core/adapter/adapters/akm-adapter.js +9 -48
- package/dist/core/adapter/adapters/akm-lint.js +10 -26
- package/dist/core/adapter/adapters/akm-metadata.js +2 -72
- package/dist/core/adapter/adapters/akm-task-adapter.js +0 -5
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +9 -28
- package/dist/core/adapter/adapters/dotenv-adapter.js +1 -20
- package/dist/core/adapter/adapters/generic-files-adapter.js +0 -10
- package/dist/core/adapter/adapters/llm-wiki-adapter.js +0 -4
- package/dist/core/adapter/adapters/tool-dir-shared.js +0 -24
- package/dist/core/adapter/adapters/website-snapshot-adapter.js +4 -4
- package/dist/core/adapter/recognize-match.js +3 -4
- package/dist/core/common.js +18 -2
- package/dist/core/config/config-schema.js +12 -15
- package/dist/core/config/config-types.js +6 -15
- package/dist/core/config/config-walker.js +6 -23
- package/dist/core/config/config.js +17 -80
- package/dist/core/config/engine-semantics.js +7 -3
- package/dist/core/config/schema/engines.js +7 -7
- package/dist/core/config/schema/improve-processes.js +10 -63
- package/dist/core/config/schema/improve.js +2 -21
- package/dist/core/config/schema/index-config.js +6 -39
- package/dist/core/env-secret-ref.js +2 -45
- package/dist/core/errors.js +1 -2
- package/dist/core/extra-params.js +2 -86
- package/dist/core/file-lock.js +2 -6
- package/dist/core/improve-result.js +13 -1
- package/dist/core/mutation-target.js +2 -2
- package/dist/core/paths.js +4 -0
- package/dist/core/recognition-util.js +4 -5
- package/dist/core/run-lock.js +1 -1
- package/dist/core/subprocess.js +49 -0
- package/dist/core/warn.js +21 -4
- package/dist/core/write-source.js +3 -3
- package/dist/execution/target-ref.js +1 -2
- package/dist/indexer/indexer.js +15 -6
- package/dist/indexer/passes/memory-inference.js +15 -6
- package/dist/indexer/search/db-search.js +49 -36
- package/dist/indexer/search/fts-query.js +0 -19
- package/dist/indexer/search/ranking.js +29 -1
- package/dist/indexer/usage/usage-events.js +21 -16
- package/dist/integrations/agent/engine-resolution.js +5 -2
- package/dist/integrations/agent/index.js +1 -1
- package/dist/integrations/agent/profiles.js +41 -98
- package/dist/integrations/agent/request-lowering.js +3 -3
- package/dist/integrations/harnesses/aider/index.js +5 -0
- package/dist/integrations/harnesses/amazonq/index.js +1 -0
- package/dist/integrations/harnesses/claude/config-import.js +2 -3
- package/dist/integrations/harnesses/claude/index.js +5 -0
- package/dist/integrations/harnesses/codex/index.js +1 -0
- package/dist/integrations/harnesses/copilot/index.js +1 -0
- package/dist/integrations/harnesses/gemini/index.js +1 -0
- package/dist/integrations/harnesses/index.js +18 -39
- package/dist/integrations/harnesses/opencode/agent-builder.js +60 -35
- package/dist/integrations/harnesses/opencode/config-import.js +121 -52
- package/dist/integrations/harnesses/opencode/index.js +22 -0
- package/dist/integrations/harnesses/opencode/plugin-layout.js +68 -0
- package/dist/integrations/harnesses/opencode/session-log-types.js +12 -0
- package/dist/integrations/harnesses/opencode/session-log-v1.js +72 -0
- package/dist/integrations/harnesses/opencode/session-log-v2.js +140 -0
- package/dist/integrations/harnesses/opencode/session-log.js +104 -115
- package/dist/integrations/harnesses/opencode/version.js +98 -0
- package/dist/integrations/harnesses/opencode-sdk/{harness.js → index.js} +5 -17
- package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +105 -206
- package/dist/integrations/harnesses/opencode-sdk/v1-adapter.js +199 -0
- package/dist/integrations/harnesses/opencode-sdk/v2-adapter.js +254 -0
- package/dist/integrations/harnesses/opencode-sdk/wire.js +66 -0
- package/dist/integrations/harnesses/openhands/index.js +1 -0
- package/dist/integrations/harnesses/pi/index.js +1 -0
- package/dist/integrations/harnesses/shared.js +2 -0
- package/dist/integrations/harnesses/types.js +2 -0
- package/dist/llm/client.js +41 -11
- package/dist/output/cli-hints.js +1 -1
- package/dist/output/context.js +10 -19
- package/dist/output/format-exempt.js +5 -8
- package/dist/output/shapes/helpers.js +1 -22
- package/dist/output/shapes/passthrough.js +0 -1
- package/dist/output/shapes.js +3 -31
- package/dist/output/stdout.js +5 -1
- package/dist/output/text/command-format.js +6 -1
- package/dist/output/text/workflow-format.js +4 -22
- package/dist/scripts/akm-migrate-node.js +15745 -16711
- package/dist/scripts/akm-migrate.js +16318 -17280
- package/dist/setup/detect.js +10 -4
- package/dist/setup/harness-config-import.js +5 -14
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/tasks.js +12 -13
- package/dist/sources/providers/git-stash.js +113 -16
- package/dist/storage/repositories/index-utility-repository.js +2 -32
- package/dist/storage/repositories/index-vec-repository.js +4 -0
- package/dist/storage/repositories/task-history-repository.js +0 -5
- package/dist/storage/repositories/workflow-runs-repository.js +5 -15
- package/dist/tasks/embedded.js +2 -1
- package/dist/tasks/run/run-command-task.js +9 -1
- package/dist/tasks/run/run-workflow-task.js +4 -3
- package/dist/tasks/run/task-history.js +0 -2
- package/dist/tasks/run/task-log.js +38 -1
- package/dist/tasks/schedule-gates.js +39 -0
- package/dist/tasks/scheduler-binding.js +0 -16
- package/dist/tasks/scheduler-sync.js +3 -97
- package/dist/tasks/source/bounded-document.js +1 -1
- package/dist/tasks/source/task-source-v4.js +2 -12
- package/dist/tasks/task-run-reserved-flags.js +2 -31
- package/dist/workflows/authoring/authoring.js +6 -6
- package/dist/workflows/compile.js +8 -65
- package/dist/workflows/exec/child-workflow.js +22 -3
- package/dist/workflows/exec/native-executor.js +12 -147
- package/dist/workflows/exec/run-workflow.js +31 -103
- package/dist/workflows/exec/step-work.js +9 -178
- package/dist/workflows/exec/unit-dispatch.js +0 -10
- package/dist/workflows/freeze/environment.js +3 -44
- package/dist/workflows/freeze/freeze.js +8 -41
- package/dist/workflows/freeze/targets/child-workflow.js +27 -63
- package/dist/workflows/freeze/targets/command.js +2 -16
- package/dist/workflows/freeze/targets/shell.js +0 -2
- package/dist/workflows/freeze/task-bindings.js +2 -28
- package/dist/workflows/ir/params.js +12 -2
- package/dist/workflows/parser.js +82 -232
- package/dist/workflows/plan.js +1 -1
- package/dist/workflows/renderer.js +10 -19
- package/dist/workflows/runtime/run-plan.js +11 -53
- package/dist/workflows/runtime/runs.js +4 -27
- package/dist/workflows/source-files.js +12 -16
- package/dist/workflows/source-semantics.js +1 -93
- package/docs/README.md +1 -1
- package/docs/agents/README.md +3 -3
- package/docs/agents/agent-install.md +1 -1
- package/docs/architecture/0.10-process-rules.md +827 -0
- package/docs/architecture/adapters.md +17 -31
- package/docs/architecture/architecture.md +4 -5
- package/docs/architecture/improvement.md +115 -76
- package/docs/architecture/internals/improve-workflow.md +22 -16
- package/docs/architecture/internals/indexing.md +1 -1
- package/docs/architecture/internals/search.md +1 -2
- package/docs/architecture/internals/storage-locations.md +6 -6
- package/docs/architecture/persisted-data-compat.md +13 -4
- package/docs/architecture/specs/0.9.0-decisions.md +6 -2
- package/docs/architecture/specs/improve-collapse-churn-detector-design.md +2 -2
- package/docs/architecture/specs/workflow-format-unification.md +1 -6
- package/docs/architecture/testing/manual-testing-checklist.md +21 -30
- package/docs/architecture/testing/task-workflow-manual-tests.md +106 -203
- package/docs/architecture/testing/testing-workflow.md +1 -1
- package/docs/architecture/workflow-engine.md +5 -56
- package/docs/guides/author-workflows.md +39 -69
- package/docs/guides/bundles.md +3 -1
- package/docs/guides/capture-knowledge.md +1 -1
- package/docs/guides/claude-code-vs-akm-workflows.md +1 -2
- package/docs/guides/discover-and-load.md +4 -4
- package/docs/guides/getting-started.md +1 -1
- package/docs/guides/improve-the-library.md +10 -9
- package/docs/guides/recipes/headless-install.md +2 -2
- package/docs/guides/run-workflows.md +1 -1
- package/docs/guides/scheduling.md +9 -1
- package/docs/guides/use-with-any-agent.md +49 -4
- package/docs/integration/bundling-akm.md +7 -6
- package/docs/maintainers/curate-workmap.md +1 -2
- package/docs/maintainers/release-checklist.md +33 -1
- package/docs/migration/README.md +2 -0
- package/docs/migration/release-notes/0.10.0.md +201 -0
- package/docs/migration/release-notes/README.md +1 -0
- package/docs/migration/v0.9-to-v0.10.md +507 -0
- package/docs/posts/akm-command-tour.md +3 -3
- package/docs/product-surface.md +1 -1
- package/docs/reference/README.md +1 -0
- package/docs/reference/api.md +65 -0
- package/docs/reference/asset-types.md +3 -5
- package/docs/reference/bundle-types.md +2 -4
- package/docs/reference/cli.md +160 -188
- package/docs/reference/configuration.md +101 -20
- package/docs/reference/data-and-telemetry.md +2 -2
- package/docs/reference/env-and-secrets.md +2 -2
- package/docs/reference/supported-formats.md +3 -4
- package/docs/reference/tasks.md +12 -19
- package/docs/reference/website-sources.md +1 -1
- package/docs/reference/workflow-schema.md +89 -317
- package/docs/reference/workflows.md +20 -48
- package/package.json +12 -2
- package/schemas/akm-config.json +2 -243
- package/schemas/akm-task.json +2 -3
- package/schemas/akm-workflow.json +47 -84
- package/dist/assets/improve-strategies/catchup.json +0 -41
- package/dist/assets/improve-strategies/proactive-maintenance.json +0 -24
- package/dist/assets/improve-strategies/quick.json +0 -17
- package/dist/assets/improve-strategies/reflect-distill.json +0 -29
- package/dist/assets/improve-strategies/thorough.json +0 -49
- package/dist/assets/prompts/retrieval-relevance-judge.md +0 -6
- package/dist/assets/tasks/improve/akm-improve-catchup.yml +0 -6
- package/dist/assets/tasks/improve/akm-improve-frequent.yml +0 -4
- package/dist/assets/tasks/improve/akm-improve-nightly.yml +0 -4
- package/dist/cli/retired-commands.js +0 -118
- package/dist/commands/improve/retrieval-gate.js +0 -132
- package/dist/commands/tasks/validate.js +0 -176
- package/dist/core/config/legacy-source-shape-shim.js +0 -117
- package/dist/indexer/feedback/utility-policy.js +0 -75
- package/dist/integrations/harnesses/ids.js +0 -28
- package/dist/tasks/source/task-source-v3-frozen.js +0 -424
- package/dist/tasks/source/task-to-v4.js +0 -863
- package/dist/workflows/exec/worktree.js +0 -372
- package/dist/workflows/freeze/child-output-references.js +0 -83
- package/dist/workflows/freeze/identity.js +0 -22
- package/dist/workflows/freeze/targets/script.js +0 -50
- package/dist/workflows/freeze/targets/task.js +0 -124
- package/dist/workflows/github-yaml.js +0 -506
- package/dist/workflows/runtime/run-outputs.js +0 -80
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Git worktree lifecycle for `isolation: worktree` units: each attempt of a
|
|
6
|
-
* file-mutating agent/sdk unit gets a fresh detached worktree of the run's base
|
|
7
|
-
* repository under `<tmp>/akm-worktrees/<runId>/<attemptId>`.
|
|
8
|
-
*
|
|
9
|
-
* 1. {@link assertGitWorkTree} — once per step, before dispatch.
|
|
10
|
-
* 2. {@link createUnitWorktree} — `git worktree add --detach`; the path is
|
|
11
|
-
* journaled on the unit row and becomes the unit's cwd.
|
|
12
|
-
* 3. {@link cleanupUnitWorktree} — a clean tree is removed, a dirty one kept.
|
|
13
|
-
* "Clean" ignores `.gitignore`d files (build outputs, `node_modules`),
|
|
14
|
-
* which the repo already declares disposable.
|
|
15
|
-
* 4. {@link sweepStaleWorktrees} — at most once per process, removes trees
|
|
16
|
-
* and run roots older than a week.
|
|
17
|
-
*
|
|
18
|
-
* Every repo-mutating git call is async and serialized per base repository
|
|
19
|
-
* ({@link withRepoWorktreeLock}), so parallel units never race on
|
|
20
|
-
* `.git/worktrees`. Nothing here throws: results are objects the executor maps
|
|
21
|
-
* onto its failure vocabulary (the fire-and-forget GC sweep reports via `warn`).
|
|
22
|
-
*/
|
|
23
|
-
import { spawnSync } from "node:child_process";
|
|
24
|
-
import fsp from "node:fs/promises";
|
|
25
|
-
import os from "node:os";
|
|
26
|
-
import path from "node:path";
|
|
27
|
-
import { isWithinAsync, safeRealpathAsync } from "../../core/common.js";
|
|
28
|
-
import { serializeByKey } from "../../core/concurrent.js";
|
|
29
|
-
import { runManagedSubprocess } from "../../core/subprocess.js";
|
|
30
|
-
import { warn } from "../../core/warn.js";
|
|
31
|
-
/** Timeout for each worktree git call (#891: a busy machine can push a healthy one past 30s). */
|
|
32
|
-
const GIT_TIMEOUT_MS = 120_000;
|
|
33
|
-
/** Directory under `os.tmpdir()` that owns every run's worktree roots. */
|
|
34
|
-
export const WORKTREES_DIR_NAME = "akm-worktrees";
|
|
35
|
-
/** Age after which an orphaned or retained worktree is swept: long enough to triage a failed run. */
|
|
36
|
-
export const STALE_WORKTREE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
37
|
-
function gitExitError(args, code, stderr, stdout) {
|
|
38
|
-
const detail = (stderr || stdout || "").trim();
|
|
39
|
-
return `git ${args.join(" ")} exited ${code}${detail ? `: ${detail}` : ""}`;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Run one git command asynchronously via the real `git` binary; `ok` = exit
|
|
43
|
-
* 0. Never throws (spawn errors and the timeout → ok: false).
|
|
44
|
-
*/
|
|
45
|
-
async function realGitExecutor(cwd, args) {
|
|
46
|
-
const result = await runManagedSubprocess(["git", "-C", cwd, ...args], {
|
|
47
|
-
capture: true,
|
|
48
|
-
timeoutMs: GIT_TIMEOUT_MS,
|
|
49
|
-
});
|
|
50
|
-
if (result.spawnError) {
|
|
51
|
-
return { ok: false, stdout: "", error: `git ${args[0]} failed to spawn: ${result.spawnError.message}` };
|
|
52
|
-
}
|
|
53
|
-
if (result.timedOut) {
|
|
54
|
-
return { ok: false, stdout: result.stdout, error: `git ${args.join(" ")} timed out after ${GIT_TIMEOUT_MS}ms` };
|
|
55
|
-
}
|
|
56
|
-
if (result.exitCode !== 0) {
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
stdout: result.stdout,
|
|
60
|
-
error: gitExitError(args, result.exitCode, result.stderr, result.stdout),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
return { ok: true, stdout: result.stdout };
|
|
64
|
-
}
|
|
65
|
-
let gitExecutor = realGitExecutor;
|
|
66
|
-
/** Test seam: swap the git executor for repo-mutating calls (`undefined` restores the real one). */
|
|
67
|
-
export function setGitExecutorForTesting(executor) {
|
|
68
|
-
gitExecutor = executor ?? realGitExecutor;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Run one git command asynchronously; `ok` = exit 0. Never throws. Async so a
|
|
72
|
-
* git lock wait parks a promise instead of blocking the event loop;
|
|
73
|
-
* repo-mutating callers must hold {@link withRepoWorktreeLock}.
|
|
74
|
-
*/
|
|
75
|
-
async function git(cwd, args) {
|
|
76
|
-
return gitExecutor(cwd, args);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Synchronous git for the two read-only probes that run before any unit is in
|
|
80
|
-
* flight ({@link isGitAvailable}, {@link assertGitWorkTree}). They take no
|
|
81
|
-
* repository lock, so blocking here cannot stall another unit's git call.
|
|
82
|
-
*/
|
|
83
|
-
function gitSync(cwd, args) {
|
|
84
|
-
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", timeout: GIT_TIMEOUT_MS });
|
|
85
|
-
if (result.error) {
|
|
86
|
-
return { ok: false, stdout: "", error: `git ${args[0]} failed to spawn: ${result.error.message}` };
|
|
87
|
-
}
|
|
88
|
-
if (result.status !== 0) {
|
|
89
|
-
return {
|
|
90
|
-
ok: false,
|
|
91
|
-
stdout: result.stdout ?? "",
|
|
92
|
-
error: gitExitError(args, result.status, result.stderr ?? "", result.stdout ?? ""),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
return { ok: true, stdout: result.stdout ?? "" };
|
|
96
|
-
}
|
|
97
|
-
/** True when a usable `git` binary is on PATH (tests skip gracefully without one). */
|
|
98
|
-
export function isGitAvailable() {
|
|
99
|
-
const result = spawnSync("git", ["--version"], { encoding: "utf8", timeout: 5_000 });
|
|
100
|
-
return !result.error && result.status === 0;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Preflight for worktree isolation: `dir` must be inside a git work tree.
|
|
104
|
-
* Returns an error message (for a clean step failure) or undefined when ok.
|
|
105
|
-
* A missing git binary reports as the same clean failure — a workflow that
|
|
106
|
-
* declares isolation cannot run without git.
|
|
107
|
-
*/
|
|
108
|
-
export function assertGitWorkTree(dir) {
|
|
109
|
-
const result = gitSync(dir, ["rev-parse", "--is-inside-work-tree"]);
|
|
110
|
-
if (!result.ok) {
|
|
111
|
-
return `"${dir}" is not a git repository (isolation: worktree requires one): ${result.error}`;
|
|
112
|
-
}
|
|
113
|
-
if (result.stdout.trim() !== "true") {
|
|
114
|
-
return `"${dir}" is not inside a git work tree (isolation: worktree requires one).`;
|
|
115
|
-
}
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
// ── Per-repository serialization ────────────────────────────────────────────
|
|
119
|
-
/** In-flight tail of each base repository's serialized git-worktree chain. */
|
|
120
|
-
const repoOperationTails = new Map();
|
|
121
|
-
/** Base repos already pruned in this process, per run id (dropped with the run's drained root). */
|
|
122
|
-
const prunedRuns = new Map();
|
|
123
|
-
/**
|
|
124
|
-
* Serialize `fn` against every other repo-mutating worktree operation on the
|
|
125
|
-
* same base repository ({@link serializeByKey}). Keyed by the RESOLVED repo
|
|
126
|
-
* path so two spellings of one repo (symlinked tmpdir, relative cwd) share a
|
|
127
|
-
* chain. A failure rejects its own caller but never wedges the chain.
|
|
128
|
-
*/
|
|
129
|
-
function withRepoWorktreeLock(repoKey, fn) {
|
|
130
|
-
return serializeByKey(repoOperationTails, repoKey, fn);
|
|
131
|
-
}
|
|
132
|
-
/** Journal-safe directory name for a unit attempt id (ids carry `:` / `~`). */
|
|
133
|
-
function sanitizeAttemptId(attemptId) {
|
|
134
|
-
return attemptId.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
135
|
-
}
|
|
136
|
-
/** Parent directory of every run's worktree root (`<tmp>/akm-worktrees`). */
|
|
137
|
-
export function worktreesRoot() {
|
|
138
|
-
return path.join(os.tmpdir(), WORKTREES_DIR_NAME);
|
|
139
|
-
}
|
|
140
|
-
/** Run-scoped parent directory for all of one run's unit worktrees. */
|
|
141
|
-
export function runWorktreeRoot(runId) {
|
|
142
|
-
return path.join(worktreesRoot(), runId);
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Move a leftover attempt directory aside to `<dest>.retained-<ts>[-n]`
|
|
146
|
-
* (never overwriting an earlier retained copy). Throws on fs errors — the
|
|
147
|
-
* caller maps them onto its result object.
|
|
148
|
-
*/
|
|
149
|
-
async function moveLeftoverAside(dest) {
|
|
150
|
-
const base = `${dest}.retained-${Date.now()}`;
|
|
151
|
-
let aside = base;
|
|
152
|
-
for (let n = 1; await pathExists(aside); n++)
|
|
153
|
-
aside = `${base}-${n}`;
|
|
154
|
-
await fsp.rename(dest, aside);
|
|
155
|
-
return aside;
|
|
156
|
-
}
|
|
157
|
-
async function pathExists(p) {
|
|
158
|
-
return fsp.access(p).then(() => true, () => false);
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* Create a fresh detached worktree at `<tmp>/akm-worktrees/<runId>/<attemptId>`.
|
|
162
|
-
* A leftover at that path is removed when clean, otherwise moved aside to
|
|
163
|
-
* `<dest>.retained-<ts>` (`preservedLeftover`). Probe, prune, and add form one
|
|
164
|
-
* critical section under {@link withRepoWorktreeLock}.
|
|
165
|
-
*/
|
|
166
|
-
export async function createUnitWorktree(baseDir, runId, attemptId, commitOid) {
|
|
167
|
-
// Opportunistic, at most once per process, never awaited — GC must never sit
|
|
168
|
-
// on the dispatch path.
|
|
169
|
-
sweepStaleWorktreesOnce();
|
|
170
|
-
const repoKey = await safeRealpathAsync(baseDir);
|
|
171
|
-
const dest = path.join(runWorktreeRoot(runId), sanitizeAttemptId(attemptId));
|
|
172
|
-
return withRepoWorktreeLock(repoKey, async () => {
|
|
173
|
-
let preservedLeftover;
|
|
174
|
-
let leftoverHandled = false;
|
|
175
|
-
try {
|
|
176
|
-
if (await pathExists(dest)) {
|
|
177
|
-
const status = await git(dest, ["status", "--porcelain"]);
|
|
178
|
-
if (status.ok && status.stdout.trim() === "") {
|
|
179
|
-
// Async on purpose: a recursive delete of a whole leftover checkout
|
|
180
|
-
// inside this critical section would otherwise block the event loop
|
|
181
|
-
// (every other in-flight unit, abort handling).
|
|
182
|
-
await fsp.rm(dest, { recursive: true, force: true });
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
preservedLeftover = await moveLeftoverAside(dest);
|
|
186
|
-
}
|
|
187
|
-
leftoverHandled = true;
|
|
188
|
-
}
|
|
189
|
-
await fsp.mkdir(path.dirname(dest), { recursive: true });
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
return {
|
|
193
|
-
ok: false,
|
|
194
|
-
error: `could not prepare worktree directory ${dest}: ${message(err)}`,
|
|
195
|
-
...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
// Prune (never touches a live worktree) after a leftover was removed, and
|
|
199
|
-
// on the first worktree of this (repo, run) to reap orphaned registrations.
|
|
200
|
-
const prunedRepos = prunedRuns.get(runId);
|
|
201
|
-
if (leftoverHandled || !prunedRepos?.has(repoKey)) {
|
|
202
|
-
if (prunedRepos)
|
|
203
|
-
prunedRepos.add(repoKey);
|
|
204
|
-
else
|
|
205
|
-
prunedRuns.set(runId, new Set([repoKey]));
|
|
206
|
-
await git(baseDir, ["worktree", "prune"]);
|
|
207
|
-
}
|
|
208
|
-
const added = await git(baseDir, ["worktree", "add", "--detach", dest, ...(commitOid ? [commitOid] : [])]);
|
|
209
|
-
if (!added.ok) {
|
|
210
|
-
return {
|
|
211
|
-
ok: false,
|
|
212
|
-
error: `could not create isolation worktree at ${dest}: ${added.error}`,
|
|
213
|
-
...(preservedLeftover !== undefined ? { preservedLeftover } : {}),
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
return { ok: true, path: dest, ...(preservedLeftover !== undefined ? { preservedLeftover } : {}) };
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Post-unit cleanup: remove a clean worktree; retain (dirty: true) one with
|
|
221
|
-
* uncommitted work or whose state could not be verified. `.gitignore`d files do
|
|
222
|
-
* not count as work.
|
|
223
|
-
*/
|
|
224
|
-
export async function cleanupUnitWorktree(baseDir, worktreePath) {
|
|
225
|
-
// The removal (without --force) is itself the cleanliness check: one git process per clean unit.
|
|
226
|
-
const removed = await withRepoWorktreeLock(await safeRealpathAsync(baseDir), () => git(baseDir, ["worktree", "remove", worktreePath]));
|
|
227
|
-
if (removed.ok) {
|
|
228
|
-
await removeRunRootIfEmpty(worktreePath);
|
|
229
|
-
return { removed: true, dirty: false };
|
|
230
|
-
}
|
|
231
|
-
// It refused, so the tree stays on disk. Ask the probe WHY it refused rather
|
|
232
|
-
// than parsing git's message, whose wording varies with version and locale —
|
|
233
|
-
// and which the caller's warn text has never been written against.
|
|
234
|
-
const status = await git(worktreePath, ["status", "--porcelain"]);
|
|
235
|
-
if (!status.ok) {
|
|
236
|
-
return { removed: false, dirty: false, error: status.error };
|
|
237
|
-
}
|
|
238
|
-
if (status.stdout.trim() !== "") {
|
|
239
|
-
return { removed: false, dirty: true };
|
|
240
|
-
}
|
|
241
|
-
return { removed: false, dirty: false, error: removed.error };
|
|
242
|
-
}
|
|
243
|
-
// ── Garbage collection ──────────────────────────────────────────────────────
|
|
244
|
-
/** Drop the run-scoped root once empty (`rmdir` keeps any retained tree). */
|
|
245
|
-
async function removeRunRootIfEmpty(worktreePath) {
|
|
246
|
-
const root = worktreesRoot();
|
|
247
|
-
const runRoot = path.dirname(path.resolve(worktreePath));
|
|
248
|
-
if (!(await isWithinAsync(runRoot, root)))
|
|
249
|
-
return;
|
|
250
|
-
if ((await safeRealpathAsync(path.dirname(runRoot))) !== (await safeRealpathAsync(root)))
|
|
251
|
-
return;
|
|
252
|
-
try {
|
|
253
|
-
await fsp.rmdir(runRoot);
|
|
254
|
-
}
|
|
255
|
-
catch {
|
|
256
|
-
// ENOTEMPTY (retained work) / ENOENT (already gone) — both fine.
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
// The run's worktrees are fully drained — drop its prune bookkeeping so
|
|
260
|
-
// `prunedRuns` never outgrows the live runs. (A later worktree of the same
|
|
261
|
-
// run simply prunes once more; the guard is an optimization, not a
|
|
262
|
-
// correctness gate.)
|
|
263
|
-
prunedRuns.delete(path.basename(runRoot));
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* Age-based GC of the worktrees root: removes `<root>/<runId>/<entry>`
|
|
267
|
-
* directories older than `maxAgeMs`, and empty stale run roots. Descends two
|
|
268
|
-
* levels only, never follows a symlink, and re-checks containment before each
|
|
269
|
-
* removal. Returns the paths removed; never throws.
|
|
270
|
-
*/
|
|
271
|
-
export async function sweepStaleWorktrees(opts = {}) {
|
|
272
|
-
const root = path.resolve(opts.root ?? worktreesRoot());
|
|
273
|
-
const removed = [];
|
|
274
|
-
if (path.basename(root) !== WORKTREES_DIR_NAME)
|
|
275
|
-
return removed;
|
|
276
|
-
const maxAgeMs = opts.maxAgeMs ?? STALE_WORKTREE_MAX_AGE_MS;
|
|
277
|
-
const now = opts.now ?? Date.now();
|
|
278
|
-
let runRoots;
|
|
279
|
-
try {
|
|
280
|
-
runRoots = await fsp.readdir(root, { withFileTypes: true, encoding: "utf8" });
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
return removed; // No root yet (or unreadable) — nothing to sweep.
|
|
284
|
-
}
|
|
285
|
-
for (const runRootEntry of runRoots) {
|
|
286
|
-
if (!runRootEntry.isDirectory())
|
|
287
|
-
continue;
|
|
288
|
-
const runRoot = path.join(root, runRootEntry.name);
|
|
289
|
-
if (!(await isWithinAsync(runRoot, root)))
|
|
290
|
-
continue;
|
|
291
|
-
let entries;
|
|
292
|
-
try {
|
|
293
|
-
entries = await fsp.readdir(runRoot, { withFileTypes: true, encoding: "utf8" });
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
let emptiedHere = false;
|
|
299
|
-
for (const entry of entries) {
|
|
300
|
-
if (!entry.isDirectory())
|
|
301
|
-
continue;
|
|
302
|
-
const candidate = path.join(runRoot, entry.name);
|
|
303
|
-
if (!(await isWithinAsync(candidate, root)))
|
|
304
|
-
continue;
|
|
305
|
-
if (now - (await lastActivityMs(candidate, entry.name)) < maxAgeMs)
|
|
306
|
-
continue;
|
|
307
|
-
try {
|
|
308
|
-
await fsp.rm(candidate, { recursive: true, force: true });
|
|
309
|
-
removed.push(candidate);
|
|
310
|
-
emptiedHere = true;
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
/* leave it for the next sweep */
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
if ((await fsp.readdir(runRoot)).length > 0)
|
|
318
|
-
continue;
|
|
319
|
-
if (!emptiedHere && now - (await lastActivityMs(runRoot, runRootEntry.name)) < maxAgeMs)
|
|
320
|
-
continue;
|
|
321
|
-
await fsp.rmdir(runRoot);
|
|
322
|
-
removed.push(runRoot);
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
/* leave it for the next sweep */
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
return removed;
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Newest evidence of activity for `p`: its mtime, or the timestamp embedded in
|
|
332
|
-
* a `.retained-<ts>[-n]` name when that is newer. Taking the max is the
|
|
333
|
-
* conservative direction — a sweep never deletes something that looks recent
|
|
334
|
-
* by either measure. An unstattable entry reports as "now" so it survives.
|
|
335
|
-
*/
|
|
336
|
-
async function lastActivityMs(p, name) {
|
|
337
|
-
let mtimeMs;
|
|
338
|
-
try {
|
|
339
|
-
mtimeMs = (await fsp.stat(p)).mtimeMs;
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
return Date.now();
|
|
343
|
-
}
|
|
344
|
-
const stamped = /\.retained-(\d{10,})(?:-\d+)?$/.exec(name);
|
|
345
|
-
return stamped ? Math.max(mtimeMs, Number(stamped[1])) : mtimeMs;
|
|
346
|
-
}
|
|
347
|
-
let sweepStarted = false;
|
|
348
|
-
/**
|
|
349
|
-
* Kick off the GC sweep at most once per process, fire-and-forget. Called from
|
|
350
|
-
* {@link createUnitWorktree} so the cost is paid by a run that is already
|
|
351
|
-
* doing worktree work, and never awaited so dispatch does not wait on it.
|
|
352
|
-
*/
|
|
353
|
-
function sweepStaleWorktreesOnce() {
|
|
354
|
-
if (sweepStarted)
|
|
355
|
-
return;
|
|
356
|
-
sweepStarted = true;
|
|
357
|
-
void sweepStaleWorktrees()
|
|
358
|
-
.then((removed) => {
|
|
359
|
-
if (removed.length === 0)
|
|
360
|
-
return;
|
|
361
|
-
const shown = removed.slice(0, 10).join(", ");
|
|
362
|
-
const rest = removed.length > 10 ? ` (+${removed.length - 10} more)` : "";
|
|
363
|
-
warn(`Workflow worktree GC: removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"} ` +
|
|
364
|
-
`older than ${STALE_WORKTREE_MAX_AGE_MS / (24 * 60 * 60 * 1000)}d under ${worktreesRoot()}: ${shown}${rest}`);
|
|
365
|
-
})
|
|
366
|
-
.catch(() => {
|
|
367
|
-
// GC is best-effort observability; a failed sweep never affects a run.
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
function message(err) {
|
|
371
|
-
return err instanceof Error ? err.message : String(err);
|
|
372
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
/**
|
|
5
|
-
* Freeze-time check of references into a child-workflow step's output: the
|
|
6
|
-
* first path segment after `steps.<child>.output` must name one of the child's
|
|
7
|
-
* declared `outputs:` (or `runId`/`status` when it declares none). Deeper
|
|
8
|
-
* segments resolve at run time.
|
|
9
|
-
*/
|
|
10
|
-
import { UsageError } from "../../core/errors.js";
|
|
11
|
-
import { formatReference, parseReference } from "../program/expressions.js";
|
|
12
|
-
/** The frozen target a step's root unit (or map template) dispatches, if any. */
|
|
13
|
-
function stepFrozenTarget(step) {
|
|
14
|
-
const root = step?.root;
|
|
15
|
-
if (!root)
|
|
16
|
-
return undefined;
|
|
17
|
-
const unit = root.kind === "map" ? root.template : root;
|
|
18
|
-
return unit.frozenTarget;
|
|
19
|
-
}
|
|
20
|
-
/** Every reference string a step's `inputs[]`, `map.over`, `route.input`, and reference-kind `inputBindings[].from` carry. */
|
|
21
|
-
function collectReferenceSites(step) {
|
|
22
|
-
const sites = [];
|
|
23
|
-
const push = (reference) => {
|
|
24
|
-
sites.push({ stepId: step.stepId, reference });
|
|
25
|
-
};
|
|
26
|
-
if (step.route)
|
|
27
|
-
push(step.route.input);
|
|
28
|
-
const root = step.root;
|
|
29
|
-
if (root) {
|
|
30
|
-
const unit = root.kind === "map" ? root.template : root;
|
|
31
|
-
if (root.kind === "map")
|
|
32
|
-
push(root.over);
|
|
33
|
-
for (const reference of unit.inputs ?? [])
|
|
34
|
-
push(reference);
|
|
35
|
-
const bindings = unit.frozenTarget.inputBindings ?? [];
|
|
36
|
-
for (const binding of bindings) {
|
|
37
|
-
if (binding.kind === "reference")
|
|
38
|
-
push(binding.from);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return sites;
|
|
42
|
-
}
|
|
43
|
-
/** The names a `child-workflow` target's own exported result carries at its first segment. */
|
|
44
|
-
function acceptedFirstSegments(target) {
|
|
45
|
-
const declared = target.frozenPlan.outputs;
|
|
46
|
-
return declared ? Object.keys(declared) : ["runId", "status"];
|
|
47
|
-
}
|
|
48
|
-
function exportsDescription(target) {
|
|
49
|
-
const declared = target.frozenPlan.outputs;
|
|
50
|
-
return declared ? `outputs: ${Object.keys(declared).join(", ")}` : "only {runId, status} — it declares no `outputs:`";
|
|
51
|
-
}
|
|
52
|
-
function checkSite(site, stepsById) {
|
|
53
|
-
const parsed = parseReference(site.reference);
|
|
54
|
-
if (!parsed.ok || parsed.expr.kind !== "stepOutput")
|
|
55
|
-
return;
|
|
56
|
-
const targetStep = stepsById.get(parsed.expr.stepId);
|
|
57
|
-
const frozenTarget = stepFrozenTarget(targetStep);
|
|
58
|
-
if (!frozenTarget || frozenTarget.kind !== "child-workflow")
|
|
59
|
-
return;
|
|
60
|
-
const first = parsed.expr.path[0];
|
|
61
|
-
if (first === undefined)
|
|
62
|
-
return; // bare steps.<child>.output — the whole exported object, always accepted.
|
|
63
|
-
const accepted = acceptedFirstSegments(frozenTarget);
|
|
64
|
-
if (typeof first === "string" && accepted.includes(first))
|
|
65
|
-
return;
|
|
66
|
-
throw new UsageError(`Workflow step ${site.stepId} reads "${formatReference(parsed.expr)}", but child workflow ${frozenTarget.ref} ` +
|
|
67
|
-
`exports ${exportsDescription(frozenTarget)}. Declare the output in the child's \`outputs:\` frontmatter, or ` +
|
|
68
|
-
`reference one of the names above.`, "COMPOSITION_INVALID", `Fix the reference to name one of: ${accepted.join(", ")} — or add the missing name to ${frozenTarget.ref}'s ` +
|
|
69
|
-
"own `outputs:` frontmatter.");
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Assert every reference into a `child-workflow`-targeted step's output
|
|
73
|
-
* names an output the child actually exports. Throws `UsageError`
|
|
74
|
-
* (`COMPOSITION_INVALID`) on the first violation found.
|
|
75
|
-
*/
|
|
76
|
-
export function assertChildOutputReferences(steps) {
|
|
77
|
-
const stepsById = new Map(steps.map((step) => [step.stepId, step]));
|
|
78
|
-
for (const step of steps) {
|
|
79
|
-
for (const site of collectReferenceSites(step)) {
|
|
80
|
-
checkSite(site, stepsById);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { UsageError } from "../../core/errors.js";
|
|
6
|
-
export function scriptExecutable(interpreter) {
|
|
7
|
-
if (interpreter === "bun" || interpreter === "bun-standalone")
|
|
8
|
-
return process.execPath;
|
|
9
|
-
if (interpreter === "kotlin")
|
|
10
|
-
return "kotlin";
|
|
11
|
-
return interpreter;
|
|
12
|
-
}
|
|
13
|
-
export function gitIdentity(unit, root) {
|
|
14
|
-
if (unit.isolation !== "worktree")
|
|
15
|
-
return {};
|
|
16
|
-
const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
17
|
-
const oid = result.status === 0 ? result.stdout.trim() : "";
|
|
18
|
-
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(oid)) {
|
|
19
|
-
throw new UsageError(`Worktree-isolated workflow root ${root} has no immutable Git HEAD OID.`, "WORKFLOW_SOURCE_INVALID");
|
|
20
|
-
}
|
|
21
|
-
return { gitCommitOid: oid };
|
|
22
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import { prepareScriptTarget } from "../../../tasks/prepare/prepare-script-target.js";
|
|
6
|
-
import { freezeEnvironment, resolveOwnedAsset } from "../environment.js";
|
|
7
|
-
import { gitIdentity, scriptExecutable } from "../identity.js";
|
|
8
|
-
import { freezeExecSpec, } from "../step-values.js";
|
|
9
|
-
export async function directScript(source, baseUnit, refInput, context) {
|
|
10
|
-
const owned = await resolveOwnedAsset(refInput, "script", context);
|
|
11
|
-
const captured = prepareScriptTarget({
|
|
12
|
-
ref: owned.ref,
|
|
13
|
-
file: owned.file,
|
|
14
|
-
bundleRoot: owned.root,
|
|
15
|
-
readFile: () => fs.readFileSync(owned.file),
|
|
16
|
-
});
|
|
17
|
-
return scriptResult(source, baseUnit, {
|
|
18
|
-
sourceRef: captured.ref,
|
|
19
|
-
interpreter: captured.interpreter,
|
|
20
|
-
extension: captured.extension,
|
|
21
|
-
bytesBase64: captured.bytesBase64,
|
|
22
|
-
byteLength: captured.byteLength,
|
|
23
|
-
sha256: captured.sha256,
|
|
24
|
-
cwdIdentity: captured.cwdIdentity,
|
|
25
|
-
}, context, []);
|
|
26
|
-
}
|
|
27
|
-
export function scriptResult(source, baseUnit, prepared, context, literals) {
|
|
28
|
-
const authoredExec = { command: [scriptExecutable(prepared.interpreter), "<frozen-script>"] };
|
|
29
|
-
const exec = freezeExecSpec(source, authoredExec, context);
|
|
30
|
-
const environment = Object.freeze([...literals, ...freezeEnvironment(source, authoredExec, context)]);
|
|
31
|
-
const target = Object.freeze({
|
|
32
|
-
kind: "script",
|
|
33
|
-
ref: prepared.sourceRef,
|
|
34
|
-
contentHash: prepared.sha256,
|
|
35
|
-
exec,
|
|
36
|
-
interpreter: prepared.interpreter,
|
|
37
|
-
extension: prepared.extension,
|
|
38
|
-
bytesBase64: prepared.bytesBase64,
|
|
39
|
-
byteLength: prepared.byteLength,
|
|
40
|
-
cwdIdentity: prepared.cwdIdentity,
|
|
41
|
-
materialization: "ephemeral-0700-delete",
|
|
42
|
-
...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
|
|
43
|
-
});
|
|
44
|
-
return {
|
|
45
|
-
target,
|
|
46
|
-
environment,
|
|
47
|
-
unit: { ...baseUnit, exec: authoredExec },
|
|
48
|
-
instructions: source.instructions ?? `Run script ${prepared.sourceRef}.`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
-
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
-
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { parseBundleRef } from "../../../core/asset/asset-ref.js";
|
|
7
|
-
import { UsageError } from "../../../core/errors.js";
|
|
8
|
-
import { prepareTaskV3Execution } from "../../../tasks/prepare/prepare.js";
|
|
9
|
-
import { parseTaskSource } from "../../../tasks/source/parse-task-source.js";
|
|
10
|
-
import { projectTaskSourceV4 } from "../../../tasks/source/project-v4.js";
|
|
11
|
-
import { workflowShellCommand } from "../../source-semantics.js";
|
|
12
|
-
import { freezeEnvironment, resolveOwnedAsset, workflowExecutionSource } from "../environment.js";
|
|
13
|
-
import { gitIdentity } from "../identity.js";
|
|
14
|
-
import { declaredParamNames, earlierStepIds, freezeExecSpec, } from "../step-values.js";
|
|
15
|
-
import { freezeTaskInputBindings } from "../task-bindings.js";
|
|
16
|
-
import { childWorkflowDispatch } from "./child-workflow.js";
|
|
17
|
-
import { commandResult } from "./command.js";
|
|
18
|
-
import { scriptResult } from "./script.js";
|
|
19
|
-
/** The rejection for a `with:` on a task that declares no inputs (or does not resolve). */
|
|
20
|
-
function noDeclaredInputsError(stepId, ref) {
|
|
21
|
-
return new UsageError(`Workflow step ${stepId} cannot pass with: to task target ${ref}; ${ref} declares no inputs.`, "COMPOSITION_INVALID");
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Resolve and parse the composed task. An authored `with:` whose target does
|
|
25
|
-
* not even resolve cannot be proven a valid binding surface, so it is refused
|
|
26
|
-
* the same no-declared-inputs way; a genuine defect in the resolved task's own
|
|
27
|
-
* source propagates unchanged.
|
|
28
|
-
*/
|
|
29
|
-
async function resolveTaskForComposition(source, refInput, context) {
|
|
30
|
-
let owned;
|
|
31
|
-
let yaml;
|
|
32
|
-
try {
|
|
33
|
-
owned = await resolveOwnedAsset(refInput, "task", context);
|
|
34
|
-
yaml = fs.readFileSync(owned.file, "utf8");
|
|
35
|
-
}
|
|
36
|
-
catch (cause) {
|
|
37
|
-
if (source.with !== undefined)
|
|
38
|
-
throw noDeclaredInputsError(source.id, refInput);
|
|
39
|
-
throw cause;
|
|
40
|
-
}
|
|
41
|
-
const parsed = parseTaskSource({ yaml, filePath: owned.file, workspaceRoot: owned.root });
|
|
42
|
-
return { owned, task: projectTaskSourceV4(parsed.v4), contract: parsed.v4.inputs };
|
|
43
|
-
}
|
|
44
|
-
export async function taskDispatch(source, baseUnit, refInput, context) {
|
|
45
|
-
const { owned, task, contract } = await resolveTaskForComposition(source, refInput, context);
|
|
46
|
-
// Any authored `with:` (even `{}`) on a task that declares no inputs is COMPOSITION_INVALID.
|
|
47
|
-
if (source.with !== undefined && contract === undefined) {
|
|
48
|
-
throw noDeclaredInputsError(source.id, refInput);
|
|
49
|
-
}
|
|
50
|
-
// This step's own `with:` against this task's own declared inputs (none → no bindings).
|
|
51
|
-
const bindings = freezeTaskInputBindings({
|
|
52
|
-
stepId: source.id,
|
|
53
|
-
targetRef: refInput,
|
|
54
|
-
with: source.with,
|
|
55
|
-
contract: contract ?? {},
|
|
56
|
-
earlierStepIds: earlierStepIds(context.plan, source.id),
|
|
57
|
-
declaredParamNames: declaredParamNames(context.plan),
|
|
58
|
-
});
|
|
59
|
-
const prepared = await prepareTaskV3Execution(task, {
|
|
60
|
-
taskId: parseBundleRef(owned.ref).conceptId.slice("tasks/".length),
|
|
61
|
-
taskRef: owned.ref,
|
|
62
|
-
bundleName: owned.bundle,
|
|
63
|
-
bundleRoot: owned.root,
|
|
64
|
-
config: context.config,
|
|
65
|
-
commandSourceLoader: (ref, kind) => workflowExecutionSource(ref, kind, context),
|
|
66
|
-
resolveAsset: async ({ ref, type }) => {
|
|
67
|
-
const target = await resolveOwnedAsset(ref, type, context);
|
|
68
|
-
return { file: target.file, bundleRoot: target.root };
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
if (prepared.kind === "workflow") {
|
|
72
|
-
// A task whose own target is a workflow composes it as a child. This
|
|
73
|
-
// task's effective inputs, already classified against its own contract,
|
|
74
|
-
// are re-bound against the child's `params:` — never round-tripped through
|
|
75
|
-
// the `with:` grammar, which would reinterpret a literal shaped like
|
|
76
|
-
// `{from: ...}` as a reference.
|
|
77
|
-
return childWorkflowDispatch({
|
|
78
|
-
source,
|
|
79
|
-
baseUnit,
|
|
80
|
-
childRefInput: prepared.ref,
|
|
81
|
-
context,
|
|
82
|
-
via: "task",
|
|
83
|
-
taskRef: prepared.taskRef,
|
|
84
|
-
authoredInputs: { kind: "bindings", value: bindings },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const taskLiterals = Object.entries(prepared.environment).map(([name, value]) => Object.freeze({ kind: "literal", name, value }));
|
|
88
|
-
if (prepared.kind === "command") {
|
|
89
|
-
return withInputBindings(commandResult(source, baseUnit, prepared.invocation, context, taskLiterals), bindings);
|
|
90
|
-
}
|
|
91
|
-
if (prepared.kind === "shell") {
|
|
92
|
-
const authoredExec = {
|
|
93
|
-
command: workflowShellCommand(prepared.shell, prepared.command),
|
|
94
|
-
...(prepared.cwdIdentity.realCwd !== prepared.cwdIdentity.realRoot
|
|
95
|
-
? { cwd: path.relative(prepared.cwdIdentity.realRoot, prepared.cwdIdentity.realCwd) }
|
|
96
|
-
: {}),
|
|
97
|
-
};
|
|
98
|
-
const exec = freezeExecSpec(source, authoredExec, context);
|
|
99
|
-
const environment = Object.freeze([...taskLiterals, ...freezeEnvironment(source, authoredExec, context)]);
|
|
100
|
-
const target = Object.freeze({
|
|
101
|
-
kind: "shell",
|
|
102
|
-
contentHash: "",
|
|
103
|
-
exec,
|
|
104
|
-
cwdIdentity: prepared.cwdIdentity,
|
|
105
|
-
...gitIdentity(baseUnit, prepared.cwdIdentity.realRoot),
|
|
106
|
-
});
|
|
107
|
-
return withInputBindings({
|
|
108
|
-
target,
|
|
109
|
-
environment,
|
|
110
|
-
unit: { ...baseUnit, exec: authoredExec },
|
|
111
|
-
instructions: source.instructions ?? `Run task ${owned.ref}.`,
|
|
112
|
-
}, bindings);
|
|
113
|
-
}
|
|
114
|
-
return withInputBindings(scriptResult(source, baseUnit, prepared, context, taskLiterals), bindings);
|
|
115
|
-
}
|
|
116
|
-
/** Attach the frozen inputBindings to whichever target shape taskDispatch produced. Absent, never [], when empty. */
|
|
117
|
-
function withInputBindings(resolved, bindings) {
|
|
118
|
-
if (bindings.length === 0)
|
|
119
|
-
return resolved;
|
|
120
|
-
return {
|
|
121
|
-
...resolved,
|
|
122
|
-
target: Object.freeze({ ...resolved.target, inputBindings: bindings }),
|
|
123
|
-
};
|
|
124
|
-
}
|