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
@@ -23,13 +23,16 @@
23
23
  * - Run `budget` ceilings are consumed per actual dispatch and abort the step
24
24
  * when crossed, regardless of `on_error`.
25
25
  */
26
- import { appendEvent } from "../../core/events.js";
26
+ import { resolveRefToken } from "../../commands/env/env-binding.js";
27
+ import { isInstalledBundle } from "../../core/env-secret-ref.js";
28
+ import { NotFoundError } from "../../core/errors.js";
27
29
  import { validateJsonSchemaSubset } from "../../core/json-schema.js";
30
+ import { substituteRefTokens } from "../../core/ref-token.js";
28
31
  import { runStructured } from "../../core/structured.js";
29
32
  import { warn } from "../../core/warn.js";
30
- import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
31
33
  import { withWorkflowRunsConnection, withWorkflowRunsRepo, } from "../../storage/repositories/workflow-runs-repository.js";
32
34
  import { WORKFLOW_UNIT_DIAGNOSTIC_CLIP } from "../resource-limits.js";
35
+ import { resolveRunBundleRoot } from "../runtime/workflow-asset-loader.js";
33
36
  // The ONE child-workflow drive — publishes and drives a
34
37
  // `child-workflow`-targeted unit; this module's dispatch seam is its only
35
38
  // production caller.
@@ -167,22 +170,28 @@ function openDispatchBudget(ctx, dispatched) {
167
170
  }
168
171
  /** Resolve the live-at-dispatch prerequisites once, after durable-row reuse is known. */
