@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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { db } from "../db";
|
|
2
|
+
import type {
|
|
3
|
+
DraftStatus,
|
|
4
|
+
HealthReport,
|
|
5
|
+
ReportSummary,
|
|
6
|
+
ReportTrigger,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
interface Row {
|
|
10
|
+
id: number;
|
|
11
|
+
session_id: string;
|
|
12
|
+
workspace_id: string | null;
|
|
13
|
+
head: string | null;
|
|
14
|
+
trigger: ReportTrigger;
|
|
15
|
+
evidence_hash: string;
|
|
16
|
+
data: string;
|
|
17
|
+
model: string | null;
|
|
18
|
+
cost_usd: number | null;
|
|
19
|
+
draft_text: string | null;
|
|
20
|
+
draft_status: DraftStatus | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// What is stored in `data`: the report without what the row itself holds.
|
|
25
|
+
type Stored = Omit<
|
|
26
|
+
HealthReport,
|
|
27
|
+
"id" | "sessionId" | "workspaceId" | "createdAt" | "stale" | "draft"
|
|
28
|
+
> & { draft: HealthReport["draft"] };
|
|
29
|
+
|
|
30
|
+
export function insertReport(
|
|
31
|
+
r: Omit<HealthReport, "id" | "createdAt" | "stale">,
|
|
32
|
+
hash: string
|
|
33
|
+
): number {
|
|
34
|
+
const { id: _i, sessionId, workspaceId, ...rest } = r as HealthReport;
|
|
35
|
+
void _i;
|
|
36
|
+
const draft = r.draft?.source === "report" ? r.draft : null;
|
|
37
|
+
db.prepare(
|
|
38
|
+
`UPDATE health_reports SET draft_status = 'superseded'
|
|
39
|
+
WHERE session_id = ? AND draft_status = 'pending'`
|
|
40
|
+
).run(sessionId);
|
|
41
|
+
const info = db
|
|
42
|
+
.prepare(
|
|
43
|
+
`INSERT INTO health_reports (session_id, workspace_id, head, trigger, evidence_hash, data, model, cost_usd, draft_text, draft_status)
|
|
44
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
45
|
+
)
|
|
46
|
+
.run(
|
|
47
|
+
sessionId,
|
|
48
|
+
workspaceId,
|
|
49
|
+
r.head,
|
|
50
|
+
r.trigger,
|
|
51
|
+
hash,
|
|
52
|
+
JSON.stringify({ ...rest, draft: null } satisfies Stored),
|
|
53
|
+
r.model,
|
|
54
|
+
r.costUsd,
|
|
55
|
+
draft?.text ?? null,
|
|
56
|
+
draft ? "pending" : null
|
|
57
|
+
);
|
|
58
|
+
return Number(info.lastInsertRowid);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toReport(row: Row, head: string | null | undefined): HealthReport {
|
|
62
|
+
const data = JSON.parse(row.data) as Stored;
|
|
63
|
+
return {
|
|
64
|
+
...data,
|
|
65
|
+
id: row.id,
|
|
66
|
+
sessionId: row.session_id,
|
|
67
|
+
workspaceId: row.workspace_id,
|
|
68
|
+
createdAt: row.created_at,
|
|
69
|
+
draft: row.draft_text
|
|
70
|
+
? {
|
|
71
|
+
source: "report",
|
|
72
|
+
id: String(row.id),
|
|
73
|
+
text: row.draft_text,
|
|
74
|
+
status: row.draft_status ?? "pending",
|
|
75
|
+
}
|
|
76
|
+
: null,
|
|
77
|
+
stale: head !== undefined && head !== row.head,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// `head` is the task's current head, to mark an older report stale.
|
|
82
|
+
export function latestReport(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
head?: string | null
|
|
85
|
+
): HealthReport | null {
|
|
86
|
+
const row = db
|
|
87
|
+
.prepare(
|
|
88
|
+
`SELECT * FROM health_reports WHERE session_id = ? ORDER BY id DESC LIMIT 1`
|
|
89
|
+
)
|
|
90
|
+
.get(sessionId) as Row | undefined;
|
|
91
|
+
return row ? toReport(row, head) : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getReport(id: number): HealthReport | null {
|
|
95
|
+
const row = db
|
|
96
|
+
.prepare(`SELECT * FROM health_reports WHERE id = ?`)
|
|
97
|
+
.get(id) as Row | undefined;
|
|
98
|
+
return row ? toReport(row, undefined) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function hashOfLatest(sessionId: string): string | null {
|
|
102
|
+
const row = db
|
|
103
|
+
.prepare(
|
|
104
|
+
`SELECT evidence_hash FROM health_reports WHERE session_id = ? ORDER BY id DESC LIMIT 1`
|
|
105
|
+
)
|
|
106
|
+
.get(sessionId) as { evidence_hash: string } | undefined;
|
|
107
|
+
return row?.evidence_hash ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Claimed first, so a double tap sends once.
|
|
111
|
+
export function claimDraft(
|
|
112
|
+
reportId: number,
|
|
113
|
+
to: Exclude<DraftStatus, "pending">
|
|
114
|
+
): boolean {
|
|
115
|
+
return (
|
|
116
|
+
db
|
|
117
|
+
.prepare(
|
|
118
|
+
`UPDATE health_reports SET draft_status = ?, draft_decided_at = datetime('now')
|
|
119
|
+
WHERE id = ? AND draft_status = 'pending'`
|
|
120
|
+
)
|
|
121
|
+
.run(to, reportId).changes === 1
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function reopenDraft(reportId: number): void {
|
|
126
|
+
db.prepare(
|
|
127
|
+
`UPDATE health_reports SET draft_status = 'pending', draft_decided_at = NULL
|
|
128
|
+
WHERE id = ? AND draft_status = 'sent'`
|
|
129
|
+
).run(reportId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function setDraftText(
|
|
133
|
+
reportId: number,
|
|
134
|
+
text: string,
|
|
135
|
+
status: DraftStatus
|
|
136
|
+
) {
|
|
137
|
+
db.prepare(
|
|
138
|
+
`UPDATE health_reports SET draft_text = ?, draft_status = ? WHERE id = ?`
|
|
139
|
+
).run(text, status, reportId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function historyOf(sessionId: string, limit = 20): ReportSummary[] {
|
|
143
|
+
const rows = db
|
|
144
|
+
.prepare(
|
|
145
|
+
`SELECT * FROM health_reports WHERE session_id = ? ORDER BY id DESC LIMIT ?`
|
|
146
|
+
)
|
|
147
|
+
.all(sessionId, limit) as Row[];
|
|
148
|
+
return rows.map((r) => summaryOf(toReport(r, undefined), ""));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function summaryOf(r: HealthReport, name: string): ReportSummary {
|
|
152
|
+
return {
|
|
153
|
+
sessionId: r.sessionId,
|
|
154
|
+
name,
|
|
155
|
+
headline: r.headline,
|
|
156
|
+
state: r.state,
|
|
157
|
+
estimate: {
|
|
158
|
+
done: r.estimate.done,
|
|
159
|
+
total: r.estimate.total,
|
|
160
|
+
basis: r.estimate.basis,
|
|
161
|
+
},
|
|
162
|
+
createdAt: r.createdAt,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The newest report of each task in a workspace.
|
|
167
|
+
export function latestByTask(
|
|
168
|
+
workspaceId: string
|
|
169
|
+
): { report: HealthReport; name: string }[] {
|
|
170
|
+
const rows = db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT h.*, s.name AS name FROM health_reports h
|
|
173
|
+
JOIN session_rows s ON s.id = h.session_id
|
|
174
|
+
WHERE h.workspace_id = ?
|
|
175
|
+
AND h.id = (SELECT MAX(id) FROM health_reports WHERE session_id = h.session_id)
|
|
176
|
+
ORDER BY h.id DESC LIMIT 100`
|
|
177
|
+
)
|
|
178
|
+
.all(workspaceId) as (Row & { name: string })[];
|
|
179
|
+
return rows.map((r) => ({ report: toReport(r, undefined), name: r.name }));
|
|
180
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// The report with no model: sentences built from the same evidence. Used when
|
|
2
|
+
// the model fails or every line it wrote was rejected, and for the headline.
|
|
3
|
+
|
|
4
|
+
import type { TaskState } from "../tasks/state";
|
|
5
|
+
import type { EvidenceRef, ReportLine } from "./types";
|
|
6
|
+
|
|
7
|
+
const HEADLINES: Record<TaskState, string> = {
|
|
8
|
+
working: "The agent is working on it",
|
|
9
|
+
"needs-input": "The agent is waiting for an answer",
|
|
10
|
+
blocked: "The task is blocked and needs a decision",
|
|
11
|
+
review: "The pull request is open and waiting on review",
|
|
12
|
+
"checks-failing": "Checks are failing on the latest push",
|
|
13
|
+
"plan-ready": "The plan is written and waits for your approval",
|
|
14
|
+
exited: "The agent stopped before finishing",
|
|
15
|
+
merged: "The work is merged",
|
|
16
|
+
dropped: "The task was dropped",
|
|
17
|
+
done: "The task is finished",
|
|
18
|
+
moving: "The task is moving to another machine",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const headlineFor = (state: TaskState) => HEADLINES[state];
|
|
22
|
+
|
|
23
|
+
const NEXT: Partial<Record<TaskState, string>> = {
|
|
24
|
+
"checks-failing": "Next: fix the failing checks, then push again.",
|
|
25
|
+
review: "Next: wait for the review and the checks to settle, then merge.",
|
|
26
|
+
blocked: "Next: answer what the task is waiting on.",
|
|
27
|
+
"needs-input": "Next: reply to the agent so it can go on.",
|
|
28
|
+
"plan-ready": "Next: read the plan and approve it.",
|
|
29
|
+
exited: "Next: restart the agent or take the work over.",
|
|
30
|
+
working: "Next: let the agent finish its turn.",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function templateLines(
|
|
34
|
+
state: TaskState,
|
|
35
|
+
evidence: EvidenceRef[]
|
|
36
|
+
): {
|
|
37
|
+
lines: ReportLine[];
|
|
38
|
+
recommendation: { text: string; evidence: string[] } | null;
|
|
39
|
+
} {
|
|
40
|
+
const lines: ReportLine[] = [];
|
|
41
|
+
const add = (tone: ReportLine["tone"], text: string, ev: EvidenceRef[]) =>
|
|
42
|
+
lines.push({
|
|
43
|
+
id: `l${lines.length + 1}`,
|
|
44
|
+
tone,
|
|
45
|
+
text,
|
|
46
|
+
evidence: ev.map((e) => e.id),
|
|
47
|
+
});
|
|
48
|
+
const of = (kind: EvidenceRef["kind"]) =>
|
|
49
|
+
evidence.filter((e) => e.kind === kind);
|
|
50
|
+
|
|
51
|
+
const ci = of("ci");
|
|
52
|
+
const ciPass = ci.filter((e) => e.status === "pass");
|
|
53
|
+
const ciFail = ci.filter((e) => e.status === "fail");
|
|
54
|
+
const ciWait = ci.filter((e) => e.status === "pending");
|
|
55
|
+
if (ciPass.length)
|
|
56
|
+
add(
|
|
57
|
+
"done",
|
|
58
|
+
`${ciPass.length} CI check${ciPass.length === 1 ? "" : "s"} passed.`,
|
|
59
|
+
ciPass
|
|
60
|
+
);
|
|
61
|
+
for (const e of ciFail) add("warn", `${e.label} is failing.`, [e]);
|
|
62
|
+
if (ciWait.length)
|
|
63
|
+
add(
|
|
64
|
+
"warn",
|
|
65
|
+
`${ciWait.length} CI check${ciWait.length === 1 ? " is" : "s are"} still running.`,
|
|
66
|
+
ciWait
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
for (const e of of("test"))
|
|
70
|
+
add(
|
|
71
|
+
e.status === "pass" ? "done" : e.status === "fail" ? "warn" : "next",
|
|
72
|
+
e.status === "pass"
|
|
73
|
+
? `Tests passed: ${e.label}.`
|
|
74
|
+
: e.status === "fail"
|
|
75
|
+
? `Tests failed: ${e.label}.`
|
|
76
|
+
: `Tests are running: ${e.label}.`,
|
|
77
|
+
[e]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
for (const e of of("review"))
|
|
81
|
+
add(
|
|
82
|
+
e.status === "pass" ? "done" : e.status === "fail" ? "warn" : "next",
|
|
83
|
+
e.status === "pass"
|
|
84
|
+
? "The independent review passed."
|
|
85
|
+
: e.status === "fail"
|
|
86
|
+
? "The independent review found problems to fix."
|
|
87
|
+
: "The independent review has not finished.",
|
|
88
|
+
[e]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (const e of of("piece"))
|
|
92
|
+
add(e.status === "pass" ? "done" : "next", `${e.label}.`, [e]);
|
|
93
|
+
for (const e of of("accept").filter((e) => e.status === "pass"))
|
|
94
|
+
add("done", `Verified: ${e.label}.`, [e]);
|
|
95
|
+
for (const e of of("ask")) add("blocked", `${e.label}.`, [e]);
|
|
96
|
+
for (const e of of("log")) add("done", `${e.label}.`, [e]);
|
|
97
|
+
|
|
98
|
+
const hasDiff = of("diff");
|
|
99
|
+
if (hasDiff.length)
|
|
100
|
+
add(
|
|
101
|
+
"done",
|
|
102
|
+
`${hasDiff.length} file${hasDiff.length === 1 ? "" : "s"} changed.`,
|
|
103
|
+
hasDiff.slice(0, 12)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const stateEv = of("state");
|
|
107
|
+
if (NEXT[state] && stateEv.length) add("next", NEXT[state]!, stateEv);
|
|
108
|
+
|
|
109
|
+
const bad = [...ciFail, ...of("test").filter((e) => e.status === "fail")];
|
|
110
|
+
const recommendation = bad.length
|
|
111
|
+
? {
|
|
112
|
+
text: `Ask the agent to fix ${bad[0].label} first; everything else waits on it.`,
|
|
113
|
+
evidence: [bad[0].id],
|
|
114
|
+
}
|
|
115
|
+
: null;
|
|
116
|
+
return { lines, recommendation };
|
|
117
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Plain-English health reports: types shared by the server, the browser and
|
|
2
|
+
// the native app (no Node modules here).
|
|
3
|
+
|
|
4
|
+
import type { TaskState } from "../tasks/state";
|
|
5
|
+
|
|
6
|
+
export const OPERATING_MODES = ["manual", "assisted", "autonomous"] as const;
|
|
7
|
+
export type OperatingMode = (typeof OPERATING_MODES)[number];
|
|
8
|
+
export const isMode = (v: unknown): v is OperatingMode =>
|
|
9
|
+
typeof v === "string" && (OPERATING_MODES as readonly string[]).includes(v);
|
|
10
|
+
|
|
11
|
+
export const MODE_COPY: Record<
|
|
12
|
+
OperatingMode,
|
|
13
|
+
{ label: string; blurb: string }
|
|
14
|
+
> = {
|
|
15
|
+
manual: {
|
|
16
|
+
label: "Manual",
|
|
17
|
+
blurb: "The analyst recommends, you decide.",
|
|
18
|
+
},
|
|
19
|
+
assisted: {
|
|
20
|
+
label: "Assisted",
|
|
21
|
+
blurb: "The analyst drafts the next prompt, you approve it with one click.",
|
|
22
|
+
},
|
|
23
|
+
autonomous: {
|
|
24
|
+
label: "Autonomous",
|
|
25
|
+
blurb: "Agents iterate on their own, within the loop's safeguards.",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ReportTrigger = "on-demand" | "cycle" | "end";
|
|
30
|
+
export type LineTone = "done" | "warn" | "blocked" | "next";
|
|
31
|
+
export type EvidenceKind =
|
|
32
|
+
| "ci"
|
|
33
|
+
| "test"
|
|
34
|
+
| "review"
|
|
35
|
+
| "diff"
|
|
36
|
+
| "piece"
|
|
37
|
+
| "accept"
|
|
38
|
+
| "ask"
|
|
39
|
+
| "state"
|
|
40
|
+
| "log";
|
|
41
|
+
export type EvidenceStatus = "pass" | "fail" | "pending" | "info";
|
|
42
|
+
|
|
43
|
+
export interface EvidenceRef {
|
|
44
|
+
id: string;
|
|
45
|
+
kind: EvidenceKind;
|
|
46
|
+
label: string;
|
|
47
|
+
status: EvidenceStatus;
|
|
48
|
+
at: string;
|
|
49
|
+
// The head it belongs to; stale when it differs from the current head.
|
|
50
|
+
sha?: string;
|
|
51
|
+
url?: string;
|
|
52
|
+
href?: string;
|
|
53
|
+
excerpt?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ReportLine {
|
|
57
|
+
id: string;
|
|
58
|
+
tone: LineTone;
|
|
59
|
+
text: string;
|
|
60
|
+
evidence: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface EstimateItem {
|
|
64
|
+
label: string;
|
|
65
|
+
verified: boolean;
|
|
66
|
+
evidence: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Estimate {
|
|
70
|
+
done: number;
|
|
71
|
+
total: number;
|
|
72
|
+
basis: "acceptance" | "stages";
|
|
73
|
+
items: EstimateItem[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type DraftStatus = "pending" | "sent" | "dismissed" | "superseded";
|
|
77
|
+
|
|
78
|
+
export interface NextPromptDraft {
|
|
79
|
+
// The report row that holds it, or the analyst recommendation.
|
|
80
|
+
source: "report" | "analyst";
|
|
81
|
+
id: string;
|
|
82
|
+
text: string;
|
|
83
|
+
status: DraftStatus;
|
|
84
|
+
// Analyst drafts send through the analyst link.
|
|
85
|
+
linkId?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface HealthReport {
|
|
89
|
+
id: number;
|
|
90
|
+
sessionId: string;
|
|
91
|
+
workspaceId: string | null;
|
|
92
|
+
head: string | null;
|
|
93
|
+
trigger: ReportTrigger;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
headline: string;
|
|
96
|
+
state: TaskState;
|
|
97
|
+
estimate: Estimate;
|
|
98
|
+
lines: ReportLine[];
|
|
99
|
+
recommendation: { text: string; evidence: string[] } | null;
|
|
100
|
+
draft: NextPromptDraft | null;
|
|
101
|
+
evidence: EvidenceRef[];
|
|
102
|
+
// "template": the model failed, or every line it wrote was rejected.
|
|
103
|
+
source: "model" | "template";
|
|
104
|
+
// Model lines the validator rejected.
|
|
105
|
+
dropped: number;
|
|
106
|
+
model: string | null;
|
|
107
|
+
costUsd: number | null;
|
|
108
|
+
// Computed on read: the head moved since.
|
|
109
|
+
stale: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ReportSummary {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
name: string;
|
|
115
|
+
headline: string;
|
|
116
|
+
state: TaskState;
|
|
117
|
+
estimate: Pick<Estimate, "done" | "total" | "basis">;
|
|
118
|
+
createdAt: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ReportResponse {
|
|
122
|
+
report: HealthReport | null;
|
|
123
|
+
generating: boolean;
|
|
124
|
+
mode: { mode: OperatingMode; source: "task" | "workspace" | "default" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const estimateLabel = (e: Pick<Estimate, "done" | "total" | "basis">) =>
|
|
128
|
+
`Estimate · ${e.done} of ${e.total} ${
|
|
129
|
+
e.basis === "acceptance"
|
|
130
|
+
? "acceptance items verified"
|
|
131
|
+
: "delivery stages reached"
|
|
132
|
+
}`;
|
|
133
|
+
|
|
134
|
+
export const estimatePercent = (e: Pick<Estimate, "done" | "total">) =>
|
|
135
|
+
e.total ? Math.round((100 * e.done) / e.total) : 0;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// What a model may say in a report. Every line must cite evidence that
|
|
2
|
+
// exists, say only numbers that evidence contains, never use a percent sign,
|
|
3
|
+
// and take a tone its evidence supports. What fails is dropped and counted.
|
|
4
|
+
|
|
5
|
+
import type { EvidenceRef, LineTone, ReportLine } from "./types";
|
|
6
|
+
import type { Mapping } from "./estimate";
|
|
7
|
+
|
|
8
|
+
export interface ModelReport {
|
|
9
|
+
headline?: unknown;
|
|
10
|
+
lines?: unknown;
|
|
11
|
+
recommendation?: unknown;
|
|
12
|
+
acceptance?: unknown;
|
|
13
|
+
nextPrompt?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface Validated {
|
|
17
|
+
headline: string | null;
|
|
18
|
+
lines: ReportLine[];
|
|
19
|
+
recommendation: { text: string; evidence: string[] } | null;
|
|
20
|
+
mappings: Mapping[];
|
|
21
|
+
nextPrompt: string | null;
|
|
22
|
+
dropped: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const MAX_LINE = 280;
|
|
26
|
+
export const MAX_LINES = 10;
|
|
27
|
+
export const MAX_PROMPT = 4000;
|
|
28
|
+
const TONES: LineTone[] = ["done", "warn", "blocked", "next"];
|
|
29
|
+
|
|
30
|
+
const str = (v: unknown, max: number) =>
|
|
31
|
+
typeof v === "string" ? v.trim().slice(0, max) : "";
|
|
32
|
+
const ids = (v: unknown): string[] =>
|
|
33
|
+
Array.isArray(v)
|
|
34
|
+
? [...new Set(v.filter((x): x is string => typeof x === "string"))]
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
// Digits in the text that none of the cited evidence contains.
|
|
38
|
+
function inventedNumber(text: string, cited: EvidenceRef[]): boolean {
|
|
39
|
+
const nums = text.match(/\d+/g);
|
|
40
|
+
if (!nums) return false;
|
|
41
|
+
const haystack = cited
|
|
42
|
+
.map((e) => `${e.id} ${e.label} ${e.excerpt ?? ""} ${e.sha ?? ""}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
return nums.some((n) => !new RegExp(`(?<!\\d)${n}(?!\\d)`).test(haystack));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sayable(text: string, cited: EvidenceRef[]): boolean {
|
|
48
|
+
return (
|
|
49
|
+
!!text &&
|
|
50
|
+
text.length <= MAX_LINE &&
|
|
51
|
+
!/%/.test(text) &&
|
|
52
|
+
!inventedNumber(text, cited)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toneFits(tone: LineTone, cited: EvidenceRef[]): boolean {
|
|
57
|
+
if (tone === "done") return cited.some((e) => e.status === "pass");
|
|
58
|
+
if (tone === "warn" || tone === "blocked")
|
|
59
|
+
return cited.some(
|
|
60
|
+
(e) => e.status === "fail" || e.status === "pending" || e.kind === "ask"
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateModelReport(
|
|
66
|
+
raw: unknown,
|
|
67
|
+
evidence: EvidenceRef[]
|
|
68
|
+
): Validated {
|
|
69
|
+
const out: Validated = {
|
|
70
|
+
headline: null,
|
|
71
|
+
lines: [],
|
|
72
|
+
recommendation: null,
|
|
73
|
+
mappings: [],
|
|
74
|
+
nextPrompt: null,
|
|
75
|
+
dropped: 0,
|
|
76
|
+
};
|
|
77
|
+
if (!raw || typeof raw !== "object") return out;
|
|
78
|
+
const m = raw as ModelReport;
|
|
79
|
+
const byId = new Map(evidence.map((e) => [e.id, e]));
|
|
80
|
+
const resolve = (cites: string[]) => {
|
|
81
|
+
const found = cites.map((c) => byId.get(c));
|
|
82
|
+
return found.some((e) => !e) ? null : (found as EvidenceRef[]);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const headline = str(m.headline, MAX_LINE);
|
|
86
|
+
if (
|
|
87
|
+
headline &&
|
|
88
|
+
sayable(
|
|
89
|
+
headline,
|
|
90
|
+
evidence.filter((e) => e.kind === "state")
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
out.headline = headline;
|
|
94
|
+
else if (headline) out.dropped++;
|
|
95
|
+
|
|
96
|
+
const rawLines = Array.isArray(m.lines) ? m.lines.slice(0, 24) : [];
|
|
97
|
+
for (const l of rawLines) {
|
|
98
|
+
const o = (l ?? {}) as Record<string, unknown>;
|
|
99
|
+
const tone = o.tone as LineTone;
|
|
100
|
+
const text = str(o.text, MAX_LINE + 1);
|
|
101
|
+
const cites = ids(o.evidence);
|
|
102
|
+
const cited = cites.length ? resolve(cites) : null;
|
|
103
|
+
if (
|
|
104
|
+
!TONES.includes(tone) ||
|
|
105
|
+
!cited ||
|
|
106
|
+
!sayable(text, cited) ||
|
|
107
|
+
!toneFits(tone, cited) ||
|
|
108
|
+
out.lines.length >= MAX_LINES
|
|
109
|
+
) {
|
|
110
|
+
out.dropped++;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
out.lines.push({
|
|
114
|
+
id: `l${out.lines.length + 1}`,
|
|
115
|
+
tone,
|
|
116
|
+
text,
|
|
117
|
+
evidence: cites,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (m.recommendation && typeof m.recommendation === "object") {
|
|
122
|
+
const r = m.recommendation as Record<string, unknown>;
|
|
123
|
+
const text = str(r.text, 600);
|
|
124
|
+
const cites = ids(r.evidence);
|
|
125
|
+
const cited = cites.length ? resolve(cites) : null;
|
|
126
|
+
if (text && cited && !/%/.test(text) && !inventedNumber(text, cited))
|
|
127
|
+
out.recommendation = { text, evidence: cites };
|
|
128
|
+
else out.dropped++;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (Array.isArray(m.acceptance))
|
|
132
|
+
for (const a of m.acceptance.slice(0, 24)) {
|
|
133
|
+
const o = (a ?? {}) as Record<string, unknown>;
|
|
134
|
+
// The model sees items numbered from 1, as in "accept:1".
|
|
135
|
+
if (typeof o.item === "number" && Number.isInteger(o.item) && o.item >= 1)
|
|
136
|
+
out.mappings.push({ item: o.item - 1, evidence: ids(o.evidence) });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const prompt = str(m.nextPrompt, MAX_PROMPT);
|
|
140
|
+
if (prompt) out.nextPrompt = prompt;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a task saves under .agentos/mockups and .agentos/screenshots. Git
|
|
3
|
+
* ignores that folder (its .gitignore is `*`) and a Bash write isn't a
|
|
4
|
+
* file-writing tool call, so neither other source lists them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import { isSecretPath } from "../secret-paths";
|
|
10
|
+
import { SCREENSHOT_DIR } from "../tasks/ui-brief";
|
|
11
|
+
import { kindForPath, type Candidate } from "./derive";
|
|
12
|
+
|
|
13
|
+
export const MOCKUP_DIR = ".agentos/mockups";
|
|
14
|
+
export const AGENTOS_DIRS = [MOCKUP_DIR, SCREENSHOT_DIR];
|
|
15
|
+
const MAX_DEPTH = 3;
|
|
16
|
+
const MAX_LISTED = 200;
|
|
17
|
+
|
|
18
|
+
const isAgentosFile = (rel: string) =>
|
|
19
|
+
AGENTOS_DIRS.some((d) => rel.startsWith(`${d}/`));
|
|
20
|
+
|
|
21
|
+
function candidate(
|
|
22
|
+
worktree: string,
|
|
23
|
+
rel: string,
|
|
24
|
+
at: number
|
|
25
|
+
): Candidate | null {
|
|
26
|
+
const file = path.join(worktree, rel);
|
|
27
|
+
if (isSecretPath(file)) return null;
|
|
28
|
+
return {
|
|
29
|
+
key: `file:${file}`,
|
|
30
|
+
kind: kindForPath(file),
|
|
31
|
+
name: path.basename(file),
|
|
32
|
+
at,
|
|
33
|
+
by: "shell",
|
|
34
|
+
source: { type: "file", path: file },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function walk(root: string, rel: string, depth: number, out: string[]): void {
|
|
39
|
+
if (out.length >= MAX_LISTED) return;
|
|
40
|
+
let names: fs.Dirent[];
|
|
41
|
+
try {
|
|
42
|
+
names = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
|
|
43
|
+
} catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const d of names) {
|
|
47
|
+
const child = `${rel}/${d.name}`;
|
|
48
|
+
// Symlinks are skipped: they could point out of the worktree.
|
|
49
|
+
if (d.isFile()) out.push(child);
|
|
50
|
+
else if (d.isDirectory() && depth < MAX_DEPTH)
|
|
51
|
+
walk(root, child, depth + 1, out);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function agentosFiles(worktree: string): { rel: string; at: number }[] {
|
|
56
|
+
const rels: string[] = [];
|
|
57
|
+
for (const dir of AGENTOS_DIRS) walk(worktree, dir, 1, rels);
|
|
58
|
+
return rels.slice(0, MAX_LISTED).flatMap((rel) => {
|
|
59
|
+
try {
|
|
60
|
+
return [{ rel, at: fs.statSync(path.join(worktree, rel)).mtimeMs }];
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fromAgentosDirs(
|
|
68
|
+
worktree: string,
|
|
69
|
+
known: Set<string>
|
|
70
|
+
): Candidate[] {
|
|
71
|
+
return agentosFiles(worktree).flatMap(({ rel, at }) => {
|
|
72
|
+
const c = candidate(worktree, rel, at);
|
|
73
|
+
return c && !known.has(c.key) ? [c] : [];
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// After the worktree is gone: the copies that were kept.
|
|
78
|
+
export function fromKept(
|
|
79
|
+
worktree: string,
|
|
80
|
+
kept: string[],
|
|
81
|
+
at: number,
|
|
82
|
+
known: Set<string>
|
|
83
|
+
): Candidate[] {
|
|
84
|
+
return kept.filter(isAgentosFile).flatMap((rel) => {
|
|
85
|
+
const c = candidate(worktree, rel, at);
|
|
86
|
+
return c && !known.has(c.key) ? [c] : [];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Changes when a file is added, removed or rewritten there.
|
|
91
|
+
export function agentosSignature(worktree: string): string {
|
|
92
|
+
const files = agentosFiles(worktree);
|
|
93
|
+
return `${files.length}:${files.reduce((m, f) => Math.max(m, f.at), 0)}`;
|
|
94
|
+
}
|