@voybio/ace-swarm 0.2.5 → 2.4.0

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 (115) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +20 -13
  3. package/assets/agent-state/EVIDENCE_LOG.md +1 -1
  4. package/assets/agent-state/MODULES/roles/capability-framework.json +41 -0
  5. package/assets/agent-state/MODULES/roles/capability-git.json +33 -0
  6. package/assets/agent-state/MODULES/roles/capability-safety.json +37 -0
  7. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +21 -0
  8. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +43 -0
  9. package/assets/agent-state/MODULES/schemas/WORKSPACE_SESSION_REGISTRY.schema.json +11 -0
  10. package/assets/agent-state/STATUS.md +2 -2
  11. package/assets/scripts/ace-hook-dispatch.mjs +70 -6
  12. package/assets/scripts/render-mcp-configs.sh +19 -5
  13. package/dist/ace-context.js +22 -1
  14. package/dist/ace-server-instructions.js +3 -3
  15. package/dist/ace-state-resolver.js +5 -3
  16. package/dist/astgrep-index.d.ts +9 -1
  17. package/dist/astgrep-index.js +14 -3
  18. package/dist/cli.js +27 -20
  19. package/dist/handoff-registry.js +5 -5
  20. package/dist/helpers/artifacts.d.ts +19 -0
  21. package/dist/helpers/artifacts.js +152 -0
  22. package/dist/helpers/bootstrap.d.ts +24 -0
  23. package/dist/helpers/bootstrap.js +894 -0
  24. package/dist/helpers/constants.d.ts +53 -0
  25. package/dist/helpers/constants.js +288 -0
  26. package/dist/helpers/drift.d.ts +13 -0
  27. package/dist/helpers/drift.js +45 -0
  28. package/dist/helpers/path-utils.d.ts +17 -0
  29. package/dist/helpers/path-utils.js +104 -0
  30. package/dist/helpers/store-resolution.d.ts +19 -0
  31. package/dist/helpers/store-resolution.js +301 -0
  32. package/dist/helpers/workspace-root.d.ts +3 -0
  33. package/dist/helpers/workspace-root.js +80 -0
  34. package/dist/helpers.d.ts +8 -125
  35. package/dist/helpers.js +8 -1768
  36. package/dist/job-scheduler.js +3 -3
  37. package/dist/local-model-runtime.js +12 -1
  38. package/dist/model-bridge.d.ts +7 -0
  39. package/dist/model-bridge.js +75 -5
  40. package/dist/orchestrator-supervisor.d.ts +14 -0
  41. package/dist/orchestrator-supervisor.js +72 -1
  42. package/dist/run-ledger.js +3 -3
  43. package/dist/runtime-command.d.ts +8 -0
  44. package/dist/runtime-command.js +38 -6
  45. package/dist/runtime-executor.d.ts +14 -0
  46. package/dist/runtime-executor.js +669 -171
  47. package/dist/runtime-profile.d.ts +32 -0
  48. package/dist/runtime-profile.js +89 -13
  49. package/dist/runtime-tool-specs.d.ts +21 -0
  50. package/dist/runtime-tool-specs.js +78 -3
  51. package/dist/safe-edit.d.ts +7 -0
  52. package/dist/safe-edit.js +163 -37
  53. package/dist/schemas.js +19 -0
  54. package/dist/shared.d.ts +2 -2
  55. package/dist/status-events.js +9 -6
  56. package/dist/store/ace-packed-store.d.ts +3 -2
  57. package/dist/store/ace-packed-store.js +188 -110
  58. package/dist/store/bootstrap-store.d.ts +1 -1
  59. package/dist/store/bootstrap-store.js +94 -81
  60. package/dist/store/cache-workspace.js +11 -5
  61. package/dist/store/materializers/context-snapshot-materializer.js +6 -2
  62. package/dist/store/materializers/hook-context-materializer.d.ts +6 -9
  63. package/dist/store/materializers/hook-context-materializer.js +11 -21
  64. package/dist/store/materializers/host-file-materializer.js +6 -0
  65. package/dist/store/materializers/projection-manager.d.ts +0 -1
  66. package/dist/store/materializers/projection-manager.js +5 -13
  67. package/dist/store/materializers/scheduler-projection-materializer.js +1 -1
  68. package/dist/store/materializers/vericify-projector.d.ts +7 -7
  69. package/dist/store/materializers/vericify-projector.js +11 -11
  70. package/dist/store/repositories/local-model-runtime-repository.d.ts +120 -3
  71. package/dist/store/repositories/local-model-runtime-repository.js +242 -6
  72. package/dist/store/skills-install.d.ts +4 -0
  73. package/dist/store/skills-install.js +21 -12
  74. package/dist/store/state-reader.d.ts +2 -0
  75. package/dist/store/state-reader.js +20 -0
  76. package/dist/store/store-artifacts.d.ts +7 -0
  77. package/dist/store/store-artifacts.js +27 -1
  78. package/dist/store/store-authority-audit.d.ts +18 -1
  79. package/dist/store/store-authority-audit.js +115 -5
  80. package/dist/store/store-snapshot.d.ts +3 -0
  81. package/dist/store/store-snapshot.js +22 -2
  82. package/dist/store/workspace-store-paths.d.ts +39 -0
  83. package/dist/store/workspace-store-paths.js +94 -0
  84. package/dist/store/write-coordinator.d.ts +65 -0
  85. package/dist/store/write-coordinator.js +386 -0
  86. package/dist/todo-state.js +5 -5
  87. package/dist/tools-agent.js +268 -14
  88. package/dist/tools-discovery.js +1 -1
  89. package/dist/tools-files.d.ts +7 -0
  90. package/dist/tools-files.js +299 -10
  91. package/dist/tools-framework.js +25 -5
  92. package/dist/tools-handoff.js +2 -2
  93. package/dist/tools-lifecycle.js +4 -4
  94. package/dist/tools-memory.js +6 -6
  95. package/dist/tools-todo.js +2 -2
  96. package/dist/tracker-adapters.d.ts +1 -1
  97. package/dist/tracker-adapters.js +13 -18
  98. package/dist/tracker-sync.js +5 -3
  99. package/dist/tui/agent-runner.js +3 -1
  100. package/dist/tui/chat.js +103 -7
  101. package/dist/tui/dashboard.d.ts +1 -0
  102. package/dist/tui/dashboard.js +43 -0
  103. package/dist/tui/layout.d.ts +20 -0
  104. package/dist/tui/layout.js +31 -1
  105. package/dist/tui/local-model-contract.d.ts +6 -2
  106. package/dist/tui/local-model-contract.js +16 -3
  107. package/dist/vericify-bridge.d.ts +5 -0
  108. package/dist/vericify-bridge.js +27 -3
  109. package/dist/workspace-manager.d.ts +30 -3
  110. package/dist/workspace-manager.js +257 -27
  111. package/package.json +1 -2
  112. package/dist/internal-tool-runtime.d.ts +0 -21
  113. package/dist/internal-tool-runtime.js +0 -136
  114. package/dist/store/workspace-snapshot.d.ts +0 -26
  115. package/dist/store/workspace-snapshot.js +0 -107
