akm-cli 0.9.11 → 0.9.12

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 (127) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/STABILITY.md +6 -1
  3. package/dist/assets/hints/cli-hints-full.md +1 -1
  4. package/dist/assets/improve-strategies/consolidate.json +1 -1
  5. package/dist/assets/improve-strategies/default.json +1 -1
  6. package/dist/assets/improve-strategies/thorough.json +1 -2
  7. package/dist/cli/shared.js +16 -4
  8. package/dist/cli.js +15 -13
  9. package/dist/commands/agent/agent-dispatch.js +8 -0
  10. package/dist/commands/command/execution-source-loader.js +25 -22
  11. package/dist/commands/command/portable-template.js +4 -26
  12. package/dist/commands/config-cli.js +10 -4
  13. package/dist/commands/env/env-binding.js +10 -3
  14. package/dist/commands/env/env-cli.js +7 -0
  15. package/dist/commands/env/secret-cli.js +15 -4
  16. package/dist/commands/health/checks.js +186 -71
  17. package/dist/commands/health.js +16 -4
  18. package/dist/commands/improve/distill/quality-gate.js +2 -2
  19. package/dist/commands/improve/distill.js +28 -12
  20. package/dist/commands/improve/execution.js +1 -2
  21. package/dist/commands/improve/extract.js +82 -56
  22. package/dist/commands/improve/improve-strategies.js +26 -8
  23. package/dist/commands/improve/improve.js +13 -0
  24. package/dist/commands/improve/preparation.js +9 -6
  25. package/dist/commands/improve/reflect.js +61 -77
  26. package/dist/commands/lint/index.js +3 -1
  27. package/dist/commands/migrate-cli.js +6 -4
  28. package/dist/commands/proposal/drain-policies.js +22 -2
  29. package/dist/commands/proposal/repository.js +4 -4
  30. package/dist/commands/proposal/validators/proposal-quality-validators.js +23 -2
  31. package/dist/commands/proposal/validators/proposals.js +10 -19
  32. package/dist/commands/read/show.js +42 -31
  33. package/dist/commands/registry-cli.js +4 -2
  34. package/dist/commands/sources/init.js +4 -8
  35. package/dist/commands/sources/self-update.js +2 -2
  36. package/dist/commands/sources/source-clone.js +5 -7
  37. package/dist/commands/sources/sources-cli.js +3 -5
  38. package/dist/commands/tasks/tasks-cli.js +4 -12
  39. package/dist/commands/tasks/tasks.js +38 -35
  40. package/dist/commands/workflow-cli.js +17 -15
  41. package/dist/core/activation-policy.js +31 -3
  42. package/dist/core/adapter/execution-source.js +39 -11
  43. package/dist/core/asset/stash-meta.js +7 -41
  44. package/dist/core/common.js +8 -17
  45. package/dist/core/config/config-schema.js +3 -23
  46. package/dist/core/config/config-walker.js +50 -4
  47. package/dist/core/config/config.js +21 -5
  48. package/dist/core/config/legacy-source-shape-shim.js +79 -0
  49. package/dist/core/config/schema/embedding.js +2 -2
  50. package/dist/core/config/schema/engines.js +2 -2
  51. package/dist/core/config/schema/index-config.js +19 -21
  52. package/dist/core/config/schema/primitives.js +21 -10
  53. package/dist/core/config/schema/sources-bundles.js +1 -6
  54. package/dist/core/errors.js +2 -3
  55. package/dist/core/improve-types.js +17 -0
  56. package/dist/core/json-schema.js +1 -11
  57. package/dist/core/maintenance-barrier.js +17 -2
  58. package/dist/core/paths.js +12 -15
  59. package/dist/core/state/migrations.js +28 -0
  60. package/dist/core/state-db.js +28 -1
  61. package/dist/core/write-source.js +6 -6
  62. package/dist/indexer/bundle-identity-guard.js +3 -0
  63. package/dist/indexer/ensure-index.js +5 -0
  64. package/dist/indexer/indexer.js +11 -3
  65. package/dist/indexer/lookup/adapter-concept-owner.js +14 -3
  66. package/dist/indexer/passes/metadata.js +16 -5
  67. package/dist/indexer/search/search-fields.js +1 -30
  68. package/dist/integrations/agent/engine-resolution.js +15 -1
  69. package/dist/integrations/agent/model-map.js +16 -10
  70. package/dist/integrations/agent/prompts.js +13 -6
  71. package/dist/integrations/lockfile.js +22 -7
  72. package/dist/llm/client.js +16 -0
  73. package/dist/llm/index-passes.js +3 -2
  74. package/dist/output/shapes/passthrough.js +9 -3
  75. package/dist/output/shapes.js +5 -3
  76. package/dist/output/text/workflow-format.js +8 -1
  77. package/dist/scripts/akm-migrate-node.js +1461 -1267
  78. package/dist/scripts/akm-migrate.js +1460 -1266
  79. package/dist/setup/setup.js +14 -21
  80. package/dist/sources/include.js +150 -20
  81. package/dist/sources/providers/git-install.js +14 -12
  82. package/dist/sources/providers/git-provider.js +3 -3
  83. package/dist/sources/snapshot-fetchers/website-ingest.js +54 -16
  84. package/dist/sources/website-url.js +12 -4
  85. package/dist/storage/engines/sqlite-migrations.js +40 -10
  86. package/dist/storage/like-pattern.js +7 -0
  87. package/dist/storage/repositories/extract-sessions-repository.js +23 -0
  88. package/dist/storage/repositories/index-connection.js +27 -10
  89. package/dist/storage/repositories/index-entry-schema.js +19 -2
  90. package/dist/storage/repositories/index-schema.js +30 -9
  91. package/dist/storage/repositories/proposals-repository.js +2 -1
  92. package/dist/storage/repositories/task-history-repository.js +14 -7
  93. package/dist/storage/repositories/workflow-runs-repository.js +15 -1
  94. package/dist/storage/sqlite-read-snapshot.js +11 -9
  95. package/dist/tasks/backends/cron.js +34 -5
  96. package/dist/tasks/backends/launchd.js +23 -26
  97. package/dist/tasks/backends/schtasks.js +50 -3
  98. package/dist/tasks/frozen-script.js +2 -0
  99. package/dist/tasks/prepare/prepare.js +2 -7
  100. package/dist/tasks/prepare/script-capture.js +38 -6
  101. package/dist/tasks/schedule.js +154 -13
  102. package/dist/tasks/source/task-source-v3-frozen.js +0 -1
  103. package/dist/tasks/source/task-source-v4.js +0 -1
  104. package/dist/workflows/exec/child-workflow.js +2 -3
  105. package/dist/workflows/exec/exec-unit.js +3 -4
  106. package/dist/workflows/exec/run-workflow.js +19 -10
  107. package/dist/workflows/exec/step-work.js +35 -56
  108. package/dist/workflows/freeze/resolve-steps.js +19 -11
  109. package/dist/workflows/freeze/source-freeze.js +7 -0
  110. package/dist/workflows/freeze/targets/child-workflow.js +12 -18
  111. package/dist/workflows/freeze/targets/command.js +14 -2
  112. package/dist/workflows/ir/environment-v4.js +4 -2
  113. package/dist/workflows/ir/freeze-v4.js +2 -5
  114. package/dist/workflows/ir/plan-hash.js +0 -3
  115. package/dist/workflows/ir/schema-v4.js +14 -9
  116. package/dist/workflows/ir/schema.js +1 -3
  117. package/dist/workflows/resource-limits.js +35 -48
  118. package/dist/workflows/runtime/plan-classifier.js +89 -41
  119. package/dist/workflows/runtime/run-outputs.js +1 -21
  120. package/dist/workflows/runtime/runs.js +76 -150
  121. package/dist/workflows/source-files.js +28 -54
  122. package/dist/workflows/source-ir/program.js +2 -2
  123. package/dist/workflows/source-ir/semantics.js +5 -23
  124. package/docs/migration/v0.9.1-to-v0.9.2.md +20 -0
  125. package/docs/reference/cli.md +74 -17
  126. package/package.json +1 -1
  127. package/schemas/akm-config.json +5 -10
