@saadnvd1/agent-os 0.3.723 → 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 +1 -0
- package/app/api/loops/route.ts +17 -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 +8 -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 +6 -1
- package/components/Chat/itemContext.ts +14 -0
- package/components/Draft/DraftChips.tsx +10 -0
- package/components/Draft/DraftPanel.tsx +7 -0
- package/components/Pane/DesktopTabBar.tsx +6 -0
- package/components/Pane/MobileTabBar.tsx +17 -0
- package/components/Pane/MobileViewPicker.tsx +4 -0
- package/components/Pane/PaneMenu.test.ts +21 -0
- package/components/Pane/PaneMenu.tsx +16 -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/SessionRowMenu.tsx +6 -0
- package/components/Sessions/RolePill.tsx +7 -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/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/push/topics.ts +6 -0
- package/data/reports/index.ts +115 -0
- package/lib/analyst/forward.ts +1 -1
- package/lib/db/changes.ts +7 -0
- package/lib/db/migrations/20261021T1600_task_loops.ts +81 -0
- package/lib/db/migrations/20261021T1800_health_reports.ts +38 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +2 -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/plan-step.ts +4 -1
- package/lib/orchestrator/digest.ts +2 -1
- package/lib/orchestrator/loop-guard.ts +5 -2
- package/lib/orchestrator/pr-wake.ts +4 -0
- package/lib/orchestrator/watcher.ts +8 -1
- package/lib/panes.ts +52 -0
- package/lib/plans/acceptance.ts +23 -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/sidebar/row-menu.ts +9 -0
- 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/move-carry.ts +1 -0
- package/package.json +2 -2
- package/stores/analystUi.ts +19 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// The developer-analyst loop: types shared by the engine, the routes and
|
|
2
|
+
// the browser (no Node modules here).
|
|
3
|
+
|
|
4
|
+
export const CRITERION_STATUSES = ["open", "failing", "met", "waived"] as const;
|
|
5
|
+
export type CriterionStatus = (typeof CRITERION_STATUSES)[number];
|
|
6
|
+
|
|
7
|
+
export const CRITERION_SOURCES = ["card", "brief", "plan", "user"] as const;
|
|
8
|
+
export type CriterionSource = (typeof CRITERION_SOURCES)[number];
|
|
9
|
+
|
|
10
|
+
export const EVIDENCE_KINDS = ["test", "diff", "typecheck", "ci"] as const;
|
|
11
|
+
export type EvidenceKind = (typeof EVIDENCE_KINDS)[number];
|
|
12
|
+
|
|
13
|
+
export interface Evidence {
|
|
14
|
+
kind: EvidenceKind;
|
|
15
|
+
ref: string;
|
|
16
|
+
note: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Criterion {
|
|
20
|
+
id: number;
|
|
21
|
+
loopId: string;
|
|
22
|
+
position: number;
|
|
23
|
+
text: string;
|
|
24
|
+
source: CriterionSource;
|
|
25
|
+
evidenceKind: EvidenceKind | null;
|
|
26
|
+
status: CriterionStatus;
|
|
27
|
+
evidence: Evidence | null;
|
|
28
|
+
// False when the analyst said "met" but the citation didn't resolve.
|
|
29
|
+
verified: boolean;
|
|
30
|
+
metCycle: number | null;
|
|
31
|
+
waivedBy: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CriterionDraft {
|
|
35
|
+
text: string;
|
|
36
|
+
source?: CriterionSource;
|
|
37
|
+
evidenceKind?: EvidenceKind | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type LoopState = "running" | "stopped";
|
|
41
|
+
|
|
42
|
+
export const STOP_REASONS = [
|
|
43
|
+
"stopped",
|
|
44
|
+
"blocked",
|
|
45
|
+
"done",
|
|
46
|
+
"repeat",
|
|
47
|
+
"no_progress",
|
|
48
|
+
"limit:cycles",
|
|
49
|
+
"limit:cost",
|
|
50
|
+
"limit:time",
|
|
51
|
+
] as const;
|
|
52
|
+
export type StopReason = (typeof STOP_REASONS)[number];
|
|
53
|
+
|
|
54
|
+
export interface Limits {
|
|
55
|
+
maxCycles: number;
|
|
56
|
+
maxCostUsd: number;
|
|
57
|
+
maxMinutes: number;
|
|
58
|
+
graceSeconds: number;
|
|
59
|
+
stallCycles: number;
|
|
60
|
+
repeatCycles: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_LIMITS: Limits = {
|
|
64
|
+
maxCycles: 8,
|
|
65
|
+
maxCostUsd: 15,
|
|
66
|
+
maxMinutes: 180,
|
|
67
|
+
graceSeconds: 30,
|
|
68
|
+
stallCycles: 3,
|
|
69
|
+
repeatCycles: 3,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const LIMIT_BOUNDS = {
|
|
73
|
+
maxCycles: [1, 30],
|
|
74
|
+
maxCostUsd: [1, 200],
|
|
75
|
+
maxMinutes: [10, 24 * 60],
|
|
76
|
+
graceSeconds: [0, 300],
|
|
77
|
+
stallCycles: [2, 10],
|
|
78
|
+
repeatCycles: [2, 10],
|
|
79
|
+
} as const satisfies Record<keyof Limits, readonly [number, number]>;
|
|
80
|
+
|
|
81
|
+
export interface LoopRow extends Limits {
|
|
82
|
+
id: string;
|
|
83
|
+
sessionId: string;
|
|
84
|
+
workspaceId: string | null;
|
|
85
|
+
state: LoopState;
|
|
86
|
+
stopReason: StopReason | null;
|
|
87
|
+
stopDetail: string | null;
|
|
88
|
+
goal: string;
|
|
89
|
+
costBaseUsd: number;
|
|
90
|
+
startedAt: number;
|
|
91
|
+
endedAt: number | null;
|
|
92
|
+
lastCycleAt: number | null;
|
|
93
|
+
runNow: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type CycleTrigger = "turn_end" | "ci" | "review" | "manual";
|
|
97
|
+
|
|
98
|
+
// Where a cycle is. The steps before "decided" are redone after a restart;
|
|
99
|
+
// "sending" is safe to redo because the message goes by a fixed id.
|
|
100
|
+
export type CycleState =
|
|
101
|
+
| "claimed"
|
|
102
|
+
| "decided"
|
|
103
|
+
| "counting"
|
|
104
|
+
| "sending"
|
|
105
|
+
| "sent"
|
|
106
|
+
| "skipped"
|
|
107
|
+
| "stopped"
|
|
108
|
+
| "superseded"
|
|
109
|
+
| "error";
|
|
110
|
+
|
|
111
|
+
export type Sender = "analyst" | "user";
|
|
112
|
+
|
|
113
|
+
export interface Finding {
|
|
114
|
+
severity: "blocking" | "minor";
|
|
115
|
+
file?: string;
|
|
116
|
+
line?: number;
|
|
117
|
+
text: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Analysis {
|
|
121
|
+
summary: string;
|
|
122
|
+
findings: Finding[];
|
|
123
|
+
criteria: {
|
|
124
|
+
id: number;
|
|
125
|
+
status: "met" | "open" | "failing";
|
|
126
|
+
evidence?: Evidence;
|
|
127
|
+
}[];
|
|
128
|
+
next: { instruction: string; focus: number[] } | null;
|
|
129
|
+
blocker: {
|
|
130
|
+
kind:
|
|
131
|
+
| "decision"
|
|
132
|
+
| "credential"
|
|
133
|
+
| "hard-line"
|
|
134
|
+
| "unclear-criteria"
|
|
135
|
+
| "environment";
|
|
136
|
+
text: string;
|
|
137
|
+
} | null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CommandResult {
|
|
141
|
+
name: "typecheck" | "test";
|
|
142
|
+
command: string;
|
|
143
|
+
exitCode: number | null;
|
|
144
|
+
durationMs: number;
|
|
145
|
+
passed: string[];
|
|
146
|
+
failed: string[];
|
|
147
|
+
errors: string[];
|
|
148
|
+
excerpt: string;
|
|
149
|
+
skipped?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface EvidenceSummary {
|
|
153
|
+
green: boolean;
|
|
154
|
+
commands: CommandResult[];
|
|
155
|
+
ci: { name: string; state: string; sha?: string | null }[];
|
|
156
|
+
note?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// The numbers "progress" is judged by: more met criteria, fewer failing
|
|
160
|
+
// tests, fewer type errors.
|
|
161
|
+
export interface Score {
|
|
162
|
+
met: number;
|
|
163
|
+
failingTests: number;
|
|
164
|
+
typeErrors: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface CycleRow {
|
|
168
|
+
id: number;
|
|
169
|
+
loopId: string;
|
|
170
|
+
n: number;
|
|
171
|
+
trigger: CycleTrigger;
|
|
172
|
+
state: CycleState;
|
|
173
|
+
snapshotSha: string | null;
|
|
174
|
+
tree: string | null;
|
|
175
|
+
changed: boolean;
|
|
176
|
+
evidence: EvidenceSummary | null;
|
|
177
|
+
logPath: string | null;
|
|
178
|
+
analysis: Analysis | null;
|
|
179
|
+
signature: string | null;
|
|
180
|
+
score: Score | null;
|
|
181
|
+
instruction: string | null;
|
|
182
|
+
instructionSent: string | null;
|
|
183
|
+
sender: Sender | null;
|
|
184
|
+
delivery: string | null;
|
|
185
|
+
sendAt: number | null;
|
|
186
|
+
input: string | null;
|
|
187
|
+
costUsd: number;
|
|
188
|
+
error: string | null;
|
|
189
|
+
startedAt: number;
|
|
190
|
+
endedAt: number | null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// What the loop view shows.
|
|
194
|
+
export interface LoopView {
|
|
195
|
+
loop: LoopRow;
|
|
196
|
+
// Why nothing is moving right now, when something is holding it.
|
|
197
|
+
hold: "paused" | "usage-window" | null;
|
|
198
|
+
holdReason: string | null;
|
|
199
|
+
spentUsd: number;
|
|
200
|
+
elapsedMs: number;
|
|
201
|
+
criteria: Criterion[];
|
|
202
|
+
cycles: CycleRow[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// What the pill on a task row shows.
|
|
206
|
+
export interface LoopSummary {
|
|
207
|
+
state: LoopState;
|
|
208
|
+
stopReason: StopReason | null;
|
|
209
|
+
cycle: number;
|
|
210
|
+
maxCycles: number;
|
|
211
|
+
met: number;
|
|
212
|
+
total: number;
|
|
213
|
+
hold: "paused" | "usage-window" | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export class LoopError extends Error {
|
|
217
|
+
constructor(
|
|
218
|
+
message: string,
|
|
219
|
+
readonly status = 409
|
|
220
|
+
) {
|
|
221
|
+
super(message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// A stubbed world for the loop: a coder that ends turns and changes files, an
|
|
2
|
+
// analyst that answers from a script, and a checks run that reports what the
|
|
3
|
+
// scenario says, on a clock the test moves. No model, shell or chat.
|
|
4
|
+
import { db, updateSessionRow } from "@/lib/db";
|
|
5
|
+
import { createProject } from "@/lib/projects";
|
|
6
|
+
import { createWorkspace, setProjectWorkspace } from "@/lib/workspaces";
|
|
7
|
+
import { ensureOrchestrator } from "@/lib/orchestrator/home";
|
|
8
|
+
import { seedSession } from "@/lib/orchestrator/testing";
|
|
9
|
+
import type { SessionFacts } from "@/lib/orchestrator/facts";
|
|
10
|
+
import type { LoopDeps } from "./deps";
|
|
11
|
+
import { resetLoopMemory, tickLoops } from "./tickLoops";
|
|
12
|
+
import { startLoop } from "./controls";
|
|
13
|
+
import { latestLoopFor } from "./store";
|
|
14
|
+
import type { Analysis, CommandResult, Limits } from "./types";
|
|
15
|
+
|
|
16
|
+
export interface Scenario {
|
|
17
|
+
// The failing tests (and so the signature) of cycle n; [] is green.
|
|
18
|
+
failing: (n: number) => string[];
|
|
19
|
+
// How many criteria the analyst marks met in cycle n (citing the diff).
|
|
20
|
+
met: (n: number) => number;
|
|
21
|
+
// Spend by the end of cycle n, in dollars.
|
|
22
|
+
spent?: (n: number) => number;
|
|
23
|
+
// The coder changed nothing in this cycle's turn.
|
|
24
|
+
unchanged?: (n: number) => boolean;
|
|
25
|
+
// Words the analyst gives as the next instruction.
|
|
26
|
+
instruction?: (n: number) => string | null;
|
|
27
|
+
blocker?: (n: number) => Analysis["blocker"];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const CRITERIA = Array.from({ length: 10 }, (_, i) => ({
|
|
31
|
+
text: `Criterion ${i + 1}`,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
export async function makeWorld(
|
|
35
|
+
scenario: Scenario,
|
|
36
|
+
limits: Partial<Limits> = {}
|
|
37
|
+
) {
|
|
38
|
+
const ws = createWorkspace(
|
|
39
|
+
`loop-ws-${Math.random().toString(36).slice(2, 8)}`
|
|
40
|
+
);
|
|
41
|
+
const project = createProject({
|
|
42
|
+
name: `loop-app-${Math.random().toString(36).slice(2, 8)}`,
|
|
43
|
+
workingDirectory: "/tmp/loop-app",
|
|
44
|
+
});
|
|
45
|
+
setProjectWorkspace(project.id, ws.id);
|
|
46
|
+
ensureOrchestrator(ws.id);
|
|
47
|
+
const taskId = seedSession({
|
|
48
|
+
projectId: project.id,
|
|
49
|
+
name: "loop-task",
|
|
50
|
+
task: true,
|
|
51
|
+
view: "chat",
|
|
52
|
+
branch: "feature/x",
|
|
53
|
+
});
|
|
54
|
+
updateSessionRow(db, taskId, {
|
|
55
|
+
base_branch: "main",
|
|
56
|
+
task_prompt: "Build the thing",
|
|
57
|
+
worktree_path: "/tmp/loop-app",
|
|
58
|
+
workspace_id: ws.id,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const world = {
|
|
62
|
+
ws: ws.id,
|
|
63
|
+
taskId,
|
|
64
|
+
t: Date.parse("2026-10-21T12:00:00Z"),
|
|
65
|
+
rev: 1,
|
|
66
|
+
cycle: 0,
|
|
67
|
+
spent: 0,
|
|
68
|
+
sent: [] as string[],
|
|
69
|
+
running: false,
|
|
70
|
+
prompts: [] as string[],
|
|
71
|
+
fact: {
|
|
72
|
+
id: taskId,
|
|
73
|
+
name: "loop-task",
|
|
74
|
+
project: project.id,
|
|
75
|
+
view: "chat",
|
|
76
|
+
status: "idle",
|
|
77
|
+
activity: null,
|
|
78
|
+
needsInput: false,
|
|
79
|
+
lastActive: 0,
|
|
80
|
+
branch: "feature/x",
|
|
81
|
+
task: { state: "working", pr: null, blocked: null },
|
|
82
|
+
stack: null,
|
|
83
|
+
} as unknown as SessionFacts,
|
|
84
|
+
};
|
|
85
|
+
world.fact.lastActive = world.t - 10 * 60_000;
|
|
86
|
+
|
|
87
|
+
const deps: LoopDeps = {
|
|
88
|
+
now: () => world.t,
|
|
89
|
+
task: (id) =>
|
|
90
|
+
id === taskId
|
|
91
|
+
? (db
|
|
92
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
93
|
+
.get(id) as never)
|
|
94
|
+
: undefined,
|
|
95
|
+
prepare: async () => ({
|
|
96
|
+
repo: "/tmp/loop-app",
|
|
97
|
+
base: "origin/main",
|
|
98
|
+
dir: "/tmp/loop-app",
|
|
99
|
+
}),
|
|
100
|
+
snapshot: async () => ({
|
|
101
|
+
sha: `sha${world.rev}`,
|
|
102
|
+
tree: `tree${world.rev}`,
|
|
103
|
+
}),
|
|
104
|
+
commands: async () => ({ typecheck: null, test: "npm test" }),
|
|
105
|
+
testFiles: async () => [],
|
|
106
|
+
gather: async () => {
|
|
107
|
+
const n = world.cycle;
|
|
108
|
+
const failing = scenario.failing(n);
|
|
109
|
+
const test: CommandResult = {
|
|
110
|
+
name: "test",
|
|
111
|
+
command: "npm test",
|
|
112
|
+
exitCode: failing.length ? 1 : 0,
|
|
113
|
+
durationMs: 1000,
|
|
114
|
+
passed: failing.length ? [] : ["all.test.ts"],
|
|
115
|
+
failed: failing,
|
|
116
|
+
errors: [],
|
|
117
|
+
excerpt: failing.length ? failing.join("\n") : "",
|
|
118
|
+
};
|
|
119
|
+
return {
|
|
120
|
+
summary: { green: !failing.length, commands: [test], ci: [] },
|
|
121
|
+
signature: failing.length ? `test: ${failing.join("; ")}` : null,
|
|
122
|
+
logPath: null,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
diff: async () => ({
|
|
126
|
+
map: "diff",
|
|
127
|
+
hunks: new Map([["a.ts", [[1, 100] as [number, number]]]]),
|
|
128
|
+
}),
|
|
129
|
+
analyse: async (input) => {
|
|
130
|
+
const n = world.cycle;
|
|
131
|
+
world.prompts.push(input.prompt.goal);
|
|
132
|
+
world.spent = scenario.spent?.(n) ?? world.spent;
|
|
133
|
+
const met = scenario.met(n);
|
|
134
|
+
const criteria = input.prompt.criteria;
|
|
135
|
+
return {
|
|
136
|
+
costUsd: 0,
|
|
137
|
+
analysis: {
|
|
138
|
+
summary: `Cycle ${n} summary`,
|
|
139
|
+
findings: [],
|
|
140
|
+
criteria: criteria.map((c, i) => ({
|
|
141
|
+
id: c.id,
|
|
142
|
+
status: i < met ? ("met" as const) : ("open" as const),
|
|
143
|
+
...(i < met
|
|
144
|
+
? {
|
|
145
|
+
evidence: {
|
|
146
|
+
kind: "diff" as const,
|
|
147
|
+
ref: "a.ts:5",
|
|
148
|
+
note: "changed",
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
: {}),
|
|
152
|
+
})),
|
|
153
|
+
next:
|
|
154
|
+
scenario.instruction?.(n) === null
|
|
155
|
+
? null
|
|
156
|
+
: {
|
|
157
|
+
instruction:
|
|
158
|
+
scenario.instruction?.(n) ??
|
|
159
|
+
`Fix the failures from cycle ${n}`,
|
|
160
|
+
focus: [],
|
|
161
|
+
},
|
|
162
|
+
blocker: scenario.blocker?.(n) ?? null,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
send: async (_task, _loopId, _n, text) => {
|
|
167
|
+
world.sent.push(text);
|
|
168
|
+
world.running = true;
|
|
169
|
+
return "sent";
|
|
170
|
+
},
|
|
171
|
+
coderReply: () => null,
|
|
172
|
+
coderRunning: async () => false,
|
|
173
|
+
amendments: () => [],
|
|
174
|
+
spent: () => world.spent,
|
|
175
|
+
hold: () => null,
|
|
176
|
+
waitingOnPerson: () => null,
|
|
177
|
+
};
|
|
178
|
+
// The cycle number gather and analyse see is the one being claimed.
|
|
179
|
+
const real = deps.snapshot;
|
|
180
|
+
deps.snapshot = async (dir) => {
|
|
181
|
+
world.cycle = latestCycleN() ?? 0;
|
|
182
|
+
return real(dir);
|
|
183
|
+
};
|
|
184
|
+
const latestCycleN = () => {
|
|
185
|
+
const loop = latestLoopFor(taskId);
|
|
186
|
+
return loop
|
|
187
|
+
? (
|
|
188
|
+
db
|
|
189
|
+
.prepare(
|
|
190
|
+
`SELECT MAX(n) AS n FROM task_loop_cycles WHERE loop_id = ?`
|
|
191
|
+
)
|
|
192
|
+
.get(loop.id) as { n: number | null }
|
|
193
|
+
).n
|
|
194
|
+
: null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
resetLoopMemory();
|
|
198
|
+
await startLoop(
|
|
199
|
+
taskId,
|
|
200
|
+
{ criteria: CRITERIA, limits: { graceSeconds: 0, ...limits } },
|
|
201
|
+
deps
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const tick = () => tickLoops(world.ws, [world.fact], deps);
|
|
205
|
+
const state = () => latestLoopFor(taskId)!;
|
|
206
|
+
// The coder takes a turn (runs, then goes quiet), then the loop looks.
|
|
207
|
+
async function round() {
|
|
208
|
+
world.fact.status = "running";
|
|
209
|
+
await tick();
|
|
210
|
+
world.t += 60_000;
|
|
211
|
+
world.fact.status = "idle";
|
|
212
|
+
world.fact.lastActive = world.t - 30_000;
|
|
213
|
+
if (!scenario.unchanged?.(world.cycle)) world.rev++;
|
|
214
|
+
await tick();
|
|
215
|
+
}
|
|
216
|
+
async function run(maxRounds = 40) {
|
|
217
|
+
await tick();
|
|
218
|
+
for (let i = 0; i < maxRounds && state().state === "running"; i++)
|
|
219
|
+
await round();
|
|
220
|
+
return state();
|
|
221
|
+
}
|
|
222
|
+
return { world, deps, tick, round, run, state };
|
|
223
|
+
}
|
package/lib/models/plan-step.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { queueEvent } from "../orchestrator/events";
|
|
|
22
22
|
import { recordAutoApproval } from "../approvals/no-approvals";
|
|
23
23
|
import { addNote } from "../orchestrator/notes";
|
|
24
24
|
import { amendBrief, scopeChangesOf } from "../orchestrator/amendments";
|
|
25
|
+
import { ACCEPTANCE_PROMPT } from "../plans/acceptance";
|
|
25
26
|
import { PIECES_PROMPT, foldedBrief, pieceBrief } from "../plans/pieces";
|
|
26
27
|
import { orchestratorOf } from "../tasks/starter";
|
|
27
28
|
import { isRemoteHost } from "../hosts";
|
|
@@ -46,7 +47,9 @@ export function planPrompt(task: string, only = false): string {
|
|
|
46
47
|
---
|
|
47
48
|
Plan first. This turn, only read and plan: write the implementation plan for the task above to \`${PLAN_FILE}\` in this worktree (what to change, file by file; the tests; how you'll verify it; anything ambiguous and the call you'd make). Change no other file, and don't commit, push or open a PR. Don't end your turn while subagents or background commands you started are still running; wait for their results and write the plan in the same turn. End the turn once the plan is written: ${after}.
|
|
48
49
|
|
|
49
|
-
${PIECES_PROMPT}
|
|
50
|
+
${PIECES_PROMPT}
|
|
51
|
+
|
|
52
|
+
${ACCEPTANCE_PROMPT}`;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export const PLAN_RETRY_PROMPT = `Write the plan to \`${PLAN_FILE}\` now, plan only.`;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* is by id, so the same database gives the same text.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { loopDigestNote } from "../loops/summary";
|
|
10
11
|
import { db } from "../db";
|
|
11
12
|
import { openAsks } from "./asks";
|
|
12
13
|
import { listRules } from "./rules";
|
|
@@ -185,7 +186,7 @@ function tasksSection(
|
|
|
185
186
|
? ` | ${t.pause_state}${t.paused_until ? ` until ${t.paused_until} UTC` : t.pause_state === "paused" ? " until resumed" : ""}${t.pause_note ? ` (${t.pause_note})` : ""}`
|
|
186
187
|
: "";
|
|
187
188
|
const placed = t.placed_note ? ` | ${t.placed_note}` : "";
|
|
188
|
-
return `- ${clip(t.name, 80)} (${t.project ?? "?"}, ${t.view}, id ${shortId(t.id)}): ${pr}${arm}${paused}${placed}${gates ? ` | ${gates}` : ""}`;
|
|
189
|
+
return `- ${clip(t.name, 80)} (${t.project ?? "?"}, ${t.view}, id ${shortId(t.id)}): ${pr}${arm}${paused}${placed}${loopDigestNote(t.id)}${gates ? ` | ${gates}` : ""}`;
|
|
189
190
|
}),
|
|
190
191
|
empty: "- None.",
|
|
191
192
|
more: "the sessions tool",
|
|
@@ -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
|
);
|
|
@@ -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.`;
|