@voybio/ace-swarm 2.4.9 → 2.4.10

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 (185) hide show
  1. package/CHANGELOG.md +52 -14
  2. package/README.md +14 -10
  3. package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
  4. package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
  5. package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
  6. package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
  7. package/assets/agent-state/ACE_WORKFLOW.md +25 -2
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
  10. package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
  11. package/assets/eval-battery/battery.json +1 -1
  12. package/assets/instructions/ACE_Coder.instructions.md +9 -13
  13. package/assets/scripts/ace-hook-dispatch.mjs +16 -411
  14. package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
  15. package/assets/vendor/README.md +9 -0
  16. package/assets/vendor/vericify-reader.mjs +272 -0
  17. package/assets/vendor/vericify-reader.provenance.json +10 -0
  18. package/dist/ace-autonomy.d.ts +58 -0
  19. package/dist/ace-autonomy.js +211 -6
  20. package/dist/ace-internal-tools.d.ts +1 -0
  21. package/dist/ace-internal-tools.js +3 -3
  22. package/dist/ace-server-instructions.d.ts +3 -3
  23. package/dist/ace-server-instructions.js +46 -192
  24. package/dist/cli.js +44 -6
  25. package/dist/experience-reflector.d.ts +7 -0
  26. package/dist/experience-reflector.js +39 -0
  27. package/dist/goal-engine.d.ts +64 -7
  28. package/dist/goal-engine.js +197 -41
  29. package/dist/handoff-registry.d.ts +10 -0
  30. package/dist/handoff-registry.js +59 -0
  31. package/dist/helpers/artifacts.d.ts +6 -2
  32. package/dist/helpers/artifacts.js +8 -6
  33. package/dist/helpers/bootstrap.js +13 -20
  34. package/dist/helpers/path-utils.js +2 -0
  35. package/dist/helpers/store-resolution.js +18 -3
  36. package/dist/helpers/workspace-root.d.ts +2 -0
  37. package/dist/helpers/workspace-root.js +9 -0
  38. package/dist/helpers.d.ts +1 -0
  39. package/dist/helpers.js +1 -0
  40. package/dist/hermes/bridge-protocol.d.ts +1 -1
  41. package/dist/hermes/session-manager.d.ts +17 -0
  42. package/dist/hermes/session-manager.js +67 -14
  43. package/dist/host-goal-engine.d.ts +138 -0
  44. package/dist/host-goal-engine.js +793 -0
  45. package/dist/host-goal-status.d.ts +113 -0
  46. package/dist/host-goal-status.js +175 -0
  47. package/dist/host-learning.d.ts +68 -0
  48. package/dist/host-learning.js +115 -0
  49. package/dist/host-observation.d.ts +12 -0
  50. package/dist/host-observation.js +91 -0
  51. package/dist/host-plan-revision.d.ts +14 -0
  52. package/dist/host-plan-revision.js +101 -0
  53. package/dist/host-verification-job.d.ts +12 -0
  54. package/dist/host-verification-job.js +183 -0
  55. package/dist/host-work-context.d.ts +12 -0
  56. package/dist/host-work-context.js +8 -0
  57. package/dist/job-scheduler.d.ts +48 -14
  58. package/dist/job-scheduler.js +169 -23
  59. package/dist/job-worker.d.ts +3 -1
  60. package/dist/job-worker.js +10 -3
  61. package/dist/kanban.d.ts +4 -1
  62. package/dist/kanban.js +44 -16
  63. package/dist/learned-artifacts.d.ts +64 -0
  64. package/dist/learned-artifacts.js +321 -0
  65. package/dist/learning-controls.d.ts +142 -0
  66. package/dist/learning-controls.js +135 -0
  67. package/dist/learning-input-context.d.ts +9 -0
  68. package/dist/learning-input-context.js +62 -0
  69. package/dist/learning-lifecycle.d.ts +10 -0
  70. package/dist/learning-lifecycle.js +11 -0
  71. package/dist/learning-overlays.d.ts +3 -0
  72. package/dist/learning-overlays.js +17 -0
  73. package/dist/learning-policy.d.ts +40 -0
  74. package/dist/learning-policy.js +38 -0
  75. package/dist/learning-records.d.ts +35 -0
  76. package/dist/learning-records.js +17 -0
  77. package/dist/learning-scorecard.d.ts +82 -0
  78. package/dist/learning-scorecard.js +86 -0
  79. package/dist/learning-serving.d.ts +29 -0
  80. package/dist/learning-serving.js +134 -0
  81. package/dist/linked-work.d.ts +9 -0
  82. package/dist/linked-work.js +28 -0
  83. package/dist/local-model-runtime.js +1 -1
  84. package/dist/mcp-command-progress.d.ts +16 -0
  85. package/dist/mcp-command-progress.js +70 -0
  86. package/dist/model-bridge.d.ts +14 -13
  87. package/dist/model-bridge.js +73 -41
  88. package/dist/orchestrator-supervisor.d.ts +108 -6
  89. package/dist/orchestrator-supervisor.js +4 -32
  90. package/dist/plan-proposal.js +3 -3
  91. package/dist/playbook-curator.d.ts +1 -0
  92. package/dist/playbook-curator.js +11 -9
  93. package/dist/process-identity.d.ts +8 -0
  94. package/dist/process-identity.js +38 -0
  95. package/dist/progress-command.d.ts +34 -0
  96. package/dist/progress-command.js +153 -0
  97. package/dist/prompts.js +15 -15
  98. package/dist/resources.js +2 -2
  99. package/dist/runtime-command.d.ts +8 -1
  100. package/dist/runtime-command.js +48 -7
  101. package/dist/runtime-executor.js +33 -11
  102. package/dist/runtime-profile-syntax.d.ts +6 -0
  103. package/dist/runtime-profile-syntax.js +121 -0
  104. package/dist/runtime-profile.d.ts +37 -5
  105. package/dist/runtime-profile.js +36 -129
  106. package/dist/runtime-tool-specs.js +1 -1
  107. package/dist/safe-edit.d.ts +7 -3
  108. package/dist/safe-edit.js +40 -47
  109. package/dist/schemas.js +4 -2
  110. package/dist/server.d.ts +2 -16
  111. package/dist/server.js +14 -20
  112. package/dist/shared.d.ts +2 -0
  113. package/dist/shared.js +2 -0
  114. package/dist/skill-auditor.d.ts +19 -0
  115. package/dist/skill-auditor.js +32 -2
  116. package/dist/skill-catalog.d.ts +4 -1
  117. package/dist/skill-catalog.js +6 -5
  118. package/dist/store/ace-packed-store.d.ts +5 -1
  119. package/dist/store/ace-packed-store.js +33 -1
  120. package/dist/store/bootstrap-store.js +19 -12
  121. package/dist/store/cache-workspace.d.ts +8 -0
  122. package/dist/store/cache-workspace.js +94 -27
  123. package/dist/store/importer.js +11 -20
  124. package/dist/store/knowledge-bake.d.ts +3 -1
  125. package/dist/store/knowledge-bake.js +10 -1
  126. package/dist/store/legacy-vericify-posts.d.ts +8 -0
  127. package/dist/store/legacy-vericify-posts.js +76 -0
  128. package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
  129. package/dist/store/materializers/hook-context-materializer.js +6 -2
  130. package/dist/store/materializers/host-file-materializer.js +2 -2
  131. package/dist/store/materializers/projection-manager.d.ts +4 -1
  132. package/dist/store/materializers/projection-manager.js +10 -3
  133. package/dist/store/materializers/todo-syncer.d.ts +39 -20
  134. package/dist/store/materializers/todo-syncer.js +208 -110
  135. package/dist/store/materializers/vericify-projector.js +15 -26
  136. package/dist/store/repositories/goal-repository.d.ts +51 -2
  137. package/dist/store/repositories/goal-repository.js +114 -48
  138. package/dist/store/repositories/playbook-repository.d.ts +41 -3
  139. package/dist/store/repositories/playbook-repository.js +61 -5
  140. package/dist/store/repositories/todo-repository.d.ts +8 -2
  141. package/dist/store/repositories/todo-repository.js +1 -1
  142. package/dist/store/repositories/vericify-repository.d.ts +4 -0
  143. package/dist/store/repositories/vericify-repository.js +46 -0
  144. package/dist/store/skills-install.d.ts +1 -0
  145. package/dist/store/skills-install.js +20 -16
  146. package/dist/store/source-preservation.d.ts +24 -0
  147. package/dist/store/source-preservation.js +79 -0
  148. package/dist/store/store-artifacts.js +8 -1
  149. package/dist/store/test-write-boundary.d.ts +16 -0
  150. package/dist/store/test-write-boundary.js +175 -0
  151. package/dist/store/types.d.ts +5 -1
  152. package/dist/store/write-coordinator.js +16 -0
  153. package/dist/task-plan-context.d.ts +11 -0
  154. package/dist/task-plan-context.js +59 -0
  155. package/dist/todo-state.d.ts +19 -9
  156. package/dist/todo-state.js +204 -321
  157. package/dist/tools-agent.d.ts +1 -1
  158. package/dist/tools-agent.js +38 -62
  159. package/dist/tools-files.d.ts +0 -3
  160. package/dist/tools-files.js +21 -11
  161. package/dist/tools-framework.js +16 -25
  162. package/dist/tools-goal.d.ts +2 -4
  163. package/dist/tools-goal.js +119 -16
  164. package/dist/tools-handoff.js +28 -4
  165. package/dist/tools-lifecycle.js +1 -1
  166. package/dist/tools-memory.d.ts +0 -3
  167. package/dist/tools-memory.js +51 -6
  168. package/dist/tools-scheduler.js +19 -17
  169. package/dist/tools-todo.js +160 -148
  170. package/dist/vericify-bridge.d.ts +8 -0
  171. package/dist/vericify-bridge.js +18 -0
  172. package/dist/vericify-context.d.ts +4 -0
  173. package/dist/vericify-context.js +26 -5
  174. package/dist/vericify-native.d.ts +101 -0
  175. package/dist/vericify-native.js +221 -0
  176. package/dist/verification-worker.d.ts +2 -0
  177. package/dist/verification-worker.js +31 -0
  178. package/docs/mcp-tool-reference.md +44 -9
  179. package/package.json +19 -6
  180. package/scripts/dev/run-tests-isolated.mjs +165 -0
  181. package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
  182. package/scripts/dev/test-isolation-guard.mjs +20 -0
  183. package/scripts/dev/test-preload.mjs +153 -0
  184. package/scripts/dev/test-source-inventory.mjs +31 -0
  185. package/docs/development.md +0 -46
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readHandoffRegistry } from "./handoff-registry.js";
3
- import { resolveWorkspaceRoot, safeRead, safeWrite, withFileLock, wsPath } from "./helpers.js";
3
+ import { resolveWorkspaceRoot, withWorkspaceRoot, safeRead, safeWrite, withFileLock, wsPath } from "./helpers.js";
4
4
  import { isReadError } from "./shared.js";
5
5
  import { appendStatusEventSafe, readStatusEvents } from "./status-events.js";
6
6
  import { appendRunLedgerEntrySafe } from "./run-ledger.js";
@@ -10,6 +10,7 @@ import { ProjectionManager } from "./store/materializers/projection-manager.js";
10
10
  import { SchedulerRepository } from "./store/repositories/scheduler-repository.js";
11
11
  import { getWorkspaceStorePath, readStoreJsonSync, storeExistsSync, } from "./store/store-snapshot.js";
12
12
  import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
13
+ import { probeProcess, readProcessIdentity } from "./process-identity.js";
13
14
  // ── Buffered transition-event infrastructure ────────────────────────
14
15
  // Events are collected during the scheduler lock and flushed after
15
16
  // the lock is released. Emission is failure-tolerant: a broken
@@ -204,6 +205,8 @@ function parseQueueFile(raw) {
204
205
  evidence_ref: typeof job.evidence_ref === "string" ? job.evidence_ref : undefined,
205
206
  error: typeof job.error === "string" ? job.error : undefined,
206
207
  reason_code: typeof job.reason_code === "string" ? job.reason_code : undefined,
208
+ execution: job.execution && typeof job.execution === "object" ? { ...job.execution } : undefined,
209
+ execution_children: Array.isArray(job.execution_children) ? job.execution_children.map(identity => ({ ...identity })) : undefined,
207
210
  });
208
211
  }
209
212
  return {
@@ -289,16 +292,28 @@ function parseLease(raw) {
289
292
  return undefined;
290
293
  }
291
294
  }
292
- export function getJobQueuePath() {
295
+ export function getJobQueuePath(workspaceRoot = resolveWorkspaceRoot()) {
296
+ return withWorkspaceRoot(workspaceRoot, () => getJobQueuePathInWorkspace());
297
+ }
298
+ function getJobQueuePathInWorkspace() {
293
299
  return wsPath(JOB_QUEUE_REL);
294
300
  }
295
- export function getJobLockTablePath() {
301
+ export function getJobLockTablePath(workspaceRoot = resolveWorkspaceRoot()) {
302
+ return withWorkspaceRoot(workspaceRoot, () => getJobLockTablePathInWorkspace());
303
+ }
304
+ function getJobLockTablePathInWorkspace() {
296
305
  return wsPath(JOB_LOCK_TABLE_REL);
297
306
  }
298
- export function getSchedulerLeasePath() {
307
+ export function getSchedulerLeasePath(workspaceRoot = resolveWorkspaceRoot()) {
308
+ return withWorkspaceRoot(workspaceRoot, () => getSchedulerLeasePathInWorkspace());
309
+ }
310
+ function getSchedulerLeasePathInWorkspace() {
299
311
  return wsPath(SCHEDULER_LEASE_REL);
300
312
  }
301
- export function readJobQueue() {
313
+ export function readJobQueue(workspaceRoot = resolveWorkspaceRoot()) {
314
+ return withWorkspaceRoot(workspaceRoot, () => readJobQueueInWorkspace());
315
+ }
316
+ function readJobQueueInWorkspace() {
302
317
  const root = resolveWorkspaceRoot();
303
318
  if (storeExistsSync(root)) {
304
319
  const stored = readStoreJsonSync(root, "state/scheduler/queue");
@@ -311,7 +326,10 @@ export function readJobQueue() {
311
326
  return defaultQueueFile();
312
327
  return parseQueueFile(raw) ?? defaultQueueFile();
313
328
  }
314
- export function readJobLockTable() {
329
+ export function readJobLockTable(workspaceRoot = resolveWorkspaceRoot()) {
330
+ return withWorkspaceRoot(workspaceRoot, () => readJobLockTableInWorkspace());
331
+ }
332
+ function readJobLockTableInWorkspace() {
315
333
  const root = resolveWorkspaceRoot();
316
334
  if (storeExistsSync(root)) {
317
335
  const stored = readStoreJsonSync(root, "state/scheduler/locks");
@@ -324,7 +342,10 @@ export function readJobLockTable() {
324
342
  return defaultJobLockFile();
325
343
  return parseJobLockFile(raw) ?? defaultJobLockFile();
326
344
  }
327
- export function readSchedulerLease() {
345
+ export function readSchedulerLease(workspaceRoot = resolveWorkspaceRoot()) {
346
+ return withWorkspaceRoot(workspaceRoot, () => readSchedulerLeaseInWorkspace());
347
+ }
348
+ function readSchedulerLeaseInWorkspace() {
328
349
  const root = resolveWorkspaceRoot();
329
350
  if (storeExistsSync(root)) {
330
351
  const stored = readStoreJsonSync(root, "state/scheduler/lease");
@@ -341,9 +362,17 @@ async function withSchedulerTransaction(fn) {
341
362
  const storePath = getWorkspaceStorePath(resolveWorkspaceRoot());
342
363
  return withFileLock(SCHEDULER_LOCK_REL, () => withStoreWriteCoordinator(storePath, fn, { operation_label: "scheduler_transaction" }));
343
364
  }
365
+ function probeJobExecution(job) {
366
+ const identities = [...(job.execution ? [job.execution] : []), ...(job.execution_children ?? [])];
367
+ const states = identities.map(probeProcess);
368
+ if (states.includes("alive"))
369
+ return "alive";
370
+ return states.includes("unknown") ? "unknown" : "dead";
371
+ }
344
372
  function hasExecutionOwnership(queue, locks) {
345
373
  return queue.jobs.some((job) => job.status === "running" ||
346
- (job.status === "unknown_recovery" && job.reason_code === "handoff_execution_unverified")) ||
374
+ (job.status === "unknown_recovery" && (job.reason_code === "handoff_execution_unverified" ||
375
+ probeJobExecution(job) !== "dead"))) ||
347
376
  locks.locks.some((lock) => isJobLockActive(lock));
348
377
  }
349
378
  function leaseForAcquisition() {
@@ -601,6 +630,11 @@ function acquireLease(existing, owner, ttlSeconds, now) {
601
630
  }
602
631
  const existingExpiry = parseDate(existing.expires_at);
603
632
  const expired = !existingExpiry || existingExpiry.getTime() <= now.getTime();
633
+ // A missed heartbeat is not evidence that an executing process has exited.
634
+ // Do not steal its owner lease while its identity is live or uncertain.
635
+ if (expired && existing.owner !== owner && readJobQueue().jobs.some((job) => (job.status === "running" || job.status === "unknown_recovery") &&
636
+ probeJobExecution(job) !== "dead"))
637
+ return { acquired: false, lease: existing, recovery_mode: false };
604
638
  if (expired || existing.owner === owner) {
605
639
  const lease = {
606
640
  version: 1,
@@ -718,11 +752,18 @@ function startJobNow(job, table, lease, now, buf, sourceTool) {
718
752
  });
719
753
  return { acquired_at: acquiredAt, resources };
720
754
  }
721
- function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
755
+ function reconcileRecoveryState(queue, table, now, recoveryMode, buf, selected) {
722
756
  let recovered = 0;
723
757
  for (const job of queue.jobs) {
758
+ if (selected && !selected.has(job.job_id))
759
+ continue;
724
760
  if (job.kind === "handoff.lifecycle")
725
761
  continue;
762
+ if ((job.status === "running" || job.status === "unknown_recovery") && probeJobExecution(job) !== "dead") {
763
+ // Preserve ownership even during lease recovery or missing lock metadata.
764
+ // A retained uncertain execution must never be dispatched a second time.
765
+ continue;
766
+ }
726
767
  if (job.status === "unknown_recovery") {
727
768
  releaseLocksForRecoveredJob(table, job.job_id, "recovery_hold", now);
728
769
  continue;
@@ -769,11 +810,13 @@ function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
769
810
  * Missing-lock jobs are intentionally left to `reconcileRecoveryState`, which
770
811
  * runs first; this pass only inspects jobs that still hold active locks.
771
812
  */
772
- function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
813
+ function reapStaleRunningJobs(queue, table, now, staleMs, buf, selected) {
773
814
  if (staleMs <= 0)
774
815
  return [];
775
816
  const reaped = [];
776
817
  for (const job of queue.jobs) {
818
+ if (selected && !selected.has(job.job_id))
819
+ continue;
777
820
  if (job.kind === "handoff.lifecycle")
778
821
  continue;
779
822
  if (job.status !== "running")
@@ -787,6 +830,14 @@ function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
787
830
  const ageMs = now.getTime() - lastBeat;
788
831
  if (ageMs <= staleMs)
789
832
  continue;
833
+ if (job.execution || job.execution_children?.length) {
834
+ const processState = probeJobExecution(job);
835
+ if (processState !== "dead") {
836
+ job.reason_code = `heartbeat_stale_process_${processState}`;
837
+ job.error = `Worker heartbeat is stale, but execution process is ${processState}; execution and resource locks are retained pending recovery.`;
838
+ continue;
839
+ }
840
+ }
790
841
  const previousStatus = job.status;
791
842
  job.status = "unknown_recovery";
792
843
  job.reason_code = "heartbeat_stale";
@@ -821,7 +872,10 @@ function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
821
872
  }
822
873
  return reaped;
823
874
  }
824
- export async function enqueueJob(input) {
875
+ export async function enqueueJob(input, workspaceRoot = resolveWorkspaceRoot()) {
876
+ return withWorkspaceRoot(workspaceRoot, () => enqueueJobInWorkspace(input));
877
+ }
878
+ async function enqueueJobInWorkspace(input) {
825
879
  const buf = newEventBuffer();
826
880
  const result = await withSchedulerTransaction(async () => {
827
881
  if (!input.kind || input.kind.trim().length === 0) {
@@ -923,7 +977,10 @@ export async function enqueueJob(input) {
923
977
  return result;
924
978
  }
925
979
  /** Mirror handoff lifecycle without admitting or executing a job. */
926
- export async function syncHandoffLifecycleJob(input) {
980
+ export async function syncHandoffLifecycleJob(input, workspaceRoot = resolveWorkspaceRoot()) {
981
+ return withWorkspaceRoot(workspaceRoot, () => syncHandoffLifecycleJobInWorkspace(input));
982
+ }
983
+ async function syncHandoffLifecycleJobInWorkspace(input) {
927
984
  return withSchedulerTransaction(async () => {
928
985
  const queue = readJobQueue();
929
986
  const locks = readJobLockTable();
@@ -986,7 +1043,10 @@ export async function syncHandoffLifecycleJob(input) {
986
1043
  };
987
1044
  });
988
1045
  }
989
- export async function acknowledgeJob(input) {
1046
+ export async function acknowledgeJob(input, workspaceRoot = resolveWorkspaceRoot()) {
1047
+ return withWorkspaceRoot(workspaceRoot, () => acknowledgeJobInWorkspace(input));
1048
+ }
1049
+ async function acknowledgeJobInWorkspace(input) {
990
1050
  const buf = newEventBuffer();
991
1051
  const result = await withSchedulerTransaction(async () => {
992
1052
  const queue = readJobQueue();
@@ -1068,8 +1128,12 @@ export async function acknowledgeJob(input) {
1068
1128
  await flushTransitionEvents(buf);
1069
1129
  return result;
1070
1130
  }
1071
- export async function dispatchJobs(input = {}) {
1131
+ export async function dispatchJobs(input = {}, workspaceRoot = resolveWorkspaceRoot()) {
1132
+ return withWorkspaceRoot(workspaceRoot, () => dispatchJobsInWorkspace(input));
1133
+ }
1134
+ async function dispatchJobsInWorkspace(input = {}) {
1072
1135
  const buf = newEventBuffer();
1136
+ const selected = input.job_ids === undefined ? undefined : new Set(input.job_ids);
1073
1137
  const result = await withSchedulerTransaction(async () => {
1074
1138
  const owner = deriveOwner(input.owner);
1075
1139
  const now = parseDate(input.now_iso) ?? new Date();
@@ -1109,15 +1173,17 @@ export async function dispatchJobs(input = {}) {
1109
1173
  handoffState: readHandoffRegistry(),
1110
1174
  statusEvents: readStatusEvents(500),
1111
1175
  };
1112
- const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf);
1176
+ const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf, selected);
1113
1177
  // Heartbeat watchdog: reap running jobs whose worker heartbeat is stale.
1114
1178
  // NOTE: dispatch deliberately does NOT re-stamp running-job heartbeats here.
1115
1179
  // Only a live worker (heartbeatJob) or a fresh start proves liveness; if the
1116
1180
  // dispatcher renewed heartbeats it would mask worker death (root cause of D3).
1117
1181
  const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
1118
- const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
1182
+ const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf, selected);
1119
1183
  const reapedJobs = reapedJobIds.length;
1120
1184
  for (const job of queue.jobs) {
1185
+ if (selected && !selected.has(job.job_id))
1186
+ continue;
1121
1187
  if (job.kind === "handoff.lifecycle")
1122
1188
  continue;
1123
1189
  if (TERMINAL_STATUSES.has(job.status))
@@ -1142,7 +1208,7 @@ export async function dispatchJobs(input = {}) {
1142
1208
  }
1143
1209
  const occupancy = buildOccupancy(table.locks);
1144
1210
  const readyJobs = queue.jobs
1145
- .filter((job) => job.status === "ready" && job.kind !== "handoff.lifecycle")
1211
+ .filter((job) => job.status === "ready" && job.kind !== "handoff.lifecycle" && (!selected || selected.has(job.job_id)))
1146
1212
  .sort(compareJobs);
1147
1213
  let startedJobs = 0;
1148
1214
  for (const job of readyJobs) {
@@ -1220,7 +1286,10 @@ export async function dispatchJobs(input = {}) {
1220
1286
  await flushTransitionEvents(buf);
1221
1287
  return result;
1222
1288
  }
1223
- export async function dispatchJobNow(input) {
1289
+ export async function dispatchJobNow(input, workspaceRoot = resolveWorkspaceRoot()) {
1290
+ return withWorkspaceRoot(workspaceRoot, () => dispatchJobNowInWorkspace(input));
1291
+ }
1292
+ async function dispatchJobNowInWorkspace(input) {
1224
1293
  const buf = newEventBuffer();
1225
1294
  const result = await withSchedulerTransaction(async () => {
1226
1295
  const owner = deriveOwner(input.owner);
@@ -1355,7 +1424,70 @@ export async function dispatchJobNow(input) {
1355
1424
  await flushTransitionEvents(buf);
1356
1425
  return result;
1357
1426
  }
1358
- export async function completeJob(input) {
1427
+ /** Atomically claim the side-effecting handler, independently of scheduler owner. */
1428
+ export async function claimJobExecution(input, workspaceRoot = resolveWorkspaceRoot()) {
1429
+ return withWorkspaceRoot(workspaceRoot, () => withSchedulerTransaction(async () => {
1430
+ const now = parseDate(input.now_iso) ?? new Date();
1431
+ const owner = deriveOwner(input.owner);
1432
+ const lease = ensureLeaseForOwner(owner, now);
1433
+ if (!lease.acquired)
1434
+ return { ok: false, reason_code: "scheduler_lease_held", error: "Execution owner does not hold the scheduler lease." };
1435
+ const queue = readJobQueue();
1436
+ const locks = readJobLockTable();
1437
+ const job = queue.jobs.find(row => row.job_id === input.job_id);
1438
+ if (!job || job.status !== "running")
1439
+ return { ok: false, reason_code: "job_not_running", job };
1440
+ if (job.execution || job.execution_children?.length) {
1441
+ const state = probeJobExecution(job);
1442
+ if (state === "dead") {
1443
+ // The prior process may have performed side effects before it exited.
1444
+ // Preserve uncertainty instead of replaying the handler automatically.
1445
+ job.status = "unknown_recovery";
1446
+ job.reason_code = "execution_process_exited";
1447
+ job.error = "Claimed execution process exited; inspect retained outcomes before retrying.";
1448
+ job.next_attempt_at = undefined;
1449
+ releaseLocksForRecoveredJob(locks, job.job_id, "execution_process_exited", now);
1450
+ await persistSchedulerState({ queue, locks, lease: lease.lease });
1451
+ }
1452
+ return { ok: false, reason_code: state === "alive" ? "execution_claimed" : state === "unknown" ? "execution_uncertain" : "execution_dead", job };
1453
+ }
1454
+ if (!hasActiveLockForJob(locks.locks, job.job_id))
1455
+ return { ok: false, reason_code: "execution_missing_locks", job };
1456
+ const identity = readProcessIdentity(process.pid);
1457
+ if (!identity)
1458
+ return { ok: false, reason_code: "identity_unavailable", error: "Cannot safely identify the worker process; handler was not invoked.", job };
1459
+ const token = randomUUID();
1460
+ job.execution = { ...identity, token };
1461
+ job.execution_children = undefined;
1462
+ await persistSchedulerState({ queue, locks, lease: lease.lease });
1463
+ return { ok: true, token, job };
1464
+ }));
1465
+ }
1466
+ /** Register a command before waiting on it; only its claimed attempt may do so. */
1467
+ export async function recordJobExecutionChild(input, workspaceRoot = resolveWorkspaceRoot()) {
1468
+ return withWorkspaceRoot(workspaceRoot, () => withSchedulerTransaction(async () => {
1469
+ const queue = readJobQueue();
1470
+ const job = queue.jobs.find(row => row.job_id === input.job_id);
1471
+ if (!job || job.status !== "running")
1472
+ return { ok: false, reason_code: "job_not_running" };
1473
+ if (!input.token || job.execution?.token !== input.token)
1474
+ return { ok: false, reason_code: "execution_token_mismatch" };
1475
+ const identity = input.identity;
1476
+ if (!Number.isSafeInteger(identity.pid) || identity.pid <= 1 || !identity.started_at || !identity.hostname) {
1477
+ return { ok: false, reason_code: "execution_identity_invalid" };
1478
+ }
1479
+ job.execution_children ??= [];
1480
+ if (!job.execution_children.some(child => child.pid === identity.pid && child.started_at === identity.started_at && child.hostname === identity.hostname)) {
1481
+ job.execution_children.push({ ...identity });
1482
+ await persistSchedulerState({ queue, locks: readJobLockTable(), lease: readSchedulerLease() });
1483
+ }
1484
+ return { ok: true };
1485
+ }));
1486
+ }
1487
+ export async function completeJob(input, workspaceRoot = resolveWorkspaceRoot()) {
1488
+ return withWorkspaceRoot(workspaceRoot, () => completeJobInWorkspace(input));
1489
+ }
1490
+ async function completeJobInWorkspace(input) {
1359
1491
  const buf = newEventBuffer();
1360
1492
  const result = await withSchedulerTransaction(async () => {
1361
1493
  const owner = deriveOwner(input.owner);
@@ -1406,6 +1538,9 @@ export async function completeJob(input) {
1406
1538
  };
1407
1539
  }
1408
1540
  const success = input.success !== false;
1541
+ // The claim belongs to this attempt only. A deliberate retry gets a new one.
1542
+ job.execution = undefined;
1543
+ job.execution_children = undefined;
1409
1544
  for (const lock of table.locks) {
1410
1545
  if (lock.job_id !== input.job_id)
1411
1546
  continue;
@@ -1497,7 +1632,10 @@ export async function completeJob(input) {
1497
1632
  * job's heartbeat — the dispatcher deliberately does not, so a missing
1498
1633
  * heartbeat reliably signals a dead worker.
1499
1634
  */
1500
- export async function heartbeatJob(input) {
1635
+ export async function heartbeatJob(input, workspaceRoot = resolveWorkspaceRoot()) {
1636
+ return withWorkspaceRoot(workspaceRoot, () => heartbeatJobInWorkspace(input));
1637
+ }
1638
+ async function heartbeatJobInWorkspace(input) {
1501
1639
  return withSchedulerTransaction(async () => {
1502
1640
  const owner = deriveOwner(input.owner);
1503
1641
  const now = parseDate(input.now_iso) ?? new Date();
@@ -1542,6 +1680,10 @@ export async function heartbeatJob(input) {
1542
1680
  };
1543
1681
  }
1544
1682
  const beatAt = nowIso(now);
1683
+ if (job.reason_code?.startsWith("heartbeat_stale_process_")) {
1684
+ job.reason_code = undefined;
1685
+ job.error = undefined;
1686
+ }
1545
1687
  for (const lock of activeLocks) {
1546
1688
  lock.heartbeat_at = beatAt;
1547
1689
  lock.lease_id = leaseResult.lease.lease_id;
@@ -1566,21 +1708,25 @@ export async function heartbeatJob(input) {
1566
1708
  * watchdog cadence separate from dispatch. Does not require lease ownership —
1567
1709
  * reaping a dead worker's job must not depend on holding the lease.
1568
1710
  */
1569
- export async function reapStaleRunningJobsTick(input = {}) {
1711
+ export async function reapStaleRunningJobsTick(input = {}, workspaceRoot = resolveWorkspaceRoot()) {
1712
+ return withWorkspaceRoot(workspaceRoot, () => reapStaleRunningJobsTickInWorkspace(input));
1713
+ }
1714
+ async function reapStaleRunningJobsTickInWorkspace(input = {}) {
1570
1715
  const buf = newEventBuffer();
1571
1716
  const result = await withSchedulerTransaction(async () => {
1572
1717
  const now = parseDate(input.now_iso) ?? new Date();
1573
1718
  const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
1574
1719
  const queue = readJobQueue();
1575
1720
  const table = readJobLockTable();
1721
+ const priorJobs = JSON.stringify(queue.jobs);
1576
1722
  const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
1577
- if (reapedJobIds.length > 0) {
1723
+ // Retained live/unknown claims also carry a persisted diagnostic.
1724
+ if (priorJobs !== JSON.stringify(queue.jobs))
1578
1725
  await persistSchedulerState({
1579
1726
  queue,
1580
1727
  locks: table,
1581
1728
  lease: readSchedulerLease(),
1582
1729
  });
1583
- }
1584
1730
  return {
1585
1731
  ok: true,
1586
1732
  queue_path: getJobQueuePath(),
@@ -35,6 +35,8 @@ export declare function listJobHandlerKinds(): string[];
35
35
  export declare function hasJobHandler(kind: string): boolean;
36
36
  export interface RunJobWorkerInput {
37
37
  owner?: string;
38
+ /** Execute only these jobs; unrelated jobs are not admitted by this tick. */
39
+ job_ids?: string[];
38
40
  /** Deterministic clock override; threaded through every scheduler call. */
39
41
  now_iso?: string;
40
42
  lease_ttl_seconds?: number;
@@ -60,7 +62,7 @@ export interface WorkerJobOutcome {
60
62
  export interface WorkerSkippedJob {
61
63
  job_id: string;
62
64
  kind: string;
63
- reason: "no_handler" | "max_jobs";
65
+ reason: "no_handler" | "max_jobs" | "execution_claimed" | "execution_uncertain";
64
66
  }
65
67
  export interface RunJobWorkerResult {
66
68
  owner: string;
@@ -17,7 +17,7 @@
17
17
  * across a handler invocation, so a slow or hung handler cannot wedge the
18
18
  * scheduler for other owners.
19
19
  */
20
- import { completeJob, dispatchJobs, heartbeatJob, readJobQueue, readSchedulerLease, } from "./job-scheduler.js";
20
+ import { completeJob, claimJobExecution, dispatchJobs, heartbeatJob, readJobQueue, readSchedulerLease, } from "./job-scheduler.js";
21
21
  import { resolveWorkspaceRoot } from "./helpers.js";
22
22
  const handlers = new Map();
23
23
  export function registerJobHandler(kind, handler) {
@@ -66,6 +66,7 @@ export async function runJobWorkerOnce(input = {}) {
66
66
  now_iso: input.now_iso,
67
67
  lease_ttl_seconds: input.lease_ttl_seconds,
68
68
  heartbeat_stale_seconds: input.heartbeat_stale_seconds,
69
+ job_ids: input.job_ids,
69
70
  });
70
71
  const executed = [];
71
72
  const skipped = [];
@@ -83,7 +84,8 @@ export async function runJobWorkerOnce(input = {}) {
83
84
  };
84
85
  }
85
86
  const queue = readJobQueue();
86
- const runningJobs = queue.jobs.filter((job) => job.status === "running");
87
+ const selected = input.job_ids === undefined ? undefined : new Set(input.job_ids);
88
+ const runningJobs = queue.jobs.filter((job) => job.status === "running" && (!selected || selected.has(job.job_id)));
87
89
  const maxJobs = typeof input.max_jobs === "number" && input.max_jobs > 0
88
90
  ? Math.floor(input.max_jobs)
89
91
  : Number.POSITIVE_INFINITY;
@@ -98,6 +100,11 @@ export async function runJobWorkerOnce(input = {}) {
98
100
  skipped.push({ job_id: job.job_id, kind: job.kind, reason: "max_jobs" });
99
101
  continue;
100
102
  }
103
+ const claim = await claimJobExecution({ job_id: job.job_id, owner, now_iso: input.now_iso });
104
+ if (!claim.ok) {
105
+ skipped.push({ job_id: job.job_id, kind: job.kind, reason: claim.reason_code === "execution_claimed" ? "execution_claimed" : "execution_uncertain" });
106
+ continue;
107
+ }
101
108
  executedCount += 1;
102
109
  // Prove liveness immediately, then keep beating while the handler runs.
103
110
  await heartbeatJob({ job_id: job.job_id, owner, now_iso: input.now_iso });
@@ -113,7 +120,7 @@ export async function runJobWorkerOnce(input = {}) {
113
120
  }
114
121
  let handlerResult;
115
122
  try {
116
- handlerResult = await handler({ job, owner });
123
+ handlerResult = await handler({ job: claim.job ?? job, owner });
117
124
  }
118
125
  catch (err) {
119
126
  handlerResult = { success: false, error: errorMessage(err) };
package/dist/kanban.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type KanbanColumn = "backlog" | "blocked" | "in_progress" | "gated" | "done";
1
+ export type KanbanColumn = "backlog" | "blocked" | "in_progress" | "gated" | "done" | "cancelled";
2
2
  export interface AceKanbanCard {
3
3
  id: string;
4
4
  title: string;
@@ -10,8 +10,10 @@ export interface AceKanbanBoard {
10
10
  in_progress: AceKanbanCard[];
11
11
  gated: AceKanbanCard[];
12
12
  done: AceKanbanCard[];
13
+ cancelled: AceKanbanCard[];
13
14
  }
14
15
  export interface AceKanbanSnapshot {
16
+ refresh_scheduled?: boolean;
15
17
  version: number;
16
18
  generated_at: string;
17
19
  trigger: string;
@@ -21,6 +23,7 @@ export interface AceKanbanSnapshot {
21
23
  json_path: string;
22
24
  }
23
25
  export declare function renderKanbanHtml(snapshot: AceKanbanSnapshot): string;
26
+ export declare function formatKanbanRefresh(snapshot: AceKanbanSnapshot): string;
24
27
  export declare function refreshKanbanArtifacts(trigger: string): AceKanbanSnapshot;
25
28
  export declare function getOrRefreshKanbanSnapshot(trigger: string): AceKanbanSnapshot;
26
29
  export declare function shouldAutoRefreshKanbanForPath(path: string): boolean;
package/dist/kanban.js CHANGED
@@ -1,13 +1,13 @@
1
- import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, safeRead, safeWrite, wsPath } from "./helpers.js";
1
+ import { ACE_ROOT_REL, ACE_TASKS_ROOT_REL, safeRead, safeWrite, wsPath, resolveWorkspaceRoot, withWorkspaceRoot } from "./helpers.js";
2
2
  import { isReadError, slugify } from "./shared.js";
3
3
  import { readHandoffRegistry } from "./handoff-registry.js";
4
4
  import { readTodoState } from "./todo-state.js";
5
5
  /** Debounce threshold in milliseconds. Skip re-render if within this window. */
6
6
  const KANBAN_DEBOUNCE_MS = 2000;
7
- let lastKanbanRenderTimestamp = 0;
7
+ const renderState = new Map();
8
8
  const KANBAN_JSON_REL = "agent-state/kanban.json";
9
9
  const KANBAN_HTML_REL = "agent-state/kanban.html";
10
- const COLUMNS = ["backlog", "blocked", "in_progress", "gated", "done"];
10
+ const COLUMNS = ["backlog", "blocked", "in_progress", "gated", "done", "cancelled"];
11
11
  function nowIso() {
12
12
  return new Date().toISOString();
13
13
  }
@@ -18,6 +18,7 @@ function emptyBoard() {
18
18
  in_progress: [],
19
19
  gated: [],
20
20
  done: [],
21
+ cancelled: [],
21
22
  };
22
23
  }
23
24
  function escapeHtml(input) {
@@ -236,7 +237,7 @@ export function renderKanbanHtml(snapshot) {
236
237
  "@keyframes spin{to{transform:rotate(360deg)}}",
237
238
  "h1{margin:0;font-family:'Space Grotesk','Avenir Next',sans-serif;font-size:24px}",
238
239
  ".stamp{margin:0;font-size:12px;opacity:.75}",
239
- ".grid{display:grid;grid-template-columns:repeat(5,minmax(220px,1fr));gap:12px}",
240
+ ".grid{display:grid;grid-template-columns:repeat(6,minmax(220px,1fr));gap:12px}",
240
241
  ".column{background:rgba(255,255,255,.62);border:1px solid var(--line);border-radius:12px;min-height:220px}",
241
242
  ".column>header{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid var(--line)}",
242
243
  ".column h2{margin:0;font-size:14px;text-transform:capitalize}",
@@ -265,16 +266,38 @@ export function renderKanbanHtml(snapshot) {
265
266
  "</html>",
266
267
  ].join("");
267
268
  }
269
+ export function formatKanbanRefresh(snapshot) {
270
+ return snapshot.refresh_scheduled
271
+ ? `Kanban refresh scheduled: ${snapshot.html_path} (last generated ${snapshot.generated_at})`
272
+ : `Kanban updated: ${snapshot.html_path}`;
273
+ }
268
274
  export function refreshKanbanArtifacts(trigger) {
269
- // Debounce: skip expensive re-render if within threshold
270
- const now = Date.now();
271
- if (now - lastKanbanRenderTimestamp < KANBAN_DEBOUNCE_MS) {
272
- // Return cached snapshot if available
273
- const cached = getOrRefreshKanbanSnapshot(trigger);
274
- if (cached.generated_at)
275
- return cached;
275
+ const root = resolveWorkspaceRoot();
276
+ const state = renderState.get(root) ?? { last: 0 };
277
+ const delay = KANBAN_DEBOUNCE_MS - (Date.now() - state.last);
278
+ const cached = readKanbanSnapshot();
279
+ if (delay > 0 && cached) {
280
+ if (state.timer)
281
+ clearTimeout(state.timer);
282
+ state.timer = setTimeout(() => withWorkspaceRoot(root, () => {
283
+ state.timer = undefined;
284
+ try {
285
+ renderKanbanNow(trigger);
286
+ }
287
+ catch (error) {
288
+ state.last = 0;
289
+ console.error(`Kanban refresh failed for ${root}: ${error instanceof Error ? error.message : String(error)}`);
290
+ }
291
+ }), delay);
292
+ state.timer.unref();
293
+ renderState.set(root, state);
294
+ return { ...cached, refresh_scheduled: true };
276
295
  }
277
- lastKanbanRenderTimestamp = now;
296
+ if (state.timer)
297
+ clearTimeout(state.timer);
298
+ return renderKanbanNow(trigger);
299
+ }
300
+ function renderKanbanNow(trigger) {
278
301
  const board = emptyBoard();
279
302
  addTodoCards(board);
280
303
  addStatusCard(board);
@@ -292,15 +315,17 @@ export function refreshKanbanArtifacts(trigger) {
292
315
  in_progress: board.in_progress.length,
293
316
  gated: board.gated.length,
294
317
  done: board.done.length,
318
+ cancelled: board.cancelled.length,
295
319
  },
296
320
  html_path: wsPath(KANBAN_HTML_REL),
297
321
  json_path: wsPath(KANBAN_JSON_REL),
298
322
  };
299
323
  safeWrite(KANBAN_HTML_REL, renderKanbanHtml(snapshot));
300
324
  safeWrite(KANBAN_JSON_REL, JSON.stringify(snapshot, null, 2));
325
+ renderState.set(resolveWorkspaceRoot(), { last: Date.now() });
301
326
  return snapshot;
302
327
  }
303
- export function getOrRefreshKanbanSnapshot(trigger) {
328
+ function readKanbanSnapshot() {
304
329
  const raw = safeRead(KANBAN_JSON_REL);
305
330
  if (!isReadError(raw)) {
306
331
  const parsed = tryParseJson(raw);
@@ -316,14 +341,17 @@ export function getOrRefreshKanbanSnapshot(trigger) {
316
341
  version,
317
342
  generated_at: generated,
318
343
  trigger: savedTrigger,
319
- board,
320
- counts,
344
+ board: { ...emptyBoard(), ...board },
345
+ counts: { ...Object.fromEntries(COLUMNS.map((key) => [key, 0])), ...counts },
321
346
  html_path: htmlPath,
322
347
  json_path: jsonPath,
323
348
  };
324
349
  }
325
350
  }
326
- return refreshKanbanArtifacts(trigger);
351
+ return undefined;
352
+ }
353
+ export function getOrRefreshKanbanSnapshot(trigger) {
354
+ return readKanbanSnapshot() ?? refreshKanbanArtifacts(trigger);
327
355
  }
328
356
  export function shouldAutoRefreshKanbanForPath(path) {
329
357
  const normalized = path.replace(/\\/g, "/").replace(/^\.?\//, "");
@@ -0,0 +1,64 @@
1
+ import { type LearnedArtifact } from './learning-records.js';
2
+ export { decodeLearned, type LearnedArtifact } from './learning-records.js';
3
+ import { type PlaybookScope, type PlaybookSection } from './store/repositories/playbook-repository.js';
4
+ import { type IAcePackedStore } from './store/types.js';
5
+ import type { GoalEngineDeps } from './goal-engine.js';
6
+ export interface LearnedPayload {
7
+ lessons?: Array<{
8
+ op: 'add' | 'edit' | 'retire';
9
+ bullet_id?: string;
10
+ section: PlaybookSection;
11
+ content: string;
12
+ scope: PlaybookScope;
13
+ episodes: string[];
14
+ retired_episodes?: string[];
15
+ }>;
16
+ skill?: {
17
+ op: 'add' | 'edit';
18
+ slug: string;
19
+ skill_md: string;
20
+ references?: Record<string, string>;
21
+ episodes: string[];
22
+ retired_episodes?: string[];
23
+ };
24
+ instruction?: {
25
+ role: string;
26
+ text: string;
27
+ episodes: string[];
28
+ };
29
+ }
30
+ export interface LearningAcceptance {
31
+ ok: boolean;
32
+ ids: string[];
33
+ errors: string[];
34
+ reason_code?: string;
35
+ options?: Array<{
36
+ tool: string;
37
+ args: Record<string, unknown>;
38
+ why: string;
39
+ }>;
40
+ }
41
+ export declare function listLearnedArtifacts(root: string): LearnedArtifact[];
42
+ export declare function withLearnedStore<T>(root: string, apply: (store: IAcePackedStore) => Promise<T>): Promise<T>;
43
+ export declare function saveLearnedArtifact(root: string, item: LearnedArtifact): Promise<void>;
44
+ export declare function updateLearnedArtifact(root: string, key: string, update: (item: LearnedArtifact) => LearnedArtifact | undefined): Promise<void>;
45
+ export declare function prepareLearned(root: string, payload: LearnedPayload, context: {
46
+ goal_id: string;
47
+ step_id: string;
48
+ attempt: number;
49
+ }, deps?: GoalEngineDeps): Promise<{
50
+ result: LearningAcceptance;
51
+ publish: () => Promise<void>;
52
+ }>;
53
+ export declare function acceptLearned(root: string, payload: LearnedPayload, context: {
54
+ goal_id: string;
55
+ step_id: string;
56
+ attempt: number;
57
+ }, deps?: GoalEngineDeps): Promise<LearningAcceptance>;
58
+ export declare function regenerateLearnedProjection(root: string): Promise<void>;
59
+ export declare function ensureLearningStep(root: string, force?: {
60
+ scope: PlaybookScope;
61
+ reason: string;
62
+ }): Promise<void>;
63
+ export declare function reopenLearningCorrection(root: string, errors: string[]): Promise<void>;
64
+ //# sourceMappingURL=learned-artifacts.d.ts.map