@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.
- package/README.md +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/spawn/route.ts +22 -1
- package/app/api/loops/route.ts +17 -0
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/route.ts +16 -0
- package/app/api/tasks/[id]/loop/cycles/[n]/log/route.ts +29 -0
- package/app/api/tasks/[id]/loop/draft-criteria/route.ts +17 -0
- package/app/api/tasks/[id]/loop/next/route.ts +25 -0
- package/app/api/tasks/[id]/loop/route.ts +72 -0
- package/app/api/tasks/[id]/loop/run/route.ts +12 -0
- package/app/api/tasks/[id]/loop/stop/route.ts +12 -0
- package/app/api/tasks/[id]/mode/route.ts +29 -0
- package/app/api/tasks/[id]/report/draft/route.ts +54 -0
- package/app/api/tasks/[id]/report/evidence/[evId]/route.ts +18 -0
- package/app/api/tasks/[id]/report/route.ts +39 -0
- package/app/api/workspaces/[id]/mode/route.ts +23 -0
- package/app/api/workspaces/[id]/reports/route.ts +17 -0
- package/app/page.tsx +13 -0
- package/components/Analyst/PairChip.tsx +131 -0
- package/components/Analyst/PairSwitch.tsx +76 -0
- package/components/Analyst/ReviewCard.tsx +272 -0
- package/components/Analyst/SignalChips.tsx +39 -0
- package/components/Analyst/StartAnalystSheet.tsx +109 -0
- package/components/Analyst/cardState.test.ts +109 -0
- package/components/Analyst/cardState.ts +95 -0
- package/components/Analyst/pair.ts +81 -0
- package/components/Chat/Code.tsx +3 -0
- package/components/Chat/Messages.tsx +110 -11
- package/components/Chat/itemContext.ts +14 -0
- package/components/Draft/DraftChips.tsx +16 -1
- package/components/Draft/DraftPanel.tsx +58 -2
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +8 -0
- package/components/Pane/MobileTabBar.tsx +29 -0
- package/components/Pane/MobileViewPicker.tsx +4 -0
- package/components/Pane/PaneMenu.test.ts +21 -0
- package/components/Pane/PaneMenu.tsx +26 -0
- package/components/Pane/PaneViewToggle.tsx +12 -0
- package/components/Pane/index.tsx +19 -0
- package/components/Report/DraftCard.tsx +89 -0
- package/components/Report/EstimateBar.tsx +51 -0
- package/components/Report/EvidenceSheet.tsx +98 -0
- package/components/Report/ModeChooser.tsx +43 -0
- package/components/Report/ReportView.tsx +289 -0
- package/components/SessionList/SessionRow.tsx +18 -7
- package/components/SessionList/SessionRowMenu.tsx +12 -0
- package/components/Sessions/RolePill.tsx +203 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/Tasks/Loop/LoopButton.tsx +74 -0
- package/components/Tasks/Loop/LoopSetup.tsx +146 -0
- package/components/Tasks/Loop/LoopView.tsx +350 -0
- package/components/Tasks/Loop/labels.ts +17 -0
- package/components/Tasks/TaskRow.tsx +2 -0
- package/components/Workspaces/WorkspaceModeForm.tsx +22 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/components/ui/segmented.tsx +55 -0
- package/components/ui/status-pill.tsx +3 -0
- package/contexts/PaneContext.tsx +21 -1
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/loops/index.ts +3 -0
- package/data/loops/keys.ts +4 -0
- package/data/loops/mutations.ts +68 -0
- package/data/loops/queries.ts +40 -0
- package/data/models/index.ts +98 -0
- package/data/push/topics.ts +11 -0
- package/data/reports/index.ts +115 -0
- package/data/sessions/queries.ts +26 -0
- package/lib/agents/brief.ts +1 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +5 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +3 -1
- package/lib/db/changes.ts +13 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
- package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
- package/lib/db/session-parts/columns.ts +2 -0
- package/lib/db/types.ts +3 -0
- package/lib/done/archive.ts +2 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +6 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/loops/analyse.ts +46 -0
- package/lib/loops/analyst-prompt.ts +203 -0
- package/lib/loops/checkCriteria.ts +114 -0
- package/lib/loops/compactEvidence.ts +140 -0
- package/lib/loops/controls.ts +317 -0
- package/lib/loops/decideStop.ts +113 -0
- package/lib/loops/deps.ts +128 -0
- package/lib/loops/diffMap.ts +39 -0
- package/lib/loops/draftCriteria.ts +71 -0
- package/lib/loops/gatherEvidence.ts +265 -0
- package/lib/loops/hooks.ts +40 -0
- package/lib/loops/index.ts +7 -0
- package/lib/loops/route.ts +83 -0
- package/lib/loops/runCycle.ts +323 -0
- package/lib/loops/screenInstruction.ts +59 -0
- package/lib/loops/sendInstruction.ts +83 -0
- package/lib/loops/snapshot.ts +38 -0
- package/lib/loops/stopLoop.ts +86 -0
- package/lib/loops/store.ts +481 -0
- package/lib/loops/summary.ts +27 -0
- package/lib/loops/tickLoops.ts +141 -0
- package/lib/loops/types.ts +223 -0
- package/lib/loops/world.test-helpers.ts +223 -0
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/plan-step.ts +4 -1
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -1
- package/lib/orchestrator/facts.ts +7 -2
- package/lib/orchestrator/loop-guard.ts +5 -2
- package/lib/orchestrator/pr-wake.ts +4 -0
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +8 -1
- package/lib/panes.ts +52 -0
- package/lib/plans/acceptance.ts +23 -0
- package/lib/projects.ts +1 -0
- package/lib/reports/drafts.ts +87 -0
- package/lib/reports/end-hook.ts +17 -0
- package/lib/reports/estimate.ts +107 -0
- package/lib/reports/evidence/build.ts +193 -0
- package/lib/reports/evidence/gather.ts +229 -0
- package/lib/reports/fixtures.ts +17 -0
- package/lib/reports/generate.ts +159 -0
- package/lib/reports/hooks.ts +30 -0
- package/lib/reports/mode.ts +65 -0
- package/lib/reports/model.ts +115 -0
- package/lib/reports/read.ts +26 -0
- package/lib/reports/store.ts +180 -0
- package/lib/reports/template.ts +117 -0
- package/lib/reports/types.ts +135 -0
- package/lib/reports/validate.ts +142 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +49 -9
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/row-menu.ts +15 -1
- package/lib/tasks/contract/routes.ts +5 -0
- package/lib/tasks/contract/types.ts +3 -0
- package/lib/tasks/finish.ts +2 -0
- package/lib/tasks/index.ts +6 -0
- package/lib/tasks/move-carry.ts +1 -0
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/package.json +2 -2
- package/server.ts +9 -1
- package/stores/analystUi.ts +19 -0
- 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
|
|
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
|
|
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
|
);
|
package/lib/orchestrator/read.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
+
}
|