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
@@ -89,7 +89,6 @@ export function listSetupTaskDefinitions() {
89
89
  const parsed = parseTaskSource({
90
90
  yaml: fs.readFileSync(filePath, "utf8"),
91
91
  filePath,
92
- workspaceRoot: target.source.path,
93
92
  });
94
93
  const document = parsed.v4;
95
94
  if (document.schedule.length === 0)
@@ -129,7 +128,7 @@ export async function prepareSetupTaskDefinitions(tasks, deps = {}) {
129
128
  document.schedule = plan.schedule;
130
129
  yaml = yamlStringify(document);
131
130
  }
132
- const parsed = parseTaskSource({ yaml, filePath, workspaceRoot: target.source.path });
131
+ const parsed = parseTaskSource({ yaml, filePath });
133
132
  for (const schedule of parsed.v4.schedule) {
134
133
  parseSchedule(schedule.cron, backendNameForPlatform());
135
134
  }
@@ -82,6 +82,18 @@ export function buildInstallCacheDir(cacheRootDir, source, id, version) {
82
82
  const versionSlug = source === "local" ? uniqueSlug() : (version?.replace(/[^a-zA-Z0-9_.-]+/g, "-") ?? uniqueSlug());
83
83
  return path.join(cacheRootDir, slug || source, versionSlug);
84
84
  }
85
+ /**
86
+ * The `<rev>` directory of a registry install that holds `contentDir`: the two
87
+ * path segments under `cacheRootDir` that {@link buildInstallCacheDir} lays out
88
+ * (`<source>-<id>/<version>`). Undefined for a directory outside that layout.
89
+ */
90
+ export function installRevisionDir(cacheRootDir, contentDir) {
91
+ const relative = path.relative(path.resolve(cacheRootDir), path.resolve(contentDir));
92
+ if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative))
93
+ return undefined;
94
+ const [slug, rev] = relative.split(path.sep);
95
+ return slug && rev ? path.join(path.resolve(cacheRootDir), slug, rev) : undefined;
96
+ }
85
97
  /**
86
98
  * Apply an `akm.include` config (a `package.json` `akm.include` array — NOT
87
99
  * a `.akm-include` dotfile) if any, by copying the selected paths into a
@@ -6,11 +6,14 @@ import path from "node:path";
6
6
  import { recognizeMatch } from "../core/adapter/recognize-match.js";
7
7
  import { assetPathForName, deriveCanonicalAssetNameFromStashRoot, isRelevantAssetFile, stashDirFor, } from "../core/asset/asset-placement.js";
8
8
  import { conceptIdFromTypeName } from "../core/asset/resolve-ref.js";
9
- import { hasErrnoCode, isWithin } from "../core/common.js";
9
+ import { hasErrnoCode, isContainedResolvedPath } from "../core/common.js";
10
10
  import { NotFoundError, UsageError } from "../core/errors.js";
11
11
  import { walkStashFlat } from "../indexer/walk/walker.js";
12
12
  /**
13
- * Resolve an asset path from a stash directory, type, and name.
13
+ * Resolve an asset path from a stash directory, type, and name. The path keeps
14
+ * the authored spelling, not the symlink target's: an asset that links to a
15
+ * file with another name (or none) is still the kind its own name says, and a
16
+ * read through it reaches the same bytes.
14
17
  */
