akm-cli 0.9.4 → 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 (123) hide show
  1. package/CHANGELOG.md +265 -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/anti-collapse.js +4 -91
  9. package/dist/commands/improve/autonomy-gate.js +1 -1
  10. package/dist/commands/improve/consolidate.js +25 -26
  11. package/dist/commands/improve/distill.js +2 -2
  12. package/dist/commands/improve/extract.js +8 -1
  13. package/dist/commands/improve/improve.js +3 -5
  14. package/dist/commands/improve/locks.js +13 -5
  15. package/dist/commands/improve/loop-stages.js +8 -9
  16. package/dist/commands/improve/memory/memory-contradiction-detect.js +1 -19
  17. package/dist/commands/improve/outcome-loop.js +0 -6
  18. package/dist/commands/improve/reflect.js +20 -9
  19. package/dist/commands/lint/index.js +4 -6
  20. package/dist/commands/proposal/validators/proposal-quality-validators.js +0 -7
  21. package/dist/commands/proposal/validators/proposal-validators.js +12 -0
  22. package/dist/commands/read/search.js +14 -24
  23. package/dist/commands/sources/bundle-config-ops.js +9 -4
  24. package/dist/commands/sources/info.js +14 -10
  25. package/dist/commands/tasks/tasks-cli.js +56 -3
  26. package/dist/commands/tasks/tasks.js +101 -18
  27. package/dist/commands/workflow-cli.js +2 -2
  28. package/dist/core/adapter/adapters/akm-adapter.js +2 -0
  29. package/dist/core/common.js +61 -24
  30. package/dist/core/config/config-io.js +2 -2
  31. package/dist/core/config/config-sources.js +32 -2
  32. package/dist/core/config/config-version-shim.js +101 -0
  33. package/dist/core/config/config.js +7 -7
  34. package/dist/core/config/schema/engines.js +9 -7
  35. package/dist/core/config/schema/primitives.js +0 -5
  36. package/dist/core/config/schema/search.js +2 -1
  37. package/dist/core/file-lock.js +2 -1
  38. package/dist/core/improve-result.js +35 -14
  39. package/dist/core/maintenance-barrier.js +2 -14
  40. package/dist/core/paths.js +0 -3
  41. package/dist/core/redaction.js +2 -2
  42. package/dist/core/spawn-env.js +8 -12
  43. package/dist/core/state/migrations.js +1 -12
  44. package/dist/core/state-db.js +9 -27
  45. package/dist/core/write-source.js +12 -19
  46. package/dist/execution/directory-identity.js +36 -10
  47. package/dist/execution/guarded-source.js +0 -10
  48. package/dist/indexer/graph/graph-boost.js +0 -4
  49. package/dist/indexer/index-writer-lock.js +43 -24
  50. package/dist/indexer/index-written-assets.js +5 -6
  51. package/dist/indexer/indexer.js +2 -39
  52. package/dist/indexer/materialize-embeddings.js +85 -41
  53. package/dist/indexer/passes/metadata.js +12 -4
  54. package/dist/indexer/scan/doc-to-entry.js +2 -0
  55. package/dist/indexer/search/db-search.js +15 -48
  56. package/dist/indexer/search/ranking-contributors.js +0 -25
  57. package/dist/indexer/search/ranking.js +3 -13
  58. package/dist/indexer/search/search-fields.js +16 -1
  59. package/dist/integrations/agent/builder-shared.js +0 -25
  60. package/dist/integrations/agent/model-map.js +2 -60
  61. package/dist/integrations/harnesses/aider/agent-builder.js +1 -3
  62. package/dist/integrations/harnesses/amazonq/agent-builder.js +2 -7
  63. package/dist/integrations/harnesses/claude/agent-builder.js +1 -4
  64. package/dist/integrations/harnesses/codex/agent-builder.js +1 -4
  65. package/dist/integrations/harnesses/copilot/agent-builder.js +2 -6
  66. package/dist/integrations/harnesses/gemini/agent-builder.js +2 -7
  67. package/dist/integrations/harnesses/opencode/agent-builder.js +1 -4
  68. package/dist/integrations/harnesses/opencode-sdk/sdk-runner.js +2 -16
  69. package/dist/integrations/harnesses/openhands/agent-builder.js +1 -3
  70. package/dist/integrations/harnesses/pi/agent-builder.js +2 -4
  71. package/dist/integrations/session-logs/index.js +0 -9
  72. package/dist/llm/client.js +75 -42
  73. package/dist/llm/embedder.js +7 -3
  74. package/dist/llm/embedders/remote.js +141 -42
  75. package/dist/output/shapes/passthrough.js +17 -5
  76. package/dist/registry/network.js +5 -37
  77. package/dist/runtime.js +2 -10
  78. package/dist/scripts/akm-migrate-node.js +180 -238
  79. package/dist/scripts/akm-migrate.js +180 -238
  80. package/dist/setup/engine-config.js +2 -5
  81. package/dist/setup/registry-stash-loader.js +0 -8
  82. package/dist/setup/setup.js +9 -46
  83. package/dist/setup/steps/connection-shared.js +10 -13
  84. package/dist/sources/providers/git-install.js +1 -1
  85. package/dist/storage/engines/sqlite-migrations.js +20 -1
  86. package/dist/storage/repositories/index-entries-repository.js +0 -15
  87. package/dist/storage/repositories/proposals-repository.js +32 -6
  88. package/dist/tasks/backends/launchd.js +15 -20
  89. package/dist/tasks/backends/schtasks.js +18 -8
  90. package/dist/tasks/run/run-native-task.js +8 -6
  91. package/dist/tasks/scheduler-binding.js +15 -5
  92. package/dist/tasks/scheduler-sync-preview.js +4 -2
  93. package/dist/tasks/scheduler-sync.js +77 -42
  94. package/dist/tasks/source/bounded-document.js +2 -4
  95. package/dist/tasks/source/task-source-v3-frozen.js +5 -7
  96. package/dist/tasks/source/task-source-v4.js +5 -10
  97. package/dist/tasks/source/task-to-v3.js +29 -22
  98. package/dist/tasks/source/task-to-v4.js +1 -14
  99. package/dist/tasks/source-v3.js +6 -6
  100. package/dist/workflows/exec/native-executor.js +21 -31
  101. package/dist/workflows/exec/run-workflow.js +5 -6
  102. package/dist/workflows/exec/scheduler.js +3 -19
  103. package/dist/workflows/exec/step-work.js +1 -4
  104. package/dist/workflows/exec/unit-dispatch.js +2 -2
  105. package/dist/workflows/exec/worktree.js +1 -13
  106. package/dist/workflows/freeze/targets/child-workflow.js +2 -10
  107. package/dist/workflows/ir/plan-hash.js +4 -6
  108. package/dist/workflows/ir/schema-v4.js +0 -12
  109. package/dist/workflows/ir/schema.js +20 -31
  110. package/dist/workflows/parser.js +11 -52
  111. package/dist/workflows/renderer.js +2 -3
  112. package/dist/workflows/resource-limits.js +11 -41
  113. package/dist/workflows/runtime/runs.js +3 -4
  114. package/dist/workflows/source-ir/schema.js +14 -30
  115. package/dist/workflows/validator.js +1 -7
  116. package/docs/reference/cli.md +25 -3
  117. package/docs/reference/configuration.md +28 -7
  118. package/docs/reference/tasks.md +10 -0
  119. package/package.json +1 -1
  120. package/schemas/akm-config.json +4 -23
  121. package/schemas/akm-task.json +1 -2
  122. package/schemas/akm-workflow.json +1 -13
  123. package/dist/indexer/search/semantic-status.js +0 -142
