akm-cli 0.10.261012-beta → 0.10.261013-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.
Files changed (146) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/SECURITY.md +2 -2
  3. package/STABILITY.md +2 -2
  4. package/dist/assets/hints/cli-hints-full.md +6 -7
  5. package/dist/assets/hints/cli-hints-short.md +3 -4
  6. package/dist/commands/command/execution-source-loader.js +10 -14
  7. package/dist/commands/env/env-binding.js +47 -58
  8. package/dist/commands/env/env-cli.js +20 -22
  9. package/dist/commands/env/env.js +10 -58
  10. package/dist/commands/env/secret-cli.js +9 -27
  11. package/dist/commands/env/secret.js +3 -10
  12. package/dist/commands/health/scheduler-binary.js +1 -1
  13. package/dist/commands/improve/distill.js +0 -2
  14. package/dist/commands/improve/eligibility.js +0 -2
  15. package/dist/commands/lint/env-key-rules.js +7 -6
  16. package/dist/commands/lint/index.js +1 -1
  17. package/dist/commands/proposal/validators/proposal-validators.js +1 -4
  18. package/dist/commands/read/show.js +4 -25
  19. package/dist/commands/sources/add-cli.js +8 -63
  20. package/dist/commands/sources/bundle-cli.js +8 -9
  21. package/dist/commands/sources/installed-stashes.js +117 -61
  22. package/dist/commands/sources/schema-repair.js +0 -2
  23. package/dist/commands/sources/source-clone.js +31 -7
  24. package/dist/commands/tasks/explain.js +1 -1
  25. package/dist/commands/tasks/tasks.js +38 -56
  26. package/dist/commands/ui/redact.js +4 -4
  27. package/dist/commands/ui/workspace-actions.js +1 -1
  28. package/dist/commands/workflow/plan.js +47 -8
  29. package/dist/commands/workflow-cli.js +37 -41
  30. package/dist/core/adapter/adapters/akm-adapter.js +3 -3
  31. package/dist/core/adapter/adapters/akm-lint.js +3 -4
  32. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
  34. package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
  35. package/dist/core/asset/resolve-ref.js +5 -0
  36. package/dist/core/bundle-rename.js +12 -4
  37. package/dist/core/common.js +4 -52
  38. package/dist/core/config/config-walker.js +1 -1
  39. package/dist/core/config/config.js +55 -18
  40. package/dist/core/config/schema/primitives.js +12 -6
  41. package/dist/core/config/schema/sources-bundles.js +2 -2
  42. package/dist/core/env-secret-ref.js +84 -73
  43. package/dist/core/errors.js +1 -1
  44. package/dist/core/paths.js +1 -6
  45. package/dist/core/redaction.js +16 -3
  46. package/dist/core/ref-token.js +82 -0
  47. package/dist/core/secret-value.js +14 -0
  48. package/dist/core/write-source.js +20 -4
  49. package/dist/indexer/indexer.js +27 -36
  50. package/dist/indexer/installations.js +1 -8
  51. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  52. package/dist/indexer/materialize-embeddings.js +1 -1
  53. package/dist/indexer/scan/drain-dir.js +3 -3
  54. package/dist/indexer/search/search-source.js +4 -2
  55. package/dist/indexer/walk/path-resolver.js +13 -68
  56. package/dist/indexer/walk/walker.js +18 -22
  57. package/dist/integrations/agent/engine-resolution.js +9 -7
  58. package/dist/integrations/agent/runner-dispatch.js +4 -5
  59. package/dist/integrations/agent/runner.js +2 -0
  60. package/dist/llm/client.js +1 -1
  61. package/dist/llm/embedders/remote.js +4 -3
  62. package/dist/output/renderers.js +3 -4
  63. package/dist/output/shapes/helpers.js +0 -2
  64. package/dist/output/text/command-format.js +12 -2
  65. package/dist/output/text/show-directives.js +16 -28
  66. package/dist/output/text/workflow-format.js +37 -7
  67. package/dist/scripts/akm-migrate-node.js +45865 -44692
  68. package/dist/scripts/akm-migrate.js +45804 -44630
  69. package/dist/setup/steps/tasks.js +1 -2
  70. package/dist/sources/providers/provider-utils.js +12 -0
  71. package/dist/sources/resolve.js +12 -16
  72. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  73. package/dist/sources/snapshot-fetchers/x.js +1 -1
  74. package/dist/storage/repositories/proposals-repository.js +2 -2
  75. package/dist/storage/repositories/task-history-repository.js +2 -2
  76. package/dist/storage/repositories/workflow-runs-repository.js +38 -116
  77. package/dist/tasks/embedded.js +1 -1
  78. package/dist/tasks/prepare/prepare-support.js +3 -2
  79. package/dist/tasks/prepare/prepare.js +3 -4
  80. package/dist/tasks/prepare/script-capture.js +8 -22
  81. package/dist/tasks/run/load-task.js +46 -14
  82. package/dist/tasks/run/run-native-task.js +0 -8
  83. package/dist/tasks/run/run-workflow-task.js +2 -2
  84. package/dist/tasks/scheduler-sync.js +0 -1
  85. package/dist/tasks/source/bounded-document.js +0 -41
  86. package/dist/tasks/source/parse-task-source.js +0 -1
  87. package/dist/tasks/source/task-source-v4.js +1 -4
  88. package/dist/workflows/compile.js +1 -15
  89. package/dist/workflows/exec/child-workflow.js +1 -2
  90. package/dist/workflows/exec/dispatch-redaction.js +24 -6
  91. package/dist/workflows/exec/environment.js +11 -74
  92. package/dist/workflows/exec/exec-unit.js +31 -23
  93. package/dist/workflows/exec/frozen-judge.js +17 -3
  94. package/dist/workflows/exec/native-executor.js +70 -26
  95. package/dist/workflows/exec/run-workflow.js +31 -18
  96. package/dist/workflows/exec/step-work.js +4 -3
  97. package/dist/workflows/exec/unit-dispatch.js +1 -8
  98. package/dist/workflows/freeze/environment.js +14 -119
  99. package/dist/workflows/freeze/freeze.js +0 -1
  100. package/dist/workflows/freeze/step-values.js +1 -2
  101. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  102. package/dist/workflows/freeze/targets/command.js +6 -9
  103. package/dist/workflows/freeze/targets/shell.js +0 -3
  104. package/dist/workflows/parser.js +6 -25
  105. package/dist/workflows/plan.js +6 -2
  106. package/dist/workflows/renderer.js +77 -4
  107. package/dist/workflows/runtime/run-plan.js +37 -23
  108. package/dist/workflows/runtime/runs.js +41 -84
  109. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  110. package/dist/workflows/source-files.js +7 -19
  111. package/docs/architecture/0.10-process-rules.md +6 -6
  112. package/docs/architecture/architecture.md +6 -6
  113. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
  114. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
  115. package/docs/architecture/internals/storage-locations.md +4 -4
  116. package/docs/architecture/persisted-data-compat.md +3 -2
  117. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
  118. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
  119. package/docs/architecture/testing/manual-testing-checklist.md +46 -78
  120. package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
  121. package/docs/architecture/testing/testing-workflow.md +9 -11
  122. package/docs/architecture/workflow-engine.md +41 -27
  123. package/docs/guides/author-bundles.md +14 -17
  124. package/docs/guides/author-workflows.md +43 -9
  125. package/docs/guides/bundles.md +16 -11
  126. package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
  127. package/docs/guides/run-workflows.md +25 -13
  128. package/docs/guides/scheduling.md +1 -1
  129. package/docs/maintainers/README.md +2 -3
  130. package/docs/maintainers/release-checklist.md +73 -126
  131. package/docs/migration/v0.9-to-v0.10.md +5 -1
  132. package/docs/reference/cli.md +124 -112
  133. package/docs/reference/configuration.md +16 -9
  134. package/docs/reference/data-and-telemetry.md +1 -1
  135. package/docs/reference/env-and-secrets.md +66 -15
  136. package/docs/reference/registry.md +6 -11
  137. package/docs/reference/tasks.md +11 -3
  138. package/docs/reference/ui-api.md +3 -3
  139. package/docs/reference/workflow-schema.md +109 -36
  140. package/package.json +1 -1
  141. package/schemas/akm-task.json +2 -4
  142. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  143. package/dist/core/activation-policy.js +0 -67
  144. package/dist/execution/directory-identity.js +0 -78
  145. package/dist/workflows/authoring/scope-key.js +0 -79
  146. package/dist/workflows/source-semantics.js +0 -110