15
18
  export async function resolveAssetPath(stashDir, type, name) {
16
19
  try {
@@ -31,25 +34,24 @@ export async function resolveAssetPath(stashDir, type, name) {
31
34
  function resolveInTypeDir(stashDir, typeDir, type, name) {
32
35
  const root = path.join(stashDir, typeDir);
33
36
  const target = assetPathForName(type, root, name);
34
- const resolvedRoot = resolveAndValidateTypeRoot(root, type, name);
37
+ resolveAndValidateTypeRoot(root, type, name);
35
38
  const resolvedTarget = path.resolve(target);
36
- if (!isWithin(resolvedTarget, resolvedRoot)) {
39
+ // Name grammar: the name must not climb out of the type directory (`..` or
40
+ // an absolute name). Lexical only: an asset that is a symlink to a file
41
+ // elsewhere resolves, because installing a bundle is the trust decision.
42
+ if (!isContainedResolvedPath(resolvedTarget, path.resolve(root))) {
37
43
  throw new UsageError("Ref resolves outside the stash root.", "PATH_ESCAPE_VIOLATION");
38
44
  }
39
45
  if (!fs.existsSync(resolvedTarget) || !fs.statSync(resolvedTarget).isFile()) {
40
46
  throw new NotFoundError(`Stash asset not found for ref: ${conceptIdFromTypeName(type, name)}`, "ASSET_NOT_FOUND");
41
47
  }
42
- const realTarget = fs.realpathSync(resolvedTarget);
43
- if (!isWithin(realTarget, resolvedRoot)) {
44
- throw new UsageError("Ref resolves outside the stash root.", "PATH_ESCAPE_VIOLATION");
45
- }
46
48
  if (!isRelevantAssetFile(type, path.basename(resolvedTarget))) {
47
49
  if (type === "script") {
48
50
  throw new NotFoundError("Script ref must resolve to a file with a supported script extension. Refer to the akm documentation for the complete list of supported script extensions.");
49
51
  }
50
52
  throw new NotFoundError(`Stash asset not found for ref: ${conceptIdFromTypeName(type, name)}`, "ASSET_NOT_FOUND");
51
53
  }
52
- return realTarget;
54
+ return resolvedTarget;
53
55
  }
54
56
  function resolveAndValidateTypeRoot(root, type, name) {
55
57
  const rootStat = readTypeRootStat(root, type, name);
@@ -57,7 +59,6 @@ function resolveAndValidateTypeRoot(root, type, name) {
57
59
  throw new NotFoundError(`Asset directory for ${type} assets is not accessible — got a file where a directory was expected for ref: ${conceptIdFromTypeName(type, name)}. ` +
58
60
  "Run `akm index` to rebuild the index, or check your source configuration.", "ASSET_NOT_FOUND", "Run `akm bundle list` to see your configured sources and verify the source path exists.");
59
61
  }
60
- return fs.realpathSync(root);
61
62
  }
62
63
  function readTypeRootStat(root, type, name) {
63
64
  try {
@@ -79,12 +80,7 @@ async function resolveByCanonicalName(stashDir, type, name) {
79
80
  const canonicalName = deriveCanonicalAssetNameFromStashRoot(type, stashDir, ctx.absPath);
80
81
  if (canonicalName !== normalizedName)
81
82
  continue;
82
- const realTarget = fs.realpathSync(ctx.absPath);
83
- const resolvedRoot = fs.realpathSync(stashDir);
84
- if (!isWithin(realTarget, resolvedRoot)) {
85
- throw new UsageError("Ref resolves outside the stash root.", "PATH_ESCAPE_VIOLATION");
86
- }
87
- return realTarget;
83
+ return ctx.absPath;
88
84
  }
89
85
  return undefined;
90
86
  }
@@ -1,8 +1,8 @@
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
4
  import { resolveSecretPath } from "../../core/env-secret-ref.js";
5
+ import { readValue } from "../../core/secret-value.js";
6
6
  /**
7
7
  * Reads a secret out of akm's secret store for the fetcher `resolveSecret`
8
8
  * seam (see `FetcherContext.resolveSecret`).
@@ -21,9 +21,7 @@ import { resolveSecretPath } from "../../core/env-secret-ref.js";
21
21
  export function resolveSecretFromStore(ref) {
22
22
  try {
23
23
  const { absPath } = resolveSecretPath(ref);
24
- if (!fs.existsSync(absPath))
25
- return null;
26
- return fs.readFileSync(absPath, "utf8").trim() || null;
24
+ return readValue(absPath) || null;
27
25
  }
28
26
  catch {
29
27
  return null;
@@ -89,7 +89,7 @@ export function resolveXBearerToken(context) {
89
89
  if (fromEnv)
90
90
  return fromEnv;
91
91
  try {
92
- return context?.resolveSecret?.(X_BEARER_TOKEN_SECRET_REF)?.trim() || null;
92
+ return context?.resolveSecret?.(X_BEARER_TOKEN_SECRET_REF) || null;
93
93
  }
94
94
  catch {
95
95
  return null;
@@ -525,8 +525,8 @@ export function renameProposalsBundleRef(db, oldBundleId, newBundleId) {
525
525
  const prefix = `${escapeLikePattern(oldBundleId)}//`;
526
526
  return db.transaction(() => {
527
527
  const refs = Number(db
528
- .prepare(`UPDATE proposals SET ref = ? || substr(ref, ?) WHERE ref LIKE ? ESCAPE '\\'`)
529
- .run(newBundleId, oldBundleId.length + 1, `${prefix}%`).changes);
528
+ .prepare(`UPDATE proposals SET ref = ? || substr(ref, length(?) + 1) WHERE ref LIKE ? ESCAPE '\\'`)
529
+ .run(newBundleId, oldBundleId, `${prefix}%`).changes);
530
530
  const targets = Number(db
531
531
  .prepare(`UPDATE proposals SET metadata_json = json_set(metadata_json, '$.proposedTarget.source', ?)
532
532
  WHERE json_extract(metadata_json, '$.proposedTarget.source') = ?`)
@@ -221,6 +221,6 @@ export function countTaskHistoryTargetRefs(db, oldBundleId) {
221
221
  export function renameTaskHistoryTargetRefs(db, oldBundleId, newBundleId) {
222
222
  const prefix = `${escapeLikePattern(oldBundleId)}//`;
223
223
  return Number(db
224
- .prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, ?) WHERE target_ref LIKE ? ESCAPE '\\'`)
225
- .run(newBundleId, oldBundleId.length + 1, `${prefix}%`).changes);
224
+ .prepare(`UPDATE task_history SET target_ref = ? || substr(target_ref, length(?) + 1) WHERE target_ref LIKE ? ESCAPE '\\'`)
225
+ .run(newBundleId, oldBundleId, `${prefix}%`).changes);
226
226
  }
@@ -42,75 +42,10 @@ export class WorkflowRunsRepository {
42
42
  return withImmediateTransaction(this.db, () => fn(this.db));
43
43
  }
44
44
  // ── reads (fully materialised) ─────────────────────────────────────────────
45
- /**
46
- * The top-level start guard `publishWorkflowRunV4` uses to refuse starting
47
- * a SECOND active run of the same ref in the same scope. `AND
48
- * parent_run_id IS NULL` (B-N10's FOURTH site, code-review round 4 finding
49
- * 2 / Review log R2): a child run carries the PARENT's `scope_key` (P3a
50
- * §5.2), so once a parent publishes a child of some ref, starting a
51
- * TOP-LEVEL run of that same ref in that scope must never resolve to the
52
- * child — the child is not "already an active run in this scope" from a
53
- * fresh top-level invocation's point of view; the PARENT, if anything, is.
54
- * Before this filter, `publishWorkflowRunV4` refused with
55
- * `RESOURCE_ALREADY_EXISTS` naming the CHILD's own run id, instructing the
56
- * operator to `akm workflow abandon` a child a parent is actively driving.
57
- * For any database with no child rows the result is byte-identical, same
58
- * as the other three B-N10 sites below.
59
- *
60
- * Always scope-local: `scopeKey` is a real scope, never "every scope" — see
61
- * {@link findActiveRunOutsideScope} for the cross-scope warning's query.
62
- */
63
- findActiveRunForScope(workflowRefs, scopeKey) {
64
- const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
65
- if (refs.length === 0)
66
- return undefined;
67
- return this.db
68
- .prepare(`SELECT id, current_step_id FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' AND parent_run_id IS NULL ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
69
- .get(...refs, scopeKey);
70
- }
71
- /**
72
- * The cross-scope start warning's query (#942): the most recently active
73
- * run of these refs OUTSIDE `scopeKey` — i.e. `scope_key IS NULL OR
74
- * scope_key != scopeKey`, not merely "the most recent active run of these
75
- * refs anywhere". A same-scope active row must never win the `LIMIT 1` and
76
- * mask a DIFFERENT scope's run: with `--new`/`--force`, the caller's own
77
- * active run could otherwise sort first (most recently updated) and hide a
78
- * third scope's run entirely, so `startWorkflowRun` silently warned about
79
- * nothing while a genuinely stale run in another scope went unreported.
80
- * `findActiveRunForScope` deliberately stays scope-local and is never used
81
- * for this purpose.
82
- */
83
- findActiveRunOutsideScope(workflowRefs, scopeKey) {
84
- const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
85
- if (refs.length === 0)
86
- return undefined;
87
- return (this.db
88
- .prepare(`SELECT * FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND (scope_key IS NULL OR scope_key != ?) AND status = 'active' AND parent_run_id IS NULL ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
89
- .get(...refs, scopeKey) ?? undefined);
90
- }
91
45
  getRunById(runId) {
92
46
  return (this.db.prepare("SELECT * FROM workflow_runs WHERE id = ?").get(runId) ??
93
47
  undefined);
94
48
  }
95
- /**
96
- * The scope-attach lookup `akm workflow run <ref>` uses to find an
97
- * in-progress top-level run to resume instead of starting a new one.
98
- * `AND parent_run_id IS NULL` (B-N10): a child run must never be attached
99
- * to directly through this path — a parent-driven child would then have
100
- * TWO drivers. For any database with no child rows the result is
101
- * byte-identical.
102
- *
103
- * Always scope-local: `scopeKey` is a real scope, never "every scope" — see
104
- * {@link findActiveRunOutsideScope} for the cross-scope warning's query.
105
- */
106
- getActiveRunRowForScope(workflowRefs, scopeKey) {
107
- const refs = typeof workflowRefs === "string" ? [workflowRefs] : [...workflowRefs];
108
- if (refs.length === 0)
109
- return undefined;
110
- return (this.db
111
- .prepare(`SELECT * FROM workflow_runs WHERE workflow_ref IN (${refs.map(() => "?").join(", ")}) AND scope_key = ? AND status = 'active' AND parent_run_id IS NULL ORDER BY updated_at DESC, created_at DESC LIMIT 1`)
112
- .get(...refs, scopeKey) ?? undefined);
113
- }
114
49
  hasRun(runId) {
115
50
  const row = this.db.prepare("SELECT 1 FROM workflow_runs WHERE id = ? LIMIT 1").get(runId);
116
51
  return !!row;
@@ -128,16 +63,9 @@ export class WorkflowRunsRepository {
128
63
  }
129
64
  throw new NotFoundError(`Workflow run "${prefix}" not found.`, "WORKFLOW_NOT_FOUND");
130
65
  }
131
- listRuns(filter) {
66
+ listRuns(filter = {}) {
132
67
  const filters = [];
133
68
  const params = [];
134
- // `scopeKey: null` (#942) is "every scope" — the predicate is omitted
135
- // rather than bound as SQL NULL (which would match nothing, since a real
136
- // `scope_key` column value is never NULL for a fresh run).
137
- if (filter.scopeKey !== null) {
138
- filters.push("scope_key = ?");
139
- params.push(filter.scopeKey);
140
- }
141
69
  if (filter.workflowRef) {
142
70
  filters.push("workflow_ref = ?");
143
71
  params.push(filter.workflowRef);
@@ -152,11 +80,7 @@ export class WorkflowRunsRepository {
152
80
  // human `resume`), so it must never appear under `--active`, or a script
153
81
  // treating `--active` output as executable work would pick up a blocked
154
82
  // run (owner manual-validation finding 1). Blocked runs stay visible in
155
- // plain `list` (all statuses) with their `blocked` status. The
156
- // active-OR-blocked scope semantics some call sites want (the `akm show`
157
- // scope guard, which surfaces a blocked run as the scope's occupant) live
158
- // in the SEPARATE {@link findActiveOrBlockedRunForScope} — never folded
159
- // into this shared list filter.
83
+ // plain `list` (all statuses) with their `blocked` status.
160
84
  filters.push("status = 'active'");
161
85
  }
162
86
  // B-N10: a child run is invisible to this scope query unless the caller
@@ -178,32 +102,23 @@ export class WorkflowRunsRepository {
178
102
  getStep(runId, stepId) {
179
103
  return this.db.prepare("SELECT * FROM workflow_run_steps WHERE run_id = ? AND step_id = ?").get(runId, stepId);
180
104
  }
181
- /**
182
- * The `akm show` active-run guard: which run (if any) currently occupies
183
- * this scope. `AND parent_run_id IS NULL` (B-N10) — a child a parent is
184
- * driving must never be reported as "the" active run; the PARENT is. For
185
- * any database with no child rows the result is byte-identical.
186
- */
187
- findActiveOrBlockedRunForScope(scopeKey) {
188
- return (this.db
189
- .prepare("SELECT id, current_step_id, workflow_ref FROM workflow_runs WHERE scope_key = ? AND status IN ('active', 'blocked') AND parent_run_id IS NULL ORDER BY updated_at DESC LIMIT 1")
190
- .get(scopeKey) ?? null);
191
- }
192
105
  // ── writes ─────────────────────────────────────────────────────────────────
193
106
  insertRun(input) {
194
107
  // R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
195
- // this 12-column list is hand-duplicated by publishChildWorkflowRun's own
108
+ // this column list is hand-duplicated by publishChildWorkflowRun's own
196
109
  // INSERT below, which extends it with parent_run_id/parent_unit_id/
197
110
  // invocation_key. A signature refactor to share one INSERT builder was
198
111
  // considered and deliberately deferred — see
199
112
  // docs/architecture/decisions/0009-child-run-publication-column-parity.md.
200
- // Keep both column lists in sync by hand.
113
+ // Keep both column lists in sync by hand. `scope_key` and
114
+ // `workflow_entry_id` are legacy columns: read tolerantly, never written
115
+ // (#1239), so a fresh row carries NULL in both.
201
116
  this.db
202
117
  .prepare(`INSERT INTO workflow_runs (
203
- id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
118
+ id, workflow_ref, workflow_title, status, params_json, current_step_id, created_at, updated_at,
204
119
  agent_harness, agent_session_id
205
- ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)`)
206
- .run(input.id, input.workflowRef, input.scopeKey, input.workflowEntryId, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId);
120
+ ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)`)
121
+ .run(input.id, input.workflowRef, input.workflowTitle, input.paramsJson, input.currentStepId, input.createdAt, input.updatedAt, input.agentHarness, input.agentSessionId);
207
122
  }
208
123
  insertSteps(steps) {
209
124
  const insertStep = this.db.prepare(`INSERT INTO workflow_run_steps (
@@ -252,22 +167,6 @@ export class WorkflowRunsRepository {
252
167
  */
253
168
  publishWorkflowRunV4(input) {
254
169
  this.immediateTransaction((db) => {
255
- if (!input.force) {
256
- // The uniqueness guard must never silently skip its scope predicate
257
- // (#942) — every real caller (`startWorkflowRun`) stamps a concrete
258
- // scope key, so a null one here means the caller is misusing this
259
- // top-level guard, not that scoping should be waived.
260
- if (input.run.scopeKey === null) {
261
- throw new Error("publishWorkflowRunV4: run.scopeKey must not be null for the scope uniqueness guard.");
262
- }
263
- const existing = this.findActiveRunForScope(input.workflowRefs, input.run.scopeKey);
264
- if (existing) {
265
- throw new UsageError(`Workflow ${input.run.workflowRef} already has an active run in this scope ` +
266
- `(id=${existing.id}, scope=${input.run.scopeKey}, step=${existing.current_step_id ?? "—"}). ` +
267
- `Use 'akm workflow run ${input.run.workflowRef}' to resume it or ` +
268
- `'akm workflow abandon ${existing.id}' to give up on it.`, "RESOURCE_ALREADY_EXISTS");
269
- }
270
- }
271
170
  this.insertRun(input.run);
272
171
  this.insertSteps(input.steps);
273
172
  db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
@@ -294,9 +193,7 @@ export class WorkflowRunsRepository {
294
193
  * (`plan_ir_version` = the current plan version), and append its
295
194
  * `workflow_started` event — then return the freshly-inserted row.
296
195
  *
297
- * Deliberately does NOT call {@link findActiveRunForScope} or raise
298
- * `RESOURCE_ALREADY_EXISTS` (top-level scope-conflict rules do not apply to
299
- * a child), and reads no source: the child plan was frozen into the parent's.
196
+ * Reads no source: the child plan was frozen into the parent's.
300
197
  *
301
198
  * This method's serialization guarantee holds only when it is the
302
199
  * OUTERMOST transaction on the connection (spec Review log R10,
@@ -332,14 +229,14 @@ export class WorkflowRunsRepository {
332
229
  if (existing)
333
230
  return existing;
334
231
  // R-R3 (P3a Review log; docs/plans/specs/p4-deletions-closeout.md §8):
335
- // the first 12 columns here must stay byte-identical to insertRun's own
232
+ // the first 10 columns here must stay byte-identical to insertRun's own
336
233
  // column list above (hand-duplicated, not shared, by deliberate choice
337
234
  // — see docs/architecture/decisions/0009-child-run-publication-column-parity.md).
338
235
  // Keep both column lists in sync by hand.
339
236
  db.prepare(`INSERT INTO workflow_runs (
340
- id, workflow_ref, scope_key, workflow_entry_id, workflow_title, status, params_json, current_step_id, created_at, updated_at,
237
+ id, workflow_ref, workflow_title, status, params_json, current_step_id, created_at, updated_at,
341
238
  agent_harness, agent_session_id, parent_run_id, parent_unit_id, invocation_key
342
- ) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.scopeKey, input.run.workflowEntryId, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
239
+ ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(input.run.id, input.run.workflowRef, input.run.workflowTitle, input.run.paramsJson, input.run.currentStepId, input.run.createdAt, input.run.updatedAt, input.run.agentHarness, input.run.agentSessionId, input.parentRunId, input.spawnedByUnitId, input.invocationKey);
343
240
  this.insertSteps(input.steps);
344
241
  db.prepare("UPDATE workflow_runs SET plan_json = ?, plan_hash = ?, plan_ir_version = ? WHERE id = ?").run(input.planJson, input.planHash, WORKFLOW_PLAN_VERSION, input.run.id);
345
242
  insertEventOnce(db, {
@@ -543,6 +440,31 @@ export class WorkflowRunsRepository {
543
440
  return this.db.prepare("SELECT * FROM workflow_run_units WHERE run_id = ? AND unit_id = ?").get(runId, unitId);
544
441
  }
545
442
  }
443
+ /**
444
+ * Preview counterpart of {@link renameWorkflowRunRefs} for `akm bundle rename`:
445
+ * how many run rows name `oldBundleId`, and which of them are still `active`
446
+ * or `blocked` (their frozen `plan_json` keeps the old qualified refs, so the operator
447
+ * finishes or abandons them — the rename lists them rather than touching them).
448
+ */
449
+ export function countWorkflowRunRefsForRename(db, oldBundleId) {
450
+ const like = `${escapeLikePattern(oldBundleId)}//%`;
451
+ const refs = db.prepare(`SELECT COUNT(*) AS n FROM workflow_runs WHERE workflow_ref LIKE ? ESCAPE '\\'`).get(like).n;
452
+ const active = db
453
+ .prepare(`SELECT id, workflow_ref AS workflowRef, status FROM workflow_runs WHERE workflow_ref LIKE ? ESCAPE '\\' AND status IN ('active', 'blocked') ORDER BY updated_at DESC, created_at DESC`)
454
+ .all(like);
455
+ return { refs, active };
456
+ }
457
+ /**
458
+ * Rewrite every `workflow_runs.workflow_ref` naming `oldBundleId` to
459
+ * `newBundleId` (`akm bundle rename`). A legacy unqualified ref is not matched
460
+ * by the `LIKE` prefix and is left alone. Returns the number of rows rewritten.
461
+ */
462
+ export function renameWorkflowRunRefs(db, oldBundleId, newBundleId) {
463
+ const prefix = `${escapeLikePattern(oldBundleId)}//`;
464
+ return Number(db
465
+ .prepare(`UPDATE workflow_runs SET workflow_ref = ? || substr(workflow_ref, length(?) + 1) WHERE workflow_ref LIKE ? ESCAPE '\\'`)
466
+ .run(newBundleId, oldBundleId, `${prefix}%`).changes);
467
+ }
546
468
  /**
547
469
  * Run `fn` against a {@link WorkflowRunsRepository} bound to state.db
548
470
  * ({@link StorageLocations.stateDb}, the post-cutover home of the
@@ -69,7 +69,7 @@ export function listEmbeddedTasks() {
69
69
  }
70
70
  let parsed;
71
71
  try {
72
- parsed = parseTaskSource({ yaml, filePath, workspaceRoot: TASKS_ASSETS_DIR });
72
+ parsed = parseTaskSource({ yaml, filePath });
73
73
  }
74
74
  catch {
75
75
  continue;
@@ -4,6 +4,7 @@
4
4
  import { makeBundleRef, parseBundleRef } from "../../core/asset/asset-ref.js";
5
5
  import { COMPOSITION_INVALID_MULTI_JOB_HINT, ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
6
6
  import { DURATION_UNITS, parseDuration } from "../../core/time.js";
7
+ import { warn } from "../../core/warn.js";
7
8
  import { isPortableExecutionAgentSelector } from "../../execution/source.js";
8
9
  import { buildExecution } from "../../integrations/agent/execution.js";
9
10
  import { resolveAssetPath } from "../../sources/resolve.js";
@@ -22,7 +23,7 @@ export function environmentSnapshot(environment) {
22
23
  continue;
23
24
  const value = String(raw);
24
25
  if (isInferredSecretName(key) || detectSecretShapedParams({ [key]: value }).length > 0) {
25
- throw new UsageError(`Task env.${key} is a secret-shaped literal env value. Store credentials in a secret/env binding rather than durable task source.`, "TASK_SOURCE_INVALID");
26
+ warn(`Task env.${key} looks like a credential. Task source is durable and readable; keep the value in a secret or env file and inject it at run time with \`akm secret run\` or \`akm env run\` in the task's \`run:\`. (Heuristic warning; the value is used as written.)`);
26
27
  }
27
28
  Object.defineProperty(out, key, { value, enumerable: true, configurable: false, writable: false });
28
29
  }
@@ -120,7 +121,7 @@ export function validatePreparedCommand(invocation, context) {
120
121
  }
121
122
  export function validateWorkflowRuntimeSource(file, workspaceRoot, readFile) {
122
123
  const source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(readFile(file, workspaceRoot));
123
- const compiled = compileWorkflowSource(source, { path: file, workspaceRoot });
124
+ const compiled = compileWorkflowSource(source, { path: file });
124
125
  if (!compiled.ok) {
125
126
  // P4-N2's mapping (docs/plans/specs/p4-deletions-closeout.md §3.3.4): a
126
127
  // task-wrapped workflow target's own multi-job source is a composition
@@ -19,25 +19,24 @@
19
19
  * `src/tasks/source/project-v4.ts`'s `projectTaskSourceV4()`.
20
20
  */
21
21
  import fs from "node:fs";
22
+ import path from "node:path";
22
23
  import { prepareCommandInvocation } from "../../commands/command/command-execution.js";
23
24
  import { UsageError } from "../../core/errors.js";
24
25
  import { warn } from "../../core/warn.js";
25
26
  import { base, commandEnvironmentSnapshot, currentExecutionValues, defaultTaskShell, environmentSnapshot, qualifyOwnedRef, resolvedOwnedAsset, validatePreparedCommand, validateWorkflowRuntimeSource, } from "./prepare-support.js";
26
- import { captureDirectoryIdentity, captureScriptTarget } from "./script-capture.js";
27
+ import { captureScriptTarget } from "./script-capture.js";
27
28
  /** Project one canonical task-v3 source into immutable executable work. */
28
29
  export async function prepareTaskV3Execution(document, context) {
29
30
  const environment = environmentSnapshot(document.env);
30
31
  const commandEnvironment = commandEnvironmentSnapshot(environment, context.schedulerContext);
31
32
  const common = base(document, context, environment);
32
33
  if (document.target.kind === "run") {
33
- const cwdIdentity = captureDirectoryIdentity(context.bundleRoot, document.target.workingDirectory);
34
34
  return Object.freeze({
35
35
  ...common,
36
36
  kind: "shell",
37
37
  command: document.target.run,
38
38
  shell: document.target.shell ?? defaultTaskShell(context.platform ?? process.platform),
39
- cwd: cwdIdentity.realCwd,
40
- cwdIdentity,
39
+ cwd: path.resolve(context.bundleRoot, document.target.workingDirectory ?? "."),
41
40
  });
42
41
  }
43
42
  const target = document.target.uses;
@@ -7,13 +7,12 @@
7
7
  * script arm (the moved prepareTaskV3Execution) and
8
8
  * prepare-script-target.ts's typed prepareScriptTarget() call
9
9
  * captureScriptTarget() below, so the two never drift into two copies of the
10
- * same capture logic. scriptInterpreter() and captureDirectoryIdentity() are
11
- * moved body-intact out of the pre-P1b src/tasks/runtime-v3.ts.
10
+ * same capture logic. scriptInterpreter() is moved body-intact out of the
11
+ * pre-P1b src/tasks/runtime-v3.ts.
12
12
  */
13
13
  import { createHash } from "node:crypto";
14
14
  import path from "node:path";
15
15
  import { UsageError } from "../../core/errors.js";
16
- import { captureFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
17
16
  import { isBunStandaloneMain } from "../resolve-akm-bin.js";
18
17
  const SCRIPT_INTERPRETERS = Object.freeze({
19
18
  ".sh": "sh",
@@ -77,36 +76,23 @@ export function scriptInterpreter(extension, ref, bytes) {
77
76
  }
78
77
  throw new UsageError(`Task v3 script target ${JSON.stringify(ref)} requires Bun for ${extension} execution, but this runtime cannot provide it.`, "TASK_TARGET_UNSUPPORTED");
79
78
  }
80
- export function captureDirectoryIdentity(bundleRoot, workingDirectory) {
81
- try {
82
- return captureFrozenDirectoryIdentity(bundleRoot, workingDirectory);
83
- }
84
- catch (cause) {
85
- if (cause instanceof UsageError)
86
- throw cause;
87
- throw new UsageError(`Task working directory ${JSON.stringify(workingDirectory ?? ".")} cannot be physically verified: ${cause instanceof Error ? cause.message : String(cause)}`, "TASK_SOURCE_INVALID");
88
- }
89
- }
90
79
  /**
91
- * Read a script's bytes off disk (or the caller's own read seam) and freeze
92
- * the interpreter/digest/directory-identity shape every script dispatch
93
- * needs. `ref` is used only for interpreter-selection error messages — it is
94
- * the script's own qualified ref, never a synthetic task ref. `bundleRoot` is
95
- * where directory identity is captured from (one level above the script file
96
- * itself, matching prepareTaskV3Execution's historical behavior).
80
+ * Read a script's bytes off disk (or the caller's own read seam, following a
81
+ * symlink to wherever it points) and freeze the interpreter/digest shape every
82
+ * script dispatch needs. `ref` is used only for interpreter-selection error
83
+ * messages — it is the script's own qualified ref, never a synthetic task ref.
84
+ * `bundleRoot` is the script's working directory.
97
85
  */
98
86
  export function captureScriptTarget(ref, file, bundleRoot, readFile) {
99
87
  const extension = path.extname(file).toLowerCase();
100
88
  const raw = readFile(file, bundleRoot);
101
89
  const bytes = Uint8Array.from(raw);
102
- const cwdIdentity = captureDirectoryIdentity(bundleRoot);
103
90
  return Object.freeze({
104
91
  interpreter: scriptInterpreter(extension, ref, bytes),
105
92
  extension,
106
93
  bytesBase64: Buffer.from(bytes).toString("base64"),
107
94
  byteLength: bytes.byteLength,
108
95
  sha256: createHash("sha256").update(bytes).digest("hex"),
109
- cwd: cwdIdentity.realCwd,
110
- cwdIdentity,
96
+ cwd: path.resolve(bundleRoot),
111
97
  });
112
98
  }
@@ -11,11 +11,16 @@
11
11
  import fs from "node:fs";
12
12
  import { detectAdapterId } from "../../core/adapter/detect-adapter.js";
13
13
  import { makeBundleRef } from "../../core/asset/asset-ref.js";
14
+ import { conceptIdFromTypeName } from "../../core/asset/resolve-ref.js";
15
+ import { resolveStashDir } from "../../core/common.js";
14
16
  import { loadConfig } from "../../core/config/config.js";
15
- import { NotFoundError } from "../../core/errors.js";
16
- import { resolveWriteTarget } from "../../core/write-source.js";
17
+ import { bundleKeyForContentRoot } from "../../core/config/config-sources.js";
18
+ import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
19
+ import { assertBundleNotDisabled } from "../../core/write-source.js";
17
20
  import { applyInputDefaults, materializeInputFlags, validateInputs, } from "../../execution/input-contract.js";
18
- import { resolveAdapterConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
21
+ import { resolveAdapterConceptOwner, resolveConceptOwner } from "../../indexer/lookup/adapter-concept-owner.js";
22
+ import { resolveSourceEntries } from "../../indexer/search/search-source.js";
23
+ import { resolveSourcesForOrigin } from "../../registry/origin-resolve.js";
19
24
  import { resolveAssetPath } from "../../sources/resolve.js";
20
25
  import { prepareTaskV3Execution } from "../prepare/prepare.js";
21
26
  import { scheduledTaskContextEnv } from "../scheduler-invocation.js";
@@ -34,6 +39,42 @@ const CONFIG_FREE_TASK_RUNTIME = Object.freeze({
34
39
  configVersion: "0.9.0",
35
40
  semanticSearchMode: "off",
36
41
  });
42
+ /**
43
+ * The active source set a task read walks. A working stash that is a disabled
44
+ * configured bundle makes that set throw; it is simply not in the set, so the
45
+ * other bundles still resolve.
46
+ */
47
+ export function activeTaskSources(config) {
48
+ try {
49
+ return resolveSourceEntries(undefined, config);
50
+ }
51
+ catch (cause) {
52
+ const working = bundleKeyForContentRoot(config, resolveStashDir());
53
+ const entry = working ? config.bundles?.[working] : undefined;
54
+ if (!(cause instanceof ConfigError) || !working || !entry || entry.enabled !== false)
55
+ throw cause;
56
+ const enabled = { ...config, bundles: { ...config.bundles, [working]: { ...entry, enabled: true } } };
57
+ return resolveSourceEntries(undefined, enabled).filter((source) => source.registryId !== working);
58
+ }
59
+ }
60
+ /**
61
+ * Resolve an asset a task names in another bundle (`uses:`) through the one
62
+ * ref-to-file walk. Reading needs no write target, so a bundle of any kind
63
+ * (git, npm, website) resolves. The file is the authored path, not the symlink
64
+ * target, so the script interpreter follows the name the author gave it.
65
+ */
66
+ export function resolveBundleAsset(config, { bundle, type, name }) {
67
+ const conceptId = conceptIdFromTypeName(type, name);
68
+ const sources = activeTaskSources(config);
69
+ const match = resolveConceptOwner({ bundle, conceptId }, sources);
70
+ if (match)
71
+ return { file: match.owner.path, bundleRoot: match.source.path };
72
+ if (resolveSourcesForOrigin(bundle, sources).length === 0) {
73
+ assertBundleNotDisabled(config, bundle);
74
+ throw new UsageError(`Bundle ${JSON.stringify(bundle)} is not configured. Run \`akm bundle list\` to see available bundles.`, "INVALID_FLAG_VALUE");
75
+ }
76
+ throw new NotFoundError(`Task asset ${makeBundleRef(bundle, conceptId)} was not found.`, "ASSET_NOT_FOUND");
77
+ }
37
78
  /** Resolve, parse, and project one task-v3 asset into a frozen, executable projection. */
38
79
  export async function loadPreparedTask(id, options) {
39
80
  const bundleDir = options.bundleDir;
@@ -49,7 +90,7 @@ export async function loadPreparedTask(id, options) {
49
90
  }
50
91
  const filePath = owner.path;
51
92
  const yaml = fs.readFileSync(filePath, "utf8");
52
- const parsed = parseTaskSource({ yaml, filePath, workspaceRoot: bundleDir });
93
+ const parsed = parseTaskSource({ yaml, filePath });
53
94
  const source = projectTaskSourceV4(parsed.v4);
54
95
  const requiresCommandConfig = source.target.kind === "uses" &&
55
96
  (source.target.uses.kind === "builtin-command" || source.target.uses.kind === "command");
@@ -106,16 +147,7 @@ export async function loadPreparedTask(id, options) {
106
147
  if (bundle === bundleName) {
107
148
  return { file: await resolveAssetPath(bundleDir, type, name), bundleRoot: bundleDir };
108
149
  }
109
- const resolutionConfig = requiresCommandConfig ? config : loadConfig();
110
- // `bundle` is the qualifier of an asset ref in the task, not a flag.
111
- const resolvedBundle = resolveWriteTarget(resolutionConfig, bundle, {
112
- requireWritable: false,
113
- flag: "The asset ref's bundle",
114
- });
115
- return {
116
- file: await resolveAssetPath(resolvedBundle.source.path, type, name),
117
- bundleRoot: resolvedBundle.source.path,
118
- };
150
+ return resolveBundleAsset(requiresCommandConfig ? config : loadConfig(), { bundle, type, name });
119
151
  },
120
152
  });
121
153
  }
@@ -20,7 +20,6 @@
20
20
  import path from "node:path";
21
21
  import { assertNever } from "../../core/assert.js";
22
22
  import { runManagedSubprocess, streamCaptureFailure } from "../../core/subprocess.js";
23
- import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
24
23
  import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../frozen-script.js";
25
24
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
26
25
  import { finishAttempt } from "./attempt-lifecycle.js";
@@ -163,13 +162,6 @@ export async function runNativeTask(input) {
163
162
  process.once("SIGTERM", onSigterm);
164
163
  process.once("SIGINT", onSigint);
165
164
  try {
166
- // Re-resolve the authored root/cwd immediately before spawn so a
167
- // symlink, ancestor, bundle-root, or directory/file swap cannot redirect
168
- // execution outside that physical workspace. This is a live containment
169
- // check, not an identity comparison — it does not care whether the
170
- // underlying device/inode changed (e.g. a remount), only whether the
171
- // resolved cwd still lives inside its root.
172
- assertFrozenDirectoryContained(task.cwdIdentity);
173
165
  if (task.kind === "script") {
174
166
  materialized = materializeFrozenScript(task);
175
167
  cmd = frozenScriptCommand(task, materialized.file);