agent-dealer 0.2.0 → 1.0.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 (276) hide show
  1. package/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
  2. package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
  3. package/bundle/server/dist/adapters/agent-deck.js +38 -50
  4. package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
  5. package/bundle/server/dist/adapters/agent-health.js +230 -33
  6. package/bundle/server/dist/adapters/agent-health.test.js +237 -0
  7. package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
  8. package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
  9. package/bundle/server/dist/adapters/execution-authority.js +103 -0
  10. package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
  11. package/bundle/server/dist/adapters/external.js +0 -1
  12. package/bundle/server/dist/adapters/git-worktree.js +514 -0
  13. package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
  14. package/bundle/server/dist/adapters/github.js +170 -0
  15. package/bundle/server/dist/adapters/github.test.js +205 -0
  16. package/bundle/server/dist/adapters/linear-inbox.js +199 -30
  17. package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
  18. package/bundle/server/dist/adapters/linear-sync.js +3 -101
  19. package/bundle/server/dist/adapters/managed-repo.js +168 -0
  20. package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
  21. package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
  22. package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
  23. package/bundle/server/dist/adapters/reflect-authority.js +43 -0
  24. package/bundle/server/dist/cli-env.js +26 -0
  25. package/bundle/server/dist/cli-env.test.js +14 -1
  26. package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
  27. package/bundle/server/dist/coordinator/admission.js +330 -0
  28. package/bundle/server/dist/coordinator/admission.test.js +338 -0
  29. package/bundle/server/dist/coordinator/args.js +128 -0
  30. package/bundle/server/dist/coordinator/args.test.js +132 -0
  31. package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
  32. package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
  33. package/bundle/server/dist/coordinator/auto-merge.js +286 -0
  34. package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
  35. package/bundle/server/dist/coordinator/branch-progress.js +81 -0
  36. package/bundle/server/dist/coordinator/clock-jump.js +98 -0
  37. package/bundle/server/dist/coordinator/commands.js +1088 -0
  38. package/bundle/server/dist/coordinator/commands.test.js +554 -0
  39. package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
  40. package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
  41. package/bundle/server/dist/coordinator/dependencies.js +242 -0
  42. package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
  43. package/bundle/server/dist/coordinator/developer-effect.js +959 -0
  44. package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
  45. package/bundle/server/dist/coordinator/dispatcher.js +58 -0
  46. package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
  47. package/bundle/server/dist/coordinator/effect-registry.js +17 -0
  48. package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
  49. package/bundle/server/dist/coordinator/failure-reason.js +273 -0
  50. package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
  51. package/bundle/server/dist/coordinator/guidance.js +33 -0
  52. package/bundle/server/dist/coordinator/guidance.test.js +110 -0
  53. package/bundle/server/dist/coordinator/human-resolution.js +88 -0
  54. package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
  55. package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
  56. package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
  57. package/bundle/server/dist/coordinator/latest-failure.js +100 -0
  58. package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
  59. package/bundle/server/dist/coordinator/metrics.js +35 -0
  60. package/bundle/server/dist/coordinator/metrics.test.js +52 -0
  61. package/bundle/server/dist/coordinator/permissions.js +127 -0
  62. package/bundle/server/dist/coordinator/permissions.test.js +184 -0
  63. package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
  64. package/bundle/server/dist/coordinator/process-liveness.js +191 -0
  65. package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
  66. package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
  67. package/bundle/server/dist/coordinator/projection.js +147 -0
  68. package/bundle/server/dist/coordinator/projection.test.js +121 -0
  69. package/bundle/server/dist/coordinator/prompts.js +164 -0
  70. package/bundle/server/dist/coordinator/prompts.test.js +177 -0
  71. package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
  72. package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
  73. package/bundle/server/dist/coordinator/recovery.js +466 -0
  74. package/bundle/server/dist/coordinator/recovery.test.js +164 -0
  75. package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
  76. package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
  77. package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
  78. package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
  79. package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
  80. package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
  81. package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
  82. package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
  83. package/bundle/server/dist/coordinator/routing.js +160 -0
  84. package/bundle/server/dist/coordinator/routing.test.js +336 -0
  85. package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
  86. package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
  87. package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
  88. package/bundle/server/dist/coordinator/session-progress.js +351 -0
  89. package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
  90. package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
  91. package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
  92. package/bundle/server/dist/coordinator/spawn.js +49 -0
  93. package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
  94. package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
  95. package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
  96. package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
  97. package/bundle/server/dist/coordinator/usage.js +40 -0
  98. package/bundle/server/dist/coordinator/usage.test.js +69 -0
  99. package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
  100. package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
  101. package/bundle/server/dist/coordinator/worker-loop.js +363 -0
  102. package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
  103. package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
  104. package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
  105. package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
  106. package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
  107. package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
  108. package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
  109. package/bundle/server/dist/db/index.js +359 -7
  110. package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
  111. package/bundle/server/dist/db/migrate-to-issues.js +824 -0
  112. package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
  113. package/bundle/server/dist/db/schema.sql +311 -1
  114. package/bundle/server/dist/db/schema.test.js +66 -0
  115. package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
  116. package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
  117. package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
  118. package/bundle/server/dist/index.js +58 -5
  119. package/bundle/server/dist/paths.js +40 -4
  120. package/bundle/server/dist/paths.test.js +42 -0
  121. package/bundle/server/dist/pid-marker-claim-child.js +18 -0
  122. package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
  123. package/bundle/server/dist/pid-marker.js +141 -0
  124. package/bundle/server/dist/pid-marker.test.js +208 -0
  125. package/bundle/server/dist/queue/approve-deliver.js +128 -28
  126. package/bundle/server/dist/queue/reflect-schedule.js +25 -0
  127. package/bundle/server/dist/queue/result-qa.js +1 -1
  128. package/bundle/server/dist/queue/send-gate.test.js +172 -36
  129. package/bundle/server/dist/repository/agents.js +55 -18
  130. package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
  131. package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
  132. package/bundle/server/dist/repository/artifacts.js +47 -0
  133. package/bundle/server/dist/repository/artifacts.test.js +48 -0
  134. package/bundle/server/dist/repository/authority-attempts.js +189 -0
  135. package/bundle/server/dist/repository/findings.js +70 -0
  136. package/bundle/server/dist/repository/findings.test.js +68 -0
  137. package/bundle/server/dist/repository/human-actions.js +142 -0
  138. package/bundle/server/dist/repository/human-actions.test.js +123 -0
  139. package/bundle/server/dist/repository/intake-settings.js +5 -59
  140. package/bundle/server/dist/repository/issues.js +305 -0
  141. package/bundle/server/dist/repository/issues.test.js +69 -0
  142. package/bundle/server/dist/repository/outbound-drafts.js +28 -0
  143. package/bundle/server/dist/repository/queue-entries.js +162 -0
  144. package/bundle/server/dist/repository/review-publications.js +153 -0
  145. package/bundle/server/dist/repository/review-publications.test.js +78 -0
  146. package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
  147. package/bundle/server/dist/repository/runs.js +4 -211
  148. package/bundle/server/dist/repository/runtime-availability.js +54 -0
  149. package/bundle/server/dist/repository/usage-events.js +60 -0
  150. package/bundle/server/dist/repository/usage-events.test.js +56 -0
  151. package/bundle/server/dist/repository/work-items.js +305 -0
  152. package/bundle/server/dist/repository/work-items.test.js +142 -0
  153. package/bundle/server/dist/repository/worker-sessions.js +219 -0
  154. package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
  155. package/bundle/server/dist/repository/workflow-events.js +193 -0
  156. package/bundle/server/dist/repository/workflow-events.test.js +89 -0
  157. package/bundle/server/dist/resolve-tsx-bin.js +19 -0
  158. package/bundle/server/dist/routes/human-actions.js +68 -0
  159. package/bundle/server/dist/routes/human-actions.test.js +284 -0
  160. package/bundle/server/dist/routes/index.js +29 -496
  161. package/bundle/server/dist/routes/issues.js +240 -0
  162. package/bundle/server/dist/routes/issues.test.js +451 -0
  163. package/bundle/server/dist/routes/queue.js +41 -0
  164. package/bundle/server/dist/routes/queue.test.js +78 -0
  165. package/bundle/server/dist/runners/claude-args.js +18 -30
  166. package/bundle/server/dist/runners/claude.js +19 -163
  167. package/bundle/server/dist/runners/claude.test.js +15 -70
  168. package/bundle/server/dist/runners/codex-args.js +59 -0
  169. package/bundle/server/dist/runners/codex-args.test.js +72 -0
  170. package/bundle/server/dist/runners/codex-jsonl.js +150 -0
  171. package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
  172. package/bundle/server/dist/runners/codex-resume.test.js +19 -0
  173. package/bundle/server/dist/runners/codex.js +46 -0
  174. package/bundle/server/dist/runners/models-codex.test.js +14 -0
  175. package/bundle/server/dist/runners/models.js +53 -1
  176. package/bundle/server/dist/runners/persist.js +4 -1
  177. package/bundle/server/dist/runners/process-registry.js +11 -0
  178. package/bundle/server/dist/runners/prompts.js +13 -280
  179. package/bundle/server/dist/runners/prompts.test.js +22 -153
  180. package/bundle/server/dist/runners/qa.js +17 -1
  181. package/bundle/server/dist/runners/reflect.js +4 -16
  182. package/bundle/server/dist/runners/spawn-cli.js +197 -0
  183. package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
  184. package/bundle/server/dist/runners/stream-json.js +0 -141
  185. package/bundle/server/dist/runners/usage-cap.js +242 -0
  186. package/bundle/server/dist/runners/usage-cap.test.js +97 -0
  187. package/bundle/server/dist/server-liveness.js +37 -0
  188. package/bundle/server/dist/server-liveness.test.js +82 -0
  189. package/bundle/server/dist/static-ui.test.js +44 -0
  190. package/bundle/server/package.json +4 -2
  191. package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
  192. package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
  193. package/bundle/server/static-ui/index.html +2 -2
  194. package/bundle/shared/dist/agents.d.ts +262 -133
  195. package/bundle/shared/dist/agents.js +85 -19
  196. package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
  197. package/bundle/shared/dist/cursor-auth-health.js +36 -0
  198. package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
  199. package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
  200. package/bundle/shared/dist/findings.d.ts +46 -0
  201. package/bundle/shared/dist/findings.js +17 -0
  202. package/bundle/shared/dist/findings.test.d.ts +1 -0
  203. package/bundle/shared/dist/findings.test.js +20 -0
  204. package/bundle/shared/dist/github-repo.d.ts +28 -0
  205. package/bundle/shared/dist/github-repo.js +98 -0
  206. package/bundle/shared/dist/github-repo.test.d.ts +1 -0
  207. package/bundle/shared/dist/github-repo.test.js +32 -0
  208. package/bundle/shared/dist/human-actions.d.ts +66 -0
  209. package/bundle/shared/dist/human-actions.js +51 -0
  210. package/bundle/shared/dist/human-actions.test.d.ts +1 -0
  211. package/bundle/shared/dist/human-actions.test.js +57 -0
  212. package/bundle/shared/dist/index.d.ts +567 -757
  213. package/bundle/shared/dist/index.js +22 -36
  214. package/bundle/shared/dist/issues.d.ts +385 -0
  215. package/bundle/shared/dist/issues.js +169 -0
  216. package/bundle/shared/dist/issues.test.d.ts +1 -0
  217. package/bundle/shared/dist/issues.test.js +72 -0
  218. package/bundle/shared/dist/outbound-draft.d.ts +6 -0
  219. package/bundle/shared/dist/outbound-draft.js +4 -0
  220. package/bundle/shared/dist/plan-triage.d.ts +6 -6
  221. package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
  222. package/bundle/shared/dist/profile-snapshot.js +145 -0
  223. package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
  224. package/bundle/shared/dist/profile-snapshot.test.js +67 -0
  225. package/bundle/shared/dist/queue-entries.d.ts +89 -0
  226. package/bundle/shared/dist/queue-entries.js +19 -0
  227. package/bundle/shared/dist/result-qa.d.ts +2 -2
  228. package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
  229. package/bundle/shared/dist/runtime-auth-health.js +186 -0
  230. package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
  231. package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
  232. package/bundle/shared/dist/runtime-availability.d.ts +51 -0
  233. package/bundle/shared/dist/runtime-availability.js +13 -0
  234. package/bundle/shared/dist/runtime.d.ts +1 -1
  235. package/bundle/shared/dist/runtime.js +1 -1
  236. package/bundle/shared/dist/usage-events.d.ts +36 -0
  237. package/bundle/shared/dist/usage-events.js +13 -0
  238. package/bundle/shared/dist/worker-sessions.d.ts +140 -0
  239. package/bundle/shared/dist/worker-sessions.js +70 -0
  240. package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
  241. package/bundle/shared/dist/worker-sessions.test.js +39 -0
  242. package/bundle/shared/dist/workflow.d.ts +78 -0
  243. package/bundle/shared/dist/workflow.js +59 -0
  244. package/bundle/shared/dist/workflow.test.d.ts +1 -0
  245. package/bundle/shared/dist/workflow.test.js +61 -0
  246. package/bundle/shared/package.json +1 -1
  247. package/dist/action.d.ts +1 -0
  248. package/dist/action.js +50 -0
  249. package/dist/action.test.d.ts +1 -0
  250. package/dist/action.test.js +70 -0
  251. package/dist/agent.d.ts +1 -0
  252. package/dist/agent.js +17 -0
  253. package/dist/agent.test.d.ts +1 -0
  254. package/dist/agent.test.js +19 -0
  255. package/dist/cli-check.d.ts +7 -0
  256. package/dist/cli-check.js +40 -0
  257. package/dist/doctor.js +59 -1
  258. package/dist/http.d.ts +5 -0
  259. package/dist/http.js +19 -0
  260. package/dist/index.js +28 -0
  261. package/dist/issue.d.ts +35 -0
  262. package/dist/issue.js +143 -0
  263. package/dist/issue.test.d.ts +1 -0
  264. package/dist/issue.test.js +240 -0
  265. package/dist/lifecycle.contract.test.d.ts +1 -0
  266. package/dist/lifecycle.contract.test.js +135 -0
  267. package/dist/queue.d.ts +11 -0
  268. package/dist/queue.js +60 -0
  269. package/dist/queue.test.d.ts +1 -0
  270. package/dist/queue.test.js +46 -0
  271. package/dist/test-fetch-stub.d.ts +6 -0
  272. package/dist/test-fetch-stub.js +31 -0
  273. package/package.json +2 -1
  274. package/templates/prod.env.example +1 -1
  275. package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
  276. package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