@@ -1,7 +1,6 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
5
4
  import { freezeEnvironment } from "../environment.js";
6
5
  import { freezeExecSpec, } from "../step-values.js";
7
6
  export function directShell(source, baseUnit, context) {
@@ -9,13 +8,11 @@ export function directShell(source, baseUnit, context) {
9
8
  if (!authoredExec)
10
9
  throw new Error(`workflow shell step ${source.id} has no argv`);
11
10
  const exec = freezeExecSpec(source, authoredExec, context);
12
- const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath, authoredExec.cwd);
13
11
  const environment = Object.freeze(freezeEnvironment(source, authoredExec, context));
14
12
  const target = Object.freeze({
15
13
  kind: "shell",
16
14
  contentHash: "",
17
15
  exec,
18
- cwdIdentity,
19
16
  });
20
17
  return {
21
18
  target,
@@ -35,7 +35,7 @@ import { LineCounter, parseDocument } from "yaml";
35
35
  import { bundleRefToString, parseBundleRef } from "../core/asset/asset-ref.js";
36
36
  import { parseFrontmatterBlock } from "../core/asset/frontmatter.js";
37
37
  import { parseMarkdownToc } from "../core/asset/markdown.js";
38
- import { isContainedRelativePath, isRecord } from "../core/common.js";
38
+ import { isRecord } from "../core/common.js";
39
39
  import { formatExtraParamsIssue, validateExtraParams } from "../core/extra-params.js";
40
40
  import { checkJsonSchemaDefinition, JSON_SCHEMA_SUBSET_SUPPORTED_KEYWORDS } from "../core/json-schema.js";
41
41
  import { INPUT_NAME_PATTERN } from "../execution/input-contract.js";
@@ -182,7 +182,6 @@ export function parseWorkflow(markdown, source) {
182
182
  const ctx = {
183
183
  filePath: path,
184
184
  errors,
185
- ...(source.validateExecCwd ? { validateExecCwd: source.validateExecCwd } : {}),
186
185
  lineAt,
187
186
  lineAtOffset: (offset) => Math.max(1, lineCounter.linePos(offset).line + lineOffset),
188
187
  nodeAt: (p) => (p.length === 0 ? doc.contents : doc.getIn(p, true)),
@@ -829,36 +828,18 @@ function parseExecCommand(ctx, raw, path, stepLabel) {
829
828
  return argv;
830
829
  }
831
830
  /**
832
- * The optional relative `cwd:`. Rejected here (statically) for absolute paths
833
- * and `..` segments; containment against the resolved base directory is
834
- * re-checked at dispatch, so a symlinked subdirectory cannot escape either.
831
+ * The optional `cwd:`: a non-empty string, relative to the workflow's bundle root or absolute, and
832
+ * free to carry `${ref}` tokens. Nothing else is checked here; the unit's working directory is
833
+ * resolved at dispatch.
835
834
  */
836
835
  function parseExecCwd(ctx, raw, path, stepLabel) {
837
836
  if (raw === undefined)
838
837
  return undefined;
839
838
  if (typeof raw !== "string" || raw.trim() === "") {
840
- ctx.err(path, `${stepLabel} "exec.cwd" must be a non-empty relative path inside the unit's working directory.`);
839
+ ctx.err(path, `${stepLabel} "exec.cwd" must be a non-empty string.`);
841
840
  return undefined;
842
841
  }
843
- const value = raw.trim();
844
- if (!isContainedRelativePath(value)) {
845
- ctx.err(path, `${stepLabel} "exec.cwd" (${JSON.stringify(value)}) must be a RELATIVE path inside the unit's working ` +
846
- `directory — absolute paths, Windows drive letters, "~", and ".." segments are rejected.`);
847
- return undefined;
848
- }
849
- if (ctx.validateExecCwd) {
850
- const semantic = ctx.validateExecCwd(value);
851
- if (!semantic.ok) {
852
- ctx.errors.push({
853
- code: semantic.code,
854
- line: ctx.lineAt(path),
855
- message: semantic.message,
856
- });
857
- return undefined;
858
- }
859
- return semantic.value;
860
- }
861
- return value;
842
+ return raw.trim();
862
843
  }
863
844
  function parseMap(ctx, raw, path, stepLabel) {
864
845
  if (!isRecord(raw)) {
@@ -1,5 +1,9 @@
1
1
  // This Source Code Form is subject to the terms of the Mozilla Public
2
2
  // License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /** Stored plans record this; 0.9.15 and 0.9.16 wrote 5, which still decodes (the 0.9.15 floor, #1091). */
5
- export const WORKFLOW_PLAN_VERSION = 6;
4
+ /**
5
+ * Stored plans record this; 0.9.15 and 0.9.16 wrote 5 and early 0.10 wrote 6,
6
+ * which still decode (the 0.9.15 floor, #1091). 7 stopped freezing locations:
7
+ * the plan names the refs the engine dereferences, never a directory or file.
8
+ */
9
+ export const WORKFLOW_PLAN_VERSION = 7;
@@ -9,7 +9,9 @@
9
9
  * declares one.
10
10
  */
11
11
  import { displayRef } from "../core/asset/resolve-ref.js";
12
+ import { isInstalledBundle, resolveRefOwner } from "../core/env-secret-ref.js";
12
13
  import { UsageError } from "../core/errors.js";
14
+ import { substituteRefTokens } from "../core/ref-token.js";
13
15
  import { compileWorkflowSource, workflowStepInstructions } from "./compile.js";
14
16
  function shellQuote(value) {
15
17
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -25,7 +27,7 @@ function deriveName(ctx) {
25
27
  return ext > 0 ? ctx.relPath.slice(0, ext) : ctx.relPath;
26
28
  }
27
29
  function loadPlan(ctx) {
28
- const result = compileWorkflowSource(ctx.content(), { path: ctx.relPath, workspaceRoot: ctx.stashRoot });
30
+ const result = compileWorkflowSource(ctx.content(), { path: ctx.relPath });
29
31
  if (result.ok)
30
32
  return result.plan;
31
33
  const summary = result.errors.map((e) => `${e.path}:${e.line} — ${e.message}`).join("\n");
@@ -70,10 +72,14 @@ function projectParameters(plan) {
70
72
  * It is far smaller than the step `instructions` this same projection
71
73
  * already carries whole.
72
74
  */
73
- function summarizeStepOrchestration(step, defaults) {
75
+ export function summarizeStepOrchestration(step, defaults) {
74
76
  const spec = step.spec;
75
77
  const unit = spec?.unit;
76
- const exec = spec?.exec;
78
+ // A stored plan carries no authored spec: read the same fields off its frozen dispatch.
79
+ const frozen = spec ? undefined : frozenDispatch(step);
80
+ const exec = spec ? spec.exec : frozen?.exec;
81
+ const env = unit?.env ?? frozen?.env;
82
+ const workflow = spec ? spec.workflow : frozen?.workflow;
77
83
  const engine = exec ? undefined : (unit?.engine ?? defaults?.engine);
78
84
  const model = exec ? undefined : (unit?.model ?? defaults?.model);
79
85
  const timeoutMs = unit?.timeoutMs !== undefined ? unit.timeoutMs : defaults?.timeoutMs;
@@ -93,10 +99,77 @@ function summarizeStepOrchestration(step, defaults) {
93
99
  }
94
100
  : {}),
95
101
  ...(unit?.output !== undefined || step.outputSchema !== undefined ? { hasSchema: true } : {}),
96
- ...(unit?.env !== undefined ? { env: [...unit.env] } : {}),
102
+ ...(env !== undefined ? { env: [...env] } : {}),
103
+ ...(workflow !== undefined ? { workflow } : {}),
97
104
  };
98
105
  return Object.keys(summary).length > 0 ? summary : undefined;
99
106
  }
107
+ /** The exec argv, env refs and child workflow a frozen step dispatches. */
108
+ function frozenDispatch(step) {
109
+ const root = step.root;
110
+ const unit = root?.kind === "map" ? root.template : root;
111
+ if (!unit)
112
+ return {};
113
+ const target = unit.frozenTarget;
114
+ const env = unit.environment.flatMap((binding) => (binding.kind === "env-ref" ? [binding.ref] : []));
115
+ return {
116
+ ...(target.kind === "shell"
117
+ ? {
118
+ exec: {
119
+ command: [...target.exec.command],
120
+ ...(target.exec.cwd !== undefined ? { cwd: target.exec.cwd } : {}),
121
+ ...(target.exec.passEnv ? { passEnv: [...target.exec.passEnv] } : {}),
122
+ },
123
+ }
124
+ : {}),
125
+ ...(env.length > 0 ? { env } : {}),
126
+ ...(target.kind === "child-workflow" ? { workflow: target.ref } : {}),
127
+ };
128
+ }
129
+ /**
130
+ * Add the bundle-qualified ref every `${ref}` token in the argv or cwd, every
131
+ * `env:` entry and the child `workflow:` resolves to right now, through the
132
+ * one owner walk (`resolveRefOwner`). A ref no bundle owns lands in
133
+ * `unresolved` instead; callers decide whether that is an error. No secret
134
+ * is read and no path is returned.
135
+ */
136
+ export function resolveOrchestrationRefs(summary) {
137
+ const refs = [];
138
+ const unresolved = [];
139
+ const note = (ref, where) => {
140
+ let owner;
141
+ try {
142
+ owner = resolveRefOwner(ref);
143
+ }
144
+ catch {
145
+ owner = undefined;
146
+ }
147
+ if (owner)
148
+ refs.push({ ref, resolved: owner.ref, in: where });
149
+ else
150
+ unresolved.push({ ref, in: where });
151
+ };
152
+ const tokens = (values, where) => {
153
+ const seen = new Set();
154
+ substituteRefTokens(Object.fromEntries(values.map((value, index) => [String(index), value])), (ref) => {
155
+ if (!seen.has(ref)) {
156
+ seen.add(ref);
157
+ note(ref, where);
158
+ }
159
+ return { kind: "path", path: "" };
160
+ }, isInstalledBundle);
161
+ };
162
+ if (summary.exec) {
163
+ tokens(summary.exec.command, "argv");
164
+ if (summary.exec.cwd !== undefined)
165
+ tokens([summary.exec.cwd], "cwd");
166
+ }
167
+ for (const ref of summary.env ?? [])
168
+ note(ref, "env");
169
+ if (summary.workflow !== undefined)
170
+ note(summary.workflow, "workflow");
171
+ return { ...summary, refs, ...(unresolved.length > 0 ? { unresolved } : {}) };
172
+ }
100
173
  function projectStepDefinitions(plan) {
101
174
  return plan.steps.map((step, sequenceIndex) => {
102
175
  const orchestration = summarizeStepOrchestration(step, plan.defaults);
@@ -7,15 +7,20 @@
7
7
  *
8
8
  * {@link decodeWorkflowPlan} accepts every plan shape a release at or above
9
9
  * the 0.9.15 floor has stored — irVersion 5 (with its `sourceReadSet` and
10
- * host-identity fields, which 0.9.15 and 0.9.16 wrote) as well as the current
11
- * one — and keeps keys it does not know rather than
12
- * refusing them. It rejects only a plan it cannot run: no steps, a unit with
13
- * no frozen target, a request that does not decode. `plan_ir_version` and
14
- * `plan_hash` are informational columns: neither gates execution. A plan that
15
- * cannot be decoded comes back from {@link readRunPlan} as a `problem`
10
+ * host-identity fields, which 0.9.15 and 0.9.16 wrote) and irVersion 6 (with
11
+ * its frozen `cwdIdentity` and env owner paths) as well as the current one —
12
+ * and keeps keys it does not know rather than refusing them. A stored target
13
+ * is kept exactly as stored, location fields included: the unit `input_hash`
14
+ * and the child invocation key derive from the whole target, so dropping them
15
+ * would make an interrupted run publish a second child after an upgrade.
16
+ * Dispatch finds the bundle and the env asset by ref and never reads them. It
17
+ * rejects only a plan it cannot run: no steps, a unit with no frozen target, a
18
+ * request that does not decode. `plan_hash` is an informational column. A plan
19
+ * that cannot be decoded comes back from {@link readRunPlan} as a `problem`
16
20
  * sentence and the caller decides the remedy (the engine abandons such a run
17
- * instead of throwing) — except for a plan a NEWER akm froze, which is `newer`
18
- * and whose problem names upgrading akm as the remedy.
21
+ * instead of throwing) — except for a plan a NEWER akm froze (`plan_ir_version`
22
+ * above this release's), which is refused unread as `newer` with a problem that
23
+ * names upgrading akm as the remedy.
19
24
  */
20
25
  import { UsageError } from "../../core/errors.js";
21
26
  import { warnOnce } from "../../core/warn.js";
@@ -26,6 +31,14 @@ export function readRunPlan(row) {
26
31
  if (!row.plan_json)
27
32
  return { ok: false, newer: false, problem: `Workflow run ${row.id} has no frozen workflow plan.` };
28
33
  const version = row.plan_ir_version ?? null;
34
+ if (version !== null && version > WORKFLOW_PLAN_VERSION) {
35
+ return {
36
+ ok: false,
37
+ newer: true,
38
+ problem: `Workflow run ${row.id} was frozen by a newer akm (plan irVersion ${version}) and this akm ` +
39
+ `(irVersion ${WORKFLOW_PLAN_VERSION}) will not run it. Upgrade akm to continue this run.`,
40
+ };
41
+ }
29
42
  try {
30
43
  const plan = decodeWorkflowPlan(JSON.parse(row.plan_json), {
31
44
  ...(row.workflow_ref ? { workflowRef: row.workflow_ref } : {}),
@@ -38,14 +51,6 @@ export function readRunPlan(row) {
38
51
  }
39
52
  catch (cause) {
40
53
  const detail = cause instanceof Error ? cause.message : String(cause);
41
- if (version !== null && version > WORKFLOW_PLAN_VERSION) {
42
- return {
43
- ok: false,
44
- newer: true,
45
- problem: `Workflow run ${row.id} was frozen by a newer akm (plan irVersion ${version}) and this akm cannot decode it: ` +
46
- `${detail}. Upgrade akm to continue this run.`,
47
- };
48
- }
49
54
  const frozenAs = version === null ? "" : ` (frozen as plan irVersion ${version})`;
50
55
  return {
51
56
  ok: false,
@@ -166,7 +171,7 @@ function decodeTarget(value, label) {
166
171
  kind: "shell",
167
172
  contentHash: string(target.contentHash, `${label} contentHash`),
168
173
  exec: decodeExec(target.exec, label),
169
- cwdIdentity: record(target.cwdIdentity, `${label} cwdIdentity`),
174
+ ...legacyField(target, "cwdIdentity"),
170
175
  ...optional,
171
176
  };
172
177
  case "child-workflow":
@@ -203,12 +208,15 @@ function decodeCommandTarget(target, label) {
203
208
  request,
204
209
  runner,
205
210
  ...(concurrency !== undefined ? { concurrency } : {}),
206
- ...(target.cwdIdentity && typeof target.cwdIdentity === "object"
207
- ? { cwdIdentity: target.cwdIdentity }
208
- : {}),
211
+ ...legacyField(target, "cwdIdentity"),
209
212
  ...(inputBindings ? { inputBindings } : {}),
210
213
  };
211
214
  }
215
+ /** A location field a stored target carries, kept as stored (see the module doc). */
216
+ function legacyField(source, key) {
217
+ const value = source[key];
218
+ return value && typeof value === "object" ? { [key]: value } : {};
219
+ }
212
220
  function decodeExec(value, label) {
213
221
  const exec = record(value, `${label} exec`);
214
222
  const command = exec.command;
@@ -246,14 +254,20 @@ function decodeEnvironmentBinding(value, label) {
246
254
  return { kind: "pass-through", name: string(binding.name, label) };
247
255
  if (binding.kind !== "env-ref")
248
256
  fail(`${label} has an unsupported kind`);
257
+ // Plans before irVersion 7 also carry the env file's owner, key names and secret names: kept as stored,
258
+ // never read (dispatch reads the env asset by ref).
249
259
  const strings = (items) => Array.isArray(items) ? items.filter((item) => typeof item === "string") : [];
250
260
  return {
251
261
  kind: "env-ref",
252
262
  ref: string(binding.ref, `${label} ref`),
253
- owner: record(binding.owner, `${label} owner`),
254
- keys: strings(binding.keys),
255
- secretNames: strings(binding.secretNames),
256
263
  precedence: typeof binding.precedence === "number" ? binding.precedence : 0,
264
+ ...(binding.owner && typeof binding.owner === "object"
265
+ ? {
266
+ owner: binding.owner,
267
+ keys: strings(binding.keys),
268
+ secretNames: strings(binding.secretNames),
269
+ }
270
+ : {}),
257
271
  };
258
272
  }
259
273
  function positiveInteger(value) {
@@ -9,17 +9,17 @@ import { appendEvent } from "../../core/events.js";
9
9
  import { warn } from "../../core/warn.js";
10
10
  import { insertEventOnce } from "../../storage/repositories/events-repository.js";
11
11
  import { withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
12
- import { getCurrentWorkflowScopeKey } from "../authoring/scope-key.js";
13
- import { frozenSummaryJudge } from "../exec/frozen-judge.js";
12
+ import { frozenSummaryJudge, resolveJudgeCwd } from "../exec/frozen-judge.js";
14
13
  import { detectSecretShapedParams } from "../exec/param-secrets.js";
15
14
  import { freezeWorkflow } from "../freeze/freeze.js";
16
15
  import { applyWorkflowParamDefaults, materializeWorkflowParameterFlags, validateWorkflowParams, } from "../ir/params.js";
17
16
  import { canonicalPlanJson, computePlanHash } from "../ir/plan-hash.js";
17
+ import { resolveOrchestrationRefs, summarizeStepOrchestration } from "../renderer.js";
18
18
  import { clip, WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
19
19
  import { validateStepSummary } from "../validate-summary.js";
20
20
  import { resolveAgentIdentity } from "./agent-identity.js";
21
- import { frozenStepRows, readRunPlan } from "./run-plan.js";
22
- import { canonicalizeWorkflowRefInput, loadWorkflowAsset, resolveWorkflowEntryId } from "./workflow-asset-loader.js";
21
+ import { decodeWorkflowPlan, frozenStepRows, readRunPlan } from "./run-plan.js";
22
+ import { canonicalizeWorkflowRefInput, loadWorkflowAsset } from "./workflow-asset-loader.js";
23
23
  /**
24
24
  * Membership test for the journaled `runner` column, which is an untyped string.
25
25
  * The `Record<WorkflowRuntimeKind, …>` is exhaustiveness-checked, so a new runtime
@@ -98,38 +98,21 @@ export async function startWorkflowRun(ref, params = {}, options) {
98
98
  return withWorkflowRunsRepo(async (repo) => {
99
99
  const now = new Date().toISOString();
100
100
  const runId = randomUUID();
101
- const scopeKey = getCurrentWorkflowScopeKey();
102
101
  const currentStepId = plan.steps[0]?.stepId ?? null;
103
- const workflowEntryId = resolveWorkflowEntryId(asset.sourcePath, asset.ref, asset.adapterId);
104
102
  // Capture the invoking harness/session identity for this run. Explicit options
105
103
  // win; otherwise fall back to best-effort environment detection. This is
106
104
  // identity-only — no background thread or timer is started here.
107
105
  const detected = resolveAgentIdentity();
108
106
  const agentHarness = options?.agentHarness !== undefined ? options.agentHarness : detected.harness;
109
107
  const agentSessionId = options?.agentSessionId !== undefined ? options.agentSessionId : detected.sessionId;
110
- // Concurrency guard (#485): if an active run already exists in this
111
- // (workflow_ref, scope_key) pair, refuse to create a parallel run unless
112
- // `force: true` is set. Previously every call inserted unconditionally,
113
- // so two terminals starting the same workflow could leave two runs racing.
114
- // The active-alias query and all inserts share one immediate transaction.
115
- // #942: an active run of this ref in a different scope (e.g. a scheduled
116
- // task's cwd vs. a shell) is warned about once; the uniqueness guard stays
117
- // scope-local. The query excludes the caller's own scope in SQL.
118
- const crossScopeActive = repo.findActiveRunOutsideScope(workflowRefs, scopeKey);
119
- const crossScopeWarning = crossScopeActive
120
- ? `Workflow ${asset.ref} already has an active run in another scope ` +
121
- `(id ${crossScopeActive.id}, started ${crossScopeActive.created_at}, scope ${crossScopeActive.scope_key ?? "unknown"}); ` +
122
- `starting a separate run here. Resume it from anywhere with "akm workflow run ${crossScopeActive.id}" ` +
123
- `or free it with "akm workflow abandon ${crossScopeActive.id}".`
124
- : undefined;
108
+ // Starting a ref always starts a run. Other active runs of the same ref
109
+ // are not an error: name them once, after the start succeeds, so the
110
+ // operator can continue or abandon the ones they no longer want.
111
+ const otherActive = repo.listRuns({ workflowRefs, activeOnly: true });
125
112
  repo.publishWorkflowRunV4({
126
- workflowRefs,
127
- ...(options?.force ? { force: true } : {}),
128
113
  run: {
129
114
  id: runId,
130
115
  workflowRef: asset.ref,
131
- scopeKey,
132
- workflowEntryId,
133
116
  workflowTitle: asset.title,
134
117
  paramsJson: JSON.stringify(effectiveParams),
135
118
  currentStepId,
@@ -149,9 +132,12 @@ export async function startWorkflowRun(ref, params = {}, options) {
149
132
  planJson,
150
133
  planHash,
151
134
  });
135
+ if (otherActive.length > 0) {
136
+ const ids = otherActive.map((run) => run.id);
137
+ warn(`Workflow ${asset.ref} already has ${ids.length} active run(s) (${ids.join(", ")}); started another. ` +
138
+ `Continue one with "akm workflow run ${ids[0]}" or give up on it with "akm workflow abandon ${ids[0]}".`);
139
+ }
152
140
  const result = await getWorkflowStatus(runId);
153
- if (crossScopeWarning)
154
- result.warnings = [...(result.warnings ?? []), crossScopeWarning];
155
141
  // Params reach every unit prompt in clear (only the journal scrubs them).
156
142
  // Surface a loud, best-effort warning when a param LOOKS like a credential
157
143
  // so the author moves it to an env binding. Advisory only — never blocks
@@ -167,6 +153,16 @@ export async function startWorkflowRun(ref, params = {}, options) {
167
153
  return result;
168
154
  });
169
155
  }
156
+ function decodeStatusPlan(planJson) {
157
+ if (!planJson)
158
+ return undefined;
159
+ try {
160
+ return decodeWorkflowPlan(JSON.parse(planJson));
161
+ }
162
+ catch {
163
+ return undefined;
164
+ }
165
+ }
170
166
  export async function getWorkflowStatus(runId, opts) {
171
167
  return withWorkflowRunsRepo((repo) => {
172
168
  const run = readWorkflowRun(repo, runId);
@@ -176,25 +172,32 @@ export async function getWorkflowStatus(runId, opts) {
176
172
  // The honest diagnostic surface (#22): read the unit journal straight and
177
173
  // project each row, INCLUDING failures whose diagnostic text the
178
174
  // deterministic evidence graph drops. Read-only; never mutates the run.
179
- detail.units = repo.getUnitsForRun(run.id).map(toUnitDiagnostic);
175
+ const rows = repo.getUnitsForRun(run.id);
176
+ // The plan is decoded only to describe a failed unit, read-only: no run
177
+ // warning (`readRunPlan` says it is "running" an older plan) and no
178
+ // orchestration when the plan cannot be decoded.
179
+ const plan = rows.some((row) => row.status === "failed") ? decodeStatusPlan(run.plan_json) : undefined;
180
+ detail.units = rows.map((row) => {
181
+ const unit = toUnitDiagnostic(row);
182
+ const step = row.status === "failed" ? plan?.steps.find((candidate) => candidate.stepId === row.step_id) : undefined;
183
+ const summary = step ? summarizeStepOrchestration(step, undefined) : undefined;
184
+ return summary ? { ...unit, orchestration: resolveOrchestrationRefs(summary) } : unit;
185
+ });
180
186
  }
181
187
  return detail;
182
188
  });
183
189
  }
184
190
  export async function listWorkflowRuns(input) {
185
- const scopeKey = input?.allScopes === true ? null : getCurrentWorkflowScopeKey();
186
191
  const activeOnly = input?.activeOnly === true;
187
192
  const includeChildren = input?.includeChildren === true;
188
193
  if (input?.workflowRef === undefined) {
189
194
  return withWorkflowRunsRepo((repo) => ({
190
195
  runs: repo
191
196
  .listRuns({
192
- scopeKey,
193
197
  ...(activeOnly ? { activeOnly: true } : {}),
194
198
  ...(includeChildren ? { includeChildren: true } : {}),
195
199
  })
196
200
  .map(toWorkflowRunSummary),
197
- scopeKey,
198
201
  }));
199
202
  }
200
203
  const exactRef = input.workflowRef.trim();
@@ -212,12 +215,11 @@ export async function listWorkflowRuns(input) {
212
215
  }
213
216
  catch (error) {
214
217
  if (parsedExactRef.bundle !== undefined) {
215
- const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ scopeKey, workflowRef: exactRef, ...(includeChildren ? { includeChildren: true } : {}) }));
218
+ const exactRows = await withWorkflowRunsRepo((repo) => repo.listRuns({ workflowRef: exactRef, ...(includeChildren ? { includeChildren: true } : {}) }));
216
219
  if (exactRows.length === 0)
217
220
  throw error;
218
221
  return {
219
222
  runs: exactRows.filter((row) => !activeOnly || row.status === "active").map(toWorkflowRunSummary),
220
- scopeKey,
221
223
  };
222
224
  }
223
225
  if (!(error instanceof NotFoundError))
@@ -226,23 +228,20 @@ export async function listWorkflowRuns(input) {
226
228
  return withWorkflowRunsRepo((repo) => ({
227
229
  runs: repo
228
230
  .listRuns({
229
- scopeKey,
230
231
  workflowRefs,
231
232
  ...(activeOnly ? { activeOnly: true } : {}),
232
233
  ...(includeChildren ? { includeChildren: true } : {}),
233
234
  })
234
235
  .map(toWorkflowRunSummary),
235
- scopeKey,
236
236
  }));
237
237
  }
238
238
  export async function getNextWorkflowStep(specifier, params, options) {
239
239
  return withWorkflowRunsRepo(async (repo) => {
240
- const { run, autoStarted, resumed, startWarnings } = await resolveRunSpecifier(repo, specifier, params, options?.parameterFlags, options?.newRun);
240
+ const { run, autoStarted, startWarnings } = await resolveRunSpecifier(repo, specifier, params, options?.parameterFlags);
241
241
  const steps = readWorkflowRunSteps(repo, run.id);
242
242
  return {
243
243
  ...projectNextResult(run, steps),
244
244
  ...(autoStarted ? { autoStarted: true } : {}),
245
- ...(resumed ? { resumed: true } : {}),
246
245
  ...(startWarnings?.length ? { startWarnings } : {}),
247
246
  };
248
247
  });
@@ -348,7 +347,7 @@ export async function completeWorkflowStep(input) {
348
347
  const stepPlan = storedPlan.steps.find((step) => step.stepId === input.stepId);
349
348
  if (!stepPlan)
350
349
  throw new NotFoundError(`Step "${input.stepId}" was not found in workflow run ${run.id}.`);
351
- return { existing, plan: storedPlan, stepPlan };
350
+ return { existing, plan: storedPlan, stepPlan, workflowRef: run.workflow_ref };
352
351
  });
353
352
  const summary = input.summary?.trim();
354
353
  // #506: completing a step requires a summary of the work done.
@@ -365,10 +364,7 @@ export async function completeWorkflowStep(input) {
365
364
  ? // Manual completion journals no gate row, so there is no `<stepId>.gate:l<loop>`
366
365
  // identity to agree with — but the dispatch still names the REAL run and
367
366
  // step (frozen-judge falls back to the gate node id for the unit id).
368
- frozenSummaryJudge(preflight.stepPlan.gate.frozenJudge, input.signal, undefined, {
369
- runId: input.runId,
370
- stepId: input.stepId,
371
- })
367
+ frozenSummaryJudge(preflight.stepPlan.gate.frozenJudge, input.signal, undefined, { runId: input.runId, stepId: input.stepId }, undefined, resolveJudgeCwd(preflight.stepPlan.gate.frozenJudge, preflight.workflowRef))
372
368
  : input.summaryJudge;
373
369
  if (criteria.length > 0 && !judge) {
374
370
  throw new ConfigError(`Workflow run ${input.runId} has completion criteria for step "${input.stepId}" but its frozen plan has no judge. ` +
@@ -457,57 +453,27 @@ export async function completeWorkflowStep(input) {
457
453
  return detail;
458
454
  });
459
455
  }
460
- async function resolveRunSpecifier(repo, specifier, params, parameterFlags, forceNew) {
461
- const hasParameters = (params && Object.keys(params).length > 0) || (parameterFlags?.length ?? 0) > 0;
456
+ async function resolveRunSpecifier(repo, specifier, params, parameterFlags) {
462
457
  const explicitRun = findRunByIdOrPrefix(repo, specifier);
463
458
  if (explicitRun) {
464
- // `--new` starts a fresh run FROM A REF; it never makes sense against a
465
- // run id (or an id prefix), which already names the run to act on (#919).
466
- if (forceNew) {
467
- throw new UsageError(`--new starts a fresh run from a workflow ref; "${specifier}" already names a run id.`, "INVALID_FLAG_VALUE");
468
- }
469
- if (hasParameters) {
459
+ if ((params && Object.keys(params).length > 0) || (parameterFlags?.length ?? 0) > 0) {
470
460
  throw new UsageError(`Workflow parameter flags can only be used when starting a new run, not with existing run id "${specifier}".`);
471
461
  }
472
462
  return { run: explicitRun, autoStarted: false };
473
463
  }
474
- const scopeKey = getCurrentWorkflowScopeKey();
475
- const exactRef = specifier.trim();
476
- const parsedExact = parseBundleRef(exactRef);
477
- const qualifiedExact = parsedExact.bundle !== undefined && parsedExact.fragment === undefined;
478
- const detached = qualifiedExact && !forceNew ? repo.getActiveRunRowForScope(exactRef, scopeKey) : undefined;
464
+ // A workflow ref always starts a new run; only a run id drives an existing one.
479
465
  let ref;
480
466
  try {
481
467
  ref = await canonicalizeWorkflowSpecifier(specifier);
482
468
  }
483
469
  catch (error) {
484
- if (detached) {
485
- if (hasParameters) {
486
- throw new UsageError(`Workflow parameter flags can only be set on a new run; ${specifier} is already active ` +
487
- `(id ${detached.id}, scope ${scopeKey}).`, "INVALID_FLAG_VALUE", `Pass --new to start a separate run, or run "akm workflow abandon ${detached.id}" to free up ${specifier} first.`);
488
- }
489
- return { run: detached, autoStarted: false, resumed: true };
490
- }
491
470
  if (error instanceof NotFoundError && !specifier.includes(":") && !specifier.includes("/")) {
492
471
  throw new NotFoundError(`Workflow run or workflow "${specifier}" not found.`, "WORKFLOW_NOT_FOUND");
493
472
  }
494
473
  throw error;
495
474
  }
496
- const active = forceNew ? undefined : repo.getActiveRunRowForScope(await workflowRunRefSet(ref, exactRef), scopeKey);
497
- if (active) {
498
- if (hasParameters) {
499
- throw new UsageError(`Workflow parameter flags can only be set on a new run; ${ref} is already active ` +
500
- `(id ${active.id}, scope ${scopeKey}).`, "INVALID_FLAG_VALUE", `Pass --new to start a separate run, or run "akm workflow abandon ${active.id}" to free up ${ref} first.`);
501
- }
502
- return { run: active, autoStarted: false, resumed: true };
503
- }
504
- // `force` (#485's own escape hatch) is what lets `--new` create a second
505
- // active run for this (ref, scope) pair instead of the concurrency guard
506
- // refusing it — the caller explicitly asked for a fresh run, so the guard
507
- // that exists to catch an ACCIDENTAL second run does not apply (#919).
508
475
  const started = await startWorkflowRun(ref, params ?? {}, {
509
476
  ...(parameterFlags !== undefined ? { parameterFlags } : {}),
510
- ...(forceNew ? { force: true } : {}),
511
477
  });
512
478
  return {
513
479
  run: readWorkflowRun(repo, started.run.id),
@@ -648,7 +614,6 @@ function toWorkflowRunSummary(run) {
648
614
  return {
649
615
  id: run.id,
650
616
  workflowRef: run.workflow_ref,
651
- scopeKey: run.scope_key,
652
617
  workflowEntryId: run.workflow_entry_id,
653
618
  workflowTitle: run.workflow_title,
654
619
  status: run.status,
@@ -733,11 +698,3 @@ function parseJsonArray(value) {
733
698
  }
734
699
  return undefined;
735
700
  }
736
- export async function getActiveWorkflowRun(scopeKey = getCurrentWorkflowScopeKey()) {
737
- return withWorkflowRunsRepo((repo) => {
738
- const row = repo.findActiveOrBlockedRunForScope(scopeKey);
739
- if (!row)
740
- return null;
741
- return { runId: row.id, stepId: row.current_step_id, workflowRef: row.workflow_ref };
742
- }).catch(() => null); // fail-open: never crash show output due to DB error
743
- }