@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.
Files changed (64) hide show
  1. package/README.md +67 -37
  2. package/app/api/fresh-start/route.ts +9 -5
  3. package/app/api/projects/clone/route.ts +4 -2
  4. package/app/api/projects/init/route.test.ts +46 -0
  5. package/app/api/projects/init/route.ts +24 -4
  6. package/app/api/usage/route.ts +4 -2
  7. package/components/Chat/ChatPanel.tsx +5 -5
  8. package/components/Chat/StaleChip.tsx +7 -6
  9. package/components/CommandPalette/CommandPalette.tsx +3 -1
  10. package/components/CommandPalette/useAppCommands.ts +10 -0
  11. package/components/Projects/AddProject/AddProjectDialog.tsx +25 -5
  12. package/components/Projects/AddProject/CloneForm.tsx +3 -1
  13. package/components/Projects/AddProject/FolderForm.tsx +98 -4
  14. package/components/Projects/AddProject/NameForm.tsx +3 -1
  15. package/components/Projects/AddProject/WorkspacePick.tsx +56 -0
  16. package/components/Projects/AddProject/useAddExistingProject.ts +58 -0
  17. package/components/SessionList/WorkspaceSwitcher.tsx +10 -0
  18. package/components/Usage/UsageDialog.tsx +31 -2
  19. package/components/Workspaces/FreshStartForm.tsx +13 -13
  20. package/data/projects/add.ts +23 -3
  21. package/data/usage/index.ts +2 -0
  22. package/lib/agents/launch.ts +2 -0
  23. package/lib/chat/driver.ts +8 -0
  24. package/lib/chat/drivers/claude-approvals.ts +40 -0
  25. package/lib/chat/drivers/claude.ts +7 -10
  26. package/lib/chat/refresh.ts +31 -13
  27. package/lib/chat/stale.ts +25 -16
  28. package/lib/chat/worker/extras.ts +1 -0
  29. package/lib/chat/worker/host.ts +61 -22
  30. package/lib/db/migrations.ts +42 -0
  31. package/lib/orchestrator/add-project.ts +35 -0
  32. package/lib/orchestrator/armed.ts +1 -2
  33. package/lib/orchestrator/brief.ts +6 -5
  34. package/lib/orchestrator/claude-cli.ts +5 -0
  35. package/lib/orchestrator/diff.ts +6 -2
  36. package/lib/orchestrator/external-gates.ts +8 -43
  37. package/lib/orchestrator/gates.ts +10 -10
  38. package/lib/orchestrator/pr-wake.ts +2 -2
  39. package/lib/orchestrator/rebase.ts +6 -45
  40. package/lib/orchestrator/review.ts +18 -5
  41. package/lib/orchestrator/scope.ts +9 -0
  42. package/lib/orchestrator/serve.ts +6 -0
  43. package/lib/orchestrator/signoff.ts +13 -8
  44. package/lib/orchestrator/tool-names.ts +13 -6
  45. package/lib/orchestrator/tool-schemas.ts +15 -0
  46. package/lib/orchestrator/tools.ts +2 -0
  47. package/lib/orchestrator/worker.ts +4 -1
  48. package/lib/orchestrator/write-guard.ts +295 -0
  49. package/lib/project-add/index.ts +133 -14
  50. package/lib/project-add/name.ts +1 -0
  51. package/lib/secret-paths.ts +9 -0
  52. package/lib/session-audit/render.ts +1 -1
  53. package/lib/stacks/land.ts +23 -16
  54. package/lib/tasks/brief.ts +3 -14
  55. package/lib/tasks/code-review.ts +69 -107
  56. package/lib/tasks/gh.ts +3 -2
  57. package/lib/tasks/index.ts +1 -1
  58. package/lib/tasks/state.ts +4 -4
  59. package/lib/usage/check-runs.ts +167 -0
  60. package/lib/usage/report.ts +25 -0
  61. package/lib/usage/turns.ts +28 -0
  62. package/package.json +1 -1
  63. package/stores/addProject.ts +12 -3
  64. package/scripts/check-code-review.ts +0 -21
@@ -1,27 +1,13 @@
1
- // The "Code review" section every task's PR body carries (the task brief
2
- // asks for it, after /do-code-review). Merges refuse a PR without one for
3
- // its head commit. No imports: CI runs this file on its own
4
- // (scripts/check-code-review.ts).
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 of the section isn't read as the section.
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 last Code review section that names a commit (or the last one, when
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): the head the task
123
- // pushed and reviewed, and the head the restack left.
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
- // Why a PR can't merge on its code review, or null when it can. Fails
130
- // closed: an unread body or an unknown head refuses. A review of the head
131
- // the task pushed still counts after AgentOS restacked it, but only while
132
- // the head is exactly the one the restack left: a push by the task after
133
- // that needs a new review.
134
- export function codeReviewRefusal(
135
- section: CodeReviewSection | null | undefined,
136
- head: string | undefined,
137
- restacked?: Restacked | null,
138
- fix = "the task has to run /do-code-review and put a Code review section naming the reviewed commit in the PR body"
139
- ): string | null {
140
- if (section === undefined) return `the PR body couldn't be read: ${fix}`;
141
- if (!head) return `the PR's head commit is unknown: ${fix}`;
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
- // The body with its Code review section saying AgentOS rebased the
155
- // reviewed head `from` to `to`: a "Restacked:" line right after the
156
- // section's "Reviewed:" line, replacing one already there. Null when the
157
- // body has no section, or its review isn't of `from`.
158
- export function withRestackedLine(
159
- body: string,
160
- from: string,
161
- to: string
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 section = parseCodeReview(body);
164
- if (!section?.sha || !from.toLowerCase().startsWith(section.sha)) return null;
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 { parseCodeReview } from "./code-review";
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
- codeReview: pr.body === undefined ? undefined : parseCodeReview(pr.body),
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
  };
@@ -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 { codeReviewRefusal, parseCodeReview } from "./code-review";
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";
@@ -1,4 +1,4 @@
1
- import type { CodeReviewSection } from "./code-review";
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's Code review section: null when it has none, undefined
26
- // when the body wasn't read.
27
- codeReview?: CodeReviewSection | null;
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
+ }
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.468",
3
+ "version": "0.3.471",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -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: { kind: AddProjectKind; hostId: string } | null;
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}`);