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,18 +23,21 @@ import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
23
23
  import { conceptIdFromTypeName, isFullRefInput } from "../../core/asset/resolve-ref.js";
24
24
  import { isWithin, resolveStashDir } from "../../core/common.js";
25
25
  import { loadConfig, mutateConfig, resetConfigCache } from "../../core/config/config.js";
26
- import { bundleKeyForContentRoot, resolveActiveConfiguredSources, resolveConfiguredSources, } from "../../core/config/config-sources.js";
26
+ import { bundleKeyForContentRoot, resolveConfiguredSources } from "../../core/config/config-sources.js";
27
27
  import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
28
28
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
29
29
  import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
30
30
  import { warn } from "../../core/warn.js";
31
- import { commitWriteTargetBoundary, isWriteCapableSourceKind, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
31
+ import { assertBundleNotDisabled, commitWriteTargetBoundary, isWriteCapableSourceKind, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
32
+ import { deriveInstallations } from "../../indexer/installations.js";
32
33
  import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
34
+ import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
33
35
  import { resolveAssetPath } from "../../sources/resolve.js";
34
36
  import { enabledRefsFromInstalled, schedulerEnabledRefs, setSchedulerRefEnabled } from "../../tasks/activation-config.js";
35
37
  import { backendNameForPlatform, selectBackend } from "../../tasks/backends/index.js";
36
38
  import { prepareTaskV3Execution } from "../../tasks/prepare/prepare.js";
37
39
  import { isCheckoutInvocation, resolveAkmInvocation } from "../../tasks/resolve-akm-bin.js";
40
+ import { activeTaskSources, resolveBundleAsset } from "../../tasks/run/load-task.js";
38
41
  import { createExecutionProvenanceContext } from "../../tasks/run/provenance.js";
39
42
  import { runTask } from "../../tasks/run/run-task.js";
40
43
  import { readTaskHistory } from "../../tasks/run/task-history.js";
@@ -78,7 +81,7 @@ export async function akmTasksAdd(input, deps = {}) {
78
81
  when_to_use: input.when_to_use,
79
82
  tags: input.tags,
80
83
  });
81
- const parsedTask = parseTaskSource({ yaml, filePath: assetPath, workspaceRoot: stashDir });
84
+ const parsedTask = parseTaskSource({ yaml, filePath: assetPath });
82
85
  const task = projectTaskSourceV4(parsedTask.v4);
83
86
  const qualifiedRef = makeBundleRef(bundle.bundleName, `tasks/${id}`);
84
87
  await prepareTaskV3Execution(task, {
@@ -234,7 +237,6 @@ function resolveTaskActivation(ref, target, requireSchedulable) {
234
237
  const parsedSource = parseTaskSource({
235
238
  yaml: fs.readFileSync(sourcePath, "utf8"),
236
239
  filePath: sourcePath,
237
- workspaceRoot: bundle.source.path,
238
240
  });
239
241
  if (parsedSource.v4.schedule.length === 0) {
240
242
  throw new UsageError(`Task ${JSON.stringify(ref)} has no schedule to enable.`, "INVALID_FLAG_VALUE");
@@ -361,24 +363,26 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
361
363
  if (options.dryRun !== true)
362
364
  config = loadConfig();
363
365
  }
364
- const activeSources = resolveActiveConfiguredSources(config);
366
+ // The one active-bundle set search, show and workflows walk, working stash included.
367
+ // A working stash that is a disabled configured bundle is left out: an
368
+ // unscoped sync still removes that bundle's rows (below) and syncs the others,
369
+ // and a scoped sync reports it as disabled only when it is the selected one.
370
+ const sources = activeTaskSources(config);
371
+ const ids = deriveInstallations(sources).map((installation) => installation.id);
372
+ const kinds = sources.map((source) => source.type);
373
+ const configuredSources = resolveConfiguredSources(config);
365
374
  if (bundleTarget) {
366
375
  // Only filesystem and git bundles can carry scheduler state.
367
- const targetSource = activeSources.find((source) => source.name === bundleTarget);
368
- if (targetSource && !isWriteCapableSourceKind(targetSource.type)) {
369
- throw new UsageError(`Bundle "${bundleTarget}" has kind "${targetSource.type}"; task scheduling is only supported for filesystem and git bundles.`, "INVALID_FLAG_VALUE");
376
+ const targetType = kinds[ids.indexOf(bundleTarget)];
377
+ if (targetType !== undefined && !isWriteCapableSourceKind(targetType)) {
378
+ throw new UsageError(`Bundle "${bundleTarget}" has kind "${targetType}"; task scheduling is only supported for filesystem and git bundles.`, "INVALID_FLAG_VALUE");
370
379
  }
371
380
  }
372
- const configuredSources = resolveConfiguredSources(config);
373
- const schedulable = activeSources
374
- .filter((source) => isWriteCapableSourceKind(source.type))
375
- .map((source) => source.name);
376
- // No configured bundle at all: the working stash (e.g. `AKM_BUNDLE_DIR`).
377
- const selected = bundleTarget
378
- ? [bundleTarget]
379
- : schedulable.length > 0 || configuredSources.length > 0
380
- ? schedulable
381
- : [undefined];
381
+ const schedulable = ids.filter((_, index) => {
382
+ const type = kinds[index];
383
+ return type !== undefined && isWriteCapableSourceKind(type);
384
+ });
385
+ const selected = bundleTarget ? [bundleTarget] : schedulable;
382
386
  const enabledRefSet = new Set(enabledRefs);
383
387
  const desired = [];
384
388
  const failures = [];
@@ -696,12 +700,7 @@ function taskProjectionAssetResolver(config, bundleName, bundleRoot) {
696
700
  if (bundle === bundleName) {
697
701
  return { file: await resolveAssetPath(bundleRoot, type, name), bundleRoot };
698
702
  }
699
- // `bundle` is the qualifier of an asset ref in the task, not a flag.
700
- const target = resolveWriteTarget(config, bundle, { requireWritable: false, flag: "The asset ref's bundle" });
701
- return {
702
- file: await resolveAssetPath(target.source.path, type, name),
703
- bundleRoot: target.source.path,
704
- };
703
+ return resolveBundleAsset(config, { bundle, type, name });
705
704
  };
706
705
  }
707
706
  /**
@@ -722,44 +721,27 @@ export function resolveTaskReadBundle(refBundle, flagBundle) {
722
721
  resolved = resolveWorkingStashTarget(config, { requireWritable: false });
723
722
  }
724
723
  else {
725
- const configured = resolveActiveConfiguredSources(config).some((source) => source.name === selector);
726
- const implicit = configured ? undefined : resolveImplicitScheduledBundleTarget(config, selector);
727
- resolved =
728
- implicit ??
729
- resolveWriteTarget(config, selector, {
730
- requireWritable: false,
731
- // The selector is `--bundle` when given, else the task ref's own bundle qualifier.
732
- flag: flagBundle !== undefined ? "--bundle" : "The task ref's bundle",
733
- });
724
+ // Reading needs no write target: any bundle of the active set resolves.
725
+ const sources = activeTaskSources(config);
726
+ const source = resolveSourcesForOrigin(selector, sources)[0];
727
+ if (!source) {
728
+ assertBundleNotDisabled(config, selector);
729
+ throw new UsageError(`${flagBundle !== undefined ? "--bundle" : "The task ref's bundle"} must reference a source name from your config. No source named "${selector}" is configured. Run \`akm bundle list\` to see available sources.`, "INVALID_FLAG_VALUE");
730
+ }
731
+ resolved = {
732
+ source: {
733
+ kind: source.type ?? "filesystem",
734
+ name: selector,
735
+ path: source.path,
736
+ adapterId: source.adapterId ?? detectAdapterId(source.path),
737
+ },
738
+ };
734
739
  }
735
740
  if (refBundle && resolved.source.name !== refBundle) {
736
741
  throw new UsageError(`Task ref bundle ${JSON.stringify(refBundle)} does not match the resolved source.`, "INVALID_FLAG_VALUE");
737
742
  }
738
743
  return resolved;
739
744
  }
740
- /**
741
- * New scheduler bindings always carry a canonical `--bundle <owner>` token,
742
- * including an env-selected working stash. That stash need not be persisted in
743
- * config (CI, one-shot tools, and fresh installs commonly use only
744
- * AKM_BUNDLE_DIR), so its scheduled child — every row sets AKM_BUNDLE_DIR to
745
- * the working stash it was synced from — must accept precisely its derived
746
- * owner name.
747
- *
748
- * This is intentionally narrower than an unknown-bundle fallback: a configured
749
- * source always wins, and an unconfigured selector is accepted only when it is
750
- * exactly the current env-selected working stash identity.
751
- */
752
- function resolveImplicitScheduledBundleTarget(config, selector) {
753
- if (!process.env.AKM_BUNDLE_DIR?.trim())
754
- return undefined;
755
- try {
756
- const working = resolveWorkingStashTarget(config, { requireWritable: false });
757
- return working.source.name === selector ? working : undefined;
758
- }
759
- catch {
760
- return undefined;
761
- }
762
- }
763
745
  /** True when `candidate` resolves to the same directory as the primary stash. */
764
746
  function isPrimaryStashPath(candidate) {
765
747
  let primary;
@@ -7,15 +7,15 @@
7
7
  * anything secret-like by key name, because the UI shows config the CLI's
8
8
  * `config list` never had to defend against a browser leaking.
9
9
  *
10
- * Symbolic references (`$VAR`, `${VAR}`, `secret://name`) are kept: they say
10
+ * Symbolic references (`$VAR`, `${VAR}`, `${secrets/name}`) are kept: they say
11
11
  * where a credential comes from without being one.
12
12
  */
13
+ import { isApiKeyReference } from "../../core/config/schema/primitives.js";
13
14
  const REDACTED = "[redacted]";
14
15
  /** Key names whose string values are credentials, or sit in a bag of them. */
15
16
  const SECRET_KEY = /(api[-_]?key|token|password|passwd|secret|credential|authorization|bearer|private[-_]?key)/i;
16
17
  /** A whole subtree of values (`env`, `secrets`) that is data, not settings. */
17
18
  const SECRET_BAG_KEY = /^(env|secrets?)$/i;
18
- const SYMBOLIC_REFERENCE = /^(\$[A-Za-z_][A-Za-z0-9_]*|\$\{[A-Za-z_][A-Za-z0-9_]*\}|secret:\/\/.+)$/;
19
19
  /** Query parameters that carry a credential in a URL. */
20
20
  const SECRET_QUERY_PARAM = /^(api[-_]?key|key|token|access[-_]?token|auth|password|secret|sig|signature)$/i;
21
21
  /** `scheme://user:pass@host/?token=x` with the credentials masked. Anything that is not a URL is returned as is. */
@@ -65,7 +65,7 @@ export function redactForUi(value, inSecretBag = false) {
65
65
  if (Array.isArray(value))
66
66
  return value.map((child) => redactForUi(child, inSecretBag));
67
67
  if (typeof value === "string") {
68
- if (SYMBOLIC_REFERENCE.test(value))
68
+ if (isApiKeyReference(value))
69
69
  return value;
70
70
  return inSecretBag ? REDACTED : redactUrlCredentials(value);
71
71
  }
@@ -73,7 +73,7 @@ export function redactForUi(value, inSecretBag = false) {
73
73
  return inSecretBag && value !== null ? REDACTED : value;
74
74
  const result = {};
75
75
  for (const [key, child] of Object.entries(value)) {
76
- if (typeof child === "string" && SECRET_KEY.test(key) && !SYMBOLIC_REFERENCE.test(child)) {
76
+ if (typeof child === "string" && SECRET_KEY.test(key) && !isApiKeyReference(child)) {
77
77
  result[key] = REDACTED;
78
78
  }
79
79
  else {
@@ -44,7 +44,7 @@ function positional(body, key) {
44
44
  }
45
45
  function preserveUnrelatedConfig(config = loadConfig()) {
46
46
  if (!isDeepStrictEqual(sanitizeConfigForWrite(config, { quiet: true }), config)) {
47
- throw new UsageError("AKM's config sanitizer would remove unrelated credentials. Move literal API keys to $VAR or secret:// references and remove registry URL credentials using the CLI before changing bundle configuration in the UI.");
47
+ throw new UsageError("AKM's config sanitizer would remove unrelated credentials. Move literal API keys to $VAR or ${secrets/name} references and remove registry URL credentials using the CLI before changing bundle configuration in the UI.");
48
48
  }
49
49
  }
50
50
  /** The error kind behind each exit code of the CLI contract (`EXIT_CODES`, `src/cli/shared.ts`). */
@@ -8,18 +8,22 @@
8
8
  * (`loadWorkflowAsset`, `freezeWorkflow`) and NOTHING else — never
9
9
  * `publishWorkflowRunV4`, `startWorkflowRun`, `appendEvent`, or `akmIndex`.
10
10
  *
11
- * SECRET-FREE, by construction (§4.6's closed print list): a resolved
12
- * reference VALUE is never printed (references resolve at pre-attempt, not
11
+ * SECRET-FREE, by construction (§4.6's closed print list): a step's
12
+ * `orchestration` is the authored argv, cwd and env names plus the
13
+ * bundle-qualified refs they resolve to (the projection `akm show` prints,
14
+ * never a value or a path); a resolved reference VALUE is never printed (references resolve at pre-attempt, not
13
15
  * here); a `literal` **environment** binding's value is never printed (only
14
- * `environment[].kind`/`.name`, and an `env-ref`'s `.ref`/`.keys`/
15
- * `.secretNames` — all NAMES); `request.command.content`,
16
+ * `environment[].kind`/`.name`, and an `env-ref`'s `.ref` — all NAMES);
17
+ * `request.command.content`,
16
18
  * `request.persona`, `request.conversation`, `request.runtime.environment`,
17
19
  * and a script target's `bytesBase64` are never read at all.
18
20
  */
19
21
  import { loadConfig } from "../../core/config/config.js";
22
+ import { NotFoundError } from "../../core/errors.js";
20
23
  import { buildExecutionFromWire } from "../../integrations/agent/execution.js";
21
24
  import { freezeWorkflow } from "../../workflows/freeze/freeze.js";
22
25
  import { computePlanHash } from "../../workflows/ir/plan-hash.js";
26
+ import { resolveOrchestrationRefs, summarizeStepOrchestration } from "../../workflows/renderer.js";
23
27
  import { loadWorkflowAsset } from "../../workflows/runtime/workflow-asset-loader.js";
24
28
  /** The step's dispatch unit — the map template for a fan-out, else the root unit. Undefined for a step not yet frozen. */
25
29
  function stepUnit(step) {
@@ -30,7 +34,7 @@ function stepUnit(step) {
30
34
  }
31
35
  function projectEnvironmentBinding(binding) {
32
36
  if (binding.kind === "env-ref") {
33
- return { kind: binding.kind, ref: binding.ref, keys: binding.keys, secretNames: binding.secretNames };
37
+ return { kind: binding.kind, ref: binding.ref };
34
38
  }
35
39
  // literal / pass-through: kind + name only — a literal's VALUE is never printed.
36
40
  return { kind: binding.kind, name: binding.name };
@@ -46,14 +50,23 @@ function childExpansion(target) {
46
50
  via: "child",
47
51
  childRef: target.ref,
48
52
  childPlanHash: target.planHash,
49
- steps: target.frozenPlan.steps.map((step, index) => projectStep(step, index)),
53
+ steps: target.frozenPlan.steps.map((step, index) => projectStep(step, index, stepOrchestration(step, target.frozenPlan))),
50
54
  };
51
55
  }
52
56
  /** The child expansion boundary for one step (§4.6). */
53
57
  function stepExpansion(frozenTarget) {
54
58
  return frozenTarget?.kind === "child-workflow" ? childExpansion(frozenTarget) : { via: "direct" };
55
59
  }
56
- function projectStep(step, sequenceIndex) {
60
+ /**
61
+ * The step's `show` orchestration projection plus the refs it resolves to.
62
+ * `authored` is the compiled step (it keeps the spelling the author wrote);
63
+ * a child plan's steps have only their frozen form.
64
+ */
65
+ function stepOrchestration(step, plan) {
66
+ const summary = summarizeStepOrchestration(step, plan.defaults);
67
+ return summary && resolveOrchestrationRefs(summary);
68
+ }
69
+ function projectStep(step, sequenceIndex, orchestration) {
57
70
  const unit = stepUnit(step);
58
71
  const frozenTarget = unit?.frozenTarget;
59
72
  const kind = step.root?.kind === "map" ? "map" : "unit";
@@ -65,6 +78,7 @@ function projectStep(step, sequenceIndex) {
65
78
  targetKind: frozenTarget?.kind ?? null,
66
79
  ...(step.root?.kind === "map" ? { concurrency: step.root.concurrency } : {}),
67
80
  inputs: unit?.inputs ?? [],
81
+ ...(orchestration ? { orchestration } : {}),
68
82
  environment: (unit?.environment ?? []).map(projectEnvironmentBinding),
69
83
  ...(inputBindings && inputBindings.length > 0 ? { inputBindings: inputBindings.map(projectInputBinding) } : {}),
70
84
  gate: {
@@ -102,15 +116,40 @@ function collectLoweringNotices(plan, config) {
102
116
  }
103
117
  return notices;
104
118
  }
119
+ /** Every ref the steps' orchestration names that no bundle owns, as `ref (where of step "id")`. */
120
+ function unresolvedRefs(steps, orchestrations) {
121
+ return orchestrations.flatMap((orchestration, index) => (orchestration?.unresolved ?? []).map((missing) => `${missing.ref} (${missing.in} of step "${steps[index]?.stepId}")`));
122
+ }
123
+ /** The unresolved refs of every embedded child plan, recursively, each prefixed with the child workflow it is in. */
124
+ function unresolvedInChildren(plan) {
125
+ return plan.steps.flatMap((step) => {
126
+ const target = stepUnit(step)?.frozenTarget;
127
+ if (target?.kind !== "child-workflow")
128
+ return [];
129
+ const child = target.frozenPlan;
130
+ const own = unresolvedRefs(child.steps, child.steps.map((childStep) => stepOrchestration(childStep, child)));
131
+ return [...own.map((entry) => `${entry} in child ${target.ref}`), ...unresolvedInChildren(child)];
132
+ });
133
+ }
134
+ function failOnUnresolved(ref, unresolved) {
135
+ if (unresolved.length === 0)
136
+ return;
137
+ throw new NotFoundError(`Workflow ${ref} names asset(s) that no bundle owns: ${unresolved.join(", ")}.`, "FILE_NOT_FOUND");
138
+ }
105
139
  /**
106
140
  * Compile + freeze `ref` and project the frozen plan into the read-only
107
141
  * `akm workflow plan` envelope. Never publishes, never writes, never warns.
108
142
  */
109
143
  export async function akmWorkflowPlan(ref) {
110
144
  const asset = await loadWorkflowAsset(ref);
145
+ // Resolve what the authored steps name before freezing: a `${ref}`, env ref or child workflow no bundle owns fails the plan.
146
+ const orchestrations = asset.plan.steps.map((step) => stepOrchestration(step, asset.plan));
147
+ failOnUnresolved(asset.ref, unresolvedRefs(asset.plan.steps, orchestrations));
111
148
  const config = loadConfig();
112
149
  const frozen = await freezeWorkflow(asset, config);
113
150
  const plan = frozen.plan;
151
+ // A child workflow's steps exist only once frozen; they fail the plan exactly as the top level's do.
152
+ failOnUnresolved(asset.ref, unresolvedInChildren(plan));
114
153
  return {
115
154
  ok: true,
116
155
  ref: asset.ref,
@@ -123,7 +162,7 @@ export async function akmWorkflowPlan(ref) {
123
162
  execution: plan.execution,
124
163
  ...(plan.budget ? { budget: plan.budget } : {}),
125
164
  ...(plan.params ? { params: plan.params } : {}),
126
- steps: plan.steps.map((step, index) => projectStep(step, index)),
165
+ steps: plan.steps.map((step, index) => projectStep(step, index, orchestrations[index])),
127
166
  notices: collectLoweringNotices(plan, config),
128
167
  warnings: frozen.warnings.map((warning) => warning.message),
129
168
  };
@@ -2,8 +2,9 @@
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
  /**
5
- * `akm workflow` command family. `run` is the canonical start/resume/execute
6
- * surface; the former public `start`, `next`, and `complete` lifecycle is gone.
5
+ * `akm workflow` command family. `run` is the canonical start/execute surface
6
+ * (a ref starts a new run, a run id drives that run) and `resume` reopens a
7
+ * run; the former public `start`, `next`, and `complete` lifecycle is gone.
7
8
  * `create --print` emits Markdown, the only workflow source format. Validate
8
9
  * with `akm lint --type workflows`.
9
10
  */
@@ -13,7 +14,6 @@ import { armAbortDeadline } from "../core/abort-deadline.js";
13
14
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
14
15
  import { NotFoundError, TransientError, UsageError } from "../core/errors.js";
15
16
  import { warn } from "../core/warn.js";
16
- import { akmIndex } from "../indexer/indexer.js";
17
17
  import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
18
18
  import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/resource-limits.js";
19
19
  import { abandonWorkflowRun, getWorkflowStatus, listWorkflowRuns, resolveWorkflowRunTarget, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
@@ -21,7 +21,7 @@ import { akmWorkflowPlan } from "./workflow/plan.js";
21
21
  const workflowStatusCommand = defineJsonCommand({
22
22
  meta: {
23
23
  name: "status",
24
- description: "Show full workflow run state for review or resume; workflow refs resolve within the current scope",
24
+ description: "Show full workflow run state for review or resume; a workflow ref shows its most recent run",
25
25
  },
26
26
  args: {
27
27
  target: { type: "positional", description: "Workflow run id or workflow ref (workflows/<name>)", required: true },
@@ -31,16 +31,17 @@ const workflowStatusCommand = defineJsonCommand({
31
31
  "diagnostic text). Diagnostics only — step evidence stays deterministic and is unaffected.",
32
32
  default: false,
33
33
  },
34
+ // Deprecated no-op, accepted through 0.10 so scripts that pass it do not
35
+ // fail with UNKNOWN_FLAG. Runs are no longer scoped to a directory.
34
36
  "all-scopes": {
35
37
  type: "boolean",
36
- description: "When resolving a workflow ref (not a run id), search every scope instead of only the current one (#942).",
38
+ description: "Deprecated, no effect: runs are host-wide, not scoped to a directory. Accepted through 0.10 for existing scripts.",
37
39
  default: false,
38
40
  },
39
41
  },
40
42
  async run({ args }) {
41
43
  const target = args.target;
42
44
  const includeUnits = args.units === true;
43
- const allScopes = args["all-scopes"] === true;
44
45
  const resolvedRunId = await resolveWorkflowRunTarget(target);
45
46
  if (resolvedRunId !== undefined) {
46
47
  const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
@@ -48,9 +49,8 @@ const workflowStatusCommand = defineJsonCommand({
48
49
  return;
49
50
  }
50
51
  let runs;
51
- let scopeKey;
52
52
  try {
53
- ({ runs, scopeKey } = await listWorkflowRuns({ workflowRef: target, allScopes }));
53
+ ({ runs } = await listWorkflowRuns({ workflowRef: target }));
54
54
  }
55
55
  catch (error) {
56
56
  if (!target.includes(":") && !target.includes("/")) {
@@ -60,14 +60,6 @@ const workflowStatusCommand = defineJsonCommand({
60
60
  }
61
61
  const mostRecent = runs[0];
62
62
  if (!mostRecent) {
63
- // #942: name the scope actually searched and point at `--all-scopes`
64
- // rather than a bare "not found" — the ref-fallthrough lookup is
65
- // scope-local by default, so "no runs" here means "none in THIS
66
- // scope", not "none anywhere". Already searching every scope (or no
67
- // real scope was filtered on) has nothing more specific to suggest.
68
- if (!allScopes && scopeKey !== null) {
69
- throw new NotFoundError(`No workflow runs found for ${target} in scope ${scopeKey}.`, "WORKFLOW_NOT_FOUND", `Run 'akm workflow status ${target} --all-scopes' to search every scope.`);
70
- }
71
63
  throw new NotFoundError(`No workflow runs found for ${target}`, "WORKFLOW_NOT_FOUND");
72
64
  }
73
65
  const result = await getWorkflowStatus(mostRecent.id, { includeUnits });
@@ -77,9 +69,7 @@ const workflowStatusCommand = defineJsonCommand({
77
69
  const workflowListCommand = defineJsonCommand({
78
70
  meta: {
79
71
  name: "list",
80
- description: "List workflow runs in the current working scope. A scheduled task runs from the scheduler's cwd, so its runs " +
81
- "sit in another scope: get the run id from `akm task history <task-id> --limit 1` (detail.runId) and pass it " +
82
- "to `akm workflow status <run-id>`, which works from any directory, or pass --all-scopes.",
72
+ description: "List workflow runs on this host, most recently updated first",
83
73
  },
84
74
  args: {
85
75
  ref: { type: "string", description: "Filter to one workflow ref" },
@@ -89,11 +79,11 @@ const workflowListCommand = defineJsonCommand({
89
79
  description: "Also include child workflow runs (hidden by default, P3b)",
90
80
  default: false,
91
81
  },
82
+ // Deprecated no-op, accepted through 0.10 so scripts that pass it do not
83
+ // fail with UNKNOWN_FLAG. Runs are no longer scoped to a directory.
92
84
  "all-scopes": {
93
85
  type: "boolean",
94
- description: "Search every scope instead of only the current one (#942). The envelope's top-level `scopeKey` is " +
95
- "`null` with this flag, otherwise the scope that was searched — so an empty `runs: []` is never " +
96
- 'indistinguishable from "nothing anywhere".',
86
+ description: "Deprecated, no effect: runs are host-wide, not scoped to a directory. Accepted through 0.10 for existing scripts.",
97
87
  default: false,
98
88
  },
99
89
  },
@@ -102,7 +92,6 @@ const workflowListCommand = defineJsonCommand({
102
92
  workflowRef: args.ref,
103
93
  activeOnly: args.active,
104
94
  includeChildren: args.children,
105
- allScopes: args["all-scopes"],
106
95
  });
107
96
  output("workflow-list", result);
108
97
  },
@@ -158,12 +147,6 @@ const workflowCreateCommand = defineJsonCommand({
158
147
  from: args.from,
159
148
  force: args.force,
160
149
  });
161
- // Index the newly-written workflow so `akm workflow run` can resolve
162
- // a workflowEntryId without requiring an explicit `akm index` call
163
- // first. Uses the same incremental index path that `akm add` uses.
164
- // `result.bundleDir` — the indexer's own `IndexOptions.stashDir` field
165
- // name is unchanged (indexer vocabulary is out of scope, P4 row B-50).
166
- await akmIndex({ stashDir: result.bundleDir });
167
150
  output("workflow-create", { ok: true, ...result });
168
151
  },
169
152
  });
@@ -184,29 +167,34 @@ const WORKFLOW_RUN_SKIP_REASONS = {
184
167
  const workflowRunCommand = defineJsonCommand({
185
168
  meta: {
186
169
  name: "run",
187
- description: "Start or resume a workflow and execute it through completion, failure, a verification gate, or an explicit limit",
170
+ description: "Start a new run of a workflow ref, or drive an existing run by id, through completion, failure, a verification gate, or an explicit limit",
188
171
  },
189
172
  args: {
190
- target: { type: "positional", description: "Workflow run id or workflow ref (auto-starts a run)", required: true },
173
+ target: {
174
+ type: "positional",
175
+ description: "Workflow ref (starts a new run) or workflow run id (drives that run)",
176
+ required: true,
177
+ },
191
178
  "max-steps": { type: "string", description: "Stop after executing this many steps" },
192
179
  "max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
193
180
  timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
181
+ // Deprecated no-op, accepted through 0.10 so scripts that pass it do not
182
+ // fail with UNKNOWN_FLAG. A workflow ref always starts a new run now.
194
183
  new: {
195
184
  type: "boolean",
196
- description: "Start a fresh run even if one is already active for this ref, leaving the existing run untouched " +
197
- "(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
185
+ description: "Deprecated, no effect: a workflow ref always starts a new run. Accepted through 0.10 for existing scripts.",
198
186
  default: false,
199
187
  },
200
188
  "skip-if-locked": {
201
189
  type: "boolean",
202
- description: "If another akm process is already driving this run, or state.db is busy with another " +
203
- "writer, skip gracefully (exit 0) instead of failing (exit 75). Use for high-frequency scheduled runs " +
204
- "so they don't pile up failures while a longer-running invocation is in progress.",
190
+ description: "If another akm process is already driving this run (or, for a workflow ref, an active run of it), or " +
191
+ "state.db is busy with another writer, skip gracefully (exit 0) instead of failing (exit 75). Use for " +
192
+ "high-frequency scheduled runs so they don't pile up failures while a longer-running invocation is in progress.",
205
193
  default: false,
206
194
  },
207
195
  },
208
196
  async run({ args, rawArgs }) {
209
- const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
197
+ const { findLiveLockedRunOfRef, runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
210
198
  const parameterFlags = parseWorkflowParameterFlags(rawArgs, args.target);
211
199
  const maxSteps = parseIntegerFlag(getStringArg(args, "max-steps"), "--max-steps", 1);
212
200
  const maxRetries = parseIntegerFlag(getStringArg(args, "max-retries"), "--max-retries", 0);
@@ -228,15 +216,25 @@ const workflowRunCommand = defineJsonCommand({
228
216
  timeoutMs,
229
217
  reason: `Workflow run timed out after ${timeoutMs}ms.`,
230
218
  });
219
+ const skip = (reason, message) => {
220
+ warn(`[workflow] ${message} skipping (--skip-if-locked)`);
221
+ output("workflow-run", { ok: true, target: args.target, skipped: { reason, message } });
222
+ };
231
223
  try {
232
224
  let result;
233
225
  try {
226
+ // #1239: a ref start never conflicts with another run of the ref, so
227
+ // the one extra condition is an active run of it a live process is driving.
228
+ const held = skipIfLocked ? await findLiveLockedRunOfRef(args.target) : undefined;
229
+ if (held) {
230
+ skip("lock-held", `Workflow run ${held.runId} of ${args.target} is already being driven by another akm process (pid ${held.holder}).`);
231
+ return;
232
+ }
234
233
  result = await runWorkflowSteps({
235
234
  target: args.target,
236
235
  parameterFlags,
237
236
  ...(maxSteps !== undefined ? { maxSteps } : {}),
238
237
  ...(maxRetries !== undefined ? { maxRetries } : {}),
239
- newRun: args.new,
240
238
  signal: controller.signal,
241
239
  });
242
240
  }
@@ -247,9 +245,7 @@ const workflowRunCommand = defineJsonCommand({
247
245
  // off UsageError) — a bad flag or malformed input still fails loudly
248
246
  // even with the flag set.
249
247
  if (skipIfLocked && err instanceof TransientError && WORKFLOW_RUN_SKIP_REASONS[err.code]) {
250
- const reason = WORKFLOW_RUN_SKIP_REASONS[err.code];
251
- warn(`[workflow] ${err.message} skipping (--skip-if-locked)`);
252
- output("workflow-run", { ok: true, target: args.target, skipped: { reason, message: err.message } });
248
+ skip(WORKFLOW_RUN_SKIP_REASONS[err.code], err.message);
253
249
  return;
254
250
  }
255
251
  throw err;
@@ -83,6 +83,7 @@ import path from "node:path";
83
83
  import { applyPostContributorFields, applyPreContributorFields, extractPackageMetadata, getMarkdownFragmentContent, hasMarkdownFragmentContent, setMarkdownFragmentContent, } from "../../../indexer/passes/metadata.js";
84
84
  import { assetPathCandidatesForName, deriveCanonicalAssetNameFromStashRoot, placementTypes, stashDirFor, stashDirNames, } from "../../asset/asset-placement.js";
85
85
  import { parseFrontmatter } from "../../asset/frontmatter.js";
86
+ import { sensitiveMarkerPath } from "../../sensitive-marker-path.js";
86
87
  import { executionDefaultsFromFrontmatter, renderMarkdownExecutionSource } from "../execution-source.js";
87
88
  import { recognizeMatch } from "../recognize-match.js";
88
89
  import { perTypeValidateChecks, skillDirectoryDiagnostics } from "./akm-lint.js";
@@ -136,7 +137,7 @@ function akmStashAbstains(root, absPath) {
136
137
  return true;
137
138
  // Skip env `.env` files that have a sibling `.sensitive` marker file.
138
139
  if (segments[0] === "env" && (absPath.endsWith(".env") || path.basename(absPath) === ".env")) {
139
- if (fs.existsSync(absPath.replace(/\.env$/, ".sensitive")))
140
+ if (fs.existsSync(sensitiveMarkerPath(absPath, "env")))
140
141
  return true;
141
142
  }
142
143
  // Skip secret files that are themselves a `.sensitive` marker, or that have a
@@ -145,7 +146,7 @@ function akmStashAbstains(root, absPath) {
145
146
  if (segments[0] === "secrets") {
146
147
  if (absPath.endsWith(".sensitive") || absPath.endsWith(".lock"))
147
148
  return true;
148
- if (fs.existsSync(`${absPath}.sensitive`))
149
+ if (fs.existsSync(sensitiveMarkerPath(absPath, "secret")))
149
150
  return true;
150
151
  }
151
152
  // Inside a `wikis/<name>/` directory, the root-level infrastructure files
@@ -439,7 +440,6 @@ async function validate(c, changes, ctx) {
439
440
  body: parsed.content,
440
441
  ext: overlay.ext,
441
442
  ctx,
442
- workspaceRoot: c.root,
443
443
  })));
444
444
  diagnostics.push(...(await skillDirectoryDiagnostics(change.path, seenSkillDirs, ctx)));
445
445
  }
@@ -207,7 +207,7 @@ export async function skillDirectoryDiagnostics(relPath, seen, ctx, skillDirs =
207
207
  * routes through `ctx`.
208
208
  */
209
209
  export async function perTypeValidateChecks(args) {
210
- const { type, relPath, raw, data, frontmatter, body, ext, ctx, workspaceRoot } = args;
210
+ const { type, relPath, raw, data, frontmatter, body, ext, ctx } = args;
211
211
  switch (type) {
212
212
  case "command":
213
213
  return nameOrTypeDiagnostics(relPath, data, frontmatter, ["command"]);
@@ -216,7 +216,7 @@ export async function perTypeValidateChecks(args) {
216
216
  case "fact":
217
217
  return factDiagnostics(relPath, data);
218
218
  case "task":
219
- return taskDiagnostics(relPath, raw, workspaceRoot);
219
+ return taskDiagnostics(relPath, raw);
220
220
  case "workflow":
221
221
  return ext === ".md" ? workflowDiagnostics(relPath, raw, body) : [];
222
222
  case "memory":
@@ -266,12 +266,11 @@ export function factDiagnostics(relPath, data) {
266
266
  * contract; the cron suits the local scheduler backend), so `akm lint --type
267
267
  * tasks` never reports clean a task that sync would refuse.
268
268
  */
269
- export function taskDiagnostics(relPath, raw, workspaceRoot) {
269
+ export function taskDiagnostics(relPath, raw) {
270
270
  try {
271
271
  const parsed = parseTaskSource({
272
272
  filePath: relPath,
273
273
  yaml: raw,
274
- ...(workspaceRoot ? { workspaceRoot } : {}),
275
274
  });
276
275
  assertTaskScheduleInputsSatisfyContract(parsed.v4, relPath);
277
276
  assertTaskScheduleCronValid(parsed.v4, backendNameForPlatform());
@@ -170,7 +170,7 @@ export function foldRecognizedMetadata(rendererName, file) {
170
170
  case "task-yaml": {
171
171
  out.tags = Array.from(new Set([...(out.tags ?? []), "task", "scheduled"]));
172
172
  try {
173
- const parsed = parseTaskSource({ yaml: file.content(), filePath: file.absPath, workspaceRoot: file.stashRoot });
173
+ const parsed = parseTaskSource({ yaml: file.content(), filePath: file.absPath });
174
174
  const v4 = parsed.v4;
175
175
  const target = v4.target;
176
176
  const hints = new Set();
@@ -261,7 +261,7 @@ export function foldRecognizedMetadata(rendererName, file) {
261
261
  // ── workflow-document-metadata (historical renderer id `workflow-md`, peer sources) ──
262
262
  case "workflow-md": {
263
263
  try {
264
- const result = compileWorkflowSource(file.content(), { path: file.relPath, workspaceRoot: file.stashRoot });
264
+ const result = compileWorkflowSource(file.content(), { path: file.relPath });
265
265
  if (!result.ok)
266
266
  return out;
267
267
  const plan = result.plan;