@@ -12,8 +12,8 @@ import fs from "node:fs";
12
12
  import path from "node:path";
13
13
  import { stashDirNames } from "../../core/asset/asset-placement.js";
14
14
  import { mutateConfig } from "../../core/config/config.js";
15
- import { ConfigError } from "../../core/errors.js";
16
15
  import { assertSafeStashDir, getConfigPath, getDefaultStashDir } from "../../core/paths.js";
16
+ import { warnOnce } from "../../core/warn.js";
17
17
  import { primaryBundlePath, withPrimaryBundle } from "./bundle-config-ops.js";
18
18
  import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./stash-skeleton.js";
19
19
  /**
@@ -37,11 +37,9 @@ import { copyStashSkeleton, ensureStashGitignore, scaffoldStashMeta } from "./st
37
37
  function assertInitSandbox(stashDir, dirExplicitlyProvided) {
38
38
  if (!dirExplicitlyProvided)
39
39
  return; // Only guard explicit --dir, not default HOME resolution.
40
- const isUnderTest = isUnderTestRunner();
40
+ const isUnderTest = process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
41
41
  if (!isUnderTest)
42
42
  return;
43
- if (process.env.AKM_FORCE_INIT_TMP_STASH === "1")
44
- return;
45
43
  const isTmp = stashDir.startsWith("/tmp/") ||
46
44
  stashDir === "/tmp" ||
47
45
  stashDir.startsWith("/var/tmp/") ||
@@ -50,10 +48,8 @@ function assertInitSandbox(stashDir, dirExplicitlyProvided) {
50
48
  stashDir.startsWith("/private/tmp/");
51
49
  if (!isTmp)
52
50
  return;
53
- throw new ConfigError(`refusing to persist --dir stashDir to a temporary path while under test runner; set AKM_FORCE_INIT_TMP_STASH=1 if you really mean it (stashDir=${stashDir})`, "INIT_TMP_STASH_REFUSED");
54
- }
55
- function isUnderTestRunner() {
56
- return process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
51
+ warnOnce(`init-tmp-stash:${stashDir}`, `Persisting --dir stashDir to a temporary path (${stashDir}) while a test-runner env var (BUN_TEST/NODE_ENV=test) is set; ` +
52
+ "the OS may reap this path, leaving the next run pointing at a deleted bundle.");
57
53
  }
58
54
  // ── Test seam ────────────────────────────────────────────────────────────────
59
55
  // Swap-and-restore override. Inert in production; only tests call the setter.
@@ -346,8 +346,8 @@ export async function performUpgrade(check, opts, dependencies) {
346
346
  }
347
347
  }
348
348
  if (fs.existsSync(backupPath)) {
349
- removeFileBestEffort(stagedPath);
350
- throw new ConfigError(`Refusing to overwrite retained previous binary at ${backupPath}.`, "UPGRADE_BLOCKED");
349
+ warn(`A previous upgrade left a stale backup at ${backupPath}; overwriting it.`);
350
+ removeFileBestEffort(backupPath);
351
351
  }
352
352
  try {
353
353
  if (IS_WINDOWS)
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { stashDirFor } from "../../core/asset/asset-placement.js";
8
8
  import { isBundleSlug } from "../../core/asset/asset-ref.js";
9
9
  import { displayRef, parseQualifiedRefInput } from "../../core/asset/resolve-ref.js";
10
+ import { isWithin } from "../../core/common.js";
10
11
  import { loadConfig } from "../../core/config/config.js";
11
12
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
12
13
  import { defaultBundleForTarget } from "../../core/mutation-target.js";
@@ -224,18 +225,15 @@ function lstatIfExists(filePath) {
224
225
  }
225
226
  function assertNoDestinationSymlinkParent(root, destination) {
226
227
  const resolvedRoot = path.resolve(root);
227
- const relativeParent = path.relative(resolvedRoot, path.dirname(path.resolve(destination)));
228
+ const parentDir = path.dirname(path.resolve(destination));
229
+ const relativeParent = path.relative(resolvedRoot, parentDir);
228
230
  if (relativeParent === "" || relativeParent === ".")
229
231
  return;
230
232
  if (relativeParent.startsWith("..") || path.isAbsolute(relativeParent)) {
231
233
  throw new UsageError(`Clone destination escapes the selected target: ${destination}.`, "PATH_ESCAPE_VIOLATION");
232
234
  }
233
- let current = resolvedRoot;
234
- for (const segment of relativeParent.split(path.sep)) {
235
- current = path.join(current, segment);
236
- if (lstatIfExists(current)?.isSymbolicLink()) {
237
- throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${current}.`, "PATH_ESCAPE_VIOLATION");
238
- }
235
+ if (!isWithin(parentDir, resolvedRoot)) {
236
+ throw new UsageError(`Clone destination has a symbolic-link parent outside the selected target boundary: ${parentDir}.`, "PATH_ESCAPE_VIOLATION");
239
237
  }
240
238
  }
241
239
  function listTreeFiles(root) {
@@ -27,7 +27,7 @@
27
27
  */
