@saadnvd1/agent-os 0.3.715 → 0.3.728

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 (242) hide show
  1. package/README.md +2 -0
  2. package/app/api/agent-roles/route.test.ts +67 -0
  3. package/app/api/agent-roles/route.ts +61 -0
  4. package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
  5. package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
  6. package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
  7. package/app/api/analyst/[id]/review/route.ts +22 -0
  8. package/app/api/analyst/[id]/route.ts +44 -0
  9. package/app/api/analyst/route.ts +10 -0
  10. package/app/api/bus/spawn/route.ts +22 -1
  11. package/app/api/loops/route.ts +17 -0
  12. package/app/api/models/availability/route.ts +9 -0
  13. package/app/api/models/choices/route.ts +9 -0
  14. package/app/api/sessions/[id]/analyst/route.ts +11 -0
  15. package/app/api/sessions/[id]/crew/route.ts +38 -0
  16. package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
  17. package/app/api/sessions/[id]/export/route.test.ts +125 -0
  18. package/app/api/sessions/[id]/export/route.ts +34 -0
  19. package/app/api/sessions/[id]/model/route.ts +49 -0
  20. package/app/api/sessions/route.ts +16 -0
  21. package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
  22. package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
  23. package/app/api/tasks/[id]/loop/next/route.ts +25 -0
  24. package/app/api/tasks/[id]/loop/route.ts +72 -0
  25. package/app/api/tasks/[id]/loop/run/route.ts +12 -0
  26. package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
  27. package/app/api/tasks/[id]/mode/route.ts +29 -0
  28. package/app/api/tasks/[id]/report/draft/route.ts +54 -0
  29. package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
  30. package/app/api/tasks/[id]/report/route.ts +39 -0
  31. package/app/api/workspaces/[id]/mode/route.ts +23 -0
  32. package/app/api/workspaces/[id]/reports/route.ts +17 -0
  33. package/app/page.tsx +13 -0
  34. package/components/Analyst/PairChip.tsx +131 -0
  35. package/components/Analyst/PairSwitch.tsx +76 -0
  36. package/components/Analyst/ReviewCard.tsx +272 -0
  37. package/components/Analyst/SignalChips.tsx +39 -0
  38. package/components/Analyst/StartAnalystSheet.tsx +109 -0
  39. package/components/Analyst/cardState.test.ts +109 -0
  40. package/components/Analyst/cardState.ts +95 -0
  41. package/components/Analyst/pair.ts +81 -0
  42. package/components/Chat/Code.tsx +3 -0
  43. package/components/Chat/Messages.tsx +110 -11
  44. package/components/Chat/itemContext.ts +14 -0
  45. package/components/Draft/DraftChips.tsx +16 -1
  46. package/components/Draft/DraftPanel.tsx +58 -2
  47. package/components/Draft/ModelUnavailableDialog.tsx +78 -0
  48. package/components/Draft/RoleChips.tsx +110 -0
  49. package/components/Export/ExportActions.tsx +136 -0
  50. package/components/Export/ExportSheet.tsx +217 -0
  51. package/components/Export/RangePicker.tsx +144 -0
  52. package/components/Export/Seg.tsx +38 -0
  53. package/components/Export/TurnSelectBar.tsx +45 -0
  54. package/components/Models/AgentRoleCard.tsx +114 -0
  55. package/components/Models/AgentRolesSection.tsx +132 -0
  56. package/components/Models/ModelBadges.tsx +48 -0
  57. package/components/Models/ModelsSection.tsx +4 -2
  58. package/components/Models/RoleModelSelect.tsx +87 -0
  59. package/components/Pane/DesktopTabBar.tsx +8 -0
  60. package/components/Pane/MobileTabBar.tsx +29 -0
  61. package/components/Pane/MobileViewPicker.tsx +4 -0
  62. package/components/Pane/PaneMenu.test.ts +21 -0
  63. package/components/Pane/PaneMenu.tsx +26 -0
  64. package/components/Pane/PaneViewToggle.tsx +12 -0
  65. package/components/Pane/index.tsx +19 -0
  66. package/components/Report/DraftCard.tsx +89 -0
  67. package/components/Report/EstimateBar.tsx +51 -0
  68. package/components/Report/EvidenceSheet.tsx +98 -0
  69. package/components/Report/ModeChooser.tsx +43 -0
  70. package/components/Report/ReportView.tsx +289 -0
  71. package/components/SessionList/SessionRow.tsx +18 -7
  72. package/components/SessionList/SessionRowMenu.tsx +12 -0
  73. package/components/Sessions/RolePill.tsx +203 -0
  74. package/components/Settings/SettingsDialog.tsx +9 -0
  75. package/components/Share/ShareDialog.tsx +13 -0
  76. package/components/Tasks/Loop/LoopButton.tsx +74 -0
  77. package/components/Tasks/Loop/LoopSetup.tsx +146 -0
  78. package/components/Tasks/Loop/LoopView.tsx +350 -0
  79. package/components/Tasks/Loop/labels.ts +17 -0
  80. package/components/Tasks/TaskRow.tsx +2 -0
  81. package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
  82. package/components/Workspaces/WorkspaceSettings.tsx +2 -0
  83. package/components/ui/segmented.tsx +55 -0
  84. package/components/ui/status-pill.tsx +3 -0
  85. package/contexts/PaneContext.tsx +21 -1
  86. package/data/analyst/index.ts +3 -0
  87. package/data/analyst/keys.ts +6 -0
  88. package/data/analyst/mutations.ts +116 -0
  89. package/data/analyst/queries.ts +35 -0
  90. package/data/exports/index.ts +2 -0
  91. package/data/exports/keys.ts +3 -0
  92. package/data/exports/queries.ts +51 -0
  93. package/data/loops/index.ts +3 -0
  94. package/data/loops/keys.ts +4 -0
  95. package/data/loops/mutations.ts +68 -0
  96. package/data/loops/queries.ts +40 -0
  97. package/data/models/index.ts +98 -0
  98. package/data/push/topics.ts +11 -0
  99. package/data/reports/index.ts +115 -0
  100. package/data/sessions/queries.ts +26 -0
  101. package/lib/agents/brief.ts +1 -0
  102. package/lib/analyst/__fixtures__/items.ts +57 -0
  103. package/lib/analyst/__fixtures__/mocks.ts +49 -0
  104. package/lib/analyst/brief.ts +17 -0
  105. package/lib/analyst/buildDigest.ts +204 -0
  106. package/lib/analyst/createLink.ts +98 -0
  107. package/lib/analyst/endLink.ts +31 -0
  108. package/lib/analyst/forward.ts +109 -0
  109. package/lib/analyst/index.ts +16 -0
  110. package/lib/analyst/linkView.ts +38 -0
  111. package/lib/analyst/onTurnEnd.ts +119 -0
  112. package/lib/analyst/parseReview.ts +41 -0
  113. package/lib/analyst/pushDigest.ts +143 -0
  114. package/lib/analyst/resume.ts +35 -0
  115. package/lib/analyst/route.ts +50 -0
  116. package/lib/analyst/signals.ts +121 -0
  117. package/lib/analyst/store.ts +303 -0
  118. package/lib/analyst/types.ts +140 -0
  119. package/lib/bus/condense.ts +5 -1
  120. package/lib/bus/index.ts +5 -0
  121. package/lib/bus/read-scope.ts +12 -0
  122. package/lib/bus/role-alias.ts +70 -0
  123. package/lib/bus/targets.ts +3 -0
  124. package/lib/chat/drivers/opencode-mapper.ts +55 -1
  125. package/lib/chat/drivers/pi-approvals.ts +7 -4
  126. package/lib/chat/drivers/pi-mapper.ts +38 -0
  127. package/lib/chat/drivers/pi-text-tools.ts +104 -0
  128. package/lib/chat/drivers/pi.ts +8 -3
  129. package/lib/chat/drivers/turn-items.ts +36 -11
  130. package/lib/chat/drivers/turn-outcome.ts +108 -0
  131. package/lib/chat/events.ts +8 -0
  132. package/lib/chat/registry.ts +13 -0
  133. package/lib/chat/store.ts +19 -0
  134. package/lib/chat/turns.ts +1 -0
  135. package/lib/chat/worker/extras.ts +4 -2
  136. package/lib/claims/collectLocal.ts +3 -1
  137. package/lib/db/changes.ts +13 -0
  138. package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
  139. package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
  140. package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
  141. package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
  142. package/lib/db/session-parts/columns.ts +2 -0
  143. package/lib/db/types.ts +3 -0
  144. package/lib/done/archive.ts +2 -0
  145. package/lib/done/index.ts +2 -0
  146. package/lib/drafts.ts +6 -0
  147. package/lib/export/build.ts +470 -0
  148. package/lib/export/compact.ts +156 -0
  149. package/lib/export/estimate.ts +83 -0
  150. package/lib/export/fixtures.ts +45 -0
  151. package/lib/export/html.tsx +195 -0
  152. package/lib/export/json.ts +39 -0
  153. package/lib/export/load.ts +64 -0
  154. package/lib/export/markdown.ts +127 -0
  155. package/lib/export/request.ts +36 -0
  156. package/lib/export/service.ts +142 -0
  157. package/lib/export/summary.ts +86 -0
  158. package/lib/export/turns.ts +63 -0
  159. package/lib/export/view.ts +173 -0
  160. package/lib/loops/analyse.ts +46 -0
  161. package/lib/loops/analyst-prompt.ts +203 -0
  162. package/lib/loops/checkCriteria.ts +114 -0
  163. package/lib/loops/compactEvidence.ts +140 -0
  164. package/lib/loops/controls.ts +317 -0
  165. package/lib/loops/decideStop.ts +113 -0
  166. package/lib/loops/deps.ts +128 -0
  167. package/lib/loops/diffMap.ts +39 -0
  168. package/lib/loops/draftCriteria.ts +71 -0
  169. package/lib/loops/gatherEvidence.ts +265 -0
  170. package/lib/loops/hooks.ts +40 -0
  171. package/lib/loops/index.ts +7 -0
  172. package/lib/loops/route.ts +83 -0
  173. package/lib/loops/runCycle.ts +323 -0
  174. package/lib/loops/screenInstruction.ts +59 -0
  175. package/lib/loops/sendInstruction.ts +83 -0
  176. package/lib/loops/snapshot.ts +38 -0
  177. package/lib/loops/stopLoop.ts +86 -0
  178. package/lib/loops/store.ts +481 -0
  179. package/lib/loops/summary.ts +27 -0
  180. package/lib/loops/tickLoops.ts +141 -0
  181. package/lib/loops/types.ts +223 -0
  182. package/lib/loops/world.test-helpers.ts +223 -0
  183. package/lib/models/agent-binding.ts +102 -0
  184. package/lib/models/agent-roles-view.ts +157 -0
  185. package/lib/models/agent-roles.ts +87 -0
  186. package/lib/models/availability-refs.ts +17 -0
  187. package/lib/models/availability.ts +91 -0
  188. package/lib/models/catalog.ts +156 -0
  189. package/lib/models/config.ts +27 -0
  190. package/lib/models/locality.ts +33 -0
  191. package/lib/models/plan-step.ts +4 -1
  192. package/lib/models/providers.ts +11 -0
  193. package/lib/models/resolve.ts +50 -12
  194. package/lib/models/roles.ts +12 -0
  195. package/lib/needs-you.ts +1 -1
  196. package/lib/orchestrator/digest.ts +2 -1
  197. package/lib/orchestrator/facts.ts +7 -2
  198. package/lib/orchestrator/loop-guard.ts +5 -2
  199. package/lib/orchestrator/pr-wake.ts +4 -0
  200. package/lib/orchestrator/read.ts +5 -1
  201. package/lib/orchestrator/untrusted.ts +5 -0
  202. package/lib/orchestrator/watcher.ts +8 -1
  203. package/lib/panes.ts +52 -0
  204. package/lib/plans/acceptance.ts +23 -0
  205. package/lib/projects.ts +1 -0
  206. package/lib/reports/drafts.ts +87 -0
  207. package/lib/reports/end-hook.ts +17 -0
  208. package/lib/reports/estimate.ts +107 -0
  209. package/lib/reports/evidence/build.ts +193 -0
  210. package/lib/reports/evidence/gather.ts +229 -0
  211. package/lib/reports/fixtures.ts +17 -0
  212. package/lib/reports/generate.ts +159 -0
  213. package/lib/reports/hooks.ts +30 -0
  214. package/lib/reports/mode.ts +65 -0
  215. package/lib/reports/model.ts +115 -0
  216. package/lib/reports/read.ts +26 -0
  217. package/lib/reports/store.ts +180 -0
  218. package/lib/reports/template.ts +117 -0
  219. package/lib/reports/types.ts +135 -0
  220. package/lib/reports/validate.ts +142 -0
  221. package/lib/session-artifacts/agentos-files.ts +94 -0
  222. package/lib/session-artifacts/collect.ts +17 -0
  223. package/lib/sessions/crew.ts +72 -0
  224. package/lib/sessions/launch.ts +49 -9
  225. package/lib/settings-url.ts +1 -0
  226. package/lib/share/markdown.ts +2 -2
  227. package/lib/share/redact.ts +16 -10
  228. package/lib/share/render.tsx +1 -1
  229. package/lib/share/snapshot.ts +2 -2
  230. package/lib/sidebar/row-menu.ts +15 -1
  231. package/lib/tasks/contract/routes.ts +5 -0
  232. package/lib/tasks/contract/types.ts +3 -0
  233. package/lib/tasks/finish.ts +2 -0
  234. package/lib/tasks/index.ts +6 -0
  235. package/lib/tasks/move-carry.ts +1 -0
  236. package/lib/worktree-deps/caches.ts +3 -2
  237. package/lib/worktree-deps/clone.ts +3 -1
  238. package/lib/worktree-deps/paths.ts +26 -0
  239. package/package.json +2 -2
  240. package/server.ts +9 -1
  241. package/stores/analystUi.ts +19 -0
  242. package/stores/exportUi.ts +87 -0
