@saadnvd1/agent-os 0.3.468 → 0.3.471
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 +67 -37
- package/app/api/fresh-start/route.ts +9 -5
- package/app/api/projects/clone/route.ts +4 -2
- package/app/api/projects/init/route.test.ts +46 -0
- package/app/api/projects/init/route.ts +24 -4
- package/app/api/usage/route.ts +4 -2
- package/components/Chat/ChatPanel.tsx +5 -5
- package/components/Chat/StaleChip.tsx +7 -6
- package/components/CommandPalette/CommandPalette.tsx +3 -1
- package/components/CommandPalette/useAppCommands.ts +10 -0
- package/components/Projects/AddProject/AddProjectDialog.tsx +25 -5
- package/components/Projects/AddProject/CloneForm.tsx +3 -1
- package/components/Projects/AddProject/FolderForm.tsx +98 -4
- package/components/Projects/AddProject/NameForm.tsx +3 -1
- package/components/Projects/AddProject/WorkspacePick.tsx +56 -0
- package/components/Projects/AddProject/useAddExistingProject.ts +58 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +10 -0
- package/components/Usage/UsageDialog.tsx +31 -2
- package/components/Workspaces/FreshStartForm.tsx +13 -13
- package/data/projects/add.ts +23 -3
- package/data/usage/index.ts +2 -0
- package/lib/agents/launch.ts +2 -0
- package/lib/chat/driver.ts +8 -0
- package/lib/chat/drivers/claude-approvals.ts +40 -0
- package/lib/chat/drivers/claude.ts +7 -10
- package/lib/chat/refresh.ts +31 -13
- package/lib/chat/stale.ts +25 -16
- package/lib/chat/worker/extras.ts +1 -0
- package/lib/chat/worker/host.ts +61 -22
- package/lib/db/migrations.ts +42 -0
- package/lib/orchestrator/add-project.ts +35 -0
- package/lib/orchestrator/armed.ts +1 -2
- package/lib/orchestrator/brief.ts +6 -5
- package/lib/orchestrator/claude-cli.ts +5 -0
- package/lib/orchestrator/diff.ts +6 -2
- package/lib/orchestrator/external-gates.ts +8 -43
- package/lib/orchestrator/gates.ts +10 -10
- package/lib/orchestrator/pr-wake.ts +2 -2
- package/lib/orchestrator/rebase.ts +6 -45
- package/lib/orchestrator/review.ts +18 -5
- package/lib/orchestrator/scope.ts +9 -0
- package/lib/orchestrator/serve.ts +6 -0
- package/lib/orchestrator/signoff.ts +13 -8
- package/lib/orchestrator/tool-names.ts +13 -6
- package/lib/orchestrator/tool-schemas.ts +15 -0
- package/lib/orchestrator/tools.ts +2 -0
- package/lib/orchestrator/worker.ts +4 -1
- package/lib/orchestrator/write-guard.ts +295 -0
- package/lib/project-add/index.ts +133 -14
- package/lib/project-add/name.ts +1 -0
- package/lib/secret-paths.ts +9 -0
- package/lib/session-audit/render.ts +1 -1
- package/lib/stacks/land.ts +23 -16
- package/lib/tasks/brief.ts +3 -14
- package/lib/tasks/code-review.ts +69 -107
- package/lib/tasks/gh.ts +3 -2
- package/lib/tasks/index.ts +1 -1
- package/lib/tasks/state.ts +4 -4
- package/lib/usage/check-runs.ts +167 -0
- package/lib/usage/report.ts +25 -0
- package/lib/usage/turns.ts +28 -0
- package/package.json +1 -1
- package/stores/addProject.ts +12 -3
- package/scripts/check-code-review.ts +0 -21
package/lib/tasks/code-review.ts
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
export interface CodeReviewSection {
|
|
7
|
-
// The commit the review covered, as written in the section.
|
|
8
|
-
sha: string | null;
|
|
9
|
-
// A "Restacked: <from> -> <to>" line AgentOS wrote after rebasing the
|
|
10
|
-
// branch itself. Only CI reads it: AgentOS's own gates go by what it
|
|
11
|
-
// recorded, never by what a body says.
|
|
12
|
-
restacked?: Restacked;
|
|
13
|
-
// What kind of AI attribution the body carries, when it does: a fixed
|
|
14
|
-
// label, never the body's own text, which goes into gate messages.
|
|
15
|
-
attribution?: AttributionKind;
|
|
16
|
-
}
|
|
1
|
+
// The code-review gate, the same for every PR: a passing independent
|
|
2
|
+
// review (lib/orchestrator/review) of its exact head, and a body with no AI
|
|
3
|
+
// attribution. Tasks don't write a review section in the body any more: the
|
|
4
|
+
// orchestrator's review is the one review.
|
|
17
5
|
|
|
18
6
|
// Matched one line at a time, never across lines: the body is written by
|
|
19
7
|
// the task, and a regex that backtracks over it would block the server.
|
|
20
8
|
// Only what renders counts: fenced code and HTML comments are skipped, so an
|
|
21
|
-
// example
|
|
9
|
+
// example isn't read as the real thing.
|
|
22
10
|
const MAX_BODY = 65_536;
|
|
23
|
-
const HEADING = /^ {0,3}(#{1,6}) *code review *:?$/i;
|
|
24
|
-
const ANY_HEADING = /^ {0,3}(#{1,6}) +\S/;
|
|
25
11
|
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
26
12
|
// AI attribution ("Co-Authored-By: Claude", "Generated with [Claude Code]").
|
|
27
13
|
// The same rule as the commit-msg hook's ATTRIBUTION_ERE in
|
|
@@ -45,10 +31,6 @@ function attributionKind(line: string): AttributionKind | undefined {
|
|
|
45
31
|
if (/^claude-session:/i.test(line)) return "a Claude-Session link";
|
|
46
32
|
return 'a "Generated with" AI footer';
|
|
47
33
|
}
|
|
48
|
-
const REVIEWED =
|
|
49
|
-
/^[>*_ -]*reviewed(?: (?:commit|sha|at))?[*_ ]*:[*_` ]*([0-9a-f]{12,40})\b/i;
|
|
50
|
-
const RESTACKED =
|
|
51
|
-
/^[>*_ -]*restacked[*_ ]*:[*_` ]*([0-9a-f]{40})[*_` ]*(?:->|→)[*_` ]*([0-9a-f]{40})\b/i;
|
|
52
34
|
|
|
53
35
|
export function renderedLines(body: string): string[] {
|
|
54
36
|
let text = body.slice(0, MAX_BODY);
|
|
@@ -70,45 +52,7 @@ export function renderedLines(body: string): string[] {
|
|
|
70
52
|
return lines;
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
// The
|
|
74
|
-
// none does); null when the body has none. A section runs to the next
|
|
75
|
-
// heading of its level or higher, so its own subheadings stay inside it.
|
|
76
|
-
export function parseCodeReview(
|
|
77
|
-
body: string | null | undefined
|
|
78
|
-
): CodeReviewSection | null {
|
|
79
|
-
if (!body) return null;
|
|
80
|
-
const sections: CodeReviewSection[] = [];
|
|
81
|
-
let level = 0;
|
|
82
|
-
let attribution: AttributionKind | undefined;
|
|
83
|
-
for (const line of renderedLines(body)) {
|
|
84
|
-
attribution ??= attributionKind(line.trimStart());
|
|
85
|
-
const heading = HEADING.exec(line);
|
|
86
|
-
if (heading) {
|
|
87
|
-
level = heading[1].length;
|
|
88
|
-
sections.push({ sha: null });
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
const other = ANY_HEADING.exec(line);
|
|
92
|
-
if (other && other[1].length <= level) level = 0;
|
|
93
|
-
const current = sections.at(-1);
|
|
94
|
-
if (!level || !current) continue;
|
|
95
|
-
if (current.sha) {
|
|
96
|
-
const moved = RESTACKED.exec(line);
|
|
97
|
-
if (moved)
|
|
98
|
-
current.restacked = {
|
|
99
|
-
from: moved[1].toLowerCase(),
|
|
100
|
-
to: moved[2].toLowerCase(),
|
|
101
|
-
};
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
const sha = REVIEWED.exec(line)?.[1];
|
|
105
|
-
if (sha) current.sha = sha.toLowerCase();
|
|
106
|
-
}
|
|
107
|
-
const found = sections.findLast((s) => s.sha) ?? sections.at(-1) ?? null;
|
|
108
|
-
return found && attribution ? { ...found, attribution } : found;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// The AI attribution a body carries anywhere, Code review section or not.
|
|
55
|
+
// The AI attribution a body carries anywhere.
|
|
112
56
|
export function attributionIn(
|
|
113
57
|
body: string | null | undefined
|
|
114
58
|
): AttributionKind | undefined {
|
|
@@ -119,57 +63,75 @@ export function attributionIn(
|
|
|
119
63
|
}
|
|
120
64
|
}
|
|
121
65
|
|
|
122
|
-
// A branch AgentOS rebased itself (a stack restack
|
|
123
|
-
//
|
|
66
|
+
// A branch AgentOS rebased itself (a stack restack, or the automatic
|
|
67
|
+
// rebase onto its base): the head the task pushed, and the head AgentOS
|
|
68
|
+
// left.
|
|
124
69
|
export interface Restacked {
|
|
125
70
|
from: string | null;
|
|
126
71
|
to: string | null;
|
|
127
72
|
}
|
|
128
73
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
export
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!section) return `the PR body has no Code review section: ${fix}`;
|
|
143
|
-
if (section.attribution)
|
|
144
|
-
return `the PR body carries AI attribution (${section.attribution}): remove it with \`gh pr edit --body-file\``;
|
|
145
|
-
if (!section.sha)
|
|
146
|
-
return `the PR's Code review section doesn't name the reviewed commit ("Reviewed: <sha>"): ${fix}`;
|
|
147
|
-
const covers = (sha: string | null | undefined) =>
|
|
148
|
-
!!sha && sha.toLowerCase().startsWith(section.sha!);
|
|
149
|
-
if (covers(head)) return null;
|
|
150
|
-
if (restacked?.to === head && covers(restacked.from)) return null;
|
|
151
|
-
return `the PR's code review covers ${section.sha.slice(0, 7)}, not its head ${head.slice(0, 7)}: ${fix}`;
|
|
74
|
+
export interface ReviewState {
|
|
75
|
+
sha: string;
|
|
76
|
+
status: "running" | "pass" | "block" | "error";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CodeReviewGate {
|
|
80
|
+
// Fails the gate: counts against the task.
|
|
81
|
+
refusal: string | null;
|
|
82
|
+
// Not covered yet, but a review will cover it.
|
|
83
|
+
wait: string | null;
|
|
84
|
+
// A review covering the head finished, passing or not. Saad's approval
|
|
85
|
+
// of the commit overrides what it found, never whether one ran.
|
|
86
|
+
ran?: boolean;
|
|
152
87
|
}
|
|
153
88
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
89
|
+
const short = (sha: string) => sha.slice(0, 7);
|
|
90
|
+
|
|
91
|
+
// Fails closed: an unread body or an unknown head refuses. A review of the
|
|
92
|
+
// head the task pushed still counts after AgentOS restacked or rebased it
|
|
93
|
+
// cleanly, but only while the head is exactly the one AgentOS left: a push
|
|
94
|
+
// by the task after that needs a new review.
|
|
95
|
+
export function codeReviewGate(opts: {
|
|
96
|
+
head: string | undefined;
|
|
97
|
+
// The AI attribution in the PR body (attributionIn): null for none,
|
|
98
|
+
// undefined when the body couldn't be read.
|
|
99
|
+
attribution: AttributionKind | null | undefined;
|
|
100
|
+
review: (sha: string) => ReviewState | null;
|
|
101
|
+
restacked?: Restacked | null;
|
|
102
|
+
}): CodeReviewGate {
|
|
103
|
+
const { head, restacked } = opts;
|
|
104
|
+
const no = (refusal: string) => ({ refusal, wait: null });
|
|
105
|
+
const { attribution } = opts;
|
|
106
|
+
if (attribution === undefined) return no("the PR body couldn't be read");
|
|
107
|
+
if (!head) return no("the PR's head commit is unknown");
|
|
108
|
+
if (attribution)
|
|
109
|
+
return no(
|
|
110
|
+
`the PR body carries AI attribution (${attribution}): remove it with \`gh pr edit --body-file\``
|
|
111
|
+
);
|
|
112
|
+
// The reviews that cover the head: its own, and the one of the head the
|
|
113
|
+
// task pushed while the head is the one AgentOS left.
|
|
114
|
+
const covering = [
|
|
115
|
+
opts.review(head),
|
|
116
|
+
restacked?.to === head && restacked.from
|
|
117
|
+
? opts.review(restacked.from)
|
|
118
|
+
: null,
|
|
119
|
+
];
|
|
120
|
+
if (covering.some((r) => r?.status === "pass"))
|
|
121
|
+
return { refusal: null, wait: null, ran: true };
|
|
122
|
+
const now = covering[0];
|
|
123
|
+
return {
|
|
124
|
+
refusal: null,
|
|
125
|
+
wait: `no passing independent review of ${short(head)}${now?.status === "running" ? " yet (it's running)" : now?.status === "block" ? " (it has blocking findings)" : ": call review"}`,
|
|
126
|
+
ran: covering.some((r) => r?.status === "block"),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The gate as one reason, for paths that only merge or refuse (a stack
|
|
131
|
+
// landing, an approved merge): a wait refuses there too.
|
|
132
|
+
export function codeReviewRefusal(
|
|
133
|
+
opts: Parameters<typeof codeReviewGate>[0]
|
|
162
134
|
): string | null {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
const lines = body.split("\n");
|
|
166
|
-
const at = lines.findLastIndex(
|
|
167
|
-
(l) =>
|
|
168
|
-
REVIEWED.exec(l.replace(/\t/g, " "))?.[1]?.toLowerCase() === section.sha
|
|
169
|
-
);
|
|
170
|
-
if (at < 0) return null;
|
|
171
|
-
const line = `Restacked: ${from} -> ${to} (AgentOS rebased the reviewed commit onto its base; the PR's own changes are the ones reviewed)`;
|
|
172
|
-
const next = lines[at + 1]?.replace(/\t/g, " ") ?? "";
|
|
173
|
-
lines.splice(at + 1, RESTACKED.test(next) ? 1 : 0, line);
|
|
174
|
-
return lines.join("\n");
|
|
135
|
+
const gate = codeReviewGate(opts);
|
|
136
|
+
return gate.refusal ?? gate.wait;
|
|
175
137
|
}
|
package/lib/tasks/gh.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { git } from "../git-exec";
|
|
4
|
-
import {
|
|
4
|
+
import { attributionIn } from "./code-review";
|
|
5
5
|
import { parseScopeChange } from "./scope-change";
|
|
6
6
|
import { assertGhAllowed, noteGhFailure, noteGhSuccess } from "./gh-limit";
|
|
7
7
|
import {
|
|
@@ -114,7 +114,8 @@ export async function toTaskPR(repoDir: string, pr: ListedPR): Promise<TaskPR> {
|
|
|
114
114
|
failing: failingCheck(rollup),
|
|
115
115
|
failingUrl: failingUrl(rollup),
|
|
116
116
|
checkCount: (pr.statusCheckRollup ?? []).length,
|
|
117
|
-
|
|
117
|
+
attribution:
|
|
118
|
+
pr.body === undefined ? undefined : (attributionIn(pr.body) ?? null),
|
|
118
119
|
scopeChange: parseScopeChange(pr.body),
|
|
119
120
|
...(pr.mergeable && { mergeable: pr.mergeable }),
|
|
120
121
|
};
|
package/lib/tasks/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { chatStateNow } from "../chat/runner";
|
|
|
49
49
|
import { saidSinceLastMessage } from "../chat/store";
|
|
50
50
|
|
|
51
51
|
export * from "./state";
|
|
52
|
-
export {
|
|
52
|
+
export { codeReviewGate, codeReviewRefusal } from "./code-review";
|
|
53
53
|
export { signOffTask, dropTask, signingOff, mergeSettled } from "./finish";
|
|
54
54
|
export { prFor as taskPR } from "./session";
|
|
55
55
|
export { moveTask } from "./move-flow";
|
package/lib/tasks/state.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AttributionKind } from "./code-review";
|
|
2
2
|
|
|
3
3
|
export type TaskStatus =
|
|
4
4
|
| "running"
|
|
@@ -22,9 +22,9 @@ export interface TaskPR {
|
|
|
22
22
|
failingUrl?: string | null;
|
|
23
23
|
// How many checks are registered on the head.
|
|
24
24
|
checkCount?: number;
|
|
25
|
-
// The body
|
|
26
|
-
// when the body wasn't read.
|
|
27
|
-
|
|
25
|
+
// The AI attribution the body carries (tasks/code-review.ts): null for
|
|
26
|
+
// none, undefined when the body wasn't read.
|
|
27
|
+
attribution?: AttributionKind | null;
|
|
28
28
|
// The body's "Scope change" note (tasks/scope-change.ts), when it has one.
|
|
29
29
|
scopeChange?: string | null;
|
|
30
30
|
// GitHub's word on merging it into its base: CONFLICTING once the base
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the orchestrator's one-shot checks cost: each `claude -p` run of the
|
|
3
|
+
* independent reviewer and the scope check, read from its JSON result.
|
|
4
|
+
* They run with --no-session-persistence and leave no transcript, so this
|
|
5
|
+
* is the only record of their tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from "../db";
|
|
9
|
+
import { formatTokens } from "../chat/context";
|
|
10
|
+
|
|
11
|
+
export interface RunUsage {
|
|
12
|
+
model: string | null;
|
|
13
|
+
inputTokens: number;
|
|
14
|
+
cacheWriteTokens: number;
|
|
15
|
+
cacheReadTokens: number;
|
|
16
|
+
outputTokens: number;
|
|
17
|
+
costUsd: number;
|
|
18
|
+
durationMs: number | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ModelUsage {
|
|
22
|
+
inputTokens?: number;
|
|
23
|
+
outputTokens?: number;
|
|
24
|
+
cacheReadInputTokens?: number;
|
|
25
|
+
cacheCreationInputTokens?: number;
|
|
26
|
+
costUSD?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const n = (v: unknown) => (typeof v === "number" && v > 0 ? v : 0);
|
|
30
|
+
|
|
31
|
+
// The usage in a `claude -p --output-format json` result, or null when it
|
|
32
|
+
// carries none. Per-model usage covers every model the run used; the model
|
|
33
|
+
// named is the one that cost the most.
|
|
34
|
+
export function runUsageOf(result: unknown): RunUsage | null {
|
|
35
|
+
if (!result || typeof result !== "object") return null;
|
|
36
|
+
const r = result as {
|
|
37
|
+
usage?: {
|
|
38
|
+
input_tokens?: number;
|
|
39
|
+
output_tokens?: number;
|
|
40
|
+
cache_read_input_tokens?: number;
|
|
41
|
+
cache_creation_input_tokens?: number;
|
|
42
|
+
};
|
|
43
|
+
modelUsage?: Record<string, ModelUsage>;
|
|
44
|
+
total_cost_usd?: number;
|
|
45
|
+
duration_ms?: number;
|
|
46
|
+
};
|
|
47
|
+
const models = Object.entries(r.modelUsage ?? {});
|
|
48
|
+
if (!r.usage && !models.length) return null;
|
|
49
|
+
const durationMs = n(r.duration_ms) || null;
|
|
50
|
+
if (models.length) {
|
|
51
|
+
const sum = (k: keyof ModelUsage) =>
|
|
52
|
+
models.reduce((t, [, m]) => t + n(m[k]), 0);
|
|
53
|
+
const [model] = models.reduce((a, b) =>
|
|
54
|
+
n(b[1].costUSD) > n(a[1].costUSD) ? b : a
|
|
55
|
+
);
|
|
56
|
+
return {
|
|
57
|
+
model,
|
|
58
|
+
inputTokens: sum("inputTokens"),
|
|
59
|
+
cacheWriteTokens: sum("cacheCreationInputTokens"),
|
|
60
|
+
cacheReadTokens: sum("cacheReadInputTokens"),
|
|
61
|
+
outputTokens: sum("outputTokens"),
|
|
62
|
+
costUsd: n(r.total_cost_usd) || sum("costUSD"),
|
|
63
|
+
durationMs,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const u = r.usage!;
|
|
67
|
+
return {
|
|
68
|
+
model: null,
|
|
69
|
+
inputTokens: n(u.input_tokens),
|
|
70
|
+
cacheWriteTokens: n(u.cache_creation_input_tokens),
|
|
71
|
+
cacheReadTokens: n(u.cache_read_input_tokens),
|
|
72
|
+
outputTokens: n(u.output_tokens),
|
|
73
|
+
costUsd: n(r.total_cost_usd),
|
|
74
|
+
durationMs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CheckRunTarget {
|
|
79
|
+
workspaceId: string;
|
|
80
|
+
// The task's session id, or an external PR's id.
|
|
81
|
+
sessionId: string;
|
|
82
|
+
// How the Usage page names it: "task add-a", "PR o/r#5".
|
|
83
|
+
label: string;
|
|
84
|
+
prNumber: number | null;
|
|
85
|
+
sha: string;
|
|
86
|
+
kind: "review" | "scope";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function recordCheckRun(
|
|
90
|
+
target: CheckRunTarget,
|
|
91
|
+
u: RunUsage,
|
|
92
|
+
at = Date.now()
|
|
93
|
+
): void {
|
|
94
|
+
db.prepare(
|
|
95
|
+
`INSERT INTO check_runs (workspace_id, session_id, label, pr_number, sha, kind, model,
|
|
96
|
+
input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, cost_usd, duration_ms, at)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
98
|
+
).run(
|
|
99
|
+
target.workspaceId,
|
|
100
|
+
target.sessionId,
|
|
101
|
+
target.label,
|
|
102
|
+
target.prNumber,
|
|
103
|
+
target.sha,
|
|
104
|
+
target.kind,
|
|
105
|
+
u.model,
|
|
106
|
+
u.inputTokens,
|
|
107
|
+
u.cacheWriteTokens,
|
|
108
|
+
u.cacheReadTokens,
|
|
109
|
+
u.outputTokens,
|
|
110
|
+
u.costUsd,
|
|
111
|
+
u.durationMs,
|
|
112
|
+
at
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A recorder for one check, that never lets a write break the check.
|
|
117
|
+
export function checkRunRecorder(target: CheckRunTarget) {
|
|
118
|
+
return (u: RunUsage) => {
|
|
119
|
+
try {
|
|
120
|
+
recordCheckRun(target, u);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error(`[check-runs] recording ${target.label}:`, e);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CheckRunTotals {
|
|
128
|
+
runs: number;
|
|
129
|
+
tokens: number;
|
|
130
|
+
costUsd: number;
|
|
131
|
+
durationMs: number;
|
|
132
|
+
models: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Everything the checks of one commit cost, every part and retry.
|
|
136
|
+
export function checkRunTotals(
|
|
137
|
+
sessionId: string,
|
|
138
|
+
sha: string
|
|
139
|
+
): CheckRunTotals | null {
|
|
140
|
+
const row = db
|
|
141
|
+
.prepare(
|
|
142
|
+
`SELECT count(*) AS runs,
|
|
143
|
+
coalesce(sum(input_tokens + cache_write_tokens + cache_read_tokens + output_tokens), 0) AS tokens,
|
|
144
|
+
coalesce(sum(cost_usd), 0) AS costUsd,
|
|
145
|
+
coalesce(sum(duration_ms), 0) AS durationMs,
|
|
146
|
+
group_concat(DISTINCT model) AS models
|
|
147
|
+
FROM check_runs WHERE session_id = ? AND sha = ?`
|
|
148
|
+
)
|
|
149
|
+
.get(sessionId, sha) as Omit<CheckRunTotals, "models"> & {
|
|
150
|
+
models: string | null;
|
|
151
|
+
};
|
|
152
|
+
if (!row.runs) return null;
|
|
153
|
+
return { ...row, models: row.models ? row.models.split(",") : [] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// "2 runs · 412k tokens · $0.84 · 3m 10s · claude-opus-5-5"
|
|
157
|
+
export function describeCheckRuns(t: CheckRunTotals): string {
|
|
158
|
+
const s = Math.round(t.durationMs / 1000);
|
|
159
|
+
const time = s >= 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
160
|
+
return [
|
|
161
|
+
`${t.runs} run${t.runs === 1 ? "" : "s"}`,
|
|
162
|
+
`${formatTokens(t.tokens)} tokens`,
|
|
163
|
+
`$${t.costUsd.toFixed(2)}`,
|
|
164
|
+
time,
|
|
165
|
+
...t.models,
|
|
166
|
+
].join(" · ");
|
|
167
|
+
}
|
package/lib/usage/report.ts
CHANGED
|
@@ -18,3 +18,28 @@ export function usageReport(range: UsageRange, now = Date.now()) {
|
|
|
18
18
|
const names = Object.fromEntries(listWorkspaces().map((w) => [w.id, w.name]));
|
|
19
19
|
return aggregateUsage(rows, range, names, now);
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
// What the orchestrator's reviewer and scope check cost over the range
|
|
23
|
+
// (lib/usage/check-runs), by PR: a "turn" is one run.
|
|
24
|
+
export function checkRunReport(range: UsageRange, now = Date.now()) {
|
|
25
|
+
const rows = db
|
|
26
|
+
.prepare(
|
|
27
|
+
`SELECT session_id, label, pr_number, workspace_id, at, cost_usd, input_tokens,
|
|
28
|
+
output_tokens, cache_read_tokens, cache_write_tokens
|
|
29
|
+
FROM check_runs WHERE at >= ? ORDER BY at`
|
|
30
|
+
)
|
|
31
|
+
.all(rangeStart(range, now)) as (Omit<TurnRow, "session_name"> & {
|
|
32
|
+
label: string;
|
|
33
|
+
pr_number: number | null;
|
|
34
|
+
})[];
|
|
35
|
+
const named: TurnRow[] = rows.map(({ label, pr_number, ...r }) => ({
|
|
36
|
+
...r,
|
|
37
|
+
// An external PR's label already names it.
|
|
38
|
+
session_name:
|
|
39
|
+
pr_number === null || label.startsWith("PR ")
|
|
40
|
+
? label
|
|
41
|
+
: `${label} · PR #${pr_number}`,
|
|
42
|
+
}));
|
|
43
|
+
const names = Object.fromEntries(listWorkspaces().map((w) => [w.id, w.name]));
|
|
44
|
+
return aggregateUsage(named, range, names, now);
|
|
45
|
+
}
|
package/lib/usage/turns.ts
CHANGED
|
@@ -68,3 +68,31 @@ export function recordTurn(
|
|
|
68
68
|
})();
|
|
69
69
|
return totals;
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
// Records one compaction: who ran it ("auto" is the agent's own, at its
|
|
73
|
+
// auto-compact window; "threshold" and "stale" are AgentOS's before a
|
|
74
|
+
// message; "manual" is a /compact someone typed), and the context before
|
|
75
|
+
// and after when the agent said.
|
|
76
|
+
export function recordCompaction(
|
|
77
|
+
session: Pick<Session, "id" | "name" | "workspace_id">,
|
|
78
|
+
c: { trigger: string; before?: number; after?: number },
|
|
79
|
+
at = Date.now()
|
|
80
|
+
): void {
|
|
81
|
+
const current = db
|
|
82
|
+
.prepare(`SELECT name, workspace_id FROM sessions WHERE id = ?`)
|
|
83
|
+
.get(session.id) as
|
|
84
|
+
| { name: string; workspace_id: string | null }
|
|
85
|
+
| undefined;
|
|
86
|
+
db.prepare(
|
|
87
|
+
`INSERT INTO chat_compactions (session_id, session_name, workspace_id, at, trigger, before_tokens, after_tokens)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
89
|
+
).run(
|
|
90
|
+
session.id,
|
|
91
|
+
current?.name ?? session.name,
|
|
92
|
+
current?.workspace_id ?? session.workspace_id,
|
|
93
|
+
at,
|
|
94
|
+
c.trigger,
|
|
95
|
+
c.before ?? null,
|
|
96
|
+
c.after ?? null
|
|
97
|
+
);
|
|
98
|
+
}
|
package/package.json
CHANGED
package/stores/addProject.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { proxy } from "valtio";
|
|
2
|
+
import { sidebarUi } from "./sidebarUi";
|
|
2
3
|
|
|
3
4
|
export type AddProjectKind = "folder" | "clone" | "name";
|
|
4
5
|
|
|
5
|
-
// The add-project form on show, once a machine and a way are picked.
|
|
6
|
+
// The add-project form on show, once a machine and a way are picked. It
|
|
7
|
+
// adds to the workspace the sidebar is on, changeable in the form.
|
|
6
8
|
export const addProjectUi = proxy<{
|
|
7
|
-
open: {
|
|
9
|
+
open: {
|
|
10
|
+
kind: AddProjectKind;
|
|
11
|
+
hostId: string;
|
|
12
|
+
workspaceId: string | null;
|
|
13
|
+
} | null;
|
|
8
14
|
}>({ open: null });
|
|
9
15
|
|
|
10
16
|
export const addProjectActions = {
|
|
11
17
|
show: (kind: AddProjectKind, hostId: string) => {
|
|
12
|
-
addProjectUi.open = { kind, hostId };
|
|
18
|
+
addProjectUi.open = { kind, hostId, workspaceId: sidebarUi.workspaceId };
|
|
19
|
+
},
|
|
20
|
+
setWorkspace: (workspaceId: string | null) => {
|
|
21
|
+
if (addProjectUi.open) addProjectUi.open.workspaceId = workspaceId;
|
|
13
22
|
},
|
|
14
23
|
close: () => {
|
|
15
24
|
addProjectUi.open = null;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// CI: fail a pull request whose body has no Code review section for its
|
|
2
|
-
// head commit (the same rule the orchestrator's sign_off and stacks' land
|
|
3
|
-
// apply). Reads PR_BODY and HEAD_SHA from the environment.
|
|
4
|
-
import { codeReviewRefusal, parseCodeReview } from "../lib/tasks/code-review";
|
|
5
|
-
|
|
6
|
-
// A head AgentOS rebased itself is covered by the review of the head it
|
|
7
|
-
// rebased, as its gates allow.
|
|
8
|
-
const section = parseCodeReview(process.env.PR_BODY ?? "");
|
|
9
|
-
const refusal = codeReviewRefusal(
|
|
10
|
-
section,
|
|
11
|
-
process.env.HEAD_SHA || undefined,
|
|
12
|
-
section?.restacked
|
|
13
|
-
);
|
|
14
|
-
if (refusal) {
|
|
15
|
-
console.error(`✗ ${refusal}.`);
|
|
16
|
-
console.error(
|
|
17
|
-
"Run /do-code-review on the branch, then put its section in the PR body:\n\n## Code review\nReviewed: <head sha>\nAgents: ...\nFixed: ...\nDeferred: ..."
|
|
18
|
-
);
|
|
19
|
-
process.exit(1);
|
|
20
|
-
}
|
|
21
|
-
console.log(`✓ the Code review section covers ${process.env.HEAD_SHA}`);
|