28
28
  import { defineCommand } from "citty";
29
29
  import { getParsedInvocation } from "../../cli/invocation.js";
30
- import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors } from "../../cli/shared.js";
30
+ import { defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
31
31
  import { loadConfig } from "../../core/config/config.js";
32
32
  import { UsageError } from "../../core/errors.js";
33
33
  import { appendEvent } from "../../core/events.js";
@@ -54,12 +54,10 @@ export const upgradeCommand = defineJsonCommand({
54
54
  }
55
55
  const skipPostUpgrade = args["skip-post-upgrade"];
56
56
  const result = await performUpgrade(check, { force: args.force, skipPostUpgrade });
57
- output("upgrade", result);
58
57
  // The install may have succeeded, but an upgrade whose migration is
59
58
  // blocked or could not run is not done: exit like `akm migrate apply` does.
60
- if (result.migration?.status === "blocked" || result.migration?.status === "failed") {
61
- process.exitCode = EXIT_CODES.GENERAL;
62
- }
59
+ const migrationFailed = result.migration?.status === "blocked" || result.migration?.status === "failed";
60
+ outputWithExitCode("upgrade", result, migrationFailed ? EXIT_CODES.GENERAL : undefined);
63
61
  },
64
62
  });
65
63
  // `sync` body, standalone so the git-commit/push logic stays in one place.
@@ -27,7 +27,7 @@
27
27
  import { defineCommand } from "citty";
28
28
  import { getParsedInvocation } from "../../cli/invocation.js";
29
29
  import { parsePositiveIntFlag } from "../../cli/parse-args.js";
30
- import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, runWithJsonErrors, } from "../../cli/shared.js";
30
+ import { defineGroupCommand, defineJsonCommand, EXIT_CODES, GLOBAL_OUTPUT_ARGS, output, outputWithExitCode, runWithJsonErrors, } from "../../cli/shared.js";
31
31
  import { UsageError } from "../../core/errors.js";
32
32
  import { TASK_RUN_BOOLEAN_FLAGS, TASK_RUN_VALUE_FLAGS } from "../../tasks/task-run-reserved-flags.js";
33
33
  import { akmTaskExplain } from "./explain.js";
@@ -356,21 +356,16 @@ const tasksSyncCommand = defineJsonCommand({
356
356
  const rebind = args.rebind === true;
357
357
  if (args["dry-run"] === true) {
358
358
  const preview = await akmTasksSyncPlan({}, args.bundle, { rebind });
359
- output("task-sync-dry-run", preview);
360
- const exitCode = taskSyncDryRunExitCode(preview);
361
- if (exitCode !== undefined)
362
- process.exitCode = exitCode;
359
+ outputWithExitCode("task-sync-dry-run", preview, taskSyncDryRunExitCode(preview));
363
360
  return;
364
361
  }
365
362
  const result = await akmTasksSync({}, args.bundle, { rebind });
366
- output("task-sync", result);
367
363
  // #867: sync degrades — sources that failed to parse/prepare are
368
364
  // excluded from reconciliation and reported in `result.failures` rather
369
365
  // than poisoning the whole sync, but their presence must still fail
370
366
  // the command's exit code so the breakage stays visible. (#906: this key
371
367
  // matches the `--dry-run` preview's `failures` field — no separate name.)
372
- if (result.failures.length > 0)
373
- process.exitCode = EXIT_CODES.GENERAL;
368
+ outputWithExitCode("task-sync", result, result.failures.length > 0 ? EXIT_CODES.GENERAL : undefined);
374
369
  },
375
370
  });
