akm-cli 0.9.5 → 0.9.6

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 (105) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/dist/commands/env/env-cli.js +1 -2
  3. package/dist/commands/env/secret-cli.js +1 -5
  4. package/dist/commands/feedback-cli.js +0 -4
  5. package/dist/commands/health/checks.js +0 -32
  6. package/dist/commands/health/surfaces.js +2 -2
  7. package/dist/commands/health.js +6 -15
  8. package/dist/commands/improve/autonomy-gate.js +1 -1
  9. package/dist/commands/improve/consolidate.js +25 -26
  10. package/dist/commands/improve/distill.js +2 -2
  11. package/dist/commands/improve/extract.js +8 -1
  12. package/dist/commands/improve/improve.js +3 -5
  13. package/dist/commands/improve/locks.js +13 -5
  14. package/dist/commands/improve/loop-stages.js +8 -9
  15. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  16. package/dist/commands/improve/outcome-loop.js +0 -6
  17. package/dist/commands/improve/reflect.js +20 -9
  18. package/dist/commands/lint/index.js +4 -6
  19. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  20. package/dist/commands/sources/bundle-config-ops.js +9 -4
  21. package/dist/commands/sources/info.js +14 -10
  22. package/dist/commands/tasks/tasks.js +12 -16
  23. package/dist/commands/workflow-cli.js +2 -2
  24. package/dist/core/common.js +61 -24
  25. package/dist/core/config/config-io.js +2 -2
  26. package/dist/core/config/config-sources.js +32 -2
  27. package/dist/core/config/config.js +1 -1
  28. package/dist/core/config/schema/engines.js +9 -7
  29. package/dist/core/config/schema/primitives.js +0 -5
  30. package/dist/core/config/schema/search.js +2 -1
  31. package/dist/core/file-lock.js +2 -1
  32. package/dist/core/maintenance-barrier.js +2 -14
  33. package/dist/core/paths.js +0 -3
  34. package/dist/core/redaction.js +2 -2
  35. package/dist/core/spawn-env.js +8 -12
  36. package/dist/core/state/migrations.js +1 -12
  37. package/dist/core/state-db.js +9 -27
  38. package/dist/core/write-source.js +12 -19
  39. package/dist/execution/directory-identity.js +36 -10
  40. package/dist/indexer/graph/graph-boost.js +0 -4
  41. package/dist/indexer/index-writer-lock.js +43 -24
  42. package/dist/indexer/index-written-assets.js +5 -6
  43. package/dist/indexer/indexer.js +2 -39
  44. package/dist/indexer/materialize-embeddings.js +85 -41
  45. package/dist/indexer/search/db-search.js +15 -48
  46. package/dist/indexer/search/ranking-contributors.js +0 -25
  47. package/dist/indexer/search/ranking.js +3 -13
  48. package/dist/integrations/agent/builder-shared.js +0 -25
  49. package/dist/integrations/agent/model-map.js +2 -60
  50. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  51. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  52. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  53. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  54. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  55. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  56. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  57. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  58. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  59. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  60. package/dist/integrations/session-logs/index.js +0 -9
  61. package/dist/llm/client.js +75 -42
  62. package/dist/llm/embedder.js +7 -3
  63. package/dist/llm/embedders/remote.js +141 -42
  64. package/dist/registry/network.js +5 -37
  65. package/dist/runtime.js +2 -10
  66. package/dist/scripts/akm-migrate-node.js +60 -126
  67. package/dist/scripts/akm-migrate.js +60 -126
  68. package/dist/setup/engine-config.js +2 -5
  69. package/dist/setup/registry-stash-loader.js +0 -8
  70. package/dist/setup/setup.js +9 -46
  71. package/dist/setup/steps/connection-shared.js +10 -13
  72. package/dist/sources/providers/git-install.js +1 -1
  73. package/dist/storage/engines/sqlite-migrations.js +20 -1
  74. package/dist/storage/repositories/index-entries-repository.js +0 -15
  75. package/dist/tasks/backends/launchd.js +15 -20
  76. package/dist/tasks/backends/schtasks.js +18 -8
  77. package/dist/tasks/run/run-native-task.js +8 -6
  78. package/dist/tasks/source/bounded-document.js +2 -4
  79. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  80. package/dist/tasks/source/task-source-v4.js +5 -10
  81. package/dist/tasks/source/task-to-v3.js +5 -10
  82. package/dist/tasks/source/task-to-v4.js +1 -4
  83. package/dist/tasks/source-v3.js +6 -6
  84. package/dist/workflows/exec/native-executor.js +21 -31
  85. package/dist/workflows/exec/run-workflow.js +5 -6
  86. package/dist/workflows/exec/scheduler.js +3 -19
  87. package/dist/workflows/exec/step-work.js +1 -4
  88. package/dist/workflows/exec/unit-dispatch.js +2 -2
  89. package/dist/workflows/exec/worktree.js +1 -13
  90. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  91. package/dist/workflows/ir/plan-hash.js +4 -6
  92. package/dist/workflows/ir/schema-v4.js +0 -12
  93. package/dist/workflows/ir/schema.js +20 -31
  94. package/dist/workflows/parser.js +11 -52
  95. package/dist/workflows/renderer.js +2 -3
  96. package/dist/workflows/resource-limits.js +11 -41
  97. package/dist/workflows/runtime/runs.js +3 -4
  98. package/dist/workflows/source-ir/schema.js +14 -30
  99. package/dist/workflows/validator.js +1 -7
  100. package/docs/reference/configuration.md +1 -1
  101. package/package.json +1 -1
  102. package/schemas/akm-config.json +4 -23
  103. package/schemas/akm-task.json +1 -2
  104. package/schemas/akm-workflow.json +1 -13
  105. package/dist/indexer/search/semantic-status.js +0 -142