169
172
  async function prepareStepDispatchPrerequisites(input) {
170
- const { plan, workUnits, willDispatch, dispatched } = input;
173
+ const { plan, ctx, workUnits, willDispatch, dispatched } = input;
171
174
  let env;
172
175
  let sensitiveValues;
176
+ // The run's bundle is found again here, never frozen: a bundle update moves it to a new directory.
177
+ let cwd = ctx.bundleRootOverride;
178
+ if (willDispatch && cwd === undefined) {
179
+ try {
180
+ cwd = resolveRunBundleRoot(ctx.workflowRef);
181
+ }
182
+ catch (err) {
183
+ return {
184
+ ok: false,
185
+ result: failedStep(dispatched, `Step "${plan.stepId}" cannot run: ${message(err).replace(/\.$/, "")}.`),
186
+ };
187
+ }
188
+ }
173
189
  const frozenEnvironment = workUnits[0]?.environment;
174
190
  if (willDispatch && frozenEnvironment && frozenEnvironment.length > 0) {
175
191
  try {
176
192
  const materialized = materializeFrozenWorkflowEnvironment(frozenEnvironment);
177
193
  env = materialized.values;
178
194
  sensitiveValues = materialized.sensitiveValues;
179
- for (const audit of materialized.audits) {
180
- appendEvent({
181
- eventType: audit.eventType,
182
- ref: audit.ref,
183
- metadata: { keys: audit.keys, secretNames: audit.secretNames },
184
- });
185
- }
186
195
  }
187
196
  catch (err) {
188
197
  return {
@@ -193,6 +202,7 @@ async function prepareStepDispatchPrerequisites(input) {
193
202
  }
194
203
  return {
195
204
  ok: true,
205
+ ...(cwd !== undefined ? { cwd } : {}),
196
206
  ...(env ? { env } : {}),
197
207
  ...(sensitiveValues ? { sensitiveValues } : {}),
198
208
  };
@@ -229,13 +239,14 @@ async function executeStepPlanInConnection(plan, ctx) {
229
239
  const willDispatch = reuseDecisions.some((decision) => decision.kind === "dispatch");
230
240
  const prerequisites = await prepareStepDispatchPrerequisites({
231
241
  plan,
242
+ ctx,
232
243
  workUnits,
233
244
  willDispatch,
234
245
  dispatched,
235
246
  });
236
247
  if (!prerequisites.ok)
237
248
  return prerequisites.result;
238
- const { env, sensitiveValues } = prerequisites;
249
+ const { cwd, env, sensitiveValues } = prerequisites;
239
250
  // Budget ceilings + lifetime-cap accounting, and the budget-chained abort
240
251
  // signal they trip. Extracted verbatim (behavior-identical) — see
241
252
  // {@link openDispatchBudget}.
@@ -246,6 +257,7 @@ async function executeStepPlanInConnection(plan, ctx) {
246
257
  outcomes = await scheduleUnits(workUnits, (workUnit, index) => runUnit({
247
258
  plan,
248
259
  workUnit,
260
+ cwd,
249
261
  env,
250
262
  sensitiveValues,
251
263
  ctx,
@@ -299,7 +311,7 @@ async function executeStepPlanInConnection(plan, ctx) {
299
311
  };
300
312
  }
301
313
  async function runUnit(input) {
302
- const { plan, workUnit, env, sensitiveValues, ctx, dispatcher } = input;
314
+ const { plan, workUnit, cwd, env, sensitiveValues, ctx, dispatcher } = input;
303
315
  const unitId = workUnit.unitId;
304
316
  // Target validity is a WHOLE-LIST invariant, never a per-unit condition:
305
317
  // computeStepWorkList rejects a step before building any unit when its sole
@@ -317,10 +329,7 @@ async function runUnit(input) {
317
329
  prompt,
318
330
  frozenTarget: workUnit.frozenTarget,
319
331
  ...(workUnit.execContext ? { execContext: workUnit.execContext } : {}),
320
- // An exec unit spawns in the engine invocation's working directory. Only
321
- // exec units get it: handing an agent unit a cwd it never had would change
322
- // harness behavior, and the agent path already takes its cwd from its profile.
323
- ...(workUnit.frozenTarget.kind !== "command" && ctx.workDir !== undefined ? { cwd: ctx.workDir } : {}),
332
+ ...(cwd !== undefined ? { cwd } : {}),
324
333
  timeoutMs: workUnit.timeoutMs,
325
334
  ...(workUnit.schema ? { schema: workUnit.schema } : {}),
326
335
  ...(env ? { env } : {}),
@@ -498,6 +507,7 @@ async function dispatchJournaledAttempt(input) {
498
507
  };
499
508
  // A child-workflow unit goes to the child executor instead of the
500
509
  // dispatcher, after its attempt row is reserved, so it journals like any unit.
510
+ const resolvedSecrets = new Set();
501
511
  const dispatched = request.frozenTarget.kind === "child-workflow"
502
512
  ? await driveChildWorkflowUnit({
503
513
  request,
@@ -507,17 +517,18 @@ async function dispatchJournaledAttempt(input) {
507
517
  inputHash: input.inputHash,
508
518
  dispatcher,
509
519
  })
510
- : await dispatchUnit(request, dispatcher);
520
+ : await dispatchUnit(request, dispatcher, resolvedSecrets);
511
521
  // Credential and passthrough values are intentionally sampled only AFTER
512
522
  // the default dispatcher has authorized/lowered the frozen request and
513
523
  // materialized credentials at its terminal dispatch boundary. Custom test
514
524
  // dispatchers receive the same post-dispatch journal scrub. Secret-shaped
515
525
  // run params join the set: they must reach the prompt in clear (the unit
516
- // needs them), but nothing about them needs to reach the journal.
526
+ // needs them), but nothing about them needs to reach the journal. So do the
527
+ // secrets the dispatcher resolved itself (an exec unit's argv/cwd tokens).
517
528
  const sensitiveValues = collectWorkflowDispatchSensitiveValues({
518
529
  ...(request.frozenTarget.kind === "command" ? { runner: request.frozenTarget.runner } : {}),
519
- sensitiveValues: [...(request.sensitiveValues ?? []), ...secretShapedParamValues(ctx.params)],
520
- }, request.env);
530
+ sensitiveValues: [...(request.sensitiveValues ?? []), ...resolvedSecrets, ...secretShapedParamValues(ctx.params)],
531
+ });
521
532
  const outcome = redactUnitOutcome(dispatched, sensitiveValues);
522
533
  const finishedAt = new Date().toISOString();
523
534
  // A dispatched unit's outcome is NEVER silently discarded: the attempt is
@@ -589,7 +600,9 @@ class UnitTransportError extends Error {
589
600
  this.name = "UnitTransportError";
590
601
  }
591
602
  }
592
- async function dispatchUnit(request, dispatcher) {
603
+ async function dispatchUnit(request, dispatcher,
604
+ /** Receives the secret values the dispatcher resolved, for the caller's journal scrub. */
605
+ resolvedSecrets) {
593
606
  let tokens = 0;
594
607
  let sawUsage = false;
595
608
  let loweringNotices;
@@ -600,6 +613,8 @@ async function dispatchUnit(request, dispatcher) {
600
613
  let sessionId;
601
614
  const dispatchOnce = async (feedback) => {
602
615
  const result = await dispatcher(request, feedback);
616
+ for (const value of result.sensitiveValues ?? [])
617
+ resolvedSecrets.add(value);
603
618
  if (result.usage) {
604
619
  sawUsage = true;
605
620
  tokens +=
@@ -700,12 +715,14 @@ async function dispatchUnit(request, dispatcher) {
700
715
  export const defaultUnitDispatcher = async (request, feedback) => {
701
716
  const frozenTarget = request.frozenTarget;
702
717
  if (frozenTarget.kind === "shell") {
703
- if (frozenTarget.cwdIdentity)
704
- assertFrozenDirectoryContained(frozenTarget.cwdIdentity);
705
- return runExecUnit({
718
+ if (request.cwd === undefined) {
719
+ throw new Error(`exec unit "${request.unitId}" was dispatched with no working directory.`);
720
+ }
721
+ const { exec, secretValues } = substituteExecRefs(request.unitId, frozenTarget.exec);
722
+ const outcome = await runExecUnit({
706
723
  unitId: request.unitId,
707
- exec: frozenTarget.exec,
708
- baseDir: request.cwd ?? frozenTarget.cwdIdentity?.realCwd ?? process.cwd(),
724
+ exec,
725
+ baseDir: request.cwd,
709
726
  ...(request.env ? { env: request.env } : {}),
710
727
  ...(request.execContext ? { context: request.execContext } : {}),
711
728
  ...(request.schema ? { hasOutputSchema: true } : {}),
@@ -713,9 +730,36 @@ export const defaultUnitDispatcher = async (request, feedback) => {
713
730
  ...(request.signal ? { signal: request.signal } : {}),
714
731
  ...(request.eventSource !== undefined ? { eventSource: request.eventSource } : {}),
715
732
  });
733
+ // A secret substituted into argv joins the redaction set, like one bound through an env asset: the
734
+ // executor scrubs after the structured parse, so a quoted or numeric secret is not missed or corrupted.
735
+ return { ...outcome, sensitiveValues: secretValues };
716
736
  }
717
737
  return dispatchWorkflowExecution(request, feedback);
718
738
  };
739
+ /**
740
+ * Replace the `${ref}` tokens in an exec unit's argv and `cwd` (prompts are not substituted): a
741
+ * `secrets/` ref with the secret's value, any other asset ref with its absolute path. A ref no
742
+ * bundle owns fails the unit before anything is spawned.
743
+ */
744
+ function substituteExecRefs(unitId, exec) {
745
+ const slots = Object.fromEntries(exec.command.entries());
746
+ if (exec.cwd !== undefined)
747
+ slots.cwd = exec.cwd;
748
+ const { values, missing, secretValues } = substituteRefTokens(slots, resolveRefToken, isInstalledBundle);
749
+ if (missing.length > 0) {
750
+ throw new NotFoundError(`exec unit "${unitId}" references asset(s) that no bundle owns: ${missing.join(", ")}. Nothing was run.`, "FILE_NOT_FOUND",
751
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: literal `${ref}` token syntax documented for users, not interpolation
752
+ "Write each `${ref}` as a full akm ref that exists, e.g. `${team//scripts/run.sh}`.");
753
+ }
754
+ return {
755
+ exec: {
756
+ ...exec,
757
+ command: exec.command.map((_, index) => values[index]),
758
+ ...(exec.cwd !== undefined ? { cwd: values.cwd } : {}),
759
+ },
760
+ secretValues,
761
+ };
762
+ }
719
763
  // ── Small helpers ────────────────────────────────────────────────────────────
720
764
  /**
721
765
  * Rehydrate a journaled completed unit row into a UnitOutcome (durable-row
@@ -14,16 +14,16 @@ import { createHash } from "node:crypto";
14
14
  import fs from "node:fs";
15
15
  import path from "node:path";
16
16
  import { TransientError, UsageError } from "../../core/errors.js";
17
- import { releaseLock } from "../../core/file-lock.js";
17
+ import { probeLock, releaseLock } from "../../core/file-lock.js";
18
18
  import { formatLockHolderPid, tryAcquireRunLock } from "../../core/run-lock.js";
19
19
  import { disposeDispatchResources } from "../../integrations/agent/runner-dispatch.js";
20
20
  import { resolveStorageLocations } from "../../storage/locations.js";
21
21
  import { withWorkflowRunsConnection, withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js";
22
22
  import { computePlanHash } from "../ir/plan-hash.js";
23
23
  import { decodeWorkflowPlan, readRunPlan } from "../runtime/run-plan.js";
24
- import { abandonWorkflowRun, getNextWorkflowStep, resumeWorkflowRun } from "../runtime/runs.js";
24
+ import { abandonWorkflowRun, getNextWorkflowStep, listWorkflowRuns, resolveWorkflowRunTarget, resumeWorkflowRun, } from "../runtime/runs.js";
25
25
  import { loadWorkflowAsset } from "../runtime/workflow-asset-loader.js";
26
- import { frozenSummaryJudge } from "./frozen-judge.js";
26
+ import { frozenSummaryJudge, resolveJudgeCwd } from "./frozen-judge.js";
27
27
  import { mergeLoweringNotices } from "./lowering-notices.js";
28
28
  import { defaultUnitDispatcher, executeStepPlan, } from "./native-executor.js";
29
29
  // Shared step semantics — gate-evaluation journaling, and the whole step-completion path
@@ -34,10 +34,6 @@ export async function runWorkflowSteps(options) {
34
34
  let target = options.target;
35
35
  let params = options.params;
36
36
  let parameterFlags = options.parameterFlags;
37
- // `--new` only applies to the FIRST resolution of `target` (a ref); every
38
- // retry re-targets the run id `startWorkflowRun` already created, so it is
39
- // cleared alongside `params`/`parameterFlags` below (#919).
40
- let newRun = options.newRun;
41
37
  let remainingRetries = options.maxRetries ?? 0;
42
38
  let remainingSteps = options.maxSteps;
43
39
  const executed = [];
@@ -53,7 +49,6 @@ export async function runWorkflowSteps(options) {
53
49
  target,
54
50
  ...(params !== undefined ? { params } : { params: undefined }),
55
51
  ...(parameterFlags !== undefined ? { parameterFlags } : { parameterFlags: undefined }),
56
- newRun,
57
52
  ...(remainingSteps !== undefined ? { maxSteps: remainingSteps } : { maxSteps: undefined }),
58
53
  }, liveEvidence);
59
54
  executed.push(...result.executed);
@@ -81,14 +76,12 @@ export async function runWorkflowSteps(options) {
81
76
  target = result.run.id;
82
77
  params = undefined;
83
78
  parameterFlags = undefined;
84
- newRun = undefined;
85
79
  remainingRetries -= 1;
86
80
  }
87
81
  }
88
82
  async function runWorkflowAttempt(options, liveEvidence) {
89
83
  const next = await getNextWorkflowStep(options.target, options.params, {
90
84
  parameterFlags: options.parameterFlags,
91
- newRun: options.newRun,
92
85
  });
93
86
  // Refuse non-active runs BEFORE any dispatch — completeWorkflowStep would
94
87
  // reject the completion anyway, but only after the units already ran (and
@@ -125,7 +118,6 @@ async function runWorkflowAttempt(options, liveEvidence) {
125
118
  executed: [],
126
119
  stepsProcessed: 0,
127
120
  warnings: [...warnings, message],
128
- ...(next.resumed ? { resumed: true } : {}),
129
121
  };
130
122
  }
131
123
  plan = read.plan;
@@ -141,11 +133,10 @@ async function runWorkflowAttempt(options, liveEvidence) {
141
133
  // Creation/resume-time notices reach the caller only here: the run row
142
134
  // has no warnings column, and a later invocation of the same run must
143
135
  // stay silent about a decision it did not make. `driveRun` never sets
144
- // `warnings` or `resumed` — both are properties of THIS resolution of
145
- // `target`, not of the run row (#919).
136
+ // `warnings` — it is a property of THIS resolution of `target`, not of
137
+ // the run row.
146
138
  return {
147
139
  ...result,
148
- ...(next.resumed ? { resumed: true } : {}),
149
140
  ...(warnings.length > 0 ? { warnings } : {}),
150
141
  };
151
142
  }
@@ -172,6 +163,27 @@ async function runWorkflowAttempt(options, liveEvidence) {
172
163
  export function workflowRunLockPath(runId) {
173
164
  return path.join(path.dirname(resolveStorageLocations().stateDb), "workflow-run-locks", `${runId}.lock`);
174
165
  }
166
+ /**
167
+ * `--skip-if-locked` for a workflow REF (#1239): an active run of `ref` whose
168
+ * lock a live process holds, or `undefined`. A run id (which drives that one
169
+ * run, and fails with `RUN_LEASE_HELD` if it is held) is never a ref. Read-only:
170
+ * `probeLock` reports a dead holder as stale without reclaiming it.
171
+ */
172
+ export async function findLiveLockedRunOfRef(target) {
173
+ if ((await resolveWorkflowRunTarget(target)) !== undefined)
174
+ return undefined;
175
+ const { runs } = await listWorkflowRuns({ workflowRef: target, activeOnly: true });
176
+ for (const run of runs) {
177
+ const probe = probeLock(workflowRunLockPath(run.id));
178
+ if (probe.state === "held") {
179
+ return {
180
+ runId: run.id,
181
+ holder: formatLockHolderPid({ pid: probe.holderPid, launcherPid: probe.launcherPid ?? null }),
182
+ };
183
+ }
184
+ }
185
+ return undefined;
186
+ }
175
187
  /**
176
188
  * Take the run's O_EXCL lock file, or refuse with `RUN_LEASE_HELD` (exit 75)
177
189
  * naming the live holder. A lock whose holder pid is dead is reclaimed by
@@ -215,7 +227,7 @@ async function workflowSourceDriftWarning(runId, workflowRef, plan) {
215
227
  if (current === plan.sourceHash)
216
228
  return undefined;
217
229
  return (`Workflow run ${runId}: ${workflowRef} (${sourcePath}) has changed since this run was frozen; ` +
218
- `continuing with the frozen plan. Start a new run with 'akm workflow run ${workflowRef} --new' to pick up the edit.`);
230
+ `continuing with the frozen plan. Start a new run with 'akm workflow run ${workflowRef}' to pick up the edit.`);
219
231
  }
220
232
  /**
221
233
  * A terminal run is a pure no-op: do not load or integrity-check its frozen
@@ -230,11 +242,11 @@ async function completedRunResult(runId) {
230
242
  ...(doneState.run.status === "completed" ? { done: true } : {}),
231
243
  };
232
244
  }
233
- function workflowSummaryJudge(options, stepPlan, signal, owner) {
245
+ function workflowSummaryJudge(options, stepPlan, signal, owner, workflowRef) {
234
246
  if (options.summaryJudge !== undefined)
235
247
  return options.summaryJudge;
236
248
  // The judge dispatches under the real run/step identity and the same event source as units.
237
- return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner, options.eventSource);
249
+ return frozenSummaryJudge(stepPlan.gate.frozenJudge, signal, options.dispatcher ?? defaultUnitDispatcher, owner, options.eventSource, options.bundleRootOverride ?? resolveJudgeCwd(stepPlan.gate.frozenJudge, workflowRef));
238
250
  }
239
251
  /**
240
252
  * Seed the run's budget from the append-only attempt journal so a resumed run
@@ -307,6 +319,7 @@ async function executeStepSubgraph(ctx, loop) {
307
319
  ...(options.eventSource !== undefined ? { eventSource: options.eventSource } : {}),
308
320
  ...(options.signal ? { signal: options.signal } : {}),
309
321
  ...(options.dispatcher ? { dispatcher: options.dispatcher } : {}),
322
+ ...(options.bundleRootOverride !== undefined ? { bundleRootOverride: options.bundleRootOverride } : {}),
310
323
  maxConcurrency: Math.min(options.maxConcurrency ?? Number.POSITIVE_INFINITY, plan.execution?.maxConcurrency ?? 1),
311
324
  });
312
325
  }
@@ -493,7 +506,7 @@ liveEvidence) {
493
506
  summaryJudge = workflowSummaryJudge(options, stepPlan, options.signal, {
494
507
  runId: next.run.id,
495
508
  stepId: step.id,
496
- });
509
+ }, next.run.workflowRef);
497
510
  }
498
511
  catch (error) {
499
512
  const detail = error instanceof Error && error.message ? ` (${error.message})` : "";
@@ -837,8 +837,8 @@ function judgeFailureNotes(runId, stepId, cause) {
837
837
  return (`Step "${stepId}" could not be verified: ${cause}. ` +
838
838
  `This is a verification-judge failure, not a verdict — no gate loop was consumed and the step's ` +
839
839
  `journaled units are preserved. Fix the verifier configuration or service, then run ` +
840
- `\`akm workflow resume ${runId}\` to re-evaluate the gate against the existing results ` +
841
- `without re-dispatching units.`);
840
+ `\`akm workflow resume ${runId}\` and \`akm workflow run ${runId}\` to re-evaluate the gate ` +
841
+ `against the existing results without re-dispatching units.`);
842
842
  }
843
843
  /**
844
844
  * Complete a step `blocked` for a verifier-INFRASTRUCTURE failure and return
@@ -1059,7 +1059,8 @@ export async function finalizeExecutedStep(input) {
1059
1059
  }
1060
1060
  // Judge infrastructure failure: the fail-closed rejection is synthetic, not a
1061
1061
  // verdict. Consume NO gate loop; block the step (and therefore the run) so
1062
- // `akm workflow resume` retries the gate over the journaled units.
1062
+ // `akm workflow resume` then `akm workflow run` retries the gate over the
1063
+ // journaled units.
1063
1064
  if (rejection && judgeFailed) {
1064
1065
  return blockFinalizedStep(input, judgeFailure ?? "the verification judge failed");
1065
1066
  }
@@ -2,24 +2,17 @@
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
4
  import { ConfigError } from "../../core/errors.js";
5
- import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
6
5
  import { canonicalResolvedExecutionRequest } from "../../execution/resolved-request.js";
7
6
  import { buildExecutionFromWire } from "../../integrations/agent/execution.js";
8
7
  import { runExecution, unwrapHarnessReply } from "../../integrations/agent/runner-dispatch.js";
9
8
  /** Lower a frozen common request through its frozen runner only. */
10
9
  export function prepareWorkflowExecution(request, prompt = request.prompt) {
11
10
  const target = request.frozenTarget;
12
- if (target.cwdIdentity)
13
- assertFrozenDirectoryContained(target.cwdIdentity);
14
11
  const wire = JSON.parse(canonicalResolvedExecutionRequest(target.request));
15
12
  const command = { ...wire.command, content: prompt };
16
13
  const runtime = {
17
14
  ...wire.runtime,
18
- ...(request.cwd !== undefined
19
- ? { workspace: request.cwd }
20
- : target.cwdIdentity
21
- ? { workspace: target.cwdIdentity.realCwd }
22
- : {}),
15
+ ...(request.cwd !== undefined ? { workspace: request.cwd } : {}),
23
16
  ...(request.env !== undefined ? { environment: request.env } : {}),
24
17
  };
25
18
  wire.command = command;
@@ -1,140 +1,35 @@
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 fs from "node:fs";
5
- import path from "node:path";
6
- import dotenv from "dotenv";
7
- import { loadAdapterExecutionSource } from "../../commands/command/execution-source-loader.js";
8
4
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
9
- import { compareCodePoints, isWithin, toPosix } from "../../core/common.js";
10
5
  import { parseEnvRef } from "../../core/env-secret-ref.js";
11
6
  import { UsageError } from "../../core/errors.js";
12
- import { deriveInstallations } from "../../indexer/installations.js";
7
+ import { resolveConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
13
8
  import { resolveSourceEntries } from "../../indexer/search/search-source.js";
14
- import { resolveAssetPath } from "../../sources/resolve.js";
15
- import { SECRET_TOKEN_RE } from "../exec/environment.js";
16
9
  /**
17
- * A step's frozen environment: `pass_env` names, then
18
- * one value-free descriptor per `unit.env` ref (owner, key set, secret-token
19
- * names). Values are read at dispatch (`exec/environment.ts`), never frozen.
10
+ * A step's frozen environment: `pass_env` names, then one bundle-qualified
11
+ * ref per `unit.env` entry. The env file, its keys and the secrets it names
12
+ * are read at dispatch (`exec/environment.ts`), never frozen.
20
13
  */
21
14
  export function freezeEnvironment(source, exec, context) {
22
15
  const passThrough = (exec?.passEnv ?? []).map((name) => Object.freeze({ kind: "pass-through", name }));
23
16
  const seen = new Set();
24
17
  const envRefs = [];
25
18
  for (const [precedence, ref] of (source.unit?.env ?? []).entries()) {
26
- if (parseEnvRef(ref).type !== "env") {
27
- throw new UsageError(`Expected an env ref; got ${ref}.`, "WORKFLOW_SOURCE_INVALID");
28
- }
29
- const owned = resolveOwnedAssetSync(ref, "env", context);
30
- if (seen.has(owned.ref))
19
+ parseEnvRef(ref);
20
+ const qualified = qualifyEnvRef(ref, context);
21
+ if (seen.has(qualified))
31
22
  continue;
32
- seen.add(owned.ref);
33
- const parsed = dotenv.parse(fs.readFileSync(owned.file));
34
- const secretNames = new Set();
35
- for (const value of Object.values(parsed)) {
36
- for (const match of value.matchAll(SECRET_TOKEN_RE))
37
- if (match[1])
38
- secretNames.add(match[1]);
39
- }
40
- envRefs.push(Object.freeze({
41
- kind: "env-ref",
42
- ref: owned.ref,
43
- owner: Object.freeze({
44
- bundle: owned.bundle,
45
- adapter: owned.adapter,
46
- requestedRoot: owned.root,
47
- requestedPath: owned.file,
48
- relativePath: toPosix(path.relative(owned.root, owned.file)),
49
- }),
50
- keys: Object.keys(parsed).sort(compareCodePoints),
51
- secretNames: [...secretNames].sort(compareCodePoints),
52
- precedence,
53
- }));
23
+ seen.add(qualified);
24
+ envRefs.push(Object.freeze({ kind: "env-ref", ref: qualified, precedence }));
54
25
  }
55
26
  return [...passThrough, ...envRefs];
56
27
  }
57
- /** Load a command or persona the workflow names, resolved through the workflow's own bundles. */
58
- export async function workflowExecutionSource(ref, kind, context) {
59
- const owned = await resolveOwnedAsset(ref, kind === "command" ? "command" : "agent", context);
60
- return kind === "command"
61
- ? loadAdapterExecutionSource(owned.ref, "command", { config: context.config })
62
- : loadAdapterExecutionSource(owned.ref, "persona", { config: context.config });
63
- }
64
- export async function resolveOwnedAsset(ref, type, context) {
65
- return resolveOwnedAssetCore(ref, type, context, false);
66
- }
67
- export function resolveOwnedAssetSync(ref, type, context) {
68
- return resolveOwnedAssetCore(ref, type, context, true);
69
- }
70
- function resolveOwnedAssetCore(refInput, type, context, sync) {
28
+ /** Qualify an env ref a step names with the bundle that owns it right now, through the one walk. */
29
+ function qualifyEnvRef(refInput, context) {
71
30
  const parsed = parseBundleRef(refInput);
72
- const plural = type === "env" ? "env" : `${type}s`;
73
- const conceptId = parsed.conceptId.startsWith(`${plural}/`) ? parsed.conceptId : `${plural}/${parsed.conceptId}`;
74
- const name = conceptId.slice(plural.length + 1);
75
- const direct = parsed.bundle ? configuredOwner(parsed.bundle, context.config) : undefined;
76
- const candidates = direct ? [direct] : installedOwners(parsed.bundle, context.config);
77
- const findSync = () => {
78
- for (const candidate of candidates) {
79
- const directory = path.join(candidate.root, plural);
80
- for (const extension of assetExtensions(type)) {
81
- const file = path.resolve(directory, `${name}${extension}`);
82
- if (fs.existsSync(file) && fs.statSync(file).isFile()) {
83
- if (!isWithin(file, candidate.root)) {
84
- throw new UsageError(`${file} resolves outside its owning root.`, "PATH_ESCAPE_VIOLATION");
85
- }
86
- return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
87
- }
88
- }
89
- }
31
+ const match = resolveConceptOwner({ bundle: parsed.bundle, conceptId: parsed.conceptId }, resolveSourceEntries(undefined, context.config), { includeSensitive: true });
32
+ if (!match)
90
33
  throw new UsageError(`Workflow source target ${refInput} was not found.`, "INVALID_FLAG_VALUE");
91
- };
92
- if (sync)
93
- return findSync();
94
- return (async () => {
95
- for (const candidate of candidates) {
96
- try {
97
- const file = await resolveAssetPath(candidate.root, type, name);
98
- return { ...candidate, ref: makeBundleRef(candidate.bundle, conceptId), file };
99
- }
100
- catch {
101
- // Continue in installation priority order.
102
- }
103
- }
104
- return findSync();
105
- })();
106
- }
107
- /** Every installed bundle root (or just `bundle`'s), in installation priority order. */
108
- function installedOwners(bundle, config) {
109
- const sources = resolveSourceEntries(undefined, config);
110
- const installations = deriveInstallations(sources);
111
- return sources.flatMap((source, index) => {
112
- const installation = installations[index];
113
- if (!installation || (bundle && installation.id !== bundle))
114
- return [];
115
- return [
116
- {
117
- bundle: installation.id,
118
- root: source.path,
119
- adapter: source.adapterId ?? installation.components[0]?.adapter ?? "akm",
120
- },
121
- ];
122
- });
123
- }
124
- function configuredOwner(bundle, config) {
125
- const entry = config.bundles?.[bundle];
126
- if (!entry || typeof entry.path !== "string")
127
- return undefined;
128
- const components = entry.components ? Object.values(entry.components) : [];
129
- const component = components[0];
130
- return {
131
- bundle,
132
- root: path.resolve(entry.path, component?.root ?? "."),
133
- adapter: component?.adapter ?? "akm",
134
- };
135
- }
136
- function assetExtensions(type) {
137
- if (type === "env")
138
- return ["", ".env"];
139
- return ["", ".md"];
34
+ return makeBundleRef(match.bundleId, parsed.conceptId);
140
35
  }
@@ -107,7 +107,6 @@ function frozenRoot(stepId, source, resolved, plan, config) {
107
107
  .update(canonicalJson({
108
108
  exec: resolved.target.exec,
109
109
  environment: resolved.environment,
110
- cwdIdentity: resolved.target.cwdIdentity,
111
110
  }))
112
111
  .digest("hex"),
113
112
  }
@@ -45,14 +45,13 @@ export function durableRequest(request) {
45
45
  wire.runtime = runtime;
46
46
  return decodeResolvedExecutionRequest(wire);
47
47
  }
48
- export function executionUnitValues(unit, workspace) {
48
+ export function executionUnitValues(unit) {
49
49
  return Object.freeze({
50
50
  ...(unit && Object.hasOwn(unit, "engine") ? { engine: unit.engine } : {}),
51
51
  ...(unit && Object.hasOwn(unit, "model") ? { model: unit.model } : {}),
52
52
  ...(unit && Object.hasOwn(unit, "llm") ? { inference: unit.llm } : {}),
53
53
  ...(unit && Object.hasOwn(unit, "timeoutMs") ? { timeout: unit.timeoutMs } : {}),
54
54
  ...(unit && "output" in unit && Object.hasOwn(unit, "output") ? { outputSchema: unit.output } : {}),
55
- workspace,
56
55
  });
57
56
  }
58
57
  /** Step ids declared BEFORE `stepId` — the same ordering `map.over` and `inputs:` rely on. */
@@ -11,7 +11,6 @@ import { UsageError } from "../../../core/errors.js";
11
11
  import { workflowParamContract } from "../../ir/params.js";
12
12
  import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js";
13
13
  import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js";
14
- import { resolveOwnedAsset } from "../environment.js";
15
14
  import { declaredParamNames, earlierStepIds, } from "../step-values.js";
16
15
  import { freezeTaskInputBindings } from "../task-bindings.js";
17
16
  /** The child target's own content identity: its ref, embedded plan hash, and bindings. */
@@ -46,10 +45,9 @@ function assertNoCompositionCycle(stepId, childRef, refPath) {
46
45
  }
47
46
  export async function childWorkflowDispatch(input) {
48
47
  const { source, baseUnit, childRefInput, context } = input;
49
- // Resolution failures propagate unchanged, in code and shape — the same
50
- // authority every other asset reference resolves through.
51
- const owned = await resolveOwnedAsset(childRefInput, "workflow", context);
52
- const childAsset = await loadWorkflowAsset(owned.ref);
48
+ // The loader resolves the ref through the one walk, so a missing child fails
49
+ // like any other missing workflow.
50
+ const childAsset = await loadWorkflowAsset(childRefInput);
53
51
  const childRef = childAsset.ref;
54
52
  // The composition cycle check, before any child compilation.
55
53
  assertNoCompositionCycle(source.id, childRef, context.refPath);
@@ -3,29 +3,24 @@
3
3
  // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
4
  import { createHash } from "node:crypto";
5
5
  import { prepareCommandInvocation } from "../../../commands/command/command-execution.js";
6
- import { captureFrozenDirectoryIdentity } from "../../../execution/directory-identity.js";
7
6
  import { canonicalResolvedExecutionRequest, } from "../../../execution/resolved-request.js";
8
7
  import { fallbackAnnouncement } from "../../../integrations/agent/engine-fallback.js";
9
8
  import { buildExecution } from "../../../integrations/agent/execution.js";
10
- import { freezeEnvironment, workflowExecutionSource } from "../environment.js";
9
+ import { freezeEnvironment } from "../environment.js";
11
10
  import { durableRequest, executionUnitValues, targetConcurrency, } from "../step-values.js";
12
11
  export async function commandDispatch(source, baseUnit, action, context) {
13
12
  const prepared = await prepareCommandInvocation({
14
13
  action,
15
14
  config: context.config,
16
- ...(context.plan.defaults
17
- ? { invocationDefaults: executionUnitValues(context.plan.defaults, context.asset.sourcePath) }
18
- : {}),
15
+ ...(context.plan.defaults ? { invocationDefaults: executionUnitValues(context.plan.defaults) } : {}),
19
16
  inlineContentMode: "literal",
20
- current: executionUnitValues(source.unit, context.asset.sourcePath),
21
- sourceLoader: (ref, kind) => workflowExecutionSource(ref, kind, context),
17
+ current: executionUnitValues(source.unit),
22
18
  });
23
19
  return commandResult(source, baseUnit, prepared, context);
24
20
  }
25
21
  export function commandResult(source, baseUnit, prepared, context, literals = []) {
26
22
  const request = durableRequest(prepared.request);
27
23
  const lowered = buildExecution(request, prepared.runner);
28
- const cwdIdentity = captureFrozenDirectoryIdentity(context.asset.sourcePath);
29
24
  const runner = lowered.runner;
30
25
  const unit = {
31
26
  ...baseUnit,
@@ -43,7 +38,9 @@ export function commandResult(source, baseUnit, prepared, context, literals = []
43
38
  request: JSON.parse(canonicalResolvedExecutionRequest(request)),
44
39
  runner,
45
40
  ...(targetConcurrency(runner, context.config) ? { concurrency: targetConcurrency(runner, context.config) } : {}),
46
- cwdIdentity,
41
+ // Not a location. akm 0.10.0 and earlier decode a command-only plan and run it in the invoking directory,
42
+ // but their dispatch refuses a directory identity it cannot verify, so they stop here instead.
43
+ cwdIdentity: {},
47
44
  });
48
45
  const engineAnnouncement = fallbackAnnouncement(prepared.fallbackEngineName, request.engine.name);
49
46
  return {