@@ -16,7 +16,8 @@ const SearchGraphBoostSchema = z
16
16
  hopBoostCap: nonNegativeNumber.optional(),
17
17
  /** Hard-capped at 3; values > 3 hard-error so users see the typo. */
18
18
  maxHops: positiveInt.max(3).optional(),
19
- confidenceMode: z.enum(["off", "blend", "multiply"]).default("blend").optional(),
19
+ /** Only "blend" is exercised; "off"/"multiply" were never set in practice and were removed. */
20
+ confidenceMode: z.enum(["blend"]).default("blend").optional(),
20
21
  /** Range [0, 1]; values > 1 hard-error (no silent clamp). */
21
22
  confidenceWeight: z.number().finite().min(0).max(1).default(0.2).optional(),
22
23
  })
@@ -4,6 +4,7 @@
4
4
  import { randomUUID } from "node:crypto";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import { sleepSync } from "../runtime.js";
7
8
  import { openDatabase } from "../storage/database.js";
8
9
  import { isProcessAlive, MAX_LOCK_METADATA_BYTES, readTextFileDescriptorWithLimit } from "./common.js";
9
10
  function readLockSnapshot(lockPath) {
@@ -51,7 +52,7 @@ function withLockOperationMutex(lockPath, run) {
51
52
  db.exec("BEGIN IMMEDIATE");
52
53
  began = db.inTransaction;
53
54
  if (!began)
54
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2 ** attempt);
55
+ sleepSync(2 ** attempt);
55
56
  }