@@ -76,9 +76,10 @@ const ERROR_LINE =
76
76
  /##\[error\]|\berror\b|\bfail(ed|ure|s)?\b|AssertionError|panic:/i;
77
77
  const RUNNER_EXIT = /Process completed with exit code/;
78
78
 
79
- // A CI failure: the job, then its failing tests, or its first error line.
79
+ // A failure's log (CI, or a local test run): the job or command, then its
80
+ // failing tests, or its first error line.
80
81
  // Null when there's nothing to tell one failure from another.
81
- export function ciSignature(
82
+ export function failureSignature(
82
83
  check: string | null | undefined,
83
84
  log: string | null
84
85
  ): string | null {
@@ -100,6 +101,8 @@ export function ciSignature(
100
101
  return error ? cap(`${check}: ${normalize(error)}`) : null;
101
102
  }
102
103
 
104
+ export const ciSignature = failureSignature;
105
+
103
106
  // A review's blocking findings, by where they are: the reviewer words the
104
107
  // same finding differently each time, but not its file and line. A finding
105
108
  // with no place is known by the start of its summary.
@@ -42,6 +42,7 @@ import { isPaused } from "./pause";
42
42
  import { repoOf } from "./repo";
43
43
  import { untrusted, UNTRUSTED_RULE } from "./untrusted";
44
44
  import { setPr } from "../db/session-parts";
45
+ import { liveLoopFor } from "../loops/store";
45
46
 
46
47
  export const COMMENT_WAKE_CAP = 10;
47
48
  // A first look at a PR this young takes its whole history as new.
@@ -170,6 +171,9 @@ export async function wakeTasks(
170
171
  for (const w of watched) {
171
172
  if (isPaused(workspaceId)) return;
172
173
  if (w.pr.checks !== "fail" || !w.pr.failing) continue;
174
+ // A running loop reads CI as part of its evidence and tells the coder
175
+ // in its next instruction: a task never has two steerers.
176
+ if (liveLoopFor(w.task.id)) continue;
173
177
  await wakeOnCheck(workspaceId, w, deps).catch((error) =>
174
178
  console.error(`Waking ${w.task.name} on CI:`, error)
175
179
  );
@@ -91,7 +91,11 @@ function itemText(i: ChatItem): string | null {
91
91
  case "error":
92
92
  return `[error] ${i.message}`;
93
93
  case "turn_end":
94
- return i.interrupted ? "[turn stopped]" : "[turn ended]";
94
+ return i.interrupted
95
+ ? "[turn stopped]"
96
+ : i.ended
97
+ ? `[turn ended early: ${i.ended.reason}] ${i.ended.notice}`
98
+ : "[turn ended]";
95
99
  case "command_output":
96
100
  return i.text;
97
101
  case "note":
@@ -31,6 +31,10 @@ export const looksRandom = (s: string) =>
31
31
  export const SECRET_ASSIGNMENT =
32
32
  /^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CREDENTIALS?|AUTH)[A-Z0-9_]*\s*[=:]\s*).+$/gm;
33
33
 
34
+ // The same assignment inline, ahead of a command: API_TOKEN=... npm test.
35
+ const INLINE_ASSIGNMENT =
36
+ /\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|CREDENTIALS?|AUTH)[A-Z0-9_]*=)(?!\[redacted\])("[^"]*"|'[^']*'|\S+)/g;
37
+
34
38
  // A URL's userinfo (https://key@host, postgres://user:pw@host) and a query
35
39
  // parameter that carries a credential (?token=, &api_key=).
36
40
  export const URL_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^@\s/]+@/gi;