@@ -1,11 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
4
- import { dirname, resolve } from "node:path";
3
+ import { existsSync, readFileSync, } from "node:fs";
4
+ import { resolve } from "node:path";
5
5
  import { DEFAULTS_ROOT, resolveWorkspaceArtifactPath as resolveWorkspaceArtifactPathHelper, resolveWorkspaceRoot, } from "./helpers.js";
6
6
  import { validateTrackerSnapshotPayload } from "./schemas.js";
7
- import { appendStatusEvent } from "./status-events.js";
7
+ import { appendStatusEventSafe } from "./status-events.js";
8
8
  import { readRuntimeProfile } from "./runtime-profile.js";
9
+ import { safeWriteAsync } from "./helpers.js";
9
10
  export const TRACKER_SNAPSHOT_REL_PATH = "agent-state/tracker-snapshot.json";
10
11
  export const TRACKER_SNAPSHOT_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/TRACKER_SNAPSHOT.schema.json";
11
12
  export const TRACKER_SNAPSHOT_SCHEMA_NAME = "tracker-snapshot@1.0.0";
@@ -40,14 +41,6 @@ function resolveTrackerSnapshotTarget(explicitPath) {
40
41
  function resolveWorkspaceArtifactPath(filePath) {
41
42
  return resolveWorkspaceArtifactPathHelper(filePath, "write");
42
43
  }
43
- function safeWriteWorkspaceFile(filePath, content) {
44
- const abs = resolveWorkspaceArtifactPath(filePath);
45
- mkdirSync(dirname(abs), { recursive: true });
46
- const tmpPath = `${abs}.${process.pid}.${Date.now()}.tmp`;
47
- writeFileSync(tmpPath, content, "utf-8");
48
- renameSync(tmpPath, abs);
49
- return abs;
50
- }
51
44
  function normalizeTrackerItem(item) {
52
45
  return {
53
46
  ...item,
@@ -150,12 +143,14 @@ function filterTrackerItems(items, filter) {
150
143
  }
151
144
  function emitTrackerEvent(eventType, summary, payload) {
152
145
  try {
153
- appendStatusEvent({
146
+ void appendStatusEventSafe({
154
147
  source_module: "capability-framework",
155
148
  event_type: eventType,
156
149
  status: eventType === "TRACKER_SNAPSHOT_REFRESH_FAILED" ? "fail" : "done",
157
150
  summary,
158
151
  payload,
152
+ }).catch(() => {
153
+ // Tracker operations must not fail because event logging failed.
159
154
  });
160
155
  }
161
156
  catch {
@@ -455,7 +450,7 @@ async function githubCreateComment(itemId, body) {
455
450
  }),
456
451
  ];
457
452
  nextSnapshot.updated_at = now;
458
- writeTrackerSnapshot(nextSnapshot);
453
+ await writeTrackerSnapshot(nextSnapshot);
459
454
  emitTrackerEvent("TRACKER_COMMENT_CREATED", `Tracker comment created for ${issue.item_id}.`, {
460
455
  adapter_kind: "external",
461
456
  provider: "github",
@@ -502,7 +497,7 @@ async function githubUpdateItemState(itemId, state) {
502
497
  try {
503
498
  const issue = (await githubGetIssuesById([itemId]))[0];
504
499
  const snapshot = upsertTrackerItem(readOrBuildExternalSnapshot(), issue);
505
- writeTrackerSnapshot(snapshot);
500
+ await writeTrackerSnapshot(snapshot);
506
501
  emitTrackerEvent("TRACKER_ITEM_STATE_UPDATED", `Tracker item ${issue.item_id} moved to ${issue.state}.`, {
507
502
  adapter_kind: "external",
508
503
  provider: "github",
@@ -569,13 +564,13 @@ export function readTrackerSnapshot() {
569
564
  export function getTrackerSnapshotPath() {
570
565
  return resolveTrackerSnapshotTarget().path;
571
566
  }
572
- export function writeTrackerSnapshot(snapshot) {
567
+ export async function writeTrackerSnapshot(snapshot) {
573
568
  const normalized = normalizeTrackerSnapshot(snapshot);
574
569
  const validation = validateTrackerSnapshotPayload(normalized);
575
570
  if (!validation.ok) {
576
571
  throw new Error(`Tracker snapshot failed validation (${validation.schema}): ${validation.errors.join("; ")}`);
577
572
  }
578
- return safeWriteWorkspaceFile(TRACKER_SNAPSHOT_REL_PATH, JSON.stringify(normalized, null, 2));
573
+ return safeWriteAsync(TRACKER_SNAPSHOT_REL_PATH, JSON.stringify(normalized, null, 2));
579
574
  }
580
575
  const noneTrackerAdapter = {
581
576
  kind: "none",
@@ -700,7 +695,7 @@ const memoryTrackerAdapter = {
700
695
  updated_at: now,
701
696
  comments: [...snapshot.comments, comment],
702
697
  };
703
- writeTrackerSnapshot(nextSnapshot);
698
+ await writeTrackerSnapshot(nextSnapshot);
704
699
  emitTrackerEvent("TRACKER_COMMENT_CREATED", `Tracker comment created for ${comment.item_id}.`, {
705
700
  adapter_kind: "memory",
706
701
  item_id: comment.item_id,
@@ -743,7 +738,7 @@ const memoryTrackerAdapter = {
743
738
  updated_at: now,
744
739
  items,
745
740
  };
746
- writeTrackerSnapshot(nextSnapshot);
741
+ await writeTrackerSnapshot(nextSnapshot);
747
742
  emitTrackerEvent("TRACKER_ITEM_STATE_UPDATED", `Tracker item ${items[itemIndex].item_id} moved to ${nextState}.`, {
748
743
  adapter_kind: "memory",
749
744
  item_id: items[itemIndex].item_id,
@@ -2,15 +2,17 @@ import { resolve } from "node:path";
2
2
  import { resolveWorkspaceRoot } from "./helpers.js";
3
3
  import { readRuntimeProfile } from "./runtime-profile.js";
4
4
  import { buildDefaultTrackerSnapshot, getTrackerAdapter, writeTrackerSnapshot, readTrackerSnapshot, TRACKER_SNAPSHOT_REL_PATH, } from "./tracker-adapters.js";
5
- import { appendStatusEvent } from "./status-events.js";
5
+ import { appendStatusEventSafe } from "./status-events.js";
6
6
  function emitTrackerSyncEvent(eventType, summary, payload) {
7
7
  try {
8
- appendStatusEvent({
8
+ void appendStatusEventSafe({
9
9
  source_module: "capability-framework",
10
10
  event_type: eventType,
11
11
  status: eventType === "TRACKER_SNAPSHOT_REFRESH_FAILED" ? "fail" : "done",
12
12
  summary,
13
13
  payload,
14
+ }).catch(() => {
15
+ // Snapshot refresh must not fail because observability logging failed.
14
16
  });
15
17
  }
16
18
  catch {
@@ -52,7 +54,7 @@ export async function refreshTrackerSnapshot(kind) {
52
54
  items,
53
55
  };
54
56
  }
55
- const writtenPath = writeTrackerSnapshot(snapshot);
57
+ const writtenPath = await writeTrackerSnapshot(snapshot);
56
58
  emitTrackerSyncEvent("TRACKER_SNAPSHOT_REFRESHED", `Tracker snapshot refreshed using ${snapshot.adapter_kind}.`, {
57
59
  adapter_kind: snapshot.adapter_kind,
58
60
  snapshot_path: writtenPath,
@@ -17,7 +17,9 @@ import { readAgentInstructions } from "../helpers.js";
17
17
  export class AgentRunner extends EventEmitter {
18
18
  agents = new Map();
19
19
  workspaceRoot;
20
- startupTimeoutMs = 5000;
20
+ // Full-suite test runs can heavily contend CPU/IO while workers bootstrap.
21
+ // Keep startup resilient to transient contention before declaring failure.
22
+ startupTimeoutMs = 12000;
21
23
  constructor(workspaceRoot) {
22
24
  super();
23
25
  this.workspaceRoot = workspaceRoot;
package/dist/tui/chat.js CHANGED
@@ -380,7 +380,11 @@ export class ChatSession extends EventEmitter {
380
380
  return;
381
381
  const { updated_at: _updatedAt, ...record } = status;
382
382
  void withLocalModelRuntimeRepository(this.workspaceRoot, async (repo) => {
383
- await repo.upsertRuntimeStatus(record);
383
+ await repo.upsertRuntimeStatusWithTransition(record, {
384
+ from: this.currentRuntimeStatus?.bridge_status,
385
+ reason: "Interactive runtime status updated after provider/tool progress.",
386
+ evidence_refs: [this.sessionId],
387
+ });
384
388
  }).catch((error) => {
385
389
  this.emit("error", `Runtime status persistence failed: ${formatErrorMessage(error)}`);
386
390
  });
@@ -390,14 +394,26 @@ export class ChatSession extends EventEmitter {
390
394
  return;
391
395
  void withLocalModelRuntimeRepository(this.workspaceRoot, async (repo) => {
392
396
  if (input.activationLedger) {
393
- await repo.upsertActivationLedger(input.activationLedger);
397
+ await repo.upsertActivationLedgerWithTransition(input.activationLedger, {
398
+ from: this.activationLedger ? "updated" : "created",
399
+ reason: "Interactive activation ledger updated after ACE session progress.",
400
+ evidence_refs: [this.sessionId],
401
+ });
394
402
  }
395
403
  if (input.runtimeStatus) {
396
404
  const { updated_at: _updatedAt, ...statusRecord } = input.runtimeStatus;
397
- await repo.upsertRuntimeStatus(statusRecord);
405
+ await repo.upsertRuntimeStatusWithTransition(statusRecord, {
406
+ from: this.currentRuntimeStatus?.bridge_status,
407
+ reason: "Interactive runtime status updated after ACE session progress.",
408
+ evidence_refs: [this.sessionId],
409
+ });
398
410
  }
399
411
  if (input.continuity) {
400
- await repo.upsertContinuityRecord(input.continuity);
412
+ await repo.upsertContinuityRecordWithTransition(input.continuity, {
413
+ from: this.currentRuntimeStatus?.bridge_status,
414
+ reason: "Interactive continuity record updated after ACE session progress.",
415
+ evidence_refs: [this.sessionId],
416
+ });
401
417
  }
402
418
  }).catch((error) => {
403
419
  this.emit("error", `ACE runtime persistence failed: ${formatErrorMessage(error)}`);
@@ -416,15 +432,79 @@ export class ChatSession extends EventEmitter {
416
432
  preferredRole: this.aceRole,
417
433
  });
418
434
  const executionRole = preflight.recommended_role ?? this.aceRole;
435
+ const prevLedger = this.activationLedger;
419
436
  let activationLedger = nextActivationLedger(this.sessionId, this.activationLedger ?? undefined, preflight.recommended_next_action);
420
- const startupNudge = buildStartupNudge(preflight, activationLedger);
437
+ // Detect previously shown but unaccepted/unauto-executed nudges as ignored
438
+ if (prevLedger) {
439
+ const autoExecuted = prevLedger.auto_executed_nudges ?? [];
440
+ const accepted = prevLedger.accepted_nudges;
441
+ const alreadyIgnored = prevLedger.ignored_nudges ?? [];
442
+ const ignoredNow = prevLedger.shown_nudges.filter((id) => !accepted.includes(id) && !autoExecuted.includes(id) && !alreadyIgnored.includes(id));
443
+ if (ignoredNow.length > 0) {
444
+ activationLedger = {
445
+ ...activationLedger,
446
+ ignored_nudges: uniqueStrings([...(activationLedger.ignored_nudges ?? []), ...ignoredNow]),
447
+ };
448
+ if (this.workspaceRoot) {
449
+ const wsr = this.workspaceRoot;
450
+ for (const nudgeId of ignoredNow) {
451
+ void withLocalModelRuntimeRepository(wsr, async (repo) => {
452
+ await repo.appendTransitionRecord({
453
+ session_id: this.sessionId,
454
+ subject_kind: "nudge",
455
+ subject_id: nudgeId,
456
+ from: "shown",
457
+ to: "ignored",
458
+ reason: `Nudge ${nudgeId} ignored: user sent new message without accepting`,
459
+ evidence_refs: [],
460
+ });
461
+ }).catch(() => { });
462
+ }
463
+ }
464
+ }
465
+ }
466
+ const startupNudge = buildStartupNudge(preflight, activationLedger, this.currentRuntimeStatus);
421
467
  if (startupNudge) {
422
468
  activationLedger = {
423
469
  ...activationLedger,
424
470
  shown_nudges: uniqueStrings([...activationLedger.shown_nudges, startupNudge.id]),
471
+ ...(startupNudge.auto_executable
472
+ ? {
473
+ auto_executed_nudges: uniqueStrings([
474
+ ...(activationLedger.auto_executed_nudges ?? []),
475
+ startupNudge.id,
476
+ ]),
477
+ }
478
+ : {}),
425
479
  };
426
480
  this.pushSystemMessage(`Hint: ${startupNudge.text}`);
427
481
  this.emit("startup_nudge", startupNudge);
482
+ if (this.workspaceRoot) {
483
+ const wsr = this.workspaceRoot;
484
+ const nudge = startupNudge;
485
+ void withLocalModelRuntimeRepository(wsr, async (repo) => {
486
+ await repo.appendTransitionRecord({
487
+ session_id: this.sessionId,
488
+ subject_kind: "nudge",
489
+ subject_id: nudge.id,
490
+ from: "new",
491
+ to: "shown",
492
+ reason: `Startup nudge shown for action: ${nudge.recommended_action}`,
493
+ evidence_refs: [],
494
+ });
495
+ if (nudge.auto_executable) {
496
+ await repo.appendTransitionRecord({
497
+ session_id: this.sessionId,
498
+ subject_kind: "nudge",
499
+ subject_id: nudge.id,
500
+ from: "shown",
501
+ to: "auto_executed",
502
+ reason: `Auto-executed safe action: ${nudge.recommended_action}`,
503
+ evidence_refs: [],
504
+ });
505
+ }
506
+ }).catch(() => { });
507
+ }
428
508
  }
429
509
  let runtimeStatus = this.buildRuntimeStatusPacket({
430
510
  role: executionRole,
@@ -488,15 +568,31 @@ export class ChatSession extends EventEmitter {
488
568
  },
489
569
  onToolCall: (tool, args) => {
490
570
  toolNames.push(tool);
571
+ const nudgeToAccept = tool === startupNudge?.recommended_action ? startupNudge : undefined;
491
572
  activationLedger = {
492
573
  ...activationLedger,
493
574
  updated_at: Date.now(),
494
575
  activated_tools: uniqueStrings([...activationLedger.activated_tools, tool]),
495
576
  activated_roles: uniqueStrings([...activationLedger.activated_roles, executionRole]),
496
- accepted_nudges: tool === startupNudge?.recommended_action
497
- ? uniqueStrings([...activationLedger.accepted_nudges, startupNudge.id])
577
+ accepted_nudges: nudgeToAccept
578
+ ? uniqueStrings([...activationLedger.accepted_nudges, nudgeToAccept.id])
498
579
  : activationLedger.accepted_nudges,
499
580
  };
581
+ if (nudgeToAccept && this.workspaceRoot) {
582
+ const wsr = this.workspaceRoot;
583
+ const nudge = nudgeToAccept;
584
+ void withLocalModelRuntimeRepository(wsr, async (repo) => {
585
+ await repo.appendTransitionRecord({
586
+ session_id: this.sessionId,
587
+ subject_kind: "nudge",
588
+ subject_id: nudge.id,
589
+ from: "shown",
590
+ to: "manually_accepted",
591
+ reason: `Nudge accepted via tool call: ${tool}`,
592
+ evidence_refs: [],
593
+ });
594
+ }).catch(() => { });
595
+ }
500
596
  runtimeStatus = {
501
597
  ...runtimeStatus,
502
598
  bridge_status: "running",
@@ -18,6 +18,7 @@ export declare class DashboardState extends EventEmitter {
18
18
  private runtimeEvents;
19
19
  private phase;
20
20
  private latestRuntimeStatus;
21
+ private latestTransitions;
21
22
  constructor(workspaceRoot: string);
22
23
  startWatching(): void;
23
24
  stopWatching(): void;
@@ -7,6 +7,7 @@
7
7
  import { EventEmitter } from "node:events";
8
8
  import { pollStore } from "../store/state-reader.js";
9
9
  import { getWorkspaceStorePath } from "../store/store-snapshot.js";
10
+ import { getLivenessDefaults } from "../runtime-profile.js";
10
11
  function dedupeEvents(events) {
11
12
  const seen = new Set();
12
13
  const ordered = [];
@@ -35,6 +36,18 @@ export function selectLiveRuntimeStatus(statuses) {
35
36
  }
36
37
  function derivePhase(snapshot) {
37
38
  const runtimeStatus = selectLiveRuntimeStatus(snapshot.runtimeStatuses);
39
+ if (runtimeStatus?.sleep_state === "stalled") {
40
+ return "Stalled";
41
+ }
42
+ if (runtimeStatus?.sleep_state === "sleeping") {
43
+ return "Sleeping";
44
+ }
45
+ if (runtimeStatus?.sleep_state === "approval_pending") {
46
+ return "Approval Pending";
47
+ }
48
+ if (runtimeStatus?.sleep_state === "dependency_wait") {
49
+ return runtimeStatus.waiting_on ? `Waiting: ${runtimeStatus.waiting_on}` : "Waiting";
50
+ }
38
51
  if (runtimeStatus?.bridge_status === "blocked") {
39
52
  return "Blocked";
40
53
  }
@@ -48,6 +61,15 @@ function derivePhase(snapshot) {
48
61
  return "Retrying";
49
62
  }
50
63
  if (runtimeStatus?.bridge_status === "running") {
64
+ const kind = runtimeStatus.surface_kind;
65
+ if (kind === "unattended_runtime")
66
+ return "Running (Unattended)";
67
+ if (kind === "scheduled_job")
68
+ return "Running (Scheduled)";
69
+ if (kind === "bridge_resumed")
70
+ return "Running (Resumed)";
71
+ if (kind === "supervised_step")
72
+ return "Running (Supervised)";
51
73
  return "Running";
52
74
  }
53
75
  if (snapshot.jobs.some((job) => String(job.status ?? "") === "running")) {
@@ -73,6 +95,7 @@ export class DashboardState extends EventEmitter {
73
95
  runtimeEvents = [];
74
96
  phase = "Initializing";
75
97
  latestRuntimeStatus;
98
+ latestTransitions = [];
76
99
  constructor(workspaceRoot) {
77
100
  super();
78
101
  this.workspaceRoot = workspaceRoot;
@@ -108,6 +131,7 @@ export class DashboardState extends EventEmitter {
108
131
  runtimeStatus: this.latestRuntimeStatus,
109
132
  tasks: this.tasks,
110
133
  events: dedupeEvents([...this.persistedEvents, ...this.runtimeEvents]).slice(-50),
134
+ recentTransitions: this.latestTransitions,
111
135
  };
112
136
  }
113
137
  getLatestRuntimeStatus() {
@@ -141,6 +165,25 @@ export class DashboardState extends EventEmitter {
141
165
  this.persistedEvents = dedupeEvents([...trackerEvents, ...ledgerEvents]).slice(-300);
142
166
  this.phase = derivePhase(snapshot);
143
167
  this.latestRuntimeStatus = selectLiveRuntimeStatus(snapshot.runtimeStatuses);
168
+ // Compute read-only staleness projection — no watcher, computed on each snapshot
169
+ if (this.latestRuntimeStatus?.last_progress_at) {
170
+ const livenessDefaults = getLivenessDefaults(this.latestRuntimeStatus.model_class);
171
+ const effectiveStallWindowMs = this.latestRuntimeStatus.stall_window_ms ?? livenessDefaults.stall_window_ms;
172
+ const stalenessSec = (Date.now() - this.latestRuntimeStatus.last_progress_at) / 1000;
173
+ const stallWindowSec = effectiveStallWindowMs / 1000;
174
+ this.latestRuntimeStatus["staleness_seconds"] = stalenessSec;
175
+ this.latestRuntimeStatus["stall_window_seconds"] = stallWindowSec;
176
+ this.latestRuntimeStatus["is_stale"] = stalenessSec >= stallWindowSec;
177
+ }
178
+ this.latestTransitions = (snapshot.recentTransitions ?? []).map((t) => ({
179
+ subject_kind: t.subject_kind,
180
+ subject_id: t.subject_id,
181
+ from: t.from,
182
+ to: t.to,
183
+ reason: t.reason,
184
+ evidence_refs: t.evidence_refs,
185
+ at: t.at,
186
+ }));
144
187
  // Surface discovered providers to the TUI controller so dashboard controls
145
188
  // reflect what's actually available in the store.
146
189
  if (snapshot.providers.length > 0) {
@@ -60,11 +60,20 @@ interface RuntimeStatusSummary {
60
60
  bridge_status: string;
61
61
  preflight_state: string;
62
62
  role?: string;
63
+ model_class?: "frontier" | "mid" | "small_local";
63
64
  approval_state?: string;
64
65
  active_tool?: string;
65
66
  active_tool_role?: string;
66
67
  recommended_next_action?: string;
67
68
  blocked_reason?: string;
69
+ active_workspace_path?: string;
70
+ workspace_hook_health?: "ok" | "degraded" | "failed";
71
+ staleness_seconds?: number;
72
+ stall_window_seconds?: number;
73
+ is_stale?: boolean;
74
+ stall_restart_count?: number;
75
+ current_task?: string;
76
+ surface_kind?: string;
68
77
  }
69
78
  export declare class LayoutManager {
70
79
  private zones;
@@ -88,6 +97,7 @@ export declare class LayoutManager {
88
97
  renderStatusBar(state: StatusBarState): void;
89
98
  renderInputLine(buffer: string, cursorPos: number, mode: string, prompt?: string): void;
90
99
  renderDashboard(data: DashboardData, controls?: DashboardControlState): void;
100
+ private renderTransitionsPanel;
91
101
  private renderStatusPanel;
92
102
  private renderControlStrip;
93
103
  private renderTasksPanel;
@@ -119,11 +129,21 @@ export interface DashboardData {
119
129
  runtimeStatus?: RuntimeStatusSummary;
120
130
  tasks: TaskItem[];
121
131
  events: ActivityEvent[];
132
+ recentTransitions?: Array<{
133
+ subject_kind: string;
134
+ subject_id: string;
135
+ from?: string;
136
+ to: string;
137
+ reason: string;
138
+ evidence_refs?: string[];
139
+ at: number;
140
+ }>;
122
141
  }
123
142
  export interface TaskItem {
124
143
  title: string;
125
144
  done: boolean;
126
145
  priority?: string;
146
+ upstream_annotation?: string;
127
147
  }
128
148
  export interface ActivityEvent {
129
149
  timestamp: number;
@@ -250,6 +250,18 @@ export class LayoutManager {
250
250
  drawBox(content.x, actY, content.width, actH, { title: "Agent Activity", titleColor: fg.brightGreen, borderColor: fg.gray, rounded: true });
251
251
  this.renderActivityLog(content.x + 2, actY + 1, content.width - 4, actH - 2, data.events);
252
252
  }
253
+ renderTransitionsPanel(x, y, w, data) {
254
+ const transitions = data.recentTransitions?.slice(-3) ?? [];
255
+ if (transitions.length === 0)
256
+ return;
257
+ writeAt(x, y, `${fg.gray}─── Recent Transitions ───`);
258
+ for (let i = 0; i < transitions.length; i++) {
259
+ const t = transitions[i];
260
+ const age = Math.round((Date.now() - t.at) / 1000);
261
+ const line = `${fg.gray}${age}s ago ${fg.cyan}${t.subject_kind}${fg.gray}:${fg.white}${t.to}${fg.gray} — ${truncate(t.reason, 28)}`;
262
+ writeAt(x, y + 1 + i, truncate(line + style.reset, w));
263
+ }
264
+ }
253
265
  renderStatusPanel(x, y, w, h, data) {
254
266
  const lines = [
255
267
  `${fg.gray}Phase: ${fg.brightWhite}${data.phase}`,
@@ -266,12 +278,29 @@ export class LayoutManager {
266
278
  data.runtimeStatus?.role
267
279
  ? `${fg.gray}Role: ${fg.brightCyan}${data.runtimeStatus.role}`
268
280
  : undefined,
281
+ data.runtimeStatus?.model_class
282
+ ? `${fg.gray}Model: ${fg.brightCyan}${data.runtimeStatus.model_class}`
283
+ : undefined,
269
284
  data.runtimeStatus?.blocked_reason
270
285
  ? `${fg.gray}Blocker: ${fg.brightRed}${data.runtimeStatus.blocked_reason}`
271
286
  : undefined,
272
287
  data.runtimeStatus?.recommended_next_action
273
288
  ? `${fg.gray}Next: ${fg.white}${data.runtimeStatus.recommended_next_action}`
274
289
  : undefined,
290
+ data.runtimeStatus?.active_workspace_path
291
+ ? `${fg.gray}Wrkspace:${fg.cyan} ${truncate(data.runtimeStatus.active_workspace_path, 32)} ${fg.gray}[hook: ${data.runtimeStatus.workspace_hook_health ?? "?"}]`
292
+ : undefined,
293
+ data.runtimeStatus?.stall_restart_count
294
+ ? `${fg.gray}Stalls: ${fg.yellow}${data.runtimeStatus.stall_restart_count} restart(s)`
295
+ : undefined,
296
+ data.runtimeStatus?.is_stale
297
+ ? `${fg.brightRed}⚠ STALE ${fg.gray}${(data.runtimeStatus.staleness_seconds ?? 0).toFixed(0)}s since last progress (window: ${(data.runtimeStatus.stall_window_seconds ?? 0).toFixed(0)}s)`
298
+ : data.runtimeStatus?.staleness_seconds !== undefined
299
+ ? `${fg.gray}Progress:${fg.brightGreen} ${(data.runtimeStatus.staleness_seconds).toFixed(0)}s ago`
300
+ : undefined,
301
+ data.runtimeStatus?.current_task
302
+ ? `${fg.gray}Task: ${fg.white}${truncate(data.runtimeStatus.current_task, 40)}`
303
+ : undefined,
275
304
  `${fg.gray}Provider:${fg.brightCyan} ${padRight(data.provider, 8)}`,
276
305
  `${fg.gray}Model: ${fg.brightCyan}${data.model}`,
277
306
  `${fg.gray}Agents: ${fg.brightGreen}${data.activeAgents}${fg.gray}/${data.totalAgents} active`,
@@ -304,7 +333,8 @@ export class LayoutManager {
304
333
  const t = tasks[i];
305
334
  const icon = t.done ? `${fg.green}${symbols.check}` : `${fg.gray}${symbols.hollowBullet}`;
306
335
  const priority = t.priority ? `${fg.gray}[${t.priority}]` : "";
307
- const line = `${icon} ${fg.white}${t.title} ${priority}${style.reset}`;
336
+ const upstream = t.upstream_annotation ? ` ${fg.gray}${t.upstream_annotation}` : "";
337
+ const line = `${icon} ${fg.white}${t.title} ${priority}${upstream}${style.reset}`;
308
338
  writeAt(x, y + i, truncate(line, w));
309
339
  }
310
340
  if (tasks.length === 0) {
@@ -15,11 +15,14 @@ export interface AcePreflightPacket {
15
15
  recall_summary?: string;
16
16
  should_synthesize_plan: boolean;
17
17
  }
18
- export interface StartupNudge {
18
+ export interface AceStartupNudge {
19
19
  id: string;
20
20
  text: string;
21
21
  recommended_action: string;
22
+ auto_executable: boolean;
22
23
  }
24
+ /** @deprecated Use AceStartupNudge */
25
+ export type StartupNudge = AceStartupNudge;
23
26
  export declare function shouldSynthesizeShortPlan(task: string): boolean;
24
27
  export declare function buildAcePreflightPacket(input: {
25
28
  sessionId: string;
@@ -28,7 +31,8 @@ export declare function buildAcePreflightPacket(input: {
28
31
  preferredRole?: string;
29
32
  }): AcePreflightPacket;
30
33
  export declare function nextActivationLedger(sessionId: string, current: AceSessionActivationLedger | undefined, recommendedAction: string | undefined): AceSessionActivationLedger;
31
- export declare function buildStartupNudge(preflight: AcePreflightPacket, ledger: AceSessionActivationLedger): StartupNudge | undefined;
34
+ export declare function shouldAutoExecuteNudge(action: string, surfaceKind: AceRuntimeStatusPacket["surface_kind"] | undefined): boolean;
35
+ export declare function buildStartupNudge(preflight: AcePreflightPacket, ledger: AceSessionActivationLedger, statusPacket?: AceRuntimeStatusPacket | null): AceStartupNudge | undefined;
32
36
  export declare function buildBridgeTaskInput(conversation: string, preflight: AcePreflightPacket): string;
33
37
  export declare function mapBridgeResultToRuntimeStatus(input: {
34
38
  current: AceRuntimeStatusPacket;
@@ -122,10 +122,18 @@ export function nextActivationLedger(sessionId, current, recommendedAction) {
122
122
  accepted_nudges: [...(current?.accepted_nudges ?? [])],
123
123
  activated_tools: [...(current?.activated_tools ?? [])],
124
124
  activated_roles: [...(current?.activated_roles ?? [])],
125
- last_recommended_action: recommendedAction ?? current?.last_recommended_action,
125
+ auto_executed_nudges: current?.auto_executed_nudges ? [...current.auto_executed_nudges] : undefined,
126
+ ignored_nudges: current?.ignored_nudges ? [...current.ignored_nudges] : undefined,
126
127
  };
127
128
  }
128
- export function buildStartupNudge(preflight, ledger) {
129
+ export function shouldAutoExecuteNudge(action, surfaceKind) {
130
+ const SAFE_TRIO = new Set(["validate_framework", "recall_context", "run_orchestrator"]);
131
+ return (SAFE_TRIO.has(action) &&
132
+ (surfaceKind === "unattended_runtime" ||
133
+ surfaceKind === "scheduled_job" ||
134
+ surfaceKind === "supervised_step"));
135
+ }
136
+ export function buildStartupNudge(preflight, ledger, statusPacket) {
129
137
  const action = preflight.recommended_next_action;
130
138
  if (!action)
131
139
  return undefined;
@@ -140,7 +148,12 @@ export function buildStartupNudge(preflight, ledger) {
140
148
  : action === "route_task"
141
149
  ? "Role selection is still ambiguous. Route the task before deeper work."
142
150
  : "Load ACE context first with recall_context before deeper work.";
143
- return { id: nudgeId, text, recommended_action: action };
151
+ return {
152
+ id: nudgeId,
153
+ text,
154
+ recommended_action: action,
155
+ auto_executable: shouldAutoExecuteNudge(action, statusPacket?.surface_kind),
156
+ };
144
157
  }
145
158
  function compactTrapPack() {
146
159
  return [
@@ -71,6 +71,11 @@ export interface VericifyBridgeSnapshot {
71
71
  process_posts: VericifyBridgeSourceState;
72
72
  };
73
73
  active_run_refs: VericifyBridgeActiveRunRef[];
74
+ ace_runtime_enrichment?: {
75
+ live_session_id?: string;
76
+ last_turn_outcome?: string;
77
+ last_turn_outcome_reason?: string;
78
+ };
74
79
  }
75
80
  export interface VericifyBridgeSnapshotResult {
76
81
  ok: boolean;
@@ -9,7 +9,7 @@ import { ProjectionManager } from "./store/materializers/projection-manager.js";
9
9
  import { VericifyRepository } from "./store/repositories/vericify-repository.js";
10
10
  import { getWorkspaceStorePath, storeExistsSync } from "./store/store-snapshot.js";
11
11
  import { isOperationalArtifactPath, operationalArtifactVirtualPath } from "./store/store-artifacts.js";
12
- import { withStoreWriteQueue } from "./store/write-queue.js";
12
+ import { withStoreWriteCoordinator } from "./store/write-coordinator.js";
13
13
  export const VERICIFY_PROCESS_POST_LOG_REL_PATH = "agent-state/vericify/process-posts.json";
14
14
  export const VERICIFY_PROCESS_POST_LOG_SCHEMA_REL_PATH = "agent-state/MODULES/schemas/VERICIFY_PROCESS_POST_LOG.schema.json";
15
15
  export const VERICIFY_PROCESS_POST_LOG_SCHEMA_NAME = "vericify-process-post-log@1.0.0";
@@ -363,7 +363,7 @@ export async function appendVericifyProcessPost(input) {
363
363
  const path = await withFileLock(VERICIFY_PROCESS_POST_LOG_REL_PATH, async () => {
364
364
  const root = workspaceRoot();
365
365
  const storePath = getWorkspaceStorePath(root);
366
- return withStoreWriteQueue(storePath, async () => {
366
+ return withStoreWriteCoordinator(storePath, async () => {
367
367
  const store = await openStore(storePath);
368
368
  try {
369
369
  const repo = new VericifyRepository(store);
@@ -390,7 +390,7 @@ export async function appendVericifyProcessPost(input) {
390
390
  await store.close();
391
391
  }
392
392
  return processPostLogPath();
393
- });
393
+ }, { operation_label: "appendVericifyProcessPost" });
394
394
  });
395
395
  return {
396
396
  path,
@@ -544,6 +544,30 @@ export function buildVericifyBridgeSnapshot() {
544
544
  }),
545
545
  },
546
546
  active_run_refs: buildActiveRunRefs(),
547
+ ace_runtime_enrichment: (() => {
548
+ const rawSessions = safeRead(runtimeExecutorPath);
549
+ if (!rawSessions)
550
+ return undefined;
551
+ try {
552
+ const parsed = JSON.parse(rawSessions);
553
+ const runningSession = Array.isArray(parsed.sessions)
554
+ ? parsed.sessions.find((s) => s.status === "running")
555
+ : undefined;
556
+ if (!runningSession)
557
+ return undefined;
558
+ const lastTurn = Array.isArray(runningSession.turns) && runningSession.turns.length > 0
559
+ ? runningSession.turns[runningSession.turns.length - 1]
560
+ : undefined;
561
+ return {
562
+ live_session_id: runningSession.session_id,
563
+ last_turn_outcome: lastTurn?.turn_outcome,
564
+ last_turn_outcome_reason: lastTurn?.outcome_reason,
565
+ };
566
+ }
567
+ catch {
568
+ return undefined;
569
+ }
570
+ })(),
547
571
  };
548
572
  }
549
573
  export function refreshVericifyBridgeSnapshot() {