@@ -29,11 +29,10 @@ import { ConfigError, UsageError } from "../core/errors.js";
29
29
  import { getConfigPath, getDefaultStashDir, isTransientStashPath } from "../core/paths.js";
30
30
  import { warn } from "../core/warn.js";
31
31
  import { akmIndex } from "../indexer/indexer.js";
32
- import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/search/semantic-status.js";
33
32
  import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../integrations/agent/index.js";
34
33
  import { defaultProfileName } from "../integrations/harnesses/index.js";
35
34
  import { readLockfile } from "../integrations/lockfile.js";
36
- import { probeLlmCapabilities } from "../llm/client.js";
35
+ import { probeLlmReachable } from "../llm/client.js";
37
36
  import { detectEnvironment, detectLMStudio, renderDetectionSummary, } from "./detect.js";
38
37
  import { upsertDetectedAgentEngine, upsertDetectedLlmEngine, verifyOpenAiCompatibleEndpoint } from "./detected-engines.js";
39
38
  import { readCurrentLlmEngine, writeAgentEngines, writeLlmEngine } from "./engine-config.js";
@@ -592,38 +591,14 @@ export async function runSetupWizard(opts) {
592
591
  // Non-interactive setup paths never reach this interactive-only step.
593
592
  p.log.step("Scheduled Tasks");
594
593
  await stepScheduledTasks();
595
- if (semanticSearchMode.mode === "off") {
596
- clearSemanticStatus();
597
- }
598
594
  if (semanticSearchMode.mode === "auto") {
599
595
  if (semanticSearchMode.prepareAssets) {
600
596
  const ready = await prepareSemanticSearchAssets(savedConfig);
601
597
  if (!ready.ok) {
602
- writeSemanticStatus({
603
- status: "blocked",
604
- reason: ready.reason,
605
- message: ready.message,
606
- providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
607
- lastCheckedAt: new Date().toISOString(),
608
- });
609
598
  p.log.warn("Semantic search remains set to auto, but is currently blocked. Re-run `akm index --full --verbose` once the issue is resolved.");
610
599
  }
611
- else {
612
- writeSemanticStatus({
613
- status: "pending",
614
- message: "Semantic prerequisites verified. Building the index to finish activation.",
615
- providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
616
- lastCheckedAt: new Date().toISOString(),
617
- });
618
- }
619
600
  }
620
601
  else {
621
- writeSemanticStatus({
622
- status: "pending",
623
- message: "Semantic search is enabled, but asset preparation was skipped.",
624
- providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
625
- lastCheckedAt: new Date().toISOString(),
626
- });
627
602
  p.log.info("Semantic search is set to auto, but asset preparation was skipped. Run `akm index --full --verbose` later to verify it.");
628
603
  }
629
604
  }
@@ -649,15 +624,6 @@ export async function runSetupWizard(opts) {
649
624
  catch (err) {
650
625
  spin.stop("Indexing failed — you can run `akm index` manually later.");
651
626
  p.log.warn(String(err));
652
- if (savedConfig.semanticSearchMode === "auto") {
653
- writeSemanticStatus({
654
- status: "blocked",
655
- reason: "index-failed",
656
- message: String(err),
657
- providerFingerprint: deriveSemanticProviderFingerprint(savedConfig.embedding),
658
- lastCheckedAt: new Date().toISOString(),
659
- });
660
- }
661
627
  }
662
628
  // API key reminder
663
629
  if (embedding?.apiKey === undefined && embedding?.provider !== "ollama") {
@@ -989,22 +955,19 @@ export async function runSetupFromConfig(opts) {
989
955
  merged = ctx.config;
990
956
  }
991
957
  // Convert the private setup draft to the persisted bundle shape.
992
- let finalizedMerged = finalizeSetupDraft(merged);
958
+ const finalizedMerged = finalizeSetupDraft(merged);
993
959
  // Reject an invalid merged engine graph before probing or touching the stash.
994
960
  validateCompleteConfig(finalizedMerged);
995
- // Optional probe
961
+ // Optional connectivity probe — informational only, never blocks or
962
+ // mutates config. `chatCompletion` attempts structured output fresh on
963
+ // every call that needs it (see `llm/client.ts`), so there is nothing to
964
+ // probe or persist here beyond "did the endpoint answer".
996
965
  const mergedLlm = readCurrentLlmEngine(finalizedMerged);
997
966
  if (opts.probe && mergedLlm) {
998
967
  try {
999
- const caps = await probeLlmCapabilities(mergedLlm);
1000
- if (caps.reachable) {
1001
- finalizedMerged = {
1002
- ...finalizedMerged,
1003
- ...writeLlmEngine(finalizedMerged, {
1004
- ...mergedLlm,
1005
- capabilities: { structuredOutput: caps.structuredOutput ?? false },
1006
- }),
1007
- };
968
+ const reach = await probeLlmReachable(mergedLlm);
969
+ if (!reach.reachable) {
970
+ warn(`[akm setup] LLM endpoint not reachable${reach.error ? `: ${reach.error}` : ""}.`);
1008
971
  }
1009
972
  }
1010
973
  catch {
@@ -9,7 +9,7 @@
9
9
  * each step keeps its own option assembly and branching.
10
10
  */
11
11
  import * as p from "../../cli/clack.js";
12
- import { probeLlmCapabilities } from "../../llm/client.js";
12
+ import { probeLlmReachable } from "../../llm/client.js";
13
13
  import { prompt, promptOrBack } from "../prompt.js";
14
14
  // ── Derive-config ───────────────────────────────────────────────────────────
15
15
  /** Standard chat-connection config shared by every provider branch. */
@@ -97,21 +97,18 @@ export async function promptApiKeyEnvVarName() {
97
97
  }
98
98
  // ── Probe ───────────────────────────────────────────────────────────────────
99
99
  /**
100
- * Best-effort structured-output probe — never blocks setup. Annotates
101
- * `llm.capabilities.structuredOutput` in place when the endpoint answers,
102
- * and warns (configuration is still saved) when it does not.
100
+ * Best-effort reachability probe — never blocks setup. Does NOT probe or
101
+ * cache structured-output support: `chatCompletion` now attempts
102
+ * `response_format: json_schema` fresh on every call that supplies a schema
103
+ * and falls back once per call on a 4xx (see `llm/client.ts`), so there is
104
+ * no verdict for setup to determine or persist here.
103
105
  */
104
106
  export async function probeLlmConnection(llm) {
105
107
  const probeSpin = p.spinner();
106
- probeSpin.start("Probing LLM (structured-output round-trip)...");
107
- const probe = await probeLlmCapabilities(llm);
108
- if (probe.reachable && probe.structuredOutput) {
109
- probeSpin.stop("LLM reachable; structured output verified.");
110
- llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
111
- }
112
- else if (probe.reachable) {
113
- probeSpin.stop("LLM reachable but structured-output probe failed.");
114
- llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
108
+ probeSpin.start("Probing LLM connectivity...");
109
+ const probe = await probeLlmReachable(llm);
110
+ if (probe.reachable) {
111
+ probeSpin.stop("LLM reachable.");
115
112
  }
116
113
  else {
117
114
  probeSpin.stop("LLM not reachable.");
@@ -198,7 +198,7 @@ async function doSyncGit(parsed, options) {
198
198
  };
199
199
  }
200
200
  }
201
- catch (error) {
201
+ catch {
202
202
  // Cache invalid, re-clone
203
203
  }
204
204
  }
@@ -1,6 +1,25 @@
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
+ * Shared SQLite migration engine.
6
+ *
7
+ * SQLite schemas evolve through this transaction-per-migration runner backed
8
+ * by a `schema_migrations` ledger.
9
+ *
10
+ * This module factors that runner out once. Each caller supplies only its own
11
+ * `MIGRATIONS` array.
12
+ *
13
+ * Ledger/transaction contract:
14
+ * - `id` is permanent and must never be reused.
15
+ * - Applied IDs must be an exact ordered prefix of the registry.
16
+ * - Each `up` body and its ledger insert commit in the same transaction.
17
+ * - Migration bodies run only after `BEGIN IMMEDIATE` is observably active,
18
+ * and the transaction must remain active through the body's completion.
19
+ * - The caller owns semantic safety classification and any policy gate;
20
+ * this generic engine intentionally does not infer risk from SQL text.
21
+ */
22
+ import { sleepSync } from "../../runtime.js";
4
23
  export function assertMigrationRegistry(migrations) {
5
24
  const seen = new Set();
6
25
  for (const migration of migrations) {
@@ -152,7 +171,7 @@ function isRetryableImmediateBeginError(error) {
152
171
  function sleepImmediateRetry(ms) {
153
172
  if (ms <= 0)
154
173
  return;
155
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
174
+ sleepSync(ms);
156
175
  }
157
176
  /**
158
177
  * Run `fn` inside a `BEGIN IMMEDIATE` transaction.
@@ -354,9 +354,6 @@ function deleteEntryRows(db, rows, options = {}) {
354
354
  }
355
355
  return rows.map((row) => row.id);
356
356
  }
357
- export function deleteEntriesByDir(db, dirPath) {
358
- db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath)))();
359
- }
360
357
  export function deleteEntriesByDirAndBundle(db, dirPath, bundleId, options = {}) {
361
358
  return db.transaction(() => deleteEntryRows(db, rowsInDirectory(db, dirPath, bundleId), options))();
362
359
  }
@@ -704,18 +701,6 @@ export function getEntryRefRowsForStashRoot(db, stashRoot) {
704
701
  });
705
702
  }
706
703
  // ── Indexer-phase helpers (moved from indexer.ts) ────────────────────────────
707
- /**
708
- * Compute a cheap FNV-1a hash of a buffer for source-identity tracking.
709
- * Not security-sensitive; used as an incremental fast-path skip key.
710
- */
711
- export function computeSourceHash(content) {
712
- let hash = 0x811c9dc5;
713
- for (let i = 0; i < content.length; i++) {
714
- hash ^= content[i];
715
- hash = Math.imul(hash, 0x01000193);
716
- }
717
- return (hash >>> 0).toString(16);
718
- }
719
704
  /**
720
705
  * Return distinct zero-result search queries from the `usage_events` table
721
706
  * within the given lookback window.
@@ -494,15 +494,20 @@ function assertUnrestorableLaunchdSnapshotUnchanged(entry, currentArtifacts) {
494
494
  throw new ConfigError(`Loaded launchd artifact ${JSON.stringify(entry.id)} has no restorable plist and changed after its snapshot; refusing mutation.`, "INVALID_CONFIG_FILE");
495
495
  }
496
496
  }
497
+ /**
498
+ * Enumerate the current launchd AKM service namespace (loaded domain members,
499
+ * print-disabled labels, and on-disk plists) in one pass.
500
+ *
501
+ * This used to run the enumeration twice and fail closed if the two passes
502
+ * disagreed, guarding against launchd state mutating between the two
503
+ * `launchctl` shell-outs (milliseconds apart, in-process). The only actor
504
+ * that could cause that is another concurrent akm process or the user
505
+ * running launchctl by hand at that exact instant; a subsequent `task sync`
506
+ * (idempotent by design) reconciles any such drift on its own, so the
507
+ * two-pass fencing added a TOCTOU-shaped guard around a race with a working
508
+ * self-heal already in place, not a way to avoid one.
509
+ */
497
510
  function inspectStableLaunchdNamespace(seedIds, context) {
498
- const first = captureLaunchdNamespacePass(seedIds, context);
499
- const second = captureLaunchdNamespacePass(seedIds, context);
500
- if (first.stabilityKey !== second.stabilityKey) {
501
- throw new ConfigError("The launchd AKM service namespace changed while scheduler state was being stabilized.", "INVALID_CONFIG_FILE");
502
- }
503
- return second.namespace;
504
- }
505
- function captureLaunchdNamespacePass(seedIds, context) {
506
511
  const domain = context.exec.run(["launchctl", "print", `gui/${context.exec.uid()}`]);
507
512
  if (domain.status !== 0) {
508
513
  throw new ConfigError(`launchctl failed to enumerate the loaded user domain during scheduler state inspection: ${domain.stderr || domain.stdout || "no output"}.`, "INVALID_CONFIG_FILE");
@@ -532,9 +537,6 @@ function captureLaunchdNamespacePass(seedIds, context) {
532
537
  ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
533
538
  for (const serviceLabel of akmDisabledLabels)
534
539
  ids.add(serviceLabel.slice(LAUNCHD_LABEL_PREFIX.length));
535
- if (ids.size > MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES) {
536
- throw new ConfigError(`launchd AKM scheduler inventory exceeds ${MAX_LAUNCHD_AKM_NAMESPACE_ENTRIES} namespace entries.`, "INVALID_CONFIG_FILE");
537
- }
538
540
  const plistByNativeId = new Map(plistEntries);
539
541
  const entries = [];
540
542
  for (const nativeId of [...ids].sort()) {
@@ -566,15 +568,8 @@ function captureLaunchdNamespacePass(seedIds, context) {
566
568
  }, parsed.invocation, entry.nativeId));
567
569
  }
568
570
  return Object.freeze({
569
- namespace: Object.freeze({
570
- inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
571
- entries: Object.freeze(entries),
572
- }),
573
- stabilityKey: JSON.stringify({
574
- loadedLabels: [...loadedLabels].sort(),
575
- disabledLabels: akmDisabledLabels,
576
- plistEntries,
577
- }),
571
+ inspection: Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) }),
572
+ entries: Object.freeze(entries),
578
573
  });
579
574
  }
580
575
  function restoreLaunchdBindingEntry(entry, context, errors) {
@@ -106,7 +106,7 @@ export function SCHTASKS_BACKEND(options = {}) {
106
106
  assertSchedulerMutationArtifact(current?.status === 0 && finalNativeId ? schtasksArtifact(finalNativeId, current.stdout) : undefined, expected);
107
107
  }
108
108
  else {
109
- assertSchtasksArtifactUnchanged(exec, taskName(nativeId), query, nativeId, task);
109
+ assertSchtasksArtifactUnchanged(exec, taskName(nativeId), nativeId, task);
110
110
  }
111
111
  try {
112
112
  // /F forces overwrite if a task with the same name exists.
@@ -461,8 +461,23 @@ function parsePowerShellSingleQuotedArgs(script, start) {
461
461
  }
462
462
  return argv;
463
463
  }
464
- /** Close the read/prepare-to-/Create ownership race at the native boundary. */
465
- function assertSchtasksArtifactUnchanged(exec, nativeTaskName, previous, nativeId, task) {
464
+ /**
465
+ * Re-verify ownership of the native artifact immediately before `/Create /F`
466
+ * overwrites it (the read/prepare-to-/Create ownership race at the native
467
+ * boundary).
468
+ *
469
+ * This used to also compare the fresh query against the first read taken
470
+ * earlier in `install` and refuse the whole operation if anything about the
471
+ * XML had changed in between -- a freshness re-check on top of the identity
472
+ * re-check right below it. That extra comparison never caught anything the
473
+ * owner check didn't already cover (a foreign/changed owner still fails
474
+ * `assertSchedulerNativeArtifactOwner`), and `task sync` is idempotent, so a
475
+ * spurious refusal here only cost the user a rerun of a command they'd
476
+ * already asked for. Dropped; the owner re-check (the actual corruption/
477
+ * clobber guard -- no fallback exists if a foreign task got silently
478
+ * overwritten) stays.
479
+ */
480
+ function assertSchtasksArtifactUnchanged(exec, nativeTaskName, nativeId, task) {
466
481
  // This runs outside install's rollback region: no scheduler mutation has
467
482
  // occurred, so a raced owner must remain untouched rather than be restored
468
483
  // from the stale first read.
@@ -470,11 +485,6 @@ function assertSchtasksArtifactUnchanged(exec, nativeTaskName, previous, nativeI
470
485
  isOk: (result) => result.status === 0 || isMissingTaskResult(result),
471
486
  message: (result) => `schtasks /Query failed during final ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
472
487
  });
473
- const changed = current.status !== previous.status ||
474
- (current.status === 0 && normalizeXmlForUtf16File(current.stdout) !== normalizeXmlForUtf16File(previous.stdout));
475
- if (changed) {
476
- throw new ConfigError(`schtasks task "${nativeTaskName}" changed while it was being prepared; refusing to replace an unverified owner.`, "INVALID_CONFIG_FILE");
477
- }
478
488
  if (current.status === 0) {
479
489
  assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(current.stdout)?.invocation);
480
490
  }
@@ -20,7 +20,7 @@
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 { assertFrozenDirectoryIdentity } from "../../execution/directory-identity.js";
23
+ import { assertFrozenDirectoryContained } from "../../execution/directory-identity.js";
24
24
  import { cleanupFrozenScript, frozenScriptCommand, materializeFrozenScript } from "../frozen-script.js";
25
25
  import { resolveAkmInvocation } from "../resolve-akm-bin.js";
26
26
  import { finishAttempt } from "./attempt-lifecycle.js";
@@ -146,11 +146,13 @@ export async function runNativeTask(input) {
146
146
  const dbLines = [{ line: header }];
147
147
  let exitCode = null;
148
148
  try {
149
- // The projector froze both canonical paths and filesystem identities before
150
- // history mutation. Re-resolve the authored root/cwd immediately before
151
- // spawn so a symlink, ancestor, bundle-root, or directory/file swap cannot
152
- // redirect execution outside that physical workspace.
153
- assertFrozenDirectoryIdentity(task.cwdIdentity);
149
+ // Re-resolve the authored root/cwd immediately before spawn so a
150
+ // symlink, ancestor, bundle-root, or directory/file swap cannot redirect
151
+ // execution outside that physical workspace. This is a live containment
152
+ // check, not an identity comparison — it does not care whether the
153
+ // underlying device/inode changed (e.g. a remount), only whether the
154
+ // resolved cwd still lives inside its root.
155
+ assertFrozenDirectoryContained(task.cwdIdentity);
154
156
  if (task.kind === "script") {
155
157
  materialized = materializeFrozenScript(task);
156
158
  cmd = frozenScriptCommand(task, materialized.file);
@@ -53,13 +53,14 @@ import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
53
53
  import { snapshotStrictRecord } from "../../execution/record.js";
54
54
  import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
55
55
  // ── Resource bounds (D2-N4: owned here, re-exported by source-v3.ts) ───────
56
- export const TASK_V3_MAX_SOURCE_BYTES = 1024 * 1024;
57
56
  export const TASK_V3_MAX_JSON_DEPTH = 64;
58
57
  export const TASK_V3_MAX_JSON_NODES = 10_000;
59
58
  export const TASK_V3_MAX_COLLECTION_ITEMS = 1024;
60
59
  export const TASK_V3_MAX_OBJECT_KEYS = 256;
61
60
  export const TASK_V3_MAX_STRING_BYTES = 256 * 1024;
62
61
  export const TASK_V3_MAX_SCHEDULES = 64;
62
+ /** Max entries in a task's `redact:` env-name list. */
63
+ export const TASK_V3_MAX_REDACT_NAMES = 32;
63
64
  export function own(value, key) {
64
65
  return Object.hasOwn(value, key);
65
66
  }
@@ -414,9 +415,6 @@ export function readBoundedTaskSourceYaml(input, options) {
414
415
  if (typeof input.yaml !== "string") {
415
416
  throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source must be a string.`, "TASK_SOURCE_INVALID");
416
417
  }
417
- if (utf8Bytes(input.yaml) > TASK_V3_MAX_SOURCE_BYTES) {
418
- throw new UsageError(`Invalid ${sourceLabel} at ${input.filePath}: source exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) resource limit.`, "TASK_SOURCE_INVALID");
419
- }
420
418
  const lineCounter = new LineCounter();
421
419
  let document;
422
420
  try {
@@ -42,8 +42,8 @@ import { parseBuiltinCommandAction } from "../../commands/command/builtin-action
42
42
  import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
43
43
  import { UsageError } from "../../core/errors.js";
44
44
  import { checkJsonSchemaDefinition } from "../../core/json-schema.js";
45
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, } from "../../workflows/resource-limits.js";
46
- import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN } from "../../workflows/resource-limits.js";
46
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, parseTimeout, parseTools, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, TASK_V3_MAX_SCHEDULES, validateWorkingDirectory, } from "./bounded-document.js";
47
47
  export const TASK_V3_SCHEMA_VERSION = 3;
48
48
  /** Closed authoring vocabulary. Arbitrary GitHub `{0}` shell templates are not accepted. */
49
49
  export const TASK_V3_HOST_SHELLS = ["bash", "sh", "zsh", "pwsh", "powershell", "cmd"];
@@ -146,7 +146,7 @@ function parseAkm(value, ctx) {
146
146
  out.timeout = parseTimeout(input.timeout, ctx);
147
147
  if (own(input, "redact")) {
148
148
  const names = parseStringArray(input.redact, ctx, ["akm", "redact"], {
149
- max: WORKFLOW_MAX_EXEC_PASS_ENV,
149
+ max: TASK_V3_MAX_REDACT_NAMES,
150
150
  pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
151
151
  });
152
152
  if (new Set(names).size !== names.length)
@@ -160,10 +160,8 @@ function parseAkm(value, ctx) {
160
160
  out.maxSteps = input.maxSteps;
161
161
  }
162
162
  if (own(input, "maxRetries")) {
163
- if (!Number.isSafeInteger(input.maxRetries) ||
164
- input.maxRetries < 0 ||
165
- input.maxRetries > WORKFLOW_MAX_RETRIES) {
166
- sourceError(ctx, ["akm", "maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
163
+ if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
164
+ sourceError(ctx, ["akm", "maxRetries"], "must be a non-negative safe integer.");
167
165
  }
168
166
  out.maxRetries = input.maxRetries;
169
167
  }
@@ -42,10 +42,10 @@ import { applyInputDefaults, INPUT_NAME_PATTERN, validateInputs, } from "../../e
42
42
  import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js";
43
43
  import { classifyTargetRef } from "../../execution/target-ref.js";
44
44
  import { detectSecretShapedParams } from "../../workflows/exec/param-secrets.js";
45
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_PARAMS, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_SCHEMA_BYTES, } from "../../workflows/resource-limits.js";
45
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_SCHEMA_BYTES } from "../../workflows/resource-limits.js";
46
46
  import { TASK_V3_HOST_SHELLS, TASK_V3_MAX_SCHEDULES } from "../source-v3.js";
47
47
  import { TASK_RUN_RESERVED_FLAG_NAMES, TASK_RUN_SELF_DIAGNOSED_FLAGS } from "../task-run-reserved-flags.js";
48
- import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
48
+ import { asRecord, checkKeys, cloneBoundedJson, noGithubExpression, own, parseEnvironment, parseStringArray, presentJsonValue, readBoundedTaskSourceYaml, sourceError, stringField, TASK_V3_MAX_REDACT_NAMES, utf8Bytes, validateWorkingDirectory, } from "./bounded-document.js";
49
49
  // ── Closed constants (D1, D2-N3, D2-N7) ─────────────────────────────────────
50
50
  export const TASK_SOURCE_V4_VERSION = 4;
51
51
  /** The exact, closed top-level key set (D2-N7) — `akm` and `on` are deliberately absent. */
@@ -298,7 +298,7 @@ function parseExecutionControls(input, ctx) {
298
298
  out.timeout = parseTimeoutTopLevel(input.timeout, ctx);
299
299
  if (own(input, "redact")) {
300
300
  const names = parseStringArray(input.redact, ctx, ["redact"], {
301
- max: WORKFLOW_MAX_EXEC_PASS_ENV,
301
+ max: TASK_V3_MAX_REDACT_NAMES,
302
302
  pattern: WORKFLOW_ENV_VAR_NAME_PATTERN,
303
303
  });
304
304
  if (new Set(names).size !== names.length)
@@ -312,10 +312,8 @@ function parseExecutionControls(input, ctx) {
312
312
  out.maxSteps = input.maxSteps;
313
313
  }
314
314
  if (own(input, "maxRetries")) {
315
- if (!Number.isSafeInteger(input.maxRetries) ||
316
- input.maxRetries < 0 ||
317
- input.maxRetries > WORKFLOW_MAX_RETRIES) {
318
- sourceError(ctx, ["maxRetries"], `must be an integer from 0 through ${WORKFLOW_MAX_RETRIES}.`);
315
+ if (!Number.isSafeInteger(input.maxRetries) || input.maxRetries < 0) {
316
+ sourceError(ctx, ["maxRetries"], "must be a non-negative safe integer.");
319
317
  }
320
318
  out.maxRetries = input.maxRetries;
321
319
  }
@@ -376,9 +374,6 @@ function parseInputDeclaration(name, raw, ctx) {
376
374
  function parseInputDeclarations(value, ctx) {
377
375
  const input = asRecord(value, ctx, ["inputs"]);
378
376
  const names = Object.keys(input);
379
- if (names.length > WORKFLOW_MAX_PARAMS) {
380
- sourceError(ctx, ["inputs"], `accepts at most ${WORKFLOW_MAX_PARAMS} declared inputs.`);
381
- }
382
377
  const result = {};
383
378
  for (const name of names) {
384
379
  if (!INPUT_NAME_PATTERN.test(name)) {
@@ -7,9 +7,9 @@ import path from "node:path";
7
7
  import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
8
8
  import { bundleRefToString, parseBundleRef } from "../../core/asset/asset-ref.js";
9
9
  import { formatExtraParamsIssue, validateExtraParams } from "../../core/extra-params.js";
10
- import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_EXEC_PASS_ENV, WORKFLOW_MAX_RETRIES, WORKFLOW_MAX_TIMEOUT_MS, } from "../../workflows/resource-limits.js";
10
+ import { WORKFLOW_ENV_VAR_NAME_PATTERN, WORKFLOW_MAX_TIMEOUT_MS } from "../../workflows/resource-limits.js";
11
11
  import { validateTaskId } from "../task-id.js";
12
- import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
12
+ import { assertBoundedTaskYamlDocument, TASK_V3_MAX_REDACT_NAMES } from "./bounded-document.js";
13
13
  import { classifyTaskV3Uses, parseTaskV3Yaml } from "./task-source-v3-frozen.js";
14
14
  import { parseTaskSourceV4 } from "./task-source-v4.js";
15
15
  const V2_KEYS = new Set([
@@ -135,9 +135,6 @@ function optionalString(value, label) {
135
135
  return exactString(value, label);
136
136
  }
137
137
  function parseLegacyTaskYaml(input) {
138
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
139
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
140
- }
141
138
  let source;
142
139
  try {
143
140
  source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
@@ -189,7 +186,7 @@ function validateCommonV2(data) {
189
186
  }
190
187
  if (data.redact !== undefined && data.redact !== null) {
191
188
  if (!Array.isArray(data.redact) ||
192
- data.redact.length > WORKFLOW_MAX_EXEC_PASS_ENV ||
189
+ data.redact.length > TASK_V3_MAX_REDACT_NAMES ||
193
190
  data.redact.some((entry) => typeof entry !== "string" || !WORKFLOW_ENV_VAR_NAME_PATTERN.test(entry))) {
194
191
  throw new Error("redact must contain only bounded environment variable names");
195
192
  }
@@ -321,10 +318,8 @@ function migratedObject(data) {
321
318
  akm.maxSteps = data.maxSteps;
322
319
  }
323
320
  if (data.maxRetries !== undefined && data.maxRetries !== null) {
324
- if (!Number.isSafeInteger(data.maxRetries) ||
325
- data.maxRetries < 0 ||
326
- data.maxRetries > WORKFLOW_MAX_RETRIES) {
327
- throw new Error(`maxRetries must be from 0 through ${WORKFLOW_MAX_RETRIES}`);
321
+ if (!Number.isSafeInteger(data.maxRetries) || data.maxRetries < 0) {
322
+ throw new Error("maxRetries must be a non-negative integer");
328
323
  }
329
324
  akm.maxRetries = data.maxRetries;
330
325
  }
@@ -18,7 +18,7 @@
18
18
  import crypto from "node:crypto";
19
19
  import path from "node:path";
20
20
  import { LineCounter, parseDocument, stringify as stringifyYaml } from "yaml";
21
- import { assertBoundedTaskYamlDocument, TASK_V3_MAX_SOURCE_BYTES } from "./bounded-document.js";
21
+ import { assertBoundedTaskYamlDocument } from "./bounded-document.js";
22
22
  import { classifyTaskV3Uses } from "./task-source-v3-frozen.js";
23
23
  import { parseTaskSourceV4 } from "./task-source-v4.js";
24
24
  /** The closed v3 top-level key set (`src/tasks/source-v3.ts`'s own, vendored — not exported there). */
@@ -114,9 +114,6 @@ function exactString(value, label, nonempty = false) {
114
114
  * away (C-N1).
115
115
  */
116
116
  function parseV3RawYaml(input) {
117
- if (input.bytes.byteLength > TASK_V3_MAX_SOURCE_BYTES) {
118
- throw new Error(`task YAML exceeds the 1 MiB (${TASK_V3_MAX_SOURCE_BYTES}-byte) source resource limit`);
119
- }
120
117
  let source;
121
118
  try {
122
119
  source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(input.bytes);
@@ -1,13 +1,13 @@
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 { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES } from "./source/bounded-document.js";
4
+ import { TASK_V3_MAX_SCHEDULES } from "./source/bounded-document.js";
5
5
  // D2-N4 (spec docs/plans/specs/p2a-task-source-v4.md §3.1, §9): re-exported
6
- // at their EXISTING names so no importer changes. Only the two names a
7
- // surviving `src` consumer still imports (task-source-v4.ts,
8
- // commands/tasks/tasks.ts) — every other bounded-document re-export this
9
- // file used to carry was dropped in P4 §3.2.3.
10
- export { TASK_V3_MAX_SCHEDULES, TASK_V3_MAX_SOURCE_BYTES };
6
+ // at its EXISTING name so no importer changes. The one name a surviving
7
+ // `src` consumer still imports (task-source-v4.ts) — every other
8
+ // bounded-document re-export this file used to carry was dropped in P4
9
+ // §3.2.3.
10
+ export { TASK_V3_MAX_SCHEDULES };
11
11
  export const TASK_EXTENSION = ".yml";
12
12
  export const TASK_NEAR_MISS_EXTENSION = ".yaml";
13
13
  /** Explain why the near-miss `.yaml` spelling is not a task source. */