@@ -47,6 +51,7 @@ const URL_PASSWORD = /\b([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)[^@\s]+@/gi;
47
51
  export function redact(text: string): string {
48
52
  let out = text
49
53
  .replace(SECRET_ASSIGNMENT, "$1[redacted]")
54
+ .replace(INLINE_ASSIGNMENT, "$1[redacted]")
50
55
  .replace(URL_PASSWORD, "$1[redacted]@");
51
56
  for (const p of TOKEN_PATTERNS) out = out.replace(p, "[redacted]");
52
57
  return out.replace(BASE64_RUN, (m) => (looksRandom(m) ? "[redacted]" : m));
@@ -26,6 +26,8 @@ import { followAskVerdicts } from "./ask-verdict";
26
26
  import { raiseHeldAsks } from "./held-asks";
27
27
  import { rebaseConflicting } from "./rebase";
28
28
  import { wakeTasks } from "./pr-wake";
29
+ import { tickLoops } from "../loops/tickLoops";
30
+ import { installReportHooks } from "../reports/hooks";
29
31
  import { awayTick } from "../away/run";
30
32
  import { healthTick } from "../health/tick";
31
33
  import { observeCi } from "../health/ci";
@@ -93,7 +95,8 @@ export async function diffWorkspace(
93
95
  return nextDiffAt(now, facts, working);
94
96
  }
95
97
 
96
- // Wakes tasks whose PRs failed CI or got reviews, rebases what conflicts
98
+ // Wakes tasks whose PRs failed CI or got reviews, advances developer-analyst
99
+ // loops, rebases what conflicts
97
100
  // with its base, then merges what's armed and passes. In the background, one run per workspace at a time, so a git
98
101
  // fetch or a merge never holds up event delivery.
99
102
  const piping = new Set<string>();
@@ -105,6 +108,9 @@ function mergePipeline(workspaceId: string, facts: SessionFacts[]): void {
105
108
  await wakeTasks(workspaceId, facts).catch((error) =>
106
109
  console.error(`Waking tasks in ${workspaceId}:`, error)
107
110
  );
111
+ await tickLoops(workspaceId, facts).catch((error) =>
112
+ console.error(`Ticking loops in ${workspaceId}:`, error)
113
+ );
108
114
  await followAskHeads(workspaceId, facts).catch((error) =>
109
115
  console.error(`Following ask heads in ${workspaceId}:`, error)
110
116
  );
@@ -189,6 +195,7 @@ async function tick(): Promise<void> {
189
195
  }
190
196
 
191
197
  export function startOrchestratorWatcher(): void {
198
+ installReportHooks();
192
199
  if (timer) return;
193
200
  timer = setInterval(() => void tick(), DELIVER_EVERY_MS);
194
201
  timer.unref();
package/lib/panes.ts CHANGED
@@ -208,6 +208,58 @@ export function closePane(state: PaneState, paneId: string): PaneState | null {
208
208
  };
209
209
  }
210
210
 
211
+ export interface OpenSession {
212
+ sessionId: string;
213
+ tmux: string;
214
+ hostId?: string | null;
215
+ }
216
+
217
+ // Shows a session beside a pane (an analyst beside its coder): where a pane
218
+ // already shows it, that one; else in a new pane to its right; with no room
219
+ // for another pane, in a new tab of this one.
220
+ export function openBeside(
221
+ state: PaneState,
222
+ paneId: string,
223
+ open: OpenSession
224
+ ): PaneState {
225
+ for (const id of getAllPaneIds(state.layout)) {
226
+ const pane = state.panes[id];
227
+ const tab = pane?.tabs.find((t) => t.sessionId === open.sessionId);
228
+ if (tab)
229
+ return {
230
+ ...state,
231
+ focusedPaneId: id,
232
+ panes: { ...state.panes, [id]: { ...pane, activeTabId: tab.id } },
233
+ };
234
+ }
235
+ const tab: TabData = {
236
+ ...createTab(),
237
+ sessionId: open.sessionId,
238
+ attachedTmux: open.tmux,
239
+ attachedHost: open.hostId ?? null,
240
+ };
241
+ const split = state.panes[paneId] && splitPane(state, paneId, "horizontal");
242
+ if (split)
243
+ return {
244
+ ...split,
245
+ panes: {
246
+ ...split.panes,
247
+ [split.focusedPaneId]: { tabs: [tab], activeTabId: tab.id },
248
+ },
249
+ };
250
+ const pane = state.panes[paneId] ?? state.panes[state.focusedPaneId];
251
+ if (!pane) return state;
252
+ const target = state.panes[paneId] ? paneId : state.focusedPaneId;
253
+ return {
254
+ ...state,
255
+ focusedPaneId: target,
256
+ panes: {
257
+ ...state.panes,
258
+ [target]: { tabs: [...pane.tabs, tab], activeTabId: tab.id },
259
+ },
260
+ };
261
+ }
262
+
211
263
  // Get all pane IDs from layout
212
264
  export function getAllPaneIds(layout: PaneLayout): string[] {
213
265
  if (layout.type === "leaf") {
@@ -0,0 +1,23 @@
1
+ // A plan's optional "## Acceptance" section: a checklist of observable
2
+ // outcomes, parsed like the "## Pieces" block.
3
+
4
+ const MAX_ITEMS = 12;
5
+ const MAX_LEN = 240;
6
+
7
+ export function parseAcceptance(plan: string | null | undefined): string[] {
8
+ if (!plan) return [];
9
+ const lines = plan.split("\n");
10
+ const start = lines.findIndex((l) => /^##\s+Acceptance\s*$/i.test(l.trim()));
11
+ if (start < 0) return [];
12
+ const items: string[] = [];
13
+ for (const line of lines.slice(start + 1)) {
14
+ if (/^#{1,6}\s/.test(line)) break;
15
+ const m = /^\s*[-*]\s+\[[ xX]\]\s+(.+?)\s*$/.exec(line);
16
+ if (!m) continue;
17
+ items.push(m[1].slice(0, MAX_LEN));
18
+ if (items.length >= MAX_ITEMS) break;
19
+ }
20
+ return items;
21
+ }
22
+
23
+ export const ACCEPTANCE_PROMPT = `Also end the plan with an \`## Acceptance\` section: 3 to 8 checklist lines (\`- [ ] ...\`), each one outcome a test, a CI check or the review could show is true. They become the progress report's completion estimate.`;
package/lib/projects.ts CHANGED
@@ -294,6 +294,7 @@ export function deleteProject(id: string): boolean {
294
294
 
295
295
  // Its merge settings override (lib/tasks/merge-policy.ts)
296
296
  db.prepare(`DELETE FROM settings WHERE key = ?`).run(`merge.project.${id}`);
297
+ db.prepare(`DELETE FROM settings WHERE key = ?`).run(`models.project.${id}`);
297
298
 
298
299
  // Delete project
299
300
  queries.deleteProject(db).run(id);
@@ -0,0 +1,87 @@
1
+ // The drafted next prompt: approve (one click), edit-and-approve, or dismiss.
2
+ // A task with an analyst pairing uses the analyst's own recommendation, so
3
+ // there is one draft system; a plain task's draft lives on its report row.
4
+
5
+ import { db, type Session } from "../db";
6
+ import { deliver } from "../bus";
7
+ import {
8
+ dismissRecommendation,
9
+ forwardToCoder,
10
+ sendRecommendation,
11
+ } from "../analyst/forward";
12
+ import { statusDetector } from "../status-detector";
13
+ import { claimDraft, getReport, reopenDraft, setDraftText } from "./store";
14
+
15
+ export class DraftError extends Error {
16
+ constructor(
17
+ message: string,
18
+ readonly status: 400 | 404 | 409 = 409
19
+ ) {
20
+ super(message);
21
+ }
22
+ }
23
+
24
+ export const draftItemId = (reportId: number) =>
25
+ `user-report-draft-${reportId}`;
26
+
27
+ export async function approveDraft(
28
+ sessionId: string,
29
+ input: {
30
+ source: "report" | "analyst";
31
+ id: string;
32
+ linkId?: string;
33
+ text?: string;
34
+ }
35
+ ): Promise<void> {
36
+ if (input.source === "analyst") {
37
+ if (!input.linkId) throw new DraftError("Missing pairing", 400);
38
+ await sendRecommendation(input.linkId, input.id, input.text);
39
+ return;
40
+ }
41
+ const reportId = Number(input.id);
42
+ const report = getReport(reportId);
43
+ if (!report || report.sessionId !== sessionId || !report.draft)
44
+ throw new DraftError("No such draft", 404);
45
+ const text = (input.text ?? report.draft.text).trim();
46
+ if (!text) throw new DraftError("There is no prompt to send", 400);
47
+ if (!claimDraft(reportId, "sent"))
48
+ throw new DraftError(`This draft was already ${report.draft.status}`);
49
+ if (input.text && text !== report.draft.text)
50
+ setDraftText(reportId, text, "sent");
51
+ try {
52
+ const session = db
53
+ .prepare(`SELECT * FROM session_rows WHERE id = ?`)
54
+ .get(sessionId) as Session | undefined;
55
+ if (!session) throw new DraftError("No such task", 404);
56
+ if (session.view === "chat")
57
+ await forwardToCoder(sessionId, {
58
+ text,
59
+ id: draftItemId(reportId),
60
+ label: "Analyst",
61
+ });
62
+ else {
63
+ await statusDetector.refreshCache();
64
+ const delivery = await deliver(session, text, { name: "Analyst" });
65
+ if (delivery.state === "failed") throw new Error(delivery.why);
66
+ }
67
+ } catch (error) {
68
+ reopenDraft(reportId);
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export function dismissDraft(
74
+ sessionId: string,
75
+ input: { source: "report" | "analyst"; id: string; linkId?: string }
76
+ ): void {
77
+ if (input.source === "analyst") {
78
+ if (!input.linkId) throw new DraftError("Missing pairing", 400);
79
+ dismissRecommendation(input.linkId, input.id);
80
+ return;
81
+ }
82
+ const report = getReport(Number(input.id));
83
+ if (!report || report.sessionId !== sessionId || !report.draft)
84
+ throw new DraftError("No such draft", 404);
85
+ if (!claimDraft(report.id, "dismissed"))
86
+ throw new DraftError(`This draft was already ${report.draft.status}`);
87
+ }
@@ -0,0 +1,17 @@
1
+ // The task-end trigger, decoupled from the code that ends tasks: finishing a
2
+ // task calls taskEnded(), and the report generator is plugged in at server
3
+ // start (hooks.ts), so ending a task never reaches for gh or a model on its own.
4
+
5
+ let onEnd: ((sessionId: string) => void) | null = null;
6
+
7
+ export const setTaskEndHook = (fn: ((sessionId: string) => void) | null) => {
8
+ onEnd = fn;
9
+ };
10
+
11
+ export function taskEnded(sessionId: string): void {
12
+ try {
13
+ onEnd?.(sessionId);
14
+ } catch (error) {
15
+ console.error(`Report at the end of ${sessionId}:`, error);
16
+ }
17
+ }
@@ -0,0 +1,107 @@
1
+ // The completion estimate. Computed here from evidence, never by a model:
2
+ // the model may only propose which evidence backs an acceptance item, and
3
+ // this decides whether the citation holds.
4
+
5
+ import type { Estimate, EstimateItem, EvidenceRef } from "./types";
6
+
7
+ export interface AcceptanceItem {
8
+ label: string;
9
+ // Verified by the system itself (a loop criterion met with resolved
10
+ // evidence), so no model mapping is needed.
11
+ evidence?: string[];
12
+ }
13
+
14
+ export interface Stages {
15
+ planned: boolean;
16
+ pushed: boolean;
17
+ prOpen: boolean;
18
+ ciGreen: boolean;
19
+ reviewed: boolean;
20
+ merged: boolean;
21
+ }
22
+
23
+ export const STAGE_COUNT = 6;
24
+
25
+ // Diff, piece and log evidence is true of the work, not of one head.
26
+ const HEAD_FREE = new Set(["diff", "piece", "log", "accept", "state"]);
27
+ // A diff alone never verifies: a file changed shows nothing works.
28
+ const PROVING = new Set(["test", "ci", "review", "piece", "accept"]);
29
+
30
+ export interface Mapping {
31
+ item: number;
32
+ evidence: string[];
33
+ }
34
+
35
+ function holds(
36
+ ids: string[],
37
+ evidence: Map<string, EvidenceRef>,
38
+ head: string | null
39
+ ): boolean {
40
+ if (!ids.length) return false;
41
+ const cited: EvidenceRef[] = [];
42
+ for (const id of ids) {
43
+ const ev = evidence.get(id);
44
+ if (!ev || ev.status !== "pass") return false;
45
+ if (!HEAD_FREE.has(ev.kind) && head && ev.sha && ev.sha !== head)
46
+ return false;
47
+ cited.push(ev);
48
+ }
49
+ if (!cited.some((e) => PROVING.has(e.kind))) return false;
50
+ const kinds = new Set(cited.map((e) => e.kind));
51
+ for (const e of evidence.values())
52
+ if (
53
+ e.status === "fail" &&
54
+ kinds.has(e.kind) &&
55
+ (HEAD_FREE.has(e.kind) || !head || !e.sha || e.sha === head)
56
+ )
57
+ return false;
58
+ return true;
59
+ }
60
+
61
+ export function computeEstimate(input: {
62
+ head: string | null;
63
+ evidence: EvidenceRef[];
64
+ items: AcceptanceItem[];
65
+ mappings: Mapping[];
66
+ stages: Stages;
67
+ }): Estimate {
68
+ const byId = new Map(input.evidence.map((e) => [e.id, e]));
69
+ if (input.items.length) {
70
+ const items: EstimateItem[] = input.items.map((it, i) => {
71
+ const own = it.evidence ?? [];
72
+ if (own.length && holds(own, byId, input.head))
73
+ return { label: it.label, verified: true, evidence: own };
74
+ const mapped = input.mappings
75
+ .filter((m) => m.item === i)
76
+ .flatMap((m) => m.evidence);
77
+ const ids = [...new Set(mapped)];
78
+ const ok = holds(ids, byId, input.head);
79
+ return { label: it.label, verified: ok, evidence: ok ? ids : [] };
80
+ });
81
+ return {
82
+ done: items.filter((i) => i.verified).length,
83
+ total: items.length,
84
+ basis: "acceptance",
85
+ items,
86
+ };
87
+ }
88
+ const s = input.stages;
89
+ const list: [string, boolean][] = [
90
+ ["Plan approved", s.planned],
91
+ ["Code pushed", s.pushed],
92
+ ["Pull request open", s.prOpen],
93
+ ["CI green on the latest push", s.ciGreen],
94
+ ["Review passed", s.reviewed],
95
+ ["Merged", s.merged],
96
+ ];
97
+ return {
98
+ done: list.filter(([, ok]) => ok).length,
99
+ total: STAGE_COUNT,
100
+ basis: "stages",
101
+ items: list.map(([label, verified]) => ({
102
+ label,
103
+ verified,
104
+ evidence: [],
105
+ })),
106
+ };
107
+ }
@@ -0,0 +1,193 @@
1
+ // Evidence items from what was read. Pure: the collectors in gather.ts do the
2
+ // reading, so these are tested with fixtures.
3
+
4
+ import type { TaskCheck, TaskReviewVerdict } from "../../tasks/loop-types";
5
+ import type { TaskState } from "../../tasks/state";
6
+ import type { DigestSignals } from "../../analyst/types";
7
+ import { latestChecks } from "../../analyst/signals";
8
+ import type { EvidenceRef } from "../types";
9
+
10
+ const EXCERPT = 1500;
11
+ const cap = (s: string, n = EXCERPT) =>
12
+ s.length > n ? `${s.slice(0, n)}…` : s;
13
+
14
+ export function ciEvidence(
15
+ checks: TaskCheck[],
16
+ head: string | null,
17
+ at: string
18
+ ): EvidenceRef[] {
19
+ return checks
20
+ .filter((c) => c.state !== "skipped" && c.state !== "neutral")
21
+ .slice(0, 30)
22
+ .map((c) => ({
23
+ id: `ci:${c.name}`,
24
+ kind: "ci",
25
+ label: `CI · ${c.name}`,
26
+ status:
27
+ c.state === "pass"
28
+ ? "pass"
29
+ : c.state === "pending"
30
+ ? "pending"
31
+ : "fail",
32
+ at: c.completedAt ?? at,
33
+ sha: head ?? undefined,
34
+ url: c.url ?? undefined,
35
+ excerpt: c.log
36
+ ? cap(`${c.log.step ?? ""}\n${c.log.tail}`.trim())
37
+ : undefined,
38
+ }));
39
+ }
40
+
41
+ export function reviewEvidence(
42
+ review: TaskReviewVerdict | null
43
+ ): EvidenceRef[] {
44
+ if (!review) return [];
45
+ const findings = review.findings
46
+ .map(
47
+ (f) =>
48
+ `[${f.severity}] ${f.file ?? ""}${f.line ? `:${f.line}` : ""} ${f.summary}`
49
+ )
50
+ .join("\n");
51
+ return [
52
+ {
53
+ id: `review:${review.sha.slice(0, 7)}`,
54
+ kind: "review",
55
+ label: "Independent review",
56
+ status:
57
+ review.status === "pass"
58
+ ? "pass"
59
+ : review.status === "running"
60
+ ? "pending"
61
+ : "fail",
62
+ at: review.at,
63
+ sha: review.sha,
64
+ excerpt: cap(`${review.summary}\n${findings}`.trim()),
65
+ },
66
+ ];
67
+ }
68
+
69
+ // The last run of each test, build, typecheck or lint command.
70
+ export function testEvidence(
71
+ signals: DigestSignals,
72
+ sessionId: string,
73
+ head: string | null,
74
+ at: string
75
+ ): EvidenceRef[] {
76
+ return latestChecks(signals)
77
+ .slice(0, 12)
78
+ .map((c, i) => ({
79
+ id: `test:${i + 1}`,
80
+ kind: "test",
81
+ label: c.command.slice(0, 120),
82
+ status: c.passed ? "pass" : "fail",
83
+ at,
84
+ sha: head ?? undefined,
85
+ href: `/?session=${sessionId}`,
86
+ }));
87
+ }
88
+
89
+ export function diffEvidence(
90
+ numstat: string,
91
+ sessionId: string,
92
+ at: string
93
+ ): EvidenceRef[] {
94
+ const out: EvidenceRef[] = [];
95
+ for (const line of numstat.split("\n")) {
96
+ const m = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
97
+ if (!m) continue;
98
+ out.push({
99
+ id: `diff:${m[3]}`,
100
+ kind: "diff",
101
+ label: `${m[3]} (+${m[1] === "-" ? 0 : m[1]} −${m[2] === "-" ? 0 : m[2]})`,
102
+ status: "info",
103
+ at,
104
+ href: `/?session=${sessionId}`,
105
+ });
106
+ if (out.length >= 40) break;
107
+ }
108
+ return out;
109
+ }
110
+
111
+ export function stateEvidence(
112
+ state: TaskState,
113
+ blocked: string | null,
114
+ at: string
115
+ ): EvidenceRef {
116
+ return {
117
+ id: "state:task",
118
+ kind: "state",
119
+ label: `Task state: ${state}`,
120
+ status: state === "checks-failing" || state === "blocked" ? "fail" : "info",
121
+ at,
122
+ excerpt: blocked ? cap(blocked) : undefined,
123
+ };
124
+ }
125
+
126
+ export function askEvidence(
127
+ asks: { id: number; title: string; detail: string; created_at: string }[]
128
+ ): EvidenceRef[] {
129
+ return asks.slice(0, 5).map((a) => ({
130
+ id: `ask:${a.id}`,
131
+ kind: "ask",
132
+ label: `Waiting on you: ${a.title}`,
133
+ status: "pending",
134
+ at: a.created_at,
135
+ excerpt: cap(a.detail),
136
+ }));
137
+ }
138
+
139
+ export function acceptEvidence(
140
+ items: { label: string; met: boolean; note?: string }[],
141
+ at: string
142
+ ): EvidenceRef[] {
143
+ return items.map((it, i) => ({
144
+ id: `accept:${i + 1}`,
145
+ kind: "accept",
146
+ label: it.label,
147
+ status: it.met ? "pass" : "pending",
148
+ at,
149
+ excerpt: it.note ? cap(it.note) : undefined,
150
+ }));
151
+ }
152
+
153
+ export function logEvidence(log: {
154
+ sessionId: string;
155
+ mergeCommit: string | null;
156
+ prNumber: number | null;
157
+ durationMs: number | null;
158
+ costUsd: number;
159
+ createdAt: string;
160
+ }): EvidenceRef {
161
+ const mins =
162
+ log.durationMs !== null
163
+ ? Math.max(1, Math.round(log.durationMs / 60000))
164
+ : null;
165
+ return {
166
+ id: "log:task",
167
+ kind: "log",
168
+ label: [
169
+ log.prNumber ? `Merged as pull request ${log.prNumber}` : "Task log",
170
+ mins ? `in ${mins} min` : "",
171
+ ]
172
+ .filter(Boolean)
173
+ .join(" "),
174
+ status: "pass",
175
+ at: log.createdAt,
176
+ excerpt: log.mergeCommit ? `Merge commit ${log.mergeCommit}` : undefined,
177
+ href: `/?session=${log.sessionId}`,
178
+ };
179
+ }
180
+
181
+ // FNV-1a over ids and statuses: times and excerpts don't change it.
182
+ export function evidenceHash(evidence: EvidenceRef[], head: string | null) {
183
+ let h = 2166136261;
184
+ const s = `${head ?? ""}|${evidence
185
+ .map((e) => `${e.id}:${e.status}`)
186
+ .sort()
187
+ .join(",")}`;
188
+ for (let i = 0; i < s.length; i++) {
189
+ h ^= s.charCodeAt(i);
190
+ h = Math.imul(h, 16777619);
191
+ }
192
+ return (h >>> 0).toString(16);
193
+ }