56
57
  if (!began)
57
58
  throw new Error(`Could not acquire lock operation mutex for ${lockPath}.`);
@@ -456,20 +456,24 @@ function validateCommon(value) {
456
456
  }
457
457
  }
458
458
  }
459
- /** Decode the persisted public result contract. */
460
- export function decodeImproveResult(input) {
461
- let parsed = input;
462
- if (typeof input === "string") {
463
- try {
464
- parsed = JSON.parse(input);
465
- }
466
- catch {
467
- fail("not valid JSON");
468
- }
469
- }
470
- if (!isRecord(parsed))
471
- fail("root must be an object");
472
- if (parsed.schemaVersion === 2) {
459
+ /**
460
+ * Per-`schemaVersion` decoders for the persisted `improve_runs.result_json`
461
+ * contract, keyed by the literal `schemaVersion` value each accepts.
462
+ *
463
+ * #866 item 5 / read-shim policy (same shape as the version router in
464
+ * src/tasks/source/parse-task-source.ts, which this mirrors): every
465
+ * `schemaVersion` this build understands gets its own entry here, so
466
+ * decoding never regresses to "reject every row" the day the version bumps
467
+ * — it only stops accepting the ONE version that changed shape. All 1,539
468
+ * live rows today are v2; when a v3 shape is introduced, add a `3` entry
469
+ * (and, if v2 rows must keep reading under the newer runtime, keep this `2`
470
+ * entry as-is — every caller already skip-and-counts a decode failure, so a
471
+ * genuinely unreadable old row degrades to "excluded from metrics", never a
472
+ * crash). Do not remove the `2` entry when adding `3` unless the schema
473
+ * bump is known to be a strict superset callers can validate with it.
474
+ */
475
+ const SCHEMA_DECODERS = {
476
+ 2: (parsed) => {
473
477
  requireExactFields(parsed, V2_FIELDS);
474
478
  validateCommon(parsed);
475
479
  if (typeof parsed.strategy !== "string" || parsed.strategy.length === 0) {
@@ -482,6 +486,23 @@ export function decodeImproveResult(input) {
482
486
  envelope: parsed,
483
487
  strategy: parsed.strategy,
484
488
  };
489
+ },
490
+ };
491
+ /** Decode the persisted public result contract. */
492
+ export function decodeImproveResult(input) {
493
+ let parsed = input;
494
+ if (typeof input === "string") {
495
+ try {
496
+ parsed = JSON.parse(input);
497
+ }
498
+ catch {
499
+ fail("not valid JSON");
500
+ }
485
501
  }
502
+ if (!isRecord(parsed))
503
+ fail("root must be an object");
504
+ const decoder = typeof parsed.schemaVersion === "number" ? SCHEMA_DECODERS[parsed.schemaVersion] : undefined;
505
+ if (decoder)
506
+ return decoder(parsed);
486
507
  fail(`unsupported schemaVersion: ${String(parsed.schemaVersion)}`);
487
508
  }
@@ -5,6 +5,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import fs from "node:fs";
7
7
  import path from "node:path";
8
+ import { sleepSync } from "../runtime.js";
8
9
  import { ConfigError } from "./errors.js";
9
10
  import { createLockPayload, probeLock, reclaimStaleLock, releaseLock, tryAcquireLockSync } from "./file-lock.js";
10
11
  import { getMaintenanceBarrierPath } from "./paths.js";
@@ -93,7 +94,7 @@ function withMaintenanceStartBarrierSyncWait(run) {
93
94
  const deadline = Date.now() + 5_000;
94
95
  let release = tryAcquireMaintenanceBarrier();
95
96
  while (!release && Date.now() < deadline) {
96
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
97
+ sleepSync(5);
97
98
  release = tryAcquireMaintenanceBarrier();
98
99
  }
99
100
  if (!release)
@@ -107,19 +108,6 @@ function withMaintenanceStartBarrierSyncWait(run) {
107
108
  release();
108
109
  }
109
110
  }
110
- /** Register a long-lived state operation atomically with other start acquisitions. */
111
- export async function acquireMaintenanceActivity(name) {
112
- return withMaintenanceStartBarrierAsync(async () => {
113
- const directory = path.join(path.dirname(getMaintenanceBarrierPath()), "maintenance-activities");
114
- fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
115
- const lockPath = path.join(directory, `${name}-${process.pid}-${randomUUID()}.lock`);
116
- const ownership = tryAcquireLockSync(lockPath, createLockPayload({ purpose: name }));
117
- if (!ownership) {
118
- throw new ConfigError(`Could not register AKM maintenance activity at ${lockPath}.`, "INVALID_CONFIG_FILE");
119
- }
120
- return () => releaseLock(ownership);
121
- });
122
- }
123
111
  /** Synchronous activity registration for synchronous database handle lifetimes. */
124
112
  export function acquireMaintenanceActivitySync(name) {
125
113
  return withMaintenanceStartBarrierSyncWait(() => {
@@ -251,9 +251,6 @@ export function getLockfilePath() {
251
251
  export function getLockfileLockPath() {
252
252
  return path.join(getDataDir(), "akm.lock.lck");
253
253
  }
254
- export function getSemanticStatusPath() {
255
- return path.join(getCacheDir(), "semantic-status.json");
256
- }
257
254
  export function getRegistryCacheDir() {
258
255
  return path.join(getCacheDir(), "registry");
259
256
  }
@@ -373,11 +373,11 @@ const ERROR_BODY_MAX_LEN = 200;
373
373
  * Make an HTTP error body safe to put in an error message: pattern-redact
374
374
  * credential shapes, then clip. Provider bodies can echo the credential that
375
375
  * was sent and can be megabytes of HTML, and these messages travel — into
376
- * persisted status files, `--json` output, and agent transcripts.
376
+ * `--json` output and agent transcripts.
377
377
  *
378
378
  * Lives here rather than beside one transport because every HTTP client in the
379
379
  * codebase needs it; the embeddings transport originally lacked it and leaked
380
- * raw 10 MB bodies into `semantic-status.json`.
380
+ * raw 10 MB bodies into error messages.
381
381
  */
382
382
  export function redactErrorBody(input) {
383
383
  if (!input)
@@ -4,19 +4,15 @@
4
4
  /**
5
5
  * The ONE allowlist-based child-environment primitive.
6
6
  *
7
- * Two akm code paths spawn a child from an explicit list of environment
7
+ * Three akm code paths spawn a child from an explicit list of environment
8
8
  * variable NAMES — the agent-CLI spawn wrapper
9
- * (`integrations/agent/spawn.ts`, `profile.envPassthrough`) and the workflow
10
- * `exec` unit runner (`workflows/exec/exec-unit.ts`) and both start from an
11
- * EMPTY environment and copy through named entries with
12
- * {@link collectAllowlistedEnv}. Keeping that in one leaf module is what makes
13
- * "allowlist" a single reviewable mechanism instead of two implementations
14
- * that drift apart.
15
- *
16
- * NOT covered: the opencode-sdk server spawn
17
- * (`integrations/harnesses/opencode-sdk/sdk-runner.ts`) keeps its own
18
- * hard-coded name list and does not route through here, so it gets neither the
19
- * platform floor below nor PATH supplementation.
9
+ * (`integrations/agent/spawn.ts`, `profile.envPassthrough`), the workflow
10
+ * `exec` unit runner (`workflows/exec/exec-unit.ts`), and the opencode-sdk
11
+ * server spawn (`integrations/harnesses/opencode-sdk/sdk-runner.ts`) and
12
+ * all build their allowlist from {@link COMMON_SPAWN_ENV_PASSTHROUGH} /
13
+ * {@link spawnEnvNamesFor}. Keeping that in one leaf module is what makes
14
+ * "allowlist" a single reviewable mechanism instead of independent
15
+ * implementations that drift apart.
20
16
  *
21
17
  * A LEAF: node built-ins only, so both the integrations layer and the workflow
22
18
  * engine can import it without opening a cycle.
@@ -1132,22 +1132,11 @@ export function runMigrations(db, options) {
1132
1132
  runSqliteMigrations(db, STATE_MIGRATIONS, {
1133
1133
  lockInitialMigrationPrefixThrough: options?.existingUnversionedDatabase ? "002-task-history-per-run" : undefined,
1134
1134
  beforeLedgerInitializationLocked(lockedDb) {
1135
- if (options?.freshDatabase) {
1136
- if (!options.verifyFreshDatabaseOwnership) {
1137
- throw new Error("A fresh state.db migration requires verified file ownership.");
1138
- }
1139
- options.verifyFreshDatabaseOwnership();
1135
+ if (options?.freshDatabase)
1140
1136
  return;
1141
- }
1142
1137
  prepareExistingUnversionedState(lockedDb);
1143
1138
  },
1144
1139
  beforeMigrationLocked(migration, lockedDb) {
1145
- if (options?.freshDatabase) {
1146
- if (!options.verifyFreshDatabaseOwnership) {
1147
- throw new Error("A fresh state.db migration requires verified file ownership.");
1148
- }
1149
- options.verifyFreshDatabaseOwnership();
1150
- }
1151
1140
  if (options?.existingUnversionedDatabase && !existingUnversionedSnapshotPrepared) {
1152
1141
  prepareExistingUnversionedState(lockedDb);
1153
1142
  }
@@ -65,6 +65,7 @@
65
65
  import { randomUUID } from "node:crypto";
66
66
  import fs from "node:fs";
67
67
  import path from "node:path";
68
+ import { sleepSync } from "../runtime.js";
68
69
  import { openDatabase } from "../storage/database.js";
69
70
  import { assertMigrationLedger } from "../storage/engines/sqlite-migrations.js";
70
71
  import { openManagedDatabase, withManagedDb } from "../storage/managed-db.js";
@@ -189,20 +190,18 @@ function reserveFreshStateDatabase(dbPath) {
189
190
  throw error;
190
191
  }
191
192
  try {
192
- const reservation = {
193
+ return {
193
194
  path: dbPath,
194
195
  fd,
195
196
  identity: fs.fstatSync(fd, { bigint: true }),
196
197
  };
197
- assertOwnedFileReservation(reservation, "Fresh state.db");
198
- return reservation;
199
198
  }
200
199
  catch (error) {
201
200
  try {
202
201
  fs.closeSync(fd);
203
202
  }
204
203
  catch {
205
- // Preserve the ownership failure.
204
+ // Preserve the fstat failure.
206
205
  }
207
206
  throw error;
208
207
  }
@@ -217,20 +216,18 @@ function openExistingStateDatabaseSource(dbPath) {
217
216
  throw new Error(`Could not bind the existing state.db source inode: ${detail}`);
218
217
  }
219
218
  try {
220
- const source = {
219
+ return {
221
220
  path: dbPath,
222
221
  fd,
223
222
  identity: fs.fstatSync(fd, { bigint: true }),
224
223
  };
225
- assertStateDatabaseSource(source);
226
- return source;
227
224
  }
228
225
  catch (error) {
229
226
  try {
230
227
  fs.closeSync(fd);
231
228
  }
232
229
  catch {
233
- // Preserve the source identity failure.
230
+ // Preserve the fstat failure.
234
231
  }
235
232
  throw error;
236
233
  }
@@ -386,10 +383,7 @@ export function openStateDatabase(dbPath, options) {
386
383
  return openManagedDatabase({
387
384
  path: resolvedPath,
388
385
  pragmas: { dataDir: path.dirname(resolvedPath) },
389
- init: (db) => runMigrations(db, {
390
- freshDatabase: true,
391
- verifyFreshDatabaseOwnership: () => { },
392
- }),
386
+ init: (db) => runMigrations(db, { freshDatabase: true }),
393
387
  });
394
388
  }
395
389
  const isCanonical = path.resolve(resolvedPath) === path.resolve(canonicalPath);
@@ -404,12 +398,10 @@ export function openStateDatabase(dbPath, options) {
404
398
  freshReservation = reserveFreshStateDatabase(resolvedPath);
405
399
  if (!freshReservation) {
406
400
  existingSource = openExistingStateDatabaseSource(resolvedPath);
407
- assertStateDatabaseSource(existingSource);
408
401
  const preflight = openDatabase(sqliteBoundFilePath(existingSource), {
409
402
  readonly: true,
410
403
  });
411
404
  try {
412
- assertStateDatabaseSource(existingSource);
413
405
  preflight.exec("PRAGMA busy_timeout = 30000");
414
406
  const ledger = assertMigrationLedger(preflight, STATE_MIGRATIONS);
415
407
  existingUnversionedDatabase = ledger.migrationIds.length === 0;
@@ -422,21 +414,13 @@ export function openStateDatabase(dbPath, options) {
422
414
  preflight.close();
423
415
  }
424
416
  }
425
- const ownedFresh = freshReservation;
426
417
  const boundSource = existingSource;
427
- if (boundSource)
428
- assertStateDatabaseSource(boundSource);
429
418
  openedDb = openManagedDatabase({
430
419
  path: boundSource ? sqliteBoundFilePath(boundSource) : resolvedPath,
431
420
  pragmas: { dataDir: path.dirname(resolvedPath) },
432
421
  init: (db) => {
433
- if (boundSource)
434
- assertStateDatabaseSource(boundSource);
435
422
  runMigrations(db, {
436
- freshDatabase: !!ownedFresh,
437
- verifyFreshDatabaseOwnership: ownedFresh
438
- ? () => assertOwnedFileReservation(ownedFresh, "Fresh state.db")
439
- : undefined,
423
+ freshDatabase: !!freshReservation,
440
424
  existingUnversionedDatabase,
441
425
  allowHistoricalDestructiveStateUpgrade: options?.allowHistoricalDestructiveStateUpgrade,
442
426
  beforeExistingUnversionedStateMigration: options?.allowHistoricalDestructiveStateUpgrade
@@ -463,12 +447,10 @@ export function openStateDatabase(dbPath, options) {
463
447
  },
464
448
  });
465
449
  if (existingSource) {
466
- assertStateDatabaseSource(existingSource);
467
450
  closeFileIdentity(existingSource);
468
451
  existingSource = undefined;
469
452
  }
470
453
  if (freshReservation) {
471
- assertOwnedFileReservation(freshReservation, "Fresh state.db");
472
454
  closeFileIdentity(freshReservation);
473
455
  freshReservation = undefined;
474
456
  }
@@ -610,11 +592,11 @@ function isRetryableBeginError(err) {
610
592
  msg.includes("did not open a transaction"));
611
593
  }
612
594
  const WITH_IMMEDIATE_TX_MAX_ATTEMPTS = 5;
613
- /** Portable synchronous sleep (works under both Bun and Node). */
595
+ /** Portable synchronous sleep (works under both Bun and Node). Delegates to the runtime boundary's `sleepSync`. */
614
596
  function sleepSyncMs(ms) {
615
597
  if (ms <= 0)
616
598
  return;
617
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
599
+ sleepSync(ms);
618
600
  }
619
601
  /**
620
602
  * Open, but deliberately do not finish, an immediate transaction.
@@ -34,7 +34,7 @@ import { assetPathForName, stashDirFor } from "./asset/asset-placement.js";
34
34
  import { conceptIdFromTypeName, displayRef } from "./asset/resolve-ref.js";
35
35
  import { deriveBundleId } from "./bundle-id.js";
36
36
  import { existingFileMode, isWithin, resolveStashDir, writeFileAtomic } from "./common.js";
37
- import { resolveConfiguredSources } from "./config/config.js";
37
+ import { bundleContentRoot, resolveConfiguredSources } from "./config/config.js";
38
38
  import { ConfigError, UsageError } from "./errors.js";
39
39
  import { sanitizeCommitMessage } from "./git-message.js";
40
40
  import { warn } from "./warn.js";
@@ -111,7 +111,15 @@ export function planWriteTargetPublication(target, filePaths, options) {
111
111
  }
112
112
  const unignoredPaths = paths.filter((_, index) => !ignored.has(relativePaths[index]));
113
113
  assertWriteTargetPathsClean(target.source, unignoredPaths);
114
- assertWriteTargetPlanBase(target, expectedBaseHead);
114
+ // Not re-verified against `expectedBaseHead` here: any drift during the
115
+ // ignored/clean-path checks above is caught by the SAME expectedBaseHead
116
+ // comparison `beginWriteTargetMutation` runs immediately before the first
117
+ // mutation (its documented contract, and every real caller calls it right
118
+ // after this function returns) -- checking it again here a few lines
119
+ // earlier for the exact same value only duplicated that check, not added
120
+ // a new one. `publishWriteTargetPlan` independently re-checks a third time
121
+ // after the actual mutation, which is a genuinely different point in time
122
+ // and stays.
115
123
  return { target, paths, publish: ignored.size === 0, expectedBaseHead };
116
124
  }
117
125
  function assertNoWritePathDescendantSymlinks(source, filePaths) {
@@ -538,7 +546,6 @@ function captureGitPathState(repoPath, absolutePath, relativePath) {
538
546
  }
539
547
  /** Create or recover the one transaction-owned commit without pushing it. */
540
548
  export function ensureGitTransactionCommit(target, publication, options) {
541
- assertGitPublicationIdentity(target, publication);
542
549
  const paths = normalizePublicationPaths(options.paths);
543
550
  validateGitWorktreeSnapshots(target, paths, options.snapshots);
544
551
  if (publication.commit !== undefined) {
@@ -626,7 +633,6 @@ export function publishWriteTargetTransaction(target, publication, options) {
626
633
  }
627
634
  /** Push only the recorded transaction commit, never later local descendants. */
628
635
  export function publishGitTransactionCommit(target, publication, transactionId, paths, snapshots) {
629
- assertGitPublicationIdentity(target, publication);
630
636
  if (publication.commit === undefined) {
631
637
  throw new Error(`Git transaction ${transactionId} has no recorded publication decision.`);
632
638
  }
@@ -703,20 +709,6 @@ function readGitPublicationIdentity(target) {
703
709
  upstream: upstreamResult.stdout.trim(),
704
710
  };
705
711
  }
706
- function assertGitPublicationIdentity(target, publication) {
707
- if (target.source.kind !== "git") {
708
- throw new Error(`Git publication is bound to non-Git target "${target.source.name}".`);
709
- }
710
- const current = readGitPublicationIdentity(target);
711
- for (const key of ["repoPath", "branch", "remote", "mergeRef", "remoteUrl", "upstream"]) {
712
- if (current[key] !== publication[key]) {
713
- throw new Error(`Git publication target identity changed at ${key} for "${target.source.name}".`);
714
- }
715
- }
716
- if (JSON.stringify(current.pushUrls ?? []) !== JSON.stringify(publication.pushUrls ?? [])) {
717
- throw new Error(`Git publication target identity changed at pushUrls for "${target.source.name}".`);
718
- }
719
- }
720
712
  function normalizePublicationPaths(paths) {
721
713
  const normalized = new Set();
722
714
  for (const filePath of paths) {
@@ -951,9 +943,10 @@ export function resolveWorkingStashTarget(akmConfig, options = {}) {
951
943
  const requireWritable = options.requireWritable !== false;
952
944
  if (process.env.AKM_BUNDLE_DIR?.trim()) {
953
945
  const stashDir = resolveStashDir();
946
+ const resolvedStashDir = path.resolve(stashDir);
954
947
  const configured = configuredSources.find((source) => {
955
948
  const sourcePath = source.source.type === "filesystem" ? source.source.path : undefined;
956
- return sourcePath !== undefined && path.resolve(sourcePath) === path.resolve(stashDir);
949
+ return sourcePath !== undefined && bundleContentRoot(sourcePath, source.componentRoot) === resolvedStashDir;
957
950
  });
958
951
  if (configured) {
959
952
  const target = adaptConfiguredSource(configured);
@@ -27,19 +27,48 @@ export function captureFrozenDirectoryIdentity(rootInput, relativeCwd) {
27
27
  cwdInode: cwdStat.ino.toString(),
28
28
  });
29
29
  }
30
- export function assertFrozenDirectoryIdentity(identity) {
31
- let actual;
30
+ /**
31
+ * Re-verify, immediately before spawn, that a frozen cwd still resolves
32
+ * inside its execution root — path containment and resolved-path identity,
33
+ * NOT device/inode identity. Unlike the removed assertFrozenDirectoryIdentity,
34
+ * this does not compare device/inode numbers against the frozen snapshot: a
35
+ * remount, an rsync, or a container rebuild that leaves the same real
36
+ * content at the same real PATH must not abort a dispatch. What still
37
+ * throws is anything that changes the resolved real PATH itself — a
38
+ * symlink swap, an ancestor swap, a file replacing the directory, or the
39
+ * root itself being replaced by a symlink elsewhere (root-vs-cwd
40
+ * containment alone can't catch that last one, since both resolve together
41
+ * under the new root; comparing the root's own resolved path to its frozen
42
+ * value catches it).
43
+ */
44
+ export function assertFrozenDirectoryContained(identity) {
45
+ let realRoot;
46
+ let realCwd;
32
47
  try {
33
- const relative = path.relative(identity.requestedRoot, identity.requestedCwd);
34
- actual = captureFrozenDirectoryIdentity(identity.requestedRoot, relative);
48
+ realRoot = fs.realpathSync(identity.requestedRoot);
49
+ realCwd = fs.realpathSync(identity.requestedCwd);
35
50
  }
36
51
  catch {
37
52
  throw changed(identity.requestedCwd);
38
53
  }
39
- for (const key of ["realRoot", "rootDevice", "rootInode", "realCwd", "cwdDevice", "cwdInode"]) {
40
- if (actual[key] !== identity[key])
41
- throw changed(identity.requestedCwd);
54
+ if (realRoot !== identity.realRoot)
55
+ throw changed(identity.requestedCwd);
56
+ const relative = path.relative(realRoot, realCwd);
57
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
58
+ throw changed(identity.requestedCwd);
42
59
  }
60
+ let cwdStat;
61
+ try {
62
+ cwdStat = fs.statSync(realCwd, { bigint: true });
63
+ }
64
+ catch {
65
+ throw changed(identity.requestedCwd);
66
+ }
67
+ if (!cwdStat.isDirectory())
68
+ throw changed(identity.requestedCwd);
69
+ }
70
+ function changed(cwd) {
71
+ return new UsageError(`Frozen execution cwd ${cwd} changed and no longer resolves inside its root.`, "PATH_ESCAPE_VIOLATION");
43
72
  }
44
73
  function requireContained(root, candidate) {
45
74
  const relative = path.relative(root, candidate);
@@ -47,6 +76,3 @@ function requireContained(root, candidate) {
47
76
  throw new UsageError(`${candidate} escapes its execution root.`, "PATH_ESCAPE_VIOLATION");
48
77
  }
49
78
  }
50
- function changed(cwd) {
51
- return new UsageError(`Frozen execution cwd ${cwd} changed after publication.`, "RESOURCE_ALREADY_EXISTS");
52
- }
@@ -7,7 +7,6 @@ import path from "node:path";
7
7
  import { parseFrontmatter } from "../core/asset/frontmatter.js";
8
8
  import { UsageError } from "../core/errors.js";
9
9
  import { createExecutionSourceIdentity } from "./source.js";
10
- export const DEFAULT_GUARDED_SOURCE_MAX_BYTES = 1024 * 1024;
11
10
  function errorMessage(cause) {
12
11
  return cause instanceof Error ? cause.message : String(cause);
13
12
  }
@@ -71,10 +70,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
71
70
  const sourcePath = path.resolve(sourcePathInput);
72
71
  const { containmentRoot, containmentRealPath, containmentStat } = requireContainmentRoot(containmentRootInput);
73
72
  const lexicalRelative = containedRelative(containmentRoot, sourcePath, false);
74
- const maxBytes = options.maxBytes ?? DEFAULT_GUARDED_SOURCE_MAX_BYTES;
75
- if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
76
- throw new UsageError("Guarded source byte limit must be a non-negative safe integer.", "INVALID_FLAG_VALUE");
77
- }
78
73
  const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
79
74
  let descriptor;
80
75
  try {
@@ -83,9 +78,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
83
78
  if (!before.isFile()) {
84
79
  throw new UsageError(`${sourcePath} is not a regular guarded source file.`, "INVALID_FLAG_VALUE");
85
80
  }
86
- if (before.size > BigInt(maxBytes)) {
87
- throw new UsageError(`${sourcePath} exceeds the guarded source size limit (1 MiB; ${maxBytes} bytes).`, "INVALID_FLAG_VALUE");
88
- }
89
81
  const bytes = fs.readFileSync(descriptor);
90
82
  const after = fs.fstatSync(descriptor, { bigint: true });
91
83
  if (!sameBigIntStat(before, after) || BigInt(bytes.byteLength) !== before.size) {
@@ -127,7 +119,6 @@ function captureRecord(sourcePathInput, containmentRootInput, options = {}) {
127
119
  ...(options.identity ? { identity: options.identity } : {}),
128
120
  }),
129
121
  stat: numberStat(before),
130
- maxBytes,
131
122
  };
132
123
  }
133
124
  catch (cause) {
@@ -402,7 +393,6 @@ export class GuardedExecutionSourceCollector {
402
393
  try {
403
394
  current = captureRecord(record.source.sourcePath, record.source.containmentRoot, {
404
395
  authored: record.source.authored,
405
- maxBytes: record.maxBytes,
406
396
  ...(record.source.identity ? { identity: record.source.identity } : {}),
407
397
  }).source;
408
398
  }
@@ -68,11 +68,7 @@ function combineConfidence(...parts) {
68
68
  return out;
69
69
  }
70
70
  function toConfidenceMultiplier(rawConfidence, weights) {
71
- if (weights.confidenceMode === "off")
72
- return 1;
73
71
  const confidence = normalizeConfidence(rawConfidence) ?? 1;
74
- if (weights.confidenceMode === "multiply")
75
- return confidence;
76
72
  const blendWeight = Math.max(0, Math.min(1, weights.confidenceWeight));
77
73
  return 1 - blendWeight + blendWeight * confidence;
78
74
  }