@@ -0,0 +1,351 @@
1
+ // packages/server/src/coordinator/session-progress.ts
2
+ //
3
+ // NOT-109: low-volume mid-session milestones + live intent updates so Issue Detail answers
4
+ // "what happened last?" while a developer/reviewer is running. Timeline events are
5
+ // milestones only — never per-tool-call. During a long agent spawn, a log sampler may
6
+ // refresh `currentIntent` without appending events.
7
+ //
8
+ // NOT-120: deriveLiveProgressFromLog parses the session NDJSON tail into a concrete
9
+ // operator-facing line (assistant sentence and/or tool activity) for the live strip.
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { getIssue, transitionIssue } from "../repository/issues.js";
13
+ import { appendWorkflowEvent } from "../repository/workflow-events.js";
14
+ import { parseNdjson } from "../runners/stream-json.js";
15
+ const ROLE_LABEL = {
16
+ developer: "Developer",
17
+ reviewer: "Reviewer",
18
+ };
19
+ const LIVE_STATUSES = new Set(["developing", "reviewing", "repairing"]);
20
+ /** Max chars for the strip primary line (one readable line). */
21
+ export const LIVE_PROGRESS_MAX_CHARS = 120;
22
+ /** Ignore Cursor token-stream crumbs and other tiny fragments. */
23
+ const MIN_ASSISTANT_CHARS = 28;
24
+ const ACTIVITY_RULES = [
25
+ { match: /lens|bugbot|security-review/i, label: "Lens check" },
26
+ { match: /pytest|npm test|vitest|jest|node --test|flow:verify|typecheck/i, label: "running tests" },
27
+ { match: /git commit|gh pr|commit/i, label: "committing" },
28
+ { match: /write|stredit|applypatch|editnotebook|search_replace/i, label: "editing" },
29
+ { match: /shell|bash|terminal/i, label: "running commands" },
30
+ { match: /call_service_tool|get_issue|linear/i, label: "fetching ticket brief" },
31
+ { match: /bind_workspace|get_bound_deck|get_playbook/i, label: "deck bind" },
32
+ { match: /read|grep|glob|semanticsearch/i, label: "exploring code" },
33
+ ];
34
+ /** Short worktree path for timeline payloads — last two path segments when present. */
35
+ export function shortWorktreePath(worktreePath) {
36
+ if (!worktreePath)
37
+ return null;
38
+ const parts = worktreePath.split(/[/\\]/).filter(Boolean);
39
+ if (parts.length <= 2)
40
+ return worktreePath;
41
+ return parts.slice(-2).join("/");
42
+ }
43
+ export function workerSessionPayload(input) {
44
+ return {
45
+ runtime: input.runtime ?? null,
46
+ model: input.model ?? null,
47
+ sessionId: input.sessionId,
48
+ ...(input.worktreePath !== undefined
49
+ ? { worktreePath: shortWorktreePath(input.worktreePath) }
50
+ : {}),
51
+ };
52
+ }
53
+ /** Updates currentIntent in place (status self-loop) while a session is live. */
54
+ export function setLiveIntent(issueId, intent) {
55
+ const issue = getIssue(issueId);
56
+ if (!issue || !LIVE_STATUSES.has(issue.status))
57
+ return;
58
+ if (issue.currentIntent === intent)
59
+ return;
60
+ transitionIssue(issueId, issue.status, { currentIntent: intent });
61
+ }
62
+ /**
63
+ * Append a milestone timeline event and refresh the issue's primary status line.
64
+ * Callers must keep volume low — setup / checks / push, not every tool use.
65
+ */
66
+ export function emitSessionMilestone(input) {
67
+ appendWorkflowEvent({
68
+ issueId: input.issueId,
69
+ workflowInstanceId: input.workflowInstanceId,
70
+ workerSessionId: input.workerSessionId,
71
+ type: input.type,
72
+ actorType: input.role,
73
+ stage: input.stage,
74
+ round: input.round,
75
+ payload: input.payload,
76
+ });
77
+ setLiveIntent(input.issueId, input.intent);
78
+ }
79
+ function readLogTail(logPath, maxBytes = 64_000) {
80
+ if (!logPath || !fs.existsSync(logPath))
81
+ return null;
82
+ try {
83
+ const size = fs.statSync(logPath).size;
84
+ const start = Math.max(0, size - maxBytes);
85
+ const fd = fs.openSync(logPath, "r");
86
+ try {
87
+ const buf = Buffer.alloc(size - start);
88
+ fs.readSync(fd, buf, 0, buf.length, start);
89
+ return buf.toString("utf8");
90
+ }
91
+ finally {
92
+ fs.closeSync(fd);
93
+ }
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ }
99
+ function basenamePath(p) {
100
+ const base = path.basename(p.replace(/\\/g, "/"));
101
+ return base || p;
102
+ }
103
+ function truncateOneLine(text, maxChars = LIVE_PROGRESS_MAX_CHARS) {
104
+ const one = text.replace(/\s+/g, " ").trim();
105
+ if (one.length <= maxChars)
106
+ return one;
107
+ const cut = one.slice(0, Math.max(0, maxChars - 1)).trimEnd();
108
+ return `${cut}…`;
109
+ }
110
+ function assistantTextFromEvent(e) {
111
+ if (e.type !== "assistant")
112
+ return null;
113
+ const msg = e.message;
114
+ if (!msg?.content) {
115
+ if (typeof e.text === "string" && e.text.trim())
116
+ return e.text;
117
+ return null;
118
+ }
119
+ const text = msg.content
120
+ .filter((c) => c.type === "text" && typeof c.text === "string")
121
+ .map((c) => c.text ?? "")
122
+ .join("");
123
+ return text.length ? text : null;
124
+ }
125
+ /** Cursor nests tools as `{ readToolCall: { args } }` — no top-level name. */
126
+ function cursorToolEntry(e) {
127
+ if (e.type !== "tool_call")
128
+ return null;
129
+ const nested = e.tool_call;
130
+ if (!nested || typeof nested !== "object" || Array.isArray(nested))
131
+ return null;
132
+ for (const [key, value] of Object.entries(nested)) {
133
+ if (!key.endsWith("ToolCall") || !value || typeof value !== "object")
134
+ continue;
135
+ const args = value.args;
136
+ return { key, args: args && typeof args === "object" ? args : {} };
137
+ }
138
+ return null;
139
+ }
140
+ function toolNameFromEvent(e) {
141
+ const cursor = cursorToolEntry(e);
142
+ if (cursor) {
143
+ // readToolCall → Read, shellToolCall → Shell, mcpToolCall → Mcp
144
+ const stem = cursor.key.replace(/ToolCall$/, "");
145
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
146
+ }
147
+ if (e.type === "tool_call") {
148
+ const name = e.name ?? e.tool_name;
149
+ return typeof name === "string" ? name : null;
150
+ }
151
+ if (e.type === "assistant") {
152
+ const msg = e.message;
153
+ const tool = msg?.content?.find((c) => c.type === "tool_use" && c.name);
154
+ return tool?.name ?? null;
155
+ }
156
+ if (typeof e.tool_name === "string")
157
+ return e.tool_name;
158
+ if (typeof e.name === "string" && (e.type === "tool" || e.type === "function_call"))
159
+ return e.name;
160
+ return null;
161
+ }
162
+ function claudeToolInput(e) {
163
+ if (e.type === "assistant") {
164
+ const msg = e.message;
165
+ const tool = msg?.content?.find((c) => c.type === "tool_use");
166
+ if (tool?.input && typeof tool.input === "object")
167
+ return tool.input;
168
+ }
169
+ if (e.type === "tool_call" && e.input && typeof e.input === "object") {
170
+ return e.input;
171
+ }
172
+ return null;
173
+ }
174
+ function isTestCommand(command) {
175
+ return /pytest|npm test|npm run test|vitest|jest|node --test|flow:verify|typecheck|poc:integration/i.test(command);
176
+ }
177
+ /** Concrete, human-readable tool activity for the live strip. */
178
+ export function formatToolProgress(e) {
179
+ const cursor = cursorToolEntry(e);
180
+ if (cursor) {
181
+ const { key, args } = cursor;
182
+ if (key === "readToolCall" && typeof args.path === "string") {
183
+ return `Reading ${basenamePath(args.path)}`;
184
+ }
185
+ if (key === "writeToolCall" || key === "editToolCall" || key === "applyPatchToolCall") {
186
+ const p = (typeof args.path === "string" && args.path) ||
187
+ (typeof args.file_path === "string" && args.file_path) ||
188
+ (typeof args.target_notebook === "string" && args.target_notebook) ||
189
+ null;
190
+ return p ? `Editing ${basenamePath(p)}` : "Editing files";
191
+ }
192
+ if (key === "shellToolCall" && typeof args.command === "string") {
193
+ const cmd = args.command.replace(/\s+/g, " ").trim();
194
+ if (isTestCommand(cmd))
195
+ return truncateOneLine(`Running tests: ${cmd}`);
196
+ return truncateOneLine(`Running: ${cmd}`);
197
+ }
198
+ if (key === "grepToolCall") {
199
+ const pattern = typeof args.pattern === "string" ? args.pattern : null;
200
+ return pattern ? truncateOneLine(`Searching: ${pattern}`) : "Searching code";
201
+ }
202
+ if (key === "globToolCall") {
203
+ const glob = (typeof args.globPattern === "string" && args.globPattern) ||
204
+ (typeof args.glob_pattern === "string" && args.glob_pattern) ||
205
+ (typeof args.glob === "string" && args.glob) ||
206
+ null;
207
+ return glob ? truncateOneLine(`Finding files: ${glob}`) : "Finding files";
208
+ }
209
+ if (key === "mcpToolCall" || key === "getMcpToolsToolCall") {
210
+ const name = (typeof args.toolName === "string" && args.toolName) ||
211
+ (typeof args.name === "string" && args.name) ||
212
+ (typeof args.tool_name === "string" && args.tool_name) ||
213
+ null;
214
+ return name ? truncateOneLine(`MCP: ${name}`) : "Calling MCP tool";
215
+ }
216
+ if (key === "updateTodosToolCall")
217
+ return "Updating todos";
218
+ const stem = key.replace(/ToolCall$/, "");
219
+ return `Using ${stem.charAt(0).toUpperCase()}${stem.slice(1)}`;
220
+ }
221
+ const name = toolNameFromEvent(e);
222
+ if (!name)
223
+ return null;
224
+ const input = claudeToolInput(e) ?? {};
225
+ const haystack = `${name} ${JSON.stringify(input).slice(0, 500)}`;
226
+ if (/^(Read|read_file)$/i.test(name) && typeof input.path === "string") {
227
+ return `Reading ${basenamePath(input.path)}`;
228
+ }
229
+ if (/^(Write|StrReplace|Edit|ApplyPatch|search_replace|EditNotebook)$/i.test(name)) {
230
+ const p = (typeof input.path === "string" && input.path) ||
231
+ (typeof input.file_path === "string" && input.file_path) ||
232
+ null;
233
+ return p ? `Editing ${basenamePath(p)}` : "Editing files";
234
+ }
235
+ if (/^(Shell|Bash|Terminal)$/i.test(name) && typeof input.command === "string") {
236
+ const cmd = input.command.replace(/\s+/g, " ").trim();
237
+ if (isTestCommand(cmd))
238
+ return truncateOneLine(`Running tests: ${cmd}`);
239
+ return truncateOneLine(`Running: ${cmd}`);
240
+ }
241
+ if (/^(Grep|rg)$/i.test(name) && typeof input.pattern === "string") {
242
+ return truncateOneLine(`Searching: ${input.pattern}`);
243
+ }
244
+ for (const rule of ACTIVITY_RULES) {
245
+ if (rule.match.test(haystack)) {
246
+ // Title-case the coarse label for the strip.
247
+ const label = rule.label.charAt(0).toUpperCase() + rule.label.slice(1);
248
+ return label;
249
+ }
250
+ }
251
+ return `Using ${name}`;
252
+ }
253
+ /**
254
+ * Derive a short operator-facing activity line from the latest tool-ish log events.
255
+ * Returns null when the log has nothing actionable yet.
256
+ */
257
+ export function deriveActivityFromLog(logPath) {
258
+ const progress = deriveLiveProgressFromLog(logPath);
259
+ if (!progress)
260
+ return null;
261
+ // Keep sampler labels short when mapping into currentIntent.
262
+ return progress;
263
+ }
264
+ /**
265
+ * Parse the session log tail into a concrete live-progress line for the issue strip.
266
+ * Prefers a meaningful coalesced assistant sentence when present after the latest tool;
267
+ * otherwise the latest tool activity. Ignores tiny token-stream fragments.
268
+ */
269
+ export function deriveLiveProgressFromLog(logPath, maxChars = LIVE_PROGRESS_MAX_CHARS) {
270
+ const raw = readLogTail(logPath);
271
+ if (!raw)
272
+ return null;
273
+ const events = parseNdjson(raw);
274
+ if (!events.length)
275
+ return null;
276
+ let lastTool = null;
277
+ let lastToolIsStarted = false;
278
+ const trailingAssistant = [];
279
+ let collectingAssistant = true;
280
+ for (let i = events.length - 1; i >= 0; i--) {
281
+ const e = events[i];
282
+ const assistant = assistantTextFromEvent(e);
283
+ if (assistant !== null) {
284
+ if (collectingAssistant)
285
+ trailingAssistant.unshift(assistant);
286
+ continue;
287
+ }
288
+ // Stop coalescing assistant tokens once we hit a non-assistant event.
289
+ collectingAssistant = false;
290
+ if (e.type === "tool_call" || toolNameFromEvent(e)) {
291
+ // Prefer "started" over "completed" for "what is happening now".
292
+ const subtype = typeof e.subtype === "string" ? e.subtype : "";
293
+ if (subtype === "completed" && lastTool)
294
+ continue;
295
+ const label = formatToolProgress(e);
296
+ if (!label)
297
+ continue;
298
+ if (!lastTool || (subtype === "started" && !lastToolIsStarted)) {
299
+ lastTool = label;
300
+ lastToolIsStarted = subtype === "started" || subtype === "";
301
+ }
302
+ // Keep scanning for a started call if we only have completed so far.
303
+ if (lastToolIsStarted)
304
+ break;
305
+ continue;
306
+ }
307
+ if (lastTool || trailingAssistant.length)
308
+ break;
309
+ }
310
+ const coalesced = trailingAssistant.join("").replace(/\s+/g, " ").trim();
311
+ if (coalesced.length >= MIN_ASSISTANT_CHARS) {
312
+ return truncateOneLine(coalesced, maxChars);
313
+ }
314
+ if (lastTool)
315
+ return truncateOneLine(lastTool, maxChars);
316
+ // No trailing assistant / recent tool — scan the tail for any earlier tool activity.
317
+ for (let i = events.length - 1; i >= 0; i--) {
318
+ const label = formatToolProgress(events[i]);
319
+ if (label)
320
+ return truncateOneLine(label, maxChars);
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * While an agent CLI is running, periodically refresh currentIntent from the session log
326
+ * without flooding the timeline. No-ops when the log has no new activity label.
327
+ */
328
+ export function startActivitySampler(opts) {
329
+ const prefix = `${ROLE_LABEL[opts.role]} ·`;
330
+ let lastLabel = null;
331
+ const tick = () => {
332
+ const activity = deriveLiveProgressFromLog(opts.logPath);
333
+ if (!activity || activity === lastLabel)
334
+ return;
335
+ lastLabel = activity;
336
+ setLiveIntent(opts.issueId, `${prefix} ${activity} (round ${opts.round})`);
337
+ };
338
+ const handle = setInterval(tick, opts.intervalMs ?? 10_000);
339
+ // First sample soon so the strip moves off "session running" once tools appear.
340
+ const first = setTimeout(tick, 2_000);
341
+ return {
342
+ stop() {
343
+ clearInterval(handle);
344
+ clearTimeout(first);
345
+ },
346
+ };
347
+ }
348
+ /** Task/AC are "complete enough" that the agent need not fetch Linear for the brief. */
349
+ export function taskBriefIsComplete(task) {
350
+ return Boolean(task.description?.trim() && task.acceptanceCriteria?.trim());
351
+ }
@@ -0,0 +1,190 @@
1
+ // packages/server/src/coordinator/session-progress.test.ts
2
+ import { test, before } from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-progress-"));
8
+ const { migrate } = await import("../db/index.js");
9
+ const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
10
+ const { createIssue, getIssue, transitionIssue } = await import("../repository/issues.js");
11
+ const { startWorkflowInstance, listWorkflowEventsForIssue } = await import("../repository/workflow-events.js");
12
+ const { createWorkerSession, startSession } = await import("../repository/worker-sessions.js");
13
+ const { deriveActivityFromLog, deriveLiveProgressFromLog, emitSessionMilestone, shortWorktreePath, taskBriefIsComplete, workerSessionPayload } = await import("./session-progress.js");
14
+ function writeLog(lines) {
15
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-progress-log-"));
16
+ const logPath = path.join(dir, "session.ndjson");
17
+ fs.writeFileSync(logPath, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
18
+ return logPath;
19
+ }
20
+ before(() => migrate());
21
+ test("shortWorktreePath keeps the last two segments", () => {
22
+ assert.equal(shortWorktreePath("/Users/me/.agent-dealer/worktrees/abc-developer"), "worktrees/abc-developer");
23
+ assert.equal(shortWorktreePath(null), null);
24
+ });
25
+ test("taskBriefIsComplete requires both description and acceptance criteria", () => {
26
+ assert.equal(taskBriefIsComplete({ description: "do it", acceptanceCriteria: "done" }), true);
27
+ assert.equal(taskBriefIsComplete({ description: "do it", acceptanceCriteria: " " }), false);
28
+ assert.equal(taskBriefIsComplete({ description: "", acceptanceCriteria: "done" }), false);
29
+ });
30
+ test("workerSessionPayload includes runtime/model/session id", () => {
31
+ assert.deepEqual(workerSessionPayload({
32
+ runtime: "cursor_local",
33
+ model: "composer",
34
+ sessionId: "11111111-1111-1111-1111-111111111111",
35
+ worktreePath: "/tmp/wt/issue-1"
36
+ }), {
37
+ runtime: "cursor_local",
38
+ model: "composer",
39
+ sessionId: "11111111-1111-1111-1111-111111111111",
40
+ worktreePath: "wt/issue-1"
41
+ });
42
+ });
43
+ test("deriveActivityFromLog maps recent tool names to operator labels", () => {
44
+ const logPath = writeLog([
45
+ { type: "assistant", message: { content: [{ type: "tool_use", name: "Read" }] } },
46
+ { type: "assistant", message: { content: [{ type: "tool_use", name: "Shell" }] } },
47
+ {
48
+ type: "assistant",
49
+ message: { content: [{ type: "tool_use", name: "Shell", input: { command: "npm test" } }] }
50
+ },
51
+ ]);
52
+ // Last event is a Shell tool with npm test → concrete test progress
53
+ assert.match(deriveActivityFromLog(logPath), /Running tests:/i);
54
+ });
55
+ test("deriveLiveProgressFromLog returns null for missing or empty logs", () => {
56
+ assert.equal(deriveLiveProgressFromLog("/no/such/session.ndjson"), null);
57
+ assert.equal(deriveLiveProgressFromLog(writeLog([{ type: "system", subtype: "init" }])), null);
58
+ });
59
+ test("deriveLiveProgressFromLog surfaces Cursor nested tool_call with file path", () => {
60
+ const logPath = writeLog([
61
+ {
62
+ type: "tool_call",
63
+ subtype: "started",
64
+ call_id: "c1",
65
+ tool_call: { readToolCall: { args: { path: "apps/web/src/pages/IssueDetailPage.tsx" } } }
66
+ },
67
+ ]);
68
+ assert.equal(deriveLiveProgressFromLog(logPath), "Reading IssueDetailPage.tsx");
69
+ });
70
+ test("deriveLiveProgressFromLog surfaces Cursor shell / test commands", () => {
71
+ const logPath = writeLog([
72
+ {
73
+ type: "tool_call",
74
+ subtype: "started",
75
+ call_id: "c2",
76
+ tool_call: {
77
+ shellToolCall: {
78
+ args: { command: "npm test -- packages/server/src/coordinator/failure-reason.test.ts" }
79
+ }
80
+ }
81
+ },
82
+ ]);
83
+ const progress = deriveLiveProgressFromLog(logPath);
84
+ assert.ok(progress);
85
+ assert.match(progress, /Running tests:/i);
86
+ assert.match(progress, /failure-reason/);
87
+ });
88
+ test("deriveLiveProgressFromLog prefers recent tool over tiny assistant token fragments", () => {
89
+ const logPath = writeLog([
90
+ {
91
+ type: "tool_call",
92
+ subtype: "started",
93
+ call_id: "c1",
94
+ tool_call: { grepToolCall: { args: { pattern: "liveProgress" } } }
95
+ },
96
+ { type: "assistant", message: { content: [{ type: "text", text: "I'll" }] } },
97
+ { type: "assistant", message: { content: [{ type: "text", text: " fix" }] } },
98
+ ]);
99
+ assert.equal(deriveLiveProgressFromLog(logPath), "Searching: liveProgress");
100
+ });
101
+ test("deriveLiveProgressFromLog coalesces a meaningful assistant sentence", () => {
102
+ const logPath = writeLog([
103
+ { type: "assistant", message: { content: [{ type: "text", text: "Addressing Lens FIX findings" }] } },
104
+ { type: "assistant", message: { content: [{ type: "text", text: " and rechecking the flaky test." }] } },
105
+ ]);
106
+ assert.equal(deriveLiveProgressFromLog(logPath), "Addressing Lens FIX findings and rechecking the flaky test.");
107
+ });
108
+ test("deriveLiveProgressFromLog truncates long progress to one readable line", () => {
109
+ const long = "Implementing the live progress strip by parsing the session NDJSON tail for assistant " +
110
+ "sentences and tool arguments so operators can see concrete work without opening the log file.";
111
+ const logPath = writeLog([
112
+ { type: "assistant", message: { content: [{ type: "text", text: long }] } },
113
+ ]);
114
+ const progress = deriveLiveProgressFromLog(logPath);
115
+ assert.ok(progress);
116
+ assert.ok(progress.length <= 120, `expected <=120 chars, got ${progress.length}`);
117
+ assert.ok(!progress.includes("\n"));
118
+ assert.match(progress, /\u2026$|…$/);
119
+ });
120
+ test("deriveLiveProgressFromLog surfaces Cursor editToolCall path", () => {
121
+ const logPath = writeLog([
122
+ {
123
+ type: "tool_call",
124
+ subtype: "started",
125
+ tool_call: {
126
+ editToolCall: { args: { path: "apps/web/src/pages/IssueDetailPage.tsx", streamContent: "…" } }
127
+ }
128
+ },
129
+ ]);
130
+ assert.equal(deriveLiveProgressFromLog(logPath), "Editing IssueDetailPage.tsx");
131
+ });
132
+ test("deriveLiveProgressFromLog surfaces Cursor globPattern", () => {
133
+ const logPath = writeLog([
134
+ {
135
+ type: "tool_call",
136
+ subtype: "started",
137
+ tool_call: { globToolCall: { args: { globPattern: "apps/web/src/pages/*.tsx" } } }
138
+ },
139
+ ]);
140
+ assert.equal(deriveLiveProgressFromLog(logPath), "Finding files: apps/web/src/pages/*.tsx");
141
+ });
142
+ test("deriveLiveProgressFromLog reads the committed Cursor fixture", async () => {
143
+ const { fileURLToPath } = await import("node:url");
144
+ const fixture = path.join(path.dirname(fileURLToPath(import.meta.url)), "../runners/fixtures/cursor-success-with-infra-attempt-limit-text.ndjson");
145
+ const progress = deriveLiveProgressFromLog(fixture);
146
+ // Fixture ends with result; last concrete signal is the read tool or the assistant sentence.
147
+ assert.ok(progress);
148
+ assert.match(progress, /routing\.ts|infra-attempt|Looking at routing/i);
149
+ });
150
+ test("emitSessionMilestone appends a role-attributed event and refreshes currentIntent", () => {
151
+ const issue = createIssue({
152
+ title: "Progress",
153
+ description: "Ship visibility",
154
+ acceptanceCriteria: "Operators see last progress",
155
+ repo: "acme/app",
156
+ developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
157
+ reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
158
+ baseBranch: "main",
159
+ source: "manual"
160
+ });
161
+ const instance = startWorkflowInstance(issue.id, "dev_reviewer_v1");
162
+ transitionIssue(issue.id, "developing", {
163
+ currentOwner: "developer",
164
+ currentIntent: "Developer implementing round 1"
165
+ });
166
+ const session = createWorkerSession({
167
+ issueId: issue.id,
168
+ role: "developer",
169
+ round: 1,
170
+ agentId: BUILTIN_AGENT_CLAUDE_ID,
171
+ runtime: "cursor_local"
172
+ });
173
+ startSession(session.id);
174
+ emitSessionMilestone({
175
+ issueId: issue.id,
176
+ workflowInstanceId: instance.id,
177
+ workerSessionId: session.id,
178
+ role: "developer",
179
+ stage: "developing",
180
+ round: 1,
181
+ type: "worktree.ready",
182
+ intent: "Developer · worktree ready (round 1)",
183
+ payload: { worktreePath: "wt/x" }
184
+ });
185
+ const events = listWorkflowEventsForIssue(issue.id);
186
+ const milestone = events.find((e) => e.type === "worktree.ready");
187
+ assert.ok(milestone);
188
+ assert.equal(milestone.actorType, "developer");
189
+ assert.equal(getIssue(issue.id).currentIntent, "Developer · worktree ready (round 1)");
190
+ });
@@ -0,0 +1,33 @@
1
+ const num = (name, dflt) => Number(process.env[name] ?? dflt);
2
+ export function developerSessionTimeoutMs() {
3
+ return num("DEVELOPER_TIMEOUT_MS", 60 * 60_000);
4
+ }
5
+ export function reviewerSessionTimeoutMs() {
6
+ return num("REVIEWER_TIMEOUT_MS", 30 * 60_000);
7
+ }
8
+ export function sessionTimeoutMsFor(role) {
9
+ return role === "reviewer" ? reviewerSessionTimeoutMs() : developerSessionTimeoutMs();
10
+ }
11
+ /**
12
+ * The longest recovery will honour an "alive" verdict before reclaiming anyway (NOT-131).
13
+ *
14
+ * Making the pid verdict portable across a restart fixes the wrong reclaim, but it opens
15
+ * the opposite failure: the `spawnCli` timeout that bounds a session is a `setTimeout` in
16
+ * the process that spawned it, and that process is exactly the one that died. A successor
17
+ * that honours "alive" unconditionally will extend a hung CLI's lease on every tick,
18
+ * forever, and the work item never resolves — the permanent stall `process-liveness.ts`
19
+ * warns about, arriving through the front door instead. So the hold is bounded: past this,
20
+ * the CLI is killed and the item reclaimed whatever the pid says.
21
+ *
22
+ * Measured from `worker_sessions.started_at`, which is stamped *before* worktree setup and
23
+ * the deck bind, so it precedes the actual spawn by an unbounded amount. Hence the 2x
24
+ * default rather than the bare timeout: a slow clone must never make this fire on a healthy
25
+ * session. The guarantee is a hard bound on the stall, not a precise re-enforcement of the
26
+ * CLI's own deadline.
27
+ */
28
+ export function maxAliveHoldMsFor(role) {
29
+ const raw = process.env.COORDINATOR_MAX_ALIVE_HOLD_MS;
30
+ if (raw !== undefined && raw !== "")
31
+ return Number(raw);
32
+ return sessionTimeoutMsFor(role) * 2;
33
+ }