376
371
  // ── `akm task explain` — read-only introspection (P2b Lane B, spec
@@ -482,10 +477,7 @@ const tasksPruneCommand = defineJsonCommand({
482
477
  .filter(Boolean)
483
478
  : undefined;
484
479
  const result = await akmTasksPrune({}, { yes: args.yes === true, id });
485
- output("task-prune", result);
486
- const exitCode = taskPruneExitCode(result);
487
- if (exitCode !== undefined)
488
- process.exitCode = exitCode;
480
+ outputWithExitCode("task-prune", result, taskPruneExitCode(result));
489
481
  },
490
482
  });
491
483
  export const taskCommand = defineGroupCommand({
@@ -22,6 +22,7 @@ import { resolveConfiguredSources } from "../../core/config/config-sources.js";
22
22
  import { IMPROVE_AUTONOMY_CONFIG_KEY, isImproveAutonomyEnabled } from "../../core/config/experimental.js";
23
23
  import { ConfigError, NotFoundError, UsageError } from "../../core/errors.js";
24
24
  import { getTaskHistoryDir, getTaskLogDir } from "../../core/paths.js";
25
+ import { warn } from "../../core/warn.js";
25
26
  import { commitWriteTargetBoundary, deleteAssetFromSource, prepareWriteTargetForMutation, resolveWorkingStashTarget, resolveWriteTarget, writeAssetToSource, } from "../../core/write-source.js";
26
27
  import { withEngineFallback } from "../../integrations/agent/engine-fallback.js";
27
28
  import { resolveAssetPath } from "../../sources/resolve.js";
@@ -391,7 +392,7 @@ async function buildSchedulerSyncPlan(deps, bundleTarget, options) {
391
392
  const expectedSignature = sched.expectedSignature?.bind(sched);
392
393
  const needsRuntime = preflight.operations.some((operation) => operation.kind !== "remove" && operation.options?.binding === undefined);
393
394
  const prepared = needsRuntime
394
- ? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, options.rebind === true, "reconcile native scheduler bindings", warnings, allEntries.map((entry) => entry.binding))
395
+ ? prepareSchedulerSyncRuntime(syncTarget ? { target: syncTarget } : undefined, deps, warnings, allEntries.map((entry) => entry.binding))
395
396
  : undefined;
396
397
  const plan = finalizeSchedulerSyncPlan({
397
398
  ...common,
@@ -768,22 +769,24 @@ async function prepareTaskAddSchedulerTransaction(input) {
768
769
  contextPath: installedEntry.contextPath,
769
770
  },
770
771
  }
771
- : prepareSchedulerSyncRuntime(input.installOpts, input.deps, input.rebind, `create scheduler entry for task "${input.id}"`, []);
772
+ : prepareSchedulerSyncRuntime(input.installOpts, input.deps, []);
772
773
  const runtimeOpts = preparedRuntime.options;
773
774
  const removals = taskEntries.map((entry) => {
774
775
  const invocation = entry.invocation;
775
- if (!invocation) {
776
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact parsed owner; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
777
- }
778
776
  const nativeId = entry.nativeId ?? schedulerNativeBindingId(entry.id);
779
777
  const artifact = assertSchedulerNativeArtifactCardinality(nativeArtifacts, nativeId, 1);
780
778
  if (!artifact?.fingerprint || artifact.bindingId !== entry.id) {
781
779
  throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact coherent fingerprint.`, "RESOURCE_ALREADY_EXISTS");
782
780
  }
783
781
  const logicalSource = primary.logicalSource;
784
- const ordinal = schedulerBindingOrdinal(entry.id, logicalSource, invocation);
785
- if (ordinal === undefined) {
786
- throw new UsageError(`Installed scheduler binding ${JSON.stringify(entry.id)} has no exact schedule ordinal; refusing replacement.`, "RESOURCE_ALREADY_EXISTS");
782
+ const ordinal = invocation ? schedulerBindingOrdinal(entry.id, logicalSource, invocation) : undefined;
783
+ if (!invocation || ordinal === undefined) {
784
+ warn(`Installed scheduler binding ${JSON.stringify(entry.id)} (native id ${JSON.stringify(nativeId)}) could not be exactly parsed — likely a hand-edited entry; replacing it without a compare-and-swap guard.`);
785
+ return Object.freeze({
786
+ kind: "remove",
787
+ id: entry.id,
788
+ nativeId,
789
+ });
787
790
  }
788
791
  return Object.freeze({
789
792
  kind: "remove",
@@ -824,6 +827,8 @@ async function prepareTaskAddSchedulerTransaction(input) {
824
827
  for (const removal of removals) {
825
828
  if (removal.kind !== "remove")
826
829
  continue;
830
+ if (!removal.expected)
831
+ continue;
827
832
  initialByKey.set(schedulerNativeArtifactKey(removal.nativeId), Object.freeze({ ...removal.expected, state: "present" }));
828
833
  }
829
834
  for (const install of installs) {
@@ -841,16 +846,16 @@ async function prepareTaskAddSchedulerTransaction(input) {
841
846
  publishOperationIndex: removals.length,
842
847
  });
843
848
  }
844
- function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warnings, installedBindings = []) {
849
+ function prepareSchedulerSyncRuntime(base, deps, warnings, installedBindings = []) {
845
850
  if (deps.backend && !deps.schedulerRuntime)
846
851
  return base ? { options: base } : {};
847
852
  if (deps.schedulerRuntime) {
848
853
  const runtime = deps.schedulerRuntime();
849
- warnIneligibleRebind(runtime, explicitRebind, warnings, installedBindings);
854
+ warnIneligibleRebind(runtime, warnings, installedBindings);
850
855
  return { options: { ...base, binding: runtime.binding, contextPath: runtime.contextPath } };
851
856
  }
852
- const invocation = resolveAndValidateSchedulerInvocation(explicitRebind, operation);
853
- warnIneligibleRebind(invocation, explicitRebind, warnings, installedBindings);
857
+ const invocation = resolveAndValidateSchedulerInvocation();
858
+ warnIneligibleRebind(invocation, warnings, installedBindings);
854
859
  const descriptor = schedulerContextDescriptor();
855
860
  const contextPath = schedulerContextPath(descriptor);
856
861
  return {
@@ -863,24 +868,21 @@ function prepareSchedulerSyncRuntime(base, deps, explicitRebind, operation, warn
863
868
  },
864
869
  };
865
870
  }
866
- function resolveAndValidateSchedulerInvocation(explicitRebind, operation) {
871
+ function resolveAndValidateSchedulerInvocation() {
867
872
  const invocation = resolveAkmInvocation();
868
- if (!invocation.eligible && !explicitRebind) {
869
- throw new UsageError(`Refusing to ${operation} from an ineligible ${invocation.kind ?? "unknown"} invocation (${invocation.argv.join(" ")}).`, "INVALID_FLAG_VALUE", "npm-global ownership could not be verified. Run `npm install --global akm-cli` and use that launcher, use a standalone installation, or explicitly repeat the operation with --rebind.");
870
- }
871
873
  return { binding: invocation.argv, contextPath: "", eligible: invocation.eligible, kind: invocation.kind };
872
874
  }
873
- function warnIneligibleRebind(runtime, explicitRebind, warnings, installedBindings) {
874
- if (!explicitRebind || runtime.eligible !== false || warnings.length > 0)
875
+ function warnIneligibleRebind(runtime, warnings, installedBindings) {
876
+ if (runtime.eligible !== false || warnings.length > 0)
875
877
  return;
876
- // #868 residue: a `--rebind` that binds every currently-installed
877
- // entry to the SAME invocation it already carries changes nothing — this
878
- // is the steady state of an image-baked install re-running `task sync
879
- // --rebind` on a timer. Only warn when the rebind actually moves an entry
880
- // to a different invocation.
878
+ // #868 residue: binding every currently-installed entry to the SAME
879
+ // invocation it already carries changes nothing — this is the steady
880
+ // state of an image-baked install re-running `task sync` on a timer.
881
+ // Only warn when the bind actually moves an entry to a different
882
+ // invocation.
881
883
  if (installedBindings.length > 0 && installedBindings.every((bound) => sameArgv(bound, runtime.binding)))
882
884
  return;
883
- warnings.push(`--rebind bound scheduled tasks to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
885
+ warnings.push(`Scheduled tasks are bound to an ineligible ${runtime.kind ?? "unknown"} invocation (${runtime.binding.join(" ")}); scheduled runs will invoke a mutable, unproven binary. Install akm via \`npm install --global akm-cli\` or a standalone release, then re-run \`akm task sync --rebind\`.`);
884
886
  }
885
887
  function groupInstalledBindings(entries, invocation) {
886
888
  const groups = new Map();
@@ -972,16 +974,20 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
972
974
  const common = { filePath, rootRealPath };
973
975
  let descriptor;
974
976
  try {
975
- const noFollow = "O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0;
976
- descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
977
- const before = fs.fstatSync(descriptor, { bigint: true });
977
+ descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
978
+ let before = fs.fstatSync(descriptor, { bigint: true });
978
979
  if (!before.isFile()) {
979
980
  throw new UsageError(`${filePath} is not a regular task source.`, "INVALID_FLAG_VALUE");
980
981
  }
981
- const bytes = fs.readFileSync(descriptor);
982
- const after = fs.fstatSync(descriptor, { bigint: true });
983
- if (!sameTaskSourceStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
984
- throw new UsageError(`${filePath} changed while its guarded bytes were read.`, "RESOURCE_ALREADY_EXISTS");
982
+ let bytes = fs.readFileSync(descriptor);
983
+ const torn = !sameTaskSourceStat(before, fs.fstatSync(descriptor, { bigint: true })) ||
984
+ BigInt(bytes.byteLength) !== before.size;
985
+ if (torn) {
986
+ fs.closeSync(descriptor);
987
+ descriptor = fs.openSync(filePath, fs.constants.O_RDONLY);
988
+ before = fs.fstatSync(descriptor, { bigint: true });
989
+ bytes = fs.readFileSync(descriptor);
990
+ warn(`${filePath} changed while its guarded bytes were read; retried once and proceeding with the latest read (its SHA-256 is re-verified before anything is published).`);
985
991
  }
986
992
  const realPath = fs.realpathSync(filePath);
987
993
  const physicalRelative = path.relative(rootRealPath, realPath);
@@ -1011,9 +1017,6 @@ function captureTaskSourceExpectation(filePathInput, rootInput) {
1011
1017
  }
1012
1018
  if (cause instanceof UsageError)
1013
1019
  throw cause;
1014
- if (cause.code === "ELOOP") {
1015
- throw new UsageError(`${filePath} must not be a symbolic task source.`, "RESOURCE_ALREADY_EXISTS");
1016
- }
1017
1020
  throw new UsageError(`${filePath} could not be guarded as a contained regular task source: ${errorMessage(cause)}`, "PATH_ESCAPE_VIOLATION");
1018
1021
  }
1019
1022
  finally {
@@ -1264,7 +1267,7 @@ function assertInlineTaskPrompt(input) {
1264
1267
  (!/\s/.test(value) && /[\\/]/.test(value) && path.extname(value) !== "");
1265
1268
  if (!isFullRefInput(value) && !pathShaped)
1266
1269
  return;
1267
- throw new UsageError("--prompt accepts inline text only; asset refs and file paths are not prompt content. Use --workflow or an authored command ref where appropriate.", "INVALID_FLAG_VALUE");
1270
+ warn(`--prompt "${input}" looks like an asset ref or file path; --prompt sends it as literal text, not a reference. Did you mean --workflow?`);
1268
1271
  }
1269
1272
  function parseJsonObjectArg(raw) {
1270
1273
  let parsed;
@@ -8,14 +8,14 @@
8
8
  * GitHub-shaped `.yml` workflow sources. Validate with `akm lint --type workflows`.
9
9
  */
10
10
  import { getStringArg } from "../cli/parse-args.js";
11
- import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output } from "../cli/shared.js";
11
+ import { defineGroupCommand, defineJsonCommand, EXIT_CODES, output, outputWithExitCode } from "../cli/shared.js";
12
12
  import { armAbortDeadline } from "../core/abort-deadline.js";
13
13
  import { assertFlatAssetName, combineCreatePath, normalizeCreateSubPath } from "../core/asset/asset-create.js";
14
14
  import { NotFoundError, UsageError } from "../core/errors.js";
15
15
  import { akmIndex } from "../indexer/indexer.js";
16
16
  import { assertWorkflowMarkdownName, createWorkflowAsset, getWorkflowTemplate } from "../workflows/authoring/authoring.js";
17
17
  import { WORKFLOW_MAX_TIMEOUT_MS } from "../workflows/ir/schema.js";
18
- import { abandonWorkflowRun, getWorkflowStatus, hasWorkflowRun, listWorkflowRuns, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
18
+ import { abandonWorkflowRun, getWorkflowStatus, listWorkflowRuns, resolveWorkflowRunTarget, resumeWorkflowRun, } from "../workflows/runtime/runs.js";
19
19
  import { akmWorkflowPlan } from "./workflow/plan.js";
20
20
  const workflowStatusCommand = defineJsonCommand({
21
21
  meta: {
@@ -34,8 +34,9 @@ const workflowStatusCommand = defineJsonCommand({
34
34
  async run({ args }) {
35
35
  const target = args.target;
36
36
  const includeUnits = args.units === true;
37
- if (await hasWorkflowRun(target)) {
38
- const result = await getWorkflowStatus(target, { includeUnits });
37
+ const resolvedRunId = await resolveWorkflowRunTarget(target);
38
+ if (resolvedRunId !== undefined) {
39
+ const result = await getWorkflowStatus(resolvedRunId, { includeUnits });
39
40
  output("workflow-status", result);
40
41
  return;
41
42
  }
@@ -100,12 +101,12 @@ const workflowCreateCommand = defineJsonCommand({
100
101
  },
101
102
  force: {
102
103
  type: "boolean",
103
- description: "Overwrite an existing workflow (requires --from or --reset)",
104
+ description: "Overwrite an existing workflow. Combined with --from, replaces its content; alone, replaces it with a fresh template.",
104
105
  default: false,
105
106
  },
106
107
  reset: {
107
108
  type: "boolean",
108
- description: "Explicitly replace an existing workflow with a fresh template (use with --force)",
109
+ description: "Deprecated alias for --force with no --from (replaces an existing workflow with a fresh template).",
109
110
  default: false,
110
111
  },
111
112
  print: {
@@ -130,9 +131,6 @@ const workflowCreateCommand = defineJsonCommand({
130
131
  process.stdout.write(getWorkflowTemplate());
131
132
  return;
132
133
  }
133
- if (args.force && !args.from && !args.reset) {
134
- throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
135
- }
136
134
  const result = createWorkflowAsset({
137
135
  name: effectiveName,
138
136
  from: args.from,
@@ -157,6 +155,12 @@ const workflowRunCommand = defineJsonCommand({
157
155
  "max-steps": { type: "string", description: "Stop after executing this many steps" },
158
156
  "max-retries": { type: "string", description: "Retry a failed workflow step this many additional times" },
159
157
  timeout: { type: "string", description: "Whole-run timeout: N, Nms, Ns, or Nm (bare N is milliseconds)" },
158
+ new: {
159
+ type: "boolean",
160
+ description: "Start a fresh run even if one is already active for this ref, leaving the existing run untouched " +
161
+ "(never abandons it). A workflow ref only — passing a run id with --new is a usage error.",
162
+ default: false,
163
+ },
160
164
  },
161
165
  async run({ args, rawArgs }) {
162
166
  const { runWorkflowSteps } = await import("../workflows/exec/run-workflow.js");
@@ -186,6 +190,7 @@ const workflowRunCommand = defineJsonCommand({
186
190
  parameterFlags,
187
191
  ...(maxSteps !== undefined ? { maxSteps } : {}),
188
192
  ...(maxRetries !== undefined ? { maxRetries } : {}),
193
+ newRun: args.new,
189
194
  signal: controller.signal,
190
195
  });
191
196
  // The abort is observed between steps, so a deadline landing in the run's
@@ -193,14 +198,11 @@ const workflowRunCommand = defineJsonCommand({
193
198
  // timed out would send an operator to resume a run with nothing left to
194
199
  // resume — `tasks/runner.ts` suppresses the same case.
195
200
  const timedOut = deadline.timedOut() && result.run.status !== "completed";
196
- const rendered = { ...result, ...(timedOut ? { timedOut: true } : {}) };
197
- output("workflow-run", rendered);
198
201
  // `blocked` is a stopped, unverified run — a verification-judge failure
199
202
  // leaves it there for `akm workflow resume` — so it must not exit 0 and
200
203
  // read as success to a script (it maps to 1 for scheduled tasks too).
201
- if (result.run.status === "failed" || result.run.status === "blocked" || result.gateRejection || result.aborted) {
202
- process.exitCode = signalExitCode ?? EXIT_CODES.GENERAL;
203
- }
204
+ const failed = result.run.status === "failed" || result.run.status === "blocked" || result.gateRejection || result.aborted;
205
+ outputWithExitCode("workflow-run", { ...result, ...(timedOut ? { timedOut: true } : {}) }, failed ? (signalExitCode ?? EXIT_CODES.GENERAL) : undefined);
204
206
  }
205
207
  finally {
206
208
  deadline.disarm();
@@ -220,7 +222,7 @@ const WORKFLOW_RUN_VALUE_FLAGS = new Set([
220
222
  "shape",
221
223
  "output",
222
224
  ]);
223
- const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose"]);
225
+ const WORKFLOW_RUN_BOOLEAN_FLAGS = new Set(["quiet", "verbose", "help", "no-quiet", "no-verbose", "new", "no-new"]);
224
226
  export function parseWorkflowParameterFlags(rawArgs, target) {
225
227
  const flags = [];
226
228
  let targetSeen = false;
@@ -1,20 +1,48 @@
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
+ /**
5
+ * `EDITOR`/`VISUAL`/`PAGER` are flagged by `isDangerousEnvKey` for a
6
+ * documented RCE vector (many tools invoke them to launch an editor/pager),
7
+ * but env-key-rules.ts's own module doc calls out their "high FP rate" in
8
+ * the same breath — an installed bundle's env file can supply a value, but
9
+ * nothing in akm's own env-injection path ever *invokes* EDITOR/VISUAL/PAGER
10
+ * with that value, so the RCE vector these three describe cannot fire from
11
+ * an injected env the way LD_PRELOAD or GIT_SSH_COMMAND can. Blocking a
12
+ * third-party install over them protects nothing while making "the operator
13
+ * legitimately wants to set their editor" the common case that eats the
14
+ * refusal. Kept as a name-level literal set (not an import of
15
+ * `commands/lint/env-key-rules.ts`) so this module stays the pure leaf its
16
+ * own doc comment promises — no new import edges into the rest of the tree.
17
+ */
18
+ const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
4
19
  /**
5
20
  * Decide whether injecting an env with the given dangerous keys is allowed,
6
- * warned, or blocked. Third-party (registry-installed) stashes hard-block;
7
- * first-party stashes warn. See rule 1 above.
21
+ * warned, or blocked. Third-party (registry-installed) stashes hard-block a
22
+ * genuine RCE-class key; first-party stashes warn. The interactive-tool
23
+ * group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
24
+ * own env-injection path never invokes those keys as a command. An explicit
25
+ * `--allow-insecure` (threaded through by the caller, same override
26
+ * `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
27
+ * downgrades a remaining block to a warning too — the operator is not racing
28
+ * themselves. See rule 1 above.
8
29
  *
9
30
  * @param dangerousKeys The subset of injected keys flagged as process-hijacking
10
31
  * (already filtered by the caller via `isDangerousEnvKey`).
11
32
  * @param thirdParty `true` when the env's source is a third-party stash — i.e.
12
33
  * its origin carries a `registryId`.
34
+ * @param allowInsecure `true` when the operator passed `--allow-insecure` (or
35
+ * its equivalent) for this injection. Defaults to `false`.
13
36
  */
14
37
  export function decideDangerousEnvInjection(input) {
15
38
  if (input.dangerousKeys.length === 0)
16
39
  return "allow";
17
- return input.thirdParty ? "block" : "warn";
40
+ if (!input.thirdParty)
41
+ return "warn";
42
+ if (input.allowInsecure)
43
+ return "warn";
44
+ const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
45
+ return onlyInteractiveTool ? "warn" : "block";
18
46
  }
19
47
  /**
20
48
  * Decide the baseline install stance for a freshly-installed stash's
@@ -6,6 +6,8 @@ import { isMap, isScalar, parseDocument, visit } from "yaml";
6
6
  import { cloneExecutionJson, cloneExecutionJsonObject, } from "../../execution/json.js";
7
7
  import { assertSnapshotKeys, snapshotStrictRecord } from "../../execution/record.js";
8
8
  import { cloneToolSelection, createAdapterRenderedExecutionSource, } from "../../execution/source.js";
9
+ import { UsageError } from "../errors.js";
10
+ import { warnOnce } from "../warn.js";
9
11
  function nextLine(text, start) {
10
12
  const lf = text.indexOf("\n", start);
11
13
  if (lf < 0)
@@ -13,16 +15,39 @@ function nextLine(text, start) {
13
15
  const end = lf > start && text[lf - 1] === "\r" ? lf - 1 : lf;
14
16
  return { line: text.slice(start, end), next: lf + 1 };
15
17
  }
16
- /** Strict execution-only frontmatter parser; indexing's tolerant parser is deliberately not reused. */
17
- export function parseExecutionMarkdown(raw) {
18
+ /**
19
+ * Strict execution-only frontmatter parser; indexing's tolerant parser is
20
+ * deliberately not reused.
21
+ *
22
+ * `filePath`, when given, names the offending asset in a thrown error so an
23
+ * operator (or CI) sees which file to fix instead of a bare message.
24
+ *
25
+ * A third party's markdown that genuinely cannot be parsed into a mapping
26
+ * (unterminated frontmatter, broken YAML syntax, a non-mapping document)
27
+ * throws {@link UsageError} \u2014 exit 2, an actionable usage problem, not exit
28
+ * 70 (`TypeError`'s "unclassified internal error" code, which is what a
29
+ * `throw new TypeError(...)` here used to surface as). Anchors, explicit
30
+ * tags, and non-string mapping keys are recognized-but-unsupported
31
+ * constructs rather than parse failures on their own \u2014 an anchor nobody
32
+ * aliases, or a tag `toJS` converts to a plain value, is harmless \u2014 so they
33
+ * warn once (naming the file) and fall through to conversion instead of
34
+ * being pre-emptively rejected by a second, stricter gate in front of it.
35
+ * `toJS({ maxAliasCount: 0 })` is the real, UNCHANGED safety bound against
36
+ * alias-expansion abuse \u2014 it disables alias RESOLUTION outright, so a value
37
+ * that actually references an anchor via `*name` still fails, now as a
38
+ * {@link UsageError} from this same conversion step rather than the removed
39
+ * duplicate pre-check.
40
+ */
41
+ export function parseExecutionMarkdown(raw, filePath) {
18
42
  if (typeof raw !== "string")
19
43
  throw new TypeError("execution source raw content must be a string");
44
+ const where = filePath ? ` (${filePath})` : "";
20
45
  const withoutBom = raw.startsWith("\uFEFF") ? raw.slice(1) : raw;
21
46
  const opening = nextLine(withoutBom, 0);
22
47
  if (opening.line !== "---")
23
48
  return { content: withoutBom, data: Object.freeze({}) };
24
49
  if (opening.next === withoutBom.length) {
25
- throw new TypeError("execution source has unterminated frontmatter");
50
+ throw new UsageError(`execution source${where} has unterminated frontmatter`);
26
51
  }
27
52
  let cursor = opening.next;
28
53
  let frontmatterEnd = -1;
@@ -40,18 +65,18 @@ export function parseExecutionMarkdown(raw) {
40
65
  cursor = current.next;
41
66
  }
42
67
  if (frontmatterEnd < 0 || bodyStart < 0) {
43
- throw new TypeError("execution source has unterminated frontmatter");
68
+ throw new UsageError(`execution source${where} has unterminated frontmatter`);
44
69
  }
45
70
  const yaml = withoutBom.slice(opening.next, frontmatterEnd);
46
71
  const document = parseDocument(yaml, { uniqueKeys: true });
47
72
  if (document.errors.length > 0) {
48
- throw new TypeError(`execution source has invalid YAML frontmatter: ${document.errors[0]?.message ?? "parse error"}`);
73
+ throw new UsageError(`execution source${where} has invalid YAML frontmatter: ${document.errors[0]?.message ?? "parse error"}`);
49
74
  }
50
75
  if (document.warnings.length > 0) {
51
- throw new TypeError(`execution source YAML frontmatter uses an unsupported tag or construct: ${document.warnings[0]?.message}`);
76
+ warnOnce(`execution-source:yaml-warning:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses an unsupported tag or construct: ${document.warnings[0]?.message}. Using it as parsed.`);
52
77
  }
53
78
  if (!isMap(document.contents)) {
54
- throw new TypeError("execution source YAML frontmatter must be a mapping");
79
+ throw new UsageError(`execution source${where} YAML frontmatter must be a mapping`);
55
80
  }
56
81
  let unsupported;
57
82
  visit(document, {
@@ -69,14 +94,16 @@ export function parseExecutionMarkdown(raw) {
69
94
  unsupported ??= "non-string mapping keys";
70
95
  },
71
96
  });
72
- if (unsupported)
73
- throw new TypeError(`execution source YAML frontmatter does not support ${unsupported}`);
97
+ if (unsupported) {
98
+ warnOnce(`execution-source:unsupported:${filePath ?? "<inline>"}`, `execution source${where} YAML frontmatter uses ${unsupported}, which akm does not fully support; using the bounded conversion's result as-is.`);
99
+ }
74
100
  let root;
75
101
  try {
76
102
  root = document.toJS({ maxAliasCount: 0 });
77
103
  }
78
104
  catch (cause) {
79
- throw new TypeError("execution source YAML frontmatter could not be converted safely", { cause });
105
+ const detail = cause instanceof Error ? cause.message : String(cause);
106
+ throw new UsageError(`execution source${where} YAML frontmatter could not be converted safely: ${detail}`);
80
107
  }
81
108
  const data = cloneExecutionJsonObject(root, "execution source YAML frontmatter");
82
109
  return { content: withoutBom.slice(bodyStart), data: Object.freeze(data) };
@@ -279,7 +306,8 @@ export function renderMarkdownExecutionSource(input) {
279
306
  const kind = snapshots.input.kind;
280
307
  if (kind !== "command" && kind !== "persona")
281
308
  throw new TypeError("adapter execution source.kind is invalid");
282
- const parsed = parseExecutionMarkdown(raw);
309
+ const identityFile = snapshots.identity.file;
310
+ const parsed = parseExecutionMarkdown(raw, typeof identityFile === "string" ? identityFile : undefined);
283
311
  const hasDefaults = Object.hasOwn(snapshots.input, "defaults");
284
312
  const defaultsProjection = snapshots.input.defaults;
285
313
  const defaults = typeof defaultsProjection === "function" ? defaultsProjection(parsed.data) : defaultsProjection;