@voybio/ace-swarm 2.4.3 → 2.4.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 (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -538,6 +538,33 @@ function deriveOwner(input) {
538
538
  function defaultLeaseTtlSeconds() {
539
539
  return 1800;
540
540
  }
541
+ /**
542
+ * Default window a running job may go without a worker heartbeat before the
543
+ * watchdog presumes the worker is dead and reaps the job into
544
+ * `unknown_recovery`. A live in-process worker is expected to call
545
+ * `heartbeatJob` well within this window (see job-worker.ts).
546
+ */
547
+ export const DEFAULT_HEARTBEAT_STALE_MS = 120_000;
548
+ function resolveStaleMs(heartbeatStaleSeconds) {
549
+ if (typeof heartbeatStaleSeconds !== "number" || !Number.isFinite(heartbeatStaleSeconds)) {
550
+ return DEFAULT_HEARTBEAT_STALE_MS;
551
+ }
552
+ if (heartbeatStaleSeconds <= 0)
553
+ return 0;
554
+ return Math.floor(heartbeatStaleSeconds * 1000);
555
+ }
556
+ function latestHeartbeatMs(job, activeLocks) {
557
+ let latest = 0;
558
+ for (const lock of activeLocks) {
559
+ const stamp = parseDate(lock.heartbeat_at ?? lock.acquired_at)?.getTime();
560
+ if (stamp && stamp > latest)
561
+ latest = stamp;
562
+ }
563
+ if (latest === 0) {
564
+ latest = parseDate(job.started_at)?.getTime() ?? 0;
565
+ }
566
+ return latest;
567
+ }
541
568
  function hasActiveLockForJob(locks, jobId) {
542
569
  return locks.some((lock) => lock.job_id === jobId && isJobLockActive(lock));
543
570
  }
@@ -712,6 +739,66 @@ function reconcileRecoveryState(queue, table, now, recoveryMode, buf) {
712
739
  }
713
740
  return recovered;
714
741
  }
742
+ /**
743
+ * Heartbeat watchdog: move any `running` job whose worker heartbeat has gone
744
+ * stale into `unknown_recovery` and release its resource locks. Unlike lease
745
+ * recovery (which only fires on owner change / lease expiry), this catches the
746
+ * common D3 failure where a worker dies — or never existed — while the job's
747
+ * owner keeps holding a valid lease, leaving the job stuck `running` forever.
748
+ *
749
+ * Missing-lock jobs are intentionally left to `reconcileRecoveryState`, which
750
+ * runs first; this pass only inspects jobs that still hold active locks.
751
+ */
752
+ function reapStaleRunningJobs(queue, table, now, staleMs, buf) {
753
+ if (staleMs <= 0)
754
+ return [];
755
+ const reaped = [];
756
+ for (const job of queue.jobs) {
757
+ if (job.status !== "running")
758
+ continue;
759
+ const activeLocks = table.locks.filter((lock) => lock.job_id === job.job_id && lock.status === "active");
760
+ if (activeLocks.length === 0)
761
+ continue;
762
+ const lastBeat = latestHeartbeatMs(job, activeLocks);
763
+ if (lastBeat === 0)
764
+ continue;
765
+ const ageMs = now.getTime() - lastBeat;
766
+ if (ageMs <= staleMs)
767
+ continue;
768
+ const previousStatus = job.status;
769
+ job.status = "unknown_recovery";
770
+ job.reason_code = "heartbeat_stale";
771
+ job.error = `Running job heartbeat is stale (${Math.round(ageMs / 1000)}s since last heartbeat, threshold ${Math.round(staleMs / 1000)}s); worker presumed dead.`;
772
+ job.next_attempt_at = undefined;
773
+ releaseLocksForRecoveredJob(table, job.job_id, "heartbeat_stale", now);
774
+ reaped.push(job.job_id);
775
+ buf.status.push({
776
+ event_type: "SCHEDULER_WATCHDOG",
777
+ status: "blocked",
778
+ summary: `Job ${job.job_id} reaped by watchdog (heartbeat_stale, ${Math.round(ageMs / 1000)}s)`,
779
+ payload: {
780
+ job_id: job.job_id,
781
+ from_status: previousStatus,
782
+ reason_code: "heartbeat_stale",
783
+ stale_ms: ageMs,
784
+ threshold_ms: staleMs,
785
+ },
786
+ });
787
+ buf.ledger.push({
788
+ tool: "scheduler_watchdog",
789
+ category: "regression",
790
+ message: `Watchdog reaped stale running job ${job.job_id} (heartbeat_stale)`,
791
+ artifacts: ["agent-state/job-queue.json", "agent-state/job-locks.json"],
792
+ metadata: {
793
+ job_id: job.job_id,
794
+ reason_code: "heartbeat_stale",
795
+ stale_ms: ageMs,
796
+ threshold_ms: staleMs,
797
+ },
798
+ });
799
+ }
800
+ return reaped;
801
+ }
715
802
  export async function enqueueJob(input) {
716
803
  const buf = newEventBuffer();
717
804
  const result = await withFileLock(SCHEDULER_LOCK_REL, async () => {
@@ -923,6 +1010,7 @@ export async function dispatchJobs(input = {}) {
923
1010
  running_jobs: queue.jobs.filter((job) => job.status === "running").length,
924
1011
  started_jobs: 0,
925
1012
  recovered_jobs: 0,
1013
+ reaped_jobs: 0,
926
1014
  },
927
1015
  };
928
1016
  }
@@ -933,7 +1021,13 @@ export async function dispatchJobs(input = {}) {
933
1021
  statusEvents: readStatusEvents(500),
934
1022
  };
935
1023
  const recoveredJobs = reconcileRecoveryState(queue, table, now, leaseResult.recovery_mode, buf);
936
- heartbeatRunningLocks(table, leaseResult.lease.lease_id, now);
1024
+ // Heartbeat watchdog: reap running jobs whose worker heartbeat is stale.
1025
+ // NOTE: dispatch deliberately does NOT re-stamp running-job heartbeats here.
1026
+ // Only a live worker (heartbeatJob) or a fresh start proves liveness; if the
1027
+ // dispatcher renewed heartbeats it would mask worker death (root cause of D3).
1028
+ const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
1029
+ const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
1030
+ const reapedJobs = reapedJobIds.length;
937
1031
  for (const job of queue.jobs) {
938
1032
  if (TERMINAL_STATUSES.has(job.status))
939
1033
  continue;
@@ -990,11 +1084,12 @@ export async function dispatchJobs(input = {}) {
990
1084
  running_jobs: queue.jobs.filter((job) => job.status === "running").length,
991
1085
  started_jobs: startedJobs,
992
1086
  recovered_jobs: recoveredJobs,
1087
+ reaped_jobs: reapedJobs,
993
1088
  };
994
1089
  buf.status.push({
995
1090
  event_type: "SCHEDULER_TICK",
996
1091
  status: "pass",
997
- summary: `Dispatch started=${summary.started_jobs} ready=${summary.ready_jobs} running=${summary.running_jobs} recovered=${recoveredJobs}`,
1092
+ summary: `Dispatch started=${summary.started_jobs} ready=${summary.ready_jobs} running=${summary.running_jobs} recovered=${recoveredJobs} reaped=${reapedJobs}`,
998
1093
  payload: {
999
1094
  owner,
1000
1095
  lease_acquired: true,
@@ -1290,4 +1385,109 @@ export async function completeJob(input) {
1290
1385
  await flushTransitionEvents(buf);
1291
1386
  return result;
1292
1387
  }
1388
+ /**
1389
+ * Prove liveness for a running job. A live in-process worker calls this
1390
+ * periodically while executing a job body so the heartbeat watchdog does not
1391
+ * reap it. Advances the job's active-lock heartbeats and renews the owner
1392
+ * lease. This is the ONLY path (besides a fresh start) that advances a running
1393
+ * job's heartbeat — the dispatcher deliberately does not, so a missing
1394
+ * heartbeat reliably signals a dead worker.
1395
+ */
1396
+ export async function heartbeatJob(input) {
1397
+ return withFileLock(SCHEDULER_LOCK_REL, async () => {
1398
+ const owner = deriveOwner(input.owner);
1399
+ const now = parseDate(input.now_iso) ?? new Date();
1400
+ const leaseTtl = typeof input.lease_ttl_seconds === "number" && input.lease_ttl_seconds > 0
1401
+ ? Math.floor(input.lease_ttl_seconds)
1402
+ : defaultLeaseTtlSeconds();
1403
+ const leaseResult = ensureLeaseForOwner(owner, now, leaseTtl);
1404
+ if (!leaseResult.acquired) {
1405
+ return {
1406
+ ok: false,
1407
+ queue_path: getJobQueuePath(),
1408
+ lock_path: getJobLockTablePath(),
1409
+ error: `Owner ${owner} does not hold an active scheduler lease.`,
1410
+ };
1411
+ }
1412
+ const queue = readJobQueue();
1413
+ const table = readJobLockTable();
1414
+ const job = queue.jobs.find((row) => row.job_id === input.job_id);
1415
+ if (!job) {
1416
+ return {
1417
+ ok: false,
1418
+ queue_path: getJobQueuePath(),
1419
+ lock_path: getJobLockTablePath(),
1420
+ error: `Unknown job_id: ${input.job_id}`,
1421
+ };
1422
+ }
1423
+ if (job.status !== "running") {
1424
+ return {
1425
+ ok: false,
1426
+ queue_path: getJobQueuePath(),
1427
+ lock_path: getJobLockTablePath(),
1428
+ error: `Job ${input.job_id} is ${job.status}; only running jobs can be heartbeated.`,
1429
+ };
1430
+ }
1431
+ const activeLocks = table.locks.filter((lock) => lock.job_id === input.job_id && lock.status === "active");
1432
+ if (activeLocks.length === 0) {
1433
+ return {
1434
+ ok: false,
1435
+ queue_path: getJobQueuePath(),
1436
+ lock_path: getJobLockTablePath(),
1437
+ error: `Job ${input.job_id} has no active resource locks to heartbeat.`,
1438
+ };
1439
+ }
1440
+ const beatAt = nowIso(now);
1441
+ for (const lock of activeLocks) {
1442
+ lock.heartbeat_at = beatAt;
1443
+ lock.lease_id = leaseResult.lease.lease_id;
1444
+ }
1445
+ await persistSchedulerState({
1446
+ queue,
1447
+ locks: table,
1448
+ lease: leaseResult.lease,
1449
+ });
1450
+ return {
1451
+ ok: true,
1452
+ queue_path: getJobQueuePath(),
1453
+ lock_path: getJobLockTablePath(),
1454
+ heartbeat_at: beatAt,
1455
+ resources: activeLocks.map((lock) => lock.resource),
1456
+ };
1457
+ });
1458
+ }
1459
+ /**
1460
+ * Standalone heartbeat-watchdog tick: reap stale running jobs without planning
1461
+ * or starting anything. Takes its own scheduler lock. Useful for a dedicated
1462
+ * watchdog cadence separate from dispatch. Does not require lease ownership —
1463
+ * reaping a dead worker's job must not depend on holding the lease.
1464
+ */
1465
+ export async function reapStaleRunningJobsTick(input = {}) {
1466
+ const buf = newEventBuffer();
1467
+ const result = await withFileLock(SCHEDULER_LOCK_REL, async () => {
1468
+ const now = parseDate(input.now_iso) ?? new Date();
1469
+ const staleMs = resolveStaleMs(input.heartbeat_stale_seconds);
1470
+ const queue = readJobQueue();
1471
+ const table = readJobLockTable();
1472
+ const reapedJobIds = reapStaleRunningJobs(queue, table, now, staleMs, buf);
1473
+ if (reapedJobIds.length > 0) {
1474
+ await persistSchedulerState({
1475
+ queue,
1476
+ locks: table,
1477
+ lease: readSchedulerLease(),
1478
+ });
1479
+ }
1480
+ return {
1481
+ ok: true,
1482
+ queue_path: getJobQueuePath(),
1483
+ lock_path: getJobLockTablePath(),
1484
+ queue,
1485
+ locks: table,
1486
+ reaped_jobs: reapedJobIds.length,
1487
+ reaped_job_ids: reapedJobIds,
1488
+ };
1489
+ });
1490
+ await flushTransitionEvents(buf);
1491
+ return result;
1492
+ }
1293
1493
  //# sourceMappingURL=job-scheduler.js.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * In-process job worker (WS-D).
3
+ *
4
+ * The scheduler (`job-scheduler.ts`) is a pure planner: it moves jobs to
5
+ * `running` and grabs collision-free resource locks, but it never executes a
6
+ * job body nor calls `completeJob`. Before this worker existed, that left every
7
+ * dispatched job stuck at `running` forever (defect D3).
8
+ *
9
+ * This module closes the loop:
10
+ * 1. dispatch runnable jobs (which also runs the heartbeat watchdog),
11
+ * 2. execute the handler registered for each newly-running job's `kind`,
12
+ * heartbeating throughout so the watchdog does not reap a live job,
13
+ * 3. finalize each job via `completeJob` (success → done, throw/fail → retry
14
+ * or terminal failure).
15
+ *
16
+ * Every scheduler call takes its own file lock; the worker never holds a lock
17
+ * across a handler invocation, so a slow or hung handler cannot wedge the
18
+ * scheduler for other owners.
19
+ */
20
+ import { readSchedulerLease, type JobRecord } from "./job-scheduler.js";
21
+ export interface JobHandlerContext {
22
+ job: JobRecord;
23
+ owner: string;
24
+ }
25
+ export interface JobHandlerResult {
26
+ success: boolean;
27
+ evidence_ref?: string;
28
+ error?: string;
29
+ }
30
+ export type JobHandler = (context: JobHandlerContext) => Promise<JobHandlerResult> | JobHandlerResult;
31
+ export declare function registerJobHandler(kind: string, handler: JobHandler): void;
32
+ export declare function unregisterJobHandler(kind: string): boolean;
33
+ export declare function clearJobHandlers(): void;
34
+ export declare function listJobHandlerKinds(): string[];
35
+ export declare function hasJobHandler(kind: string): boolean;
36
+ export interface RunJobWorkerInput {
37
+ owner?: string;
38
+ /** Deterministic clock override; threaded through every scheduler call. */
39
+ now_iso?: string;
40
+ lease_ttl_seconds?: number;
41
+ /** Max jobs to execute this tick (default: unlimited). */
42
+ max_jobs?: number;
43
+ /**
44
+ * Heartbeat cadence while a handler runs. Set to 0 to disable the interval
45
+ * timer (handlers still get a heartbeat when picked up). Default 15s.
46
+ */
47
+ heartbeat_ms?: number;
48
+ /** Watchdog staleness window forwarded to the dispatch tick. */
49
+ heartbeat_stale_seconds?: number;
50
+ }
51
+ export type WorkerJobStatus = "done" | "blocked" | "failed_terminal" | "failed" | "errored" | "unknown";
52
+ export interface WorkerJobOutcome {
53
+ job_id: string;
54
+ kind: string;
55
+ handler_success: boolean;
56
+ final_status: WorkerJobStatus;
57
+ evidence_ref?: string;
58
+ error?: string;
59
+ }
60
+ export interface WorkerSkippedJob {
61
+ job_id: string;
62
+ kind: string;
63
+ reason: "no_handler" | "max_jobs";
64
+ }
65
+ export interface RunJobWorkerResult {
66
+ owner: string;
67
+ lease_acquired: boolean;
68
+ dispatch: {
69
+ started_jobs: number;
70
+ running_jobs: number;
71
+ reaped_jobs: number;
72
+ };
73
+ executed: WorkerJobOutcome[];
74
+ skipped: WorkerSkippedJob[];
75
+ }
76
+ /**
77
+ * Run one worker tick: dispatch, then execute handlers for running jobs.
78
+ * Idempotent and safe to call on a timer or on demand.
79
+ */
80
+ export declare function runJobWorkerOnce(input?: RunJobWorkerInput): Promise<RunJobWorkerResult>;
81
+ /**
82
+ * Convenience: report whether any running job currently lacks a handler.
83
+ * Useful for diagnostics ("why is running=1 not draining?").
84
+ */
85
+ export declare function findUnhandledRunningKinds(): string[];
86
+ export { readSchedulerLease };
87
+ //# sourceMappingURL=job-worker.d.ts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * In-process job worker (WS-D).
3
+ *
4
+ * The scheduler (`job-scheduler.ts`) is a pure planner: it moves jobs to
5
+ * `running` and grabs collision-free resource locks, but it never executes a
6
+ * job body nor calls `completeJob`. Before this worker existed, that left every
7
+ * dispatched job stuck at `running` forever (defect D3).
8
+ *
9
+ * This module closes the loop:
10
+ * 1. dispatch runnable jobs (which also runs the heartbeat watchdog),
11
+ * 2. execute the handler registered for each newly-running job's `kind`,
12
+ * heartbeating throughout so the watchdog does not reap a live job,
13
+ * 3. finalize each job via `completeJob` (success → done, throw/fail → retry
14
+ * or terminal failure).
15
+ *
16
+ * Every scheduler call takes its own file lock; the worker never holds a lock
17
+ * across a handler invocation, so a slow or hung handler cannot wedge the
18
+ * scheduler for other owners.
19
+ */
20
+ import { completeJob, dispatchJobs, heartbeatJob, readJobQueue, readSchedulerLease, } from "./job-scheduler.js";
21
+ import { resolveWorkspaceRoot } from "./helpers.js";
22
+ const handlers = new Map();
23
+ export function registerJobHandler(kind, handler) {
24
+ const key = kind.trim();
25
+ if (key.length === 0) {
26
+ throw new Error("registerJobHandler: kind must be a non-empty string");
27
+ }
28
+ handlers.set(key, handler);
29
+ }
30
+ export function unregisterJobHandler(kind) {
31
+ return handlers.delete(kind.trim());
32
+ }
33
+ export function clearJobHandlers() {
34
+ handlers.clear();
35
+ builtInHandlersRegistered = false;
36
+ }
37
+ export function listJobHandlerKinds() {
38
+ return Array.from(handlers.keys()).sort();
39
+ }
40
+ export function hasJobHandler(kind) {
41
+ return handlers.has(kind.trim());
42
+ }
43
+ const DEFAULT_HEARTBEAT_MS = 15_000;
44
+ let builtInHandlersRegistered = false;
45
+ function deriveOwner(owner) {
46
+ const trimmed = owner?.trim();
47
+ return trimmed && trimmed.length > 0 ? trimmed : "capability-ops";
48
+ }
49
+ function errorMessage(err) {
50
+ if (err instanceof Error)
51
+ return err.message;
52
+ return String(err);
53
+ }
54
+ /**
55
+ * Run one worker tick: dispatch, then execute handlers for running jobs.
56
+ * Idempotent and safe to call on a timer or on demand.
57
+ */
58
+ export async function runJobWorkerOnce(input = {}) {
59
+ await ensureBuiltInHandlers();
60
+ const owner = deriveOwner(input.owner);
61
+ const heartbeatMs = typeof input.heartbeat_ms === "number" && input.heartbeat_ms >= 0
62
+ ? Math.floor(input.heartbeat_ms)
63
+ : DEFAULT_HEARTBEAT_MS;
64
+ const dispatch = await dispatchJobs({
65
+ owner,
66
+ now_iso: input.now_iso,
67
+ lease_ttl_seconds: input.lease_ttl_seconds,
68
+ heartbeat_stale_seconds: input.heartbeat_stale_seconds,
69
+ });
70
+ const executed = [];
71
+ const skipped = [];
72
+ if (!dispatch.lease_acquired) {
73
+ return {
74
+ owner,
75
+ lease_acquired: false,
76
+ dispatch: {
77
+ started_jobs: dispatch.summary.started_jobs,
78
+ running_jobs: dispatch.summary.running_jobs,
79
+ reaped_jobs: dispatch.summary.reaped_jobs,
80
+ },
81
+ executed,
82
+ skipped,
83
+ };
84
+ }
85
+ const queue = readJobQueue();
86
+ const runningJobs = queue.jobs.filter((job) => job.status === "running");
87
+ const maxJobs = typeof input.max_jobs === "number" && input.max_jobs > 0
88
+ ? Math.floor(input.max_jobs)
89
+ : Number.POSITIVE_INFINITY;
90
+ let executedCount = 0;
91
+ for (const job of runningJobs) {
92
+ const handler = handlers.get(job.kind);
93
+ if (!handler) {
94
+ skipped.push({ job_id: job.job_id, kind: job.kind, reason: "no_handler" });
95
+ continue;
96
+ }
97
+ if (executedCount >= maxJobs) {
98
+ skipped.push({ job_id: job.job_id, kind: job.kind, reason: "max_jobs" });
99
+ continue;
100
+ }
101
+ executedCount += 1;
102
+ // Prove liveness immediately, then keep beating while the handler runs.
103
+ await heartbeatJob({ job_id: job.job_id, owner, now_iso: input.now_iso });
104
+ let heartbeatTimer;
105
+ if (heartbeatMs > 0 && input.now_iso === undefined) {
106
+ heartbeatTimer = setInterval(() => {
107
+ void heartbeatJob({ job_id: job.job_id, owner }).catch(() => {
108
+ // Best-effort liveness signal; the watchdog is the backstop.
109
+ });
110
+ }, heartbeatMs);
111
+ // Never keep the event loop alive purely for a heartbeat.
112
+ heartbeatTimer.unref?.();
113
+ }
114
+ let handlerResult;
115
+ try {
116
+ handlerResult = await handler({ job, owner });
117
+ }
118
+ catch (err) {
119
+ handlerResult = { success: false, error: errorMessage(err) };
120
+ }
121
+ finally {
122
+ if (heartbeatTimer)
123
+ clearInterval(heartbeatTimer);
124
+ }
125
+ const completion = await completeJob({
126
+ job_id: job.job_id,
127
+ owner,
128
+ success: handlerResult.success,
129
+ evidence_ref: handlerResult.evidence_ref,
130
+ error: handlerResult.error,
131
+ now_iso: input.now_iso,
132
+ });
133
+ const finalStatus = completion.ok
134
+ ? completion.status ?? "unknown"
135
+ : "errored";
136
+ executed.push({
137
+ job_id: job.job_id,
138
+ kind: job.kind,
139
+ handler_success: handlerResult.success,
140
+ final_status: finalStatus,
141
+ evidence_ref: handlerResult.evidence_ref,
142
+ error: completion.ok ? handlerResult.error : completion.error ?? handlerResult.error,
143
+ });
144
+ }
145
+ return {
146
+ owner,
147
+ lease_acquired: true,
148
+ dispatch: {
149
+ started_jobs: dispatch.summary.started_jobs,
150
+ running_jobs: dispatch.summary.running_jobs,
151
+ reaped_jobs: dispatch.summary.reaped_jobs,
152
+ },
153
+ executed,
154
+ skipped,
155
+ };
156
+ }
157
+ async function ensureBuiltInHandlers() {
158
+ if (builtInHandlersRegistered)
159
+ return;
160
+ builtInHandlersRegistered = true;
161
+ if (!handlers.has("playbook.reflect")) {
162
+ const { createPlaybookReflectJobHandler } = await import("./playbook-llm-reflector.js");
163
+ handlers.set("playbook.reflect", createPlaybookReflectJobHandler({ workspaceRoot: resolveWorkspaceRoot() }));
164
+ }
165
+ }
166
+ /**
167
+ * Convenience: report whether any running job currently lacks a handler.
168
+ * Useful for diagnostics ("why is running=1 not draining?").
169
+ */
170
+ export function findUnhandledRunningKinds() {
171
+ const queue = readJobQueue();
172
+ const kinds = new Set();
173
+ for (const job of queue.jobs) {
174
+ if (job.status === "running" && !handlers.has(job.kind)) {
175
+ kinds.add(job.kind);
176
+ }
177
+ }
178
+ return Array.from(kinds).sort();
179
+ }
180
+ // Re-export so callers can inspect lease state without importing the scheduler.
181
+ export { readSchedulerLease };
182
+ //# sourceMappingURL=job-worker.js.map
@@ -1,4 +1,4 @@
1
- import type { AceContextTier } from "./ace-context.js";
1
+ import { type AceContextTier } from "./ace-context.js";
2
2
  import { type RuntimeModelClass } from "./runtime-profile.js";
3
3
  export interface LocalModelPolicyInput {
4
4
  provider: string;
@@ -19,9 +19,15 @@ export declare function resolveLocalModelClass(input: {
19
19
  provider: string;
20
20
  model: string;
21
21
  requested_model_class?: RuntimeModelClass;
22
+ /**
23
+ * Class derived from a connect-time capability measurement (E10). Consulted
24
+ * ahead of the name regex so classing rests on observed behavior/metadata,
25
+ * not brittle string patterns. See measured-classing.ts.
26
+ */
27
+ measured_model_class?: RuntimeModelClass;
22
28
  }): RuntimeModelClass;
23
29
  export declare function tierForModelClass(role: string, modelClass: RuntimeModelClass, requestedTier?: "auto" | AceContextTier): AceContextTier;
24
30
  export declare function defaultTurnsForPolicy(role: string, policy: LocalModelExecutionPolicy): number;
25
- export declare function defaultToolScopeForPolicy(role: string, policy: LocalModelExecutionPolicy): string[] | undefined;
31
+ export declare function defaultToolScopeForPolicy(role: string, policy: LocalModelExecutionPolicy): string[];
26
32
  export declare function resolveLocalModelExecutionPolicy(input: LocalModelPolicyInput): LocalModelExecutionPolicy;
27
33
  //# sourceMappingURL=local-model-policy.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { ROLE_TOOL_DEFAULTS } from "./ace-context.js";
1
2
  import { DEFAULT_SURGICAL_READ_BUDGETS, } from "./runtime-profile.js";
2
3
  const FRONTIER_MODEL_PATTERN = /\b(70b|72b|90b|405b|claude|gpt-|codex|gemini|o[1345])\b/i;
3
4
  const SMALL_LOCAL_MODEL_PATTERN = /\b(1\.5b|2b|3b|4b|5b|6b|7b|8b|mini|small|phi|tiny|qwen2\.5-coder|llama-3\.2-3b)\b/i;
@@ -14,6 +15,8 @@ function isMutationTask(task, role) {
14
15
  export function resolveLocalModelClass(input) {
15
16
  if (input.requested_model_class)
16
17
  return input.requested_model_class;
18
+ if (input.measured_model_class)
19
+ return input.measured_model_class;
17
20
  const provider = normalize(input.provider);
18
21
  const model = normalize(input.model);
19
22
  const joined = `${provider} ${model}`;
@@ -44,25 +47,36 @@ export function defaultTurnsForPolicy(role, policy) {
44
47
  return 6;
45
48
  return policy.model_class === "small_local" ? 4 : 4;
46
49
  }
50
+ const STRUCTURAL_TOOL_SCOPE = [
51
+ "outline_file",
52
+ "astgrep_query",
53
+ "astgrep_locate",
54
+ "read_file_lines",
55
+ "compile_structural_edit",
56
+ "preview_structural_edit",
57
+ "astgrep_rewrite",
58
+ "safe_edit_file",
59
+ "write_workspace_file",
60
+ "run_tests",
61
+ "git_diff",
62
+ "git_status",
63
+ "execute_gates",
64
+ ];
65
+ // Returns the default tool scope for a Hermes-bound run; never returns undefined or [].
66
+ // For small_local mutation coders/builder: structural-edit scope.
67
+ // For all other (role, policy) combinations: the role's default scope from ROLE_TOOL_DEFAULTS.
68
+ // For roles absent from the map: orchestrator defaults minus mutation tools.
47
69
  export function defaultToolScopeForPolicy(role, policy) {
48
- if (!policy.structural_tools_required || !(role === "coders" || role === "builder")) {
49
- return undefined;
70
+ if (policy.structural_tools_required && (role === "coders" || role === "builder")) {
71
+ return STRUCTURAL_TOOL_SCOPE;
50
72
  }
51
- return [
52
- "outline_file",
53
- "astgrep_query",
54
- "astgrep_locate",
55
- "read_file_lines",
56
- "compile_structural_edit",
57
- "preview_structural_edit",
58
- "astgrep_rewrite",
59
- "safe_edit_file",
60
- "write_workspace_file",
61
- "run_tests",
62
- "git_diff",
63
- "git_status",
64
- "execute_gates",
65
- ];
73
+ const normalized = role.trim().toLowerCase();
74
+ const roleDefaults = ROLE_TOOL_DEFAULTS[normalized];
75
+ if (roleDefaults && roleDefaults.length > 0) {
76
+ return roleDefaults;
77
+ }
78
+ // Unknown role: orchestrator defaults minus mutation tools
79
+ return (ROLE_TOOL_DEFAULTS["orchestrator"] ?? []).filter((t) => !["run_orchestrator", "enqueue_job", "dispatch_jobs"].includes(t));
66
80
  }
67
81
  export function resolveLocalModelExecutionPolicy(input) {
68
82
  const role = normalize(input.role);
@@ -31,6 +31,12 @@ export interface RunLocalModelTaskOptions {
31
31
  clients?: ModelBridgeClients;
32
32
  hermesExecutor?: HermesLocalExecutor;
33
33
  hermesLaunchProfile?: HermesLaunchProfile;
34
+ expectedArtifacts?: {
35
+ path: string;
36
+ required?: boolean;
37
+ }[];
38
+ requiredEvidenceRefs?: string[];
39
+ timeoutMs?: number;
34
40
  }
35
41
  export interface RunLocalModelTaskResult {
36
42
  runtime: LocalModelRuntimeConfig;