@saadnvd1/agent-os 0.3.819 → 0.3.824

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/app/api/client-config/route.test.ts +3 -1
  2. package/app/api/owner/route.test.ts +27 -0
  3. package/app/api/owner/route.ts +17 -0
  4. package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +2 -2
  5. package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +1 -1
  6. package/app/api/workspaces/[id]/orchestrator/rules/route.ts +2 -2
  7. package/app/api/workspaces/[id]/orchestrator/tools/route.ts +6 -1
  8. package/app/api/worktree-sweep/route.ts +2 -2
  9. package/components/Chat/EventRow.tsx +5 -2
  10. package/components/Chat/Messages.tsx +9 -2
  11. package/components/SessionList/statusDot.test.ts +1 -1
  12. package/components/Settings/OwnerNameRow.tsx +55 -0
  13. package/components/Settings/SetupSection.tsx +2 -0
  14. package/content/whats-new.json +24 -0
  15. package/data/client-config/queries.ts +3 -0
  16. package/data/owner/mutations.ts +19 -0
  17. package/lib/approvals/enable.ts +2 -1
  18. package/lib/artifacts/tools.ts +2 -1
  19. package/lib/away/brief.ts +2 -1
  20. package/lib/away/run.ts +5 -4
  21. package/lib/away/sweep.ts +2 -1
  22. package/lib/chat/drivers/copilot-ask.ts +38 -5
  23. package/lib/chat/runner.ts +2 -1
  24. package/lib/chat/turns.ts +1 -1
  25. package/lib/config/client-config-types.ts +2 -0
  26. package/lib/config/client-config.ts +2 -0
  27. package/lib/learning-cards/analyze.ts +2 -1
  28. package/lib/models/plan-step.ts +2 -1
  29. package/lib/orchestrator/approval-carry.ts +2 -1
  30. package/lib/orchestrator/armed.ts +9 -8
  31. package/lib/orchestrator/ask-heads.ts +2 -1
  32. package/lib/orchestrator/ask-tool.ts +6 -5
  33. package/lib/orchestrator/asks.ts +6 -4
  34. package/lib/orchestrator/brakes.ts +3 -2
  35. package/lib/orchestrator/brief.ts +61 -50
  36. package/lib/orchestrator/cross.ts +2 -1
  37. package/lib/orchestrator/describe.ts +5 -2
  38. package/lib/orchestrator/digest.ts +4 -3
  39. package/lib/orchestrator/escalate.ts +7 -6
  40. package/lib/orchestrator/external-gates.ts +5 -4
  41. package/lib/orchestrator/held-asks.ts +2 -2
  42. package/lib/orchestrator/held.ts +3 -2
  43. package/lib/orchestrator/mcp-servers.ts +1 -1
  44. package/lib/orchestrator/pause.ts +4 -3
  45. package/lib/orchestrator/questions.ts +2 -1
  46. package/lib/orchestrator/recipe-tools.ts +5 -4
  47. package/lib/orchestrator/review.ts +4 -3
  48. package/lib/orchestrator/rules.ts +2 -1
  49. package/lib/orchestrator/serve.ts +1 -1
  50. package/lib/orchestrator/shell-guard.ts +4 -4
  51. package/lib/orchestrator/signoff.test-helpers.ts +1 -1
  52. package/lib/orchestrator/signoff.ts +10 -7
  53. package/lib/orchestrator/sweep-arm.ts +2 -1
  54. package/lib/orchestrator/tool-names.ts +1 -1
  55. package/lib/orchestrator/tool-schemas.ts +13 -13
  56. package/lib/orchestrator/tools.ts +21 -21
  57. package/lib/owner-label.ts +25 -0
  58. package/lib/owner.ts +21 -0
  59. package/lib/project-add/index.ts +2 -1
  60. package/lib/schedules/index.ts +2 -1
  61. package/lib/session-logs/gather.ts +3 -2
  62. package/lib/tasks/queue.ts +3 -1
  63. package/lib/tasks/start.ts +3 -1
  64. package/package.json +1 -1
@@ -12,9 +12,10 @@ import {
12
12
  } from "./gates";
13
13
  import { LOOP_GUARD_COMMITS, stuckBy, stuckWhy } from "./loop-guard";
14
14
  import { addNote } from "./notes";
15
+ import { owner, Owner } from "../owner";
15
16
 
16
17
  // Puts the ask on Saad's list (or notes why it couldn't be).
17
- export function askSaadAbout(
18
+ export function askOwnerAbout(
18
19
  workspaceId: string,
19
20
  task: { id: string; name: string },
20
21
  why: string,
@@ -36,14 +37,14 @@ export function askSaadAbout(
36
37
  if (!(error instanceof AskRefused)) throw error;
37
38
  addNote(
38
39
  workspaceId,
39
- `Saad must decide on ${task.name} (${url}): ${why}. Not added to his asks: ${error.message}.`,
40
+ `${Owner()} must decide on ${task.name} (${url}): ${why}. Not added to his asks: ${error.message}.`,
40
41
  "escalation"
41
42
  );
42
43
  }
43
44
  if (created)
44
45
  addNote(
45
46
  workspaceId,
46
- `Saad must decide on ${task.name} (${url}): ${why}. The orchestrator won't merge it; it's on his asks list to approve, decline, or sign off or drop himself.`,
47
+ `${Owner()} must decide on ${task.name} (${url}): ${why}. The orchestrator won't merge it; it's on his asks list to approve, decline, or sign off or drop himself.`,
47
48
  "escalation"
48
49
  );
49
50
  }
@@ -64,10 +65,10 @@ export function escalate(
64
65
  markEscalated(task.id, gate, why);
65
66
  if (opts.hold) {
66
67
  holdAsk(task.id, gate);
67
- return `Escalated to Saad: ${why}. His ask goes up once ${task.name}'s agent is idle and CI has settled on its latest head, so it names the commit he'd merge; his answer reaches you as an event. Don't retry; say so in your chat.`;
68
+ return `Escalated to ${owner()}: ${why}. His ask goes up once ${task.name}'s agent is idle and CI has settled on its latest head, so it names the commit he'd merge; his answer reaches you as an event. Don't retry; say so in your chat.`;
68
69
  }
69
- askSaadAbout(workspaceId, task, why, url, sha);
70
- return `Escalated to Saad: ${why}. It's on his asks list; his answer reaches you as an event. Don't retry; say so in your chat.`;
70
+ askOwnerAbout(workspaceId, task, why, url, sha);
71
+ return `Escalated to ${owner()}: ${why}. It's on his asks list; his answer reaches you as an event. Don't retry; say so in your chat.`;
71
72
  }
72
73
 
73
74
  // Counts a gate's failure on this head against the task. A failure is the
@@ -30,6 +30,7 @@ import { signOffTask } from "../tasks";
30
30
  import { prFor } from "../tasks/session";
31
31
  import { ciSettleIn } from "./task-state";
32
32
  import { untrusted } from "./untrusted";
33
+ import { owner, Owner } from "../owner";
33
34
 
34
35
  const short = (sha: string) => sha.slice(0, 7);
35
36
 
@@ -124,7 +125,7 @@ export async function signOffExternal(
124
125
  const verdict = await judgeExternal(workspaceId, ext);
125
126
  if (!verdict.ok) throw new Error(verdict.text);
126
127
  if (verdict.approval && !spendApproval(verdict.approval))
127
- throw new Error(`Saad's approval for ${ext.name} was already used`);
128
+ throw new Error(`${Owner()}'s approval for ${ext.name} was already used`);
128
129
  const { method } = mergePolicy(ext.project);
129
130
  try {
130
131
  await mergePR({
@@ -147,7 +148,7 @@ export async function signOffExternal(
147
148
  addNote(
148
149
  workspaceId,
149
150
  verdict.approval
150
- ? `Merged ${ext.name} (not an AgentOS task, ${short(verdict.sha)}) on Saad's approval of that commit.`
151
+ ? `Merged ${ext.name} (not an AgentOS task, ${short(verdict.sha)}) on ${owner()}'s approval of that commit.`
151
152
  : `Merged ${ext.name} (not an AgentOS task, ${short(verdict.sha)}): CI green, review passed, in scope.`
152
153
  );
153
154
  return `Merged ${ext.name}: ${MERGED_AS[method]} at ${short(verdict.sha)}.`;
@@ -164,7 +165,7 @@ export async function reviewExternal(
164
165
  held.length &&
165
166
  !(ext.pr.head && reviewWanted(workspaceId, ext.id, ext.pr.head))
166
167
  )
167
- return `${ext.name} is with Saad (${held.map((h) => h.gate).join(", ")}); no review needed from you.`;
168
+ return `${ext.name} is with ${owner()} (${held.map((h) => h.gate).join(", ")}); no review needed from you.`;
168
169
  if (!ext.pr.head) throw new Error(`${ext.name}'s head commit is unknown`);
169
170
  return reviewAt(
170
171
  workspaceId,
@@ -242,7 +243,7 @@ async function signOffDirect(
242
243
  const task = t.task;
243
244
  if (holdingEscalations(task.id).length)
244
245
  throw new Error(
245
- `${task.name} is with Saad; he signs it off or drops it, a direct merge doesn't go around that`
246
+ `${task.name} is with ${owner()}; he signs it off or drops it, a direct merge doesn't go around that`
246
247
  );
247
248
  const pr = await prFor(task, true);
248
249
  if (!pr || pr.state !== "OPEN" || !pr.head)
@@ -6,7 +6,7 @@
6
6
  import { db, type Session } from "../db";
7
7
  import { prFor } from "../tasks/session";
8
8
  import { latestApproval } from "./ask-approvals";
9
- import { askSaadAbout } from "./escalate";
9
+ import { askOwnerAbout } from "./escalate";
10
10
  import { getCheck } from "./checks";
11
11
  import { openBySubject, workSubject } from "./asks";
12
12
  import { recentlyMessaged } from "./conditions";
@@ -109,6 +109,6 @@ async function raiseFor(
109
109
  ? [`${task.name} was still working after an hour; this is its head now.`]
110
110
  : []),
111
111
  ].join("\n");
112
- askSaadAbout(workspaceId, task, why, pr.url, pr.head);
112
+ askOwnerAbout(workspaceId, task, why, pr.url, pr.head);
113
113
  releaseHeldAsks(taskId);
114
114
  }
@@ -5,6 +5,7 @@ import { AskRefused, raiseAsk, workSubject } from "./asks";
5
5
  import { latestApproval } from "./ask-approvals";
6
6
  import type { FailureRow } from "./gates";
7
7
  import type { Verdict } from "./signoff";
8
+ import { owner, Owner } from "../owner";
8
9
 
9
10
  const short = (sha: string) => sha.slice(0, 7);
10
11
 
@@ -20,7 +21,7 @@ export function heldVerdict(
20
21
  // An approval of this head was judged before this (signoff.ts).
21
22
  const approval = latestApproval(workspaceId, workSubject(task.id));
22
23
  if (approval) {
23
- const why = `Saad approved ${approval.sha ? short(approval.sha) : "an earlier commit"}, but PR #${pr}'s head is now ${short(sha)}${moved ? ` and ${moved}` : ""}`;
24
+ const why = `${Owner()} approved ${approval.sha ? short(approval.sha) : "an earlier commit"}, but PR #${pr}'s head is now ${short(sha)}${moved ? ` and ${moved}` : ""}`;
24
25
  try {
25
26
  raiseAsk({
26
27
  workspaceId,
@@ -46,6 +47,6 @@ export function heldVerdict(
46
47
  return {
47
48
  ok: false,
48
49
  wait: false,
49
- text: `${task.name} is with Saad (${held.map((h) => h.gate).join(", ")}: ${held[0].last_reason}). Only he can merge it now, or approve it on his asks list.${waiting}`,
50
+ text: `${task.name} is with ${owner()} (${held.map((h) => h.gate).join(", ")}: ${held[0].last_reason}). Only he can merge it now, or approve it on his asks list.${waiting}`,
50
51
  };
51
52
  }
@@ -3,7 +3,7 @@
3
3
  * fixed list (tool-names.ts); each workspace adds whole servers, by name, from
4
4
  * Settings → Workspaces. Servers carry their own auth and some send outbound
5
5
  * (a message, an email), so none is allowed unless named; the brief keeps
6
- * those actions as hard lines for ask_saad.
6
+ * those actions as hard lines for ask_owner.
7
7
  *
8
8
  * Only names are ever read from the Claude config: never env, headers, URLs
9
9
  * or tokens.
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { db } from "../db";
10
10
  import { getWorkspace } from "../workspaces";
11
+ import { ownerUi } from "../owner";
11
12
  import { addNote } from "./notes";
12
13
 
13
14
  export function isPaused(workspaceId: string): boolean {
@@ -15,7 +16,7 @@ export function isPaused(workspaceId: string): boolean {
15
16
  }
16
17
 
17
18
  export const PAUSED_REFUSAL =
18
- "Paused by Saad: acting tools refuse until he resumes. You can still read, note and ask_saad; events wait until then.";
19
+ "Paused by the owner: acting tools refuse until they resume. You can still read, note and ask_owner; events wait until then.";
19
20
 
20
21
  export function setPaused(workspaceId: string, paused: boolean): boolean {
21
22
  const changed = db
@@ -29,8 +30,8 @@ export function setPaused(workspaceId: string, paused: boolean): boolean {
29
30
  addNote(
30
31
  workspaceId,
31
32
  paused
32
- ? "Paused by Saad. Events wait and acting tools refuse until he resumes."
33
- : "Resumed by Saad. Events that queued while paused are delivered now.",
33
+ ? `Paused by ${ownerUi()}. Events wait and acting tools refuse until ${ownerUi() === "you" ? "you resume" : "they resume"}.`
34
+ : `Resumed by ${ownerUi()}. Events that queued while paused are delivered now.`,
34
35
  "pause"
35
36
  );
36
37
  return changed === 1;
@@ -9,6 +9,7 @@
9
9
  import { db } from "../db";
10
10
  import type { QuestionView } from "./ask-view";
11
11
  import { workspaceTask } from "./targets";
12
+ import { owner } from "../owner";
12
13
 
13
14
  export const MAX_OPEN_QUESTIONS = 10;
14
15
 
@@ -79,7 +80,7 @@ export function askQuestion(
79
80
  `INSERT INTO orchestrator_questions (workspace_id, text, task) VALUES (?, ?, ?)`
80
81
  )
81
82
  .run(workspaceId, text, task);
82
- return `Question ${lastInsertRowid} is open on Saad's Needs-you strip. Call resolve_question ${lastInsertRowid} once he has answered or it no longer matters.`;
83
+ return `Question ${lastInsertRowid} is open on ${owner()}'s Needs-you strip. Call resolve_question ${lastInsertRowid} once he has answered or it no longer matters.`;
83
84
  }
84
85
 
85
86
  export function resolveQuestion(
@@ -22,6 +22,7 @@ import {
22
22
  type RecipeRun,
23
23
  } from "../recipes";
24
24
  import { outputsText } from "../recipes/result";
25
+ import { owner } from "../owner";
25
26
 
26
27
  const when = (iso: string | null) =>
27
28
  iso
@@ -38,7 +39,7 @@ function runLine(r: RecipeRun, recipe: Recipe): string {
38
39
  ? "timer"
39
40
  : r.trigger === "tool"
40
41
  ? "you"
41
- : "Saad";
42
+ : owner();
42
43
  return `#${r.seq} ${r.status} (${when(r.ended_at ?? r.started_at ?? r.created_at)}, by ${by})${pr}${text}`;
43
44
  }
44
45
 
@@ -61,7 +62,7 @@ export function recipesText(w: string, name?: string): string {
61
62
  return [
62
63
  `Recipe "${r.name}" (${r.kind}, ${r.view}, project ${projectName(w, r)})${r.model ? `, model ${r.model}` : ""}${r.max_minutes ? `, time limit ${r.max_minutes} min` : ""}.`,
63
64
  timer
64
- ? `Timer: ${timer.description} ${timer.timezone}${timer.enabled ? "" : " (off)"}, set by Saad.`
65
+ ? `Timer: ${timer.description} ${timer.timezone}${timer.enabled ? "" : " (off)"}, set by ${owner()}.`
65
66
  : "No timer.",
66
67
  `Brief:\n${r.brief}`,
67
68
  runs.length
@@ -126,7 +127,7 @@ export function saveRecipe(
126
127
  },
127
128
  orchestrator?.id ?? null
128
129
  );
129
- return `Saved recipe "${recipe.name}" (${recipe.kind}, ${project.name}). Run it with run_recipe; Saad sees it in Recipes and sets any timer.`;
130
+ return `Saved recipe "${recipe.name}" (${recipe.kind}, ${project.name}). Run it with run_recipe; ${owner()} sees it in Recipes and sets any timer.`;
130
131
  }
131
132
 
132
133
  export function updateRecipeTool(
@@ -155,5 +156,5 @@ export function updateRecipeTool(
155
156
  },
156
157
  orchestrator?.id ?? null
157
158
  );
158
- return `Updated recipe "${a.new_name ?? recipe.name}". Earlier versions of the brief are kept; Saad sees the change.`;
159
+ return `Updated recipe "${a.new_name ?? recipe.name}". Earlier versions of the brief are kept; ${owner()} sees the change.`;
159
160
  }
@@ -56,6 +56,7 @@ import { noteQuickReview } from "./quick";
56
56
  import { workspaceTask } from "./targets";
57
57
  import { untrusted } from "./untrusted";
58
58
  import { isFast } from "../project-tier/getProjectTier";
59
+ import { owner } from "../owner";
59
60
 
60
61
  const short = (sha: string) => sha.slice(0, 7);
61
62
  // The most diff one reviewer is given, and the most parts a change too big
@@ -135,7 +136,7 @@ async function reviewJob(
135
136
  ...row,
136
137
  kind: "review",
137
138
  status: "error",
138
- detail: `${why}; it's with Saad`,
139
+ detail: `${why}; it's with ${owner()}`,
139
140
  });
140
141
  }
141
142
  const files = await changedFiles(repo, base, sha);
@@ -247,7 +248,7 @@ function countVerdicts(
247
248
  }
248
249
  }
249
250
  return escalated.length
250
- ? `; the ${escalated.join(" and ")} gate is stuck in a loop (the loop guard), so it's with Saad now`
251
+ ? `; the ${escalated.join(" and ")} gate is stuck in a loop (the loop guard), so it's with ${owner()} now`
251
252
  : "";
252
253
  }
253
254
 
@@ -335,7 +336,7 @@ export async function review(
335
336
  const held = holdingEscalations(task.id);
336
337
  const pr = held.length ? await prFor(task, true) : null;
337
338
  if (held.length && !(pr?.head && reviewWanted(workspaceId, task.id, pr.head)))
338
- return `${task.name} is with Saad (${held.map((h) => h.gate).join(", ")}); no review needed from you.`;
339
+ return `${task.name} is with ${owner()} (${held.map((h) => h.gate).join(", ")}); no review needed from you.`;
339
340
  const there = pr ?? (await prFor(task, true));
340
341
  if (!there?.head) throw new Error(`${task.name} has no PR to review yet`);
341
342
  return reviewAt(workspaceId, taskReviewTarget(task, there), there.head, opts);
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { db } from "../db";
11
11
  import { redact } from "./untrusted";
12
+ import { owner } from "../owner";
12
13
 
13
14
  export type RuleAuthor = "orchestrator" | "saad";
14
15
 
@@ -114,7 +115,7 @@ export function removeRule(
114
115
  // read can't talk it into removing one.
115
116
  if (by === "orchestrator" && rule.added_by === "saad")
116
117
  throw new Error(
117
- `Rule ${id} is Saad's: only he removes it, in Settings → Workspaces`
118
+ `Rule ${id} is ${owner()}'s: only he removes it, in Settings → Workspaces`
118
119
  );
119
120
  db.prepare(
120
121
  `UPDATE orchestrator_rules SET removed_at = datetime('now'), removed_by = ?
@@ -320,7 +320,7 @@ export async function runTool(
320
320
  return await arm(w, p(tool, raw).task);
321
321
  case "disarm":
322
322
  return disarm(w, p(tool, raw).task);
323
- case "ask_saad":
323
+ case "ask_owner":
324
324
  return askSaad(w, p(tool, raw));
325
325
  case "question": {
326
326
  const a = p(tool, raw);
@@ -230,7 +230,7 @@ function aosRefusal(args: string[]): string | null {
230
230
  stop: T.stop,
231
231
  };
232
232
  if (sub === "sweep" && args.includes("--arm"))
233
- return `Refused: \`aos sweep --arm\` turns on removing files and databases, which is Saad's call: Health, Clean-up, Turn on. When he has said to, use ${T.sweep_arm} (after ${T.sweep_dry_run}). \`--dry-run\` and \`--disarm\` are fine.`;
233
+ return `Refused: \`aos sweep --arm\` turns on removing files and databases, which is the owner's call: Health, Clean-up, Turn on. When he has said to, use ${T.sweep_arm} (after ${T.sweep_dry_run}). \`--dry-run\` and \`--disarm\` are fine.`;
234
234
  if (sub && tool[sub])
235
235
  return `Refused: \`aos ${sub}\` isn't for you; use ${tool[sub]}, which is scoped to this workspace and held by Pause.`;
236
236
  return null;
@@ -269,7 +269,7 @@ function ghRefusal(args: string[]): string | null {
269
269
  sub === "edit" &&
270
270
  optionValues(args, ["-B"], ["--base"]).length
271
271
  )
272
- return `Refused: retargeting a PR changes what its gates judged; a stack sets its own bases (${T.stack}), and anything else is Saad's call (${T.ask_saad}).`;
272
+ return `Refused: retargeting a PR changes what its gates judged; a stack sets its own bases (${T.stack}), and anything else is the owner's call (${T.ask_owner}).`;
273
273
  if (group !== "api") return null;
274
274
  const endpoint = positionals(args, values)[1] ?? "";
275
275
  const method = optionValues(args, ["-X"], ["--method"]).pop();
@@ -494,7 +494,7 @@ async function sqliteRefusal(
494
494
  }
495
495
 
496
496
  const SERVER_PATTERN = /node|tsx|server|agent-?os|tmux|next/i;
497
- const STOP_MSG = `Refused: never stop or restart AgentOS or its tmux; that ends every session it runs, this one included. Stop a session with ${T.stop} or finish it with ${T.done}, and if AgentOS itself needs a restart, ${T.ask_saad}.`;
497
+ const STOP_MSG = `Refused: never stop or restart AgentOS or its tmux; that ends every session it runs, this one included. Stop a session with ${T.stop} or finish it with ${T.done}, and if AgentOS itself needs a restart, ${T.ask_owner}.`;
498
498
 
499
499
  async function stopRefusal(
500
500
  inv: Invocation,
@@ -538,7 +538,7 @@ async function stopRefusal(
538
538
  return null;
539
539
  }
540
540
 
541
- const SECRET_MSG = `Refused: orchestrators never read secrets (env files, keys, credential stores). If a step needs one, ${T.ask_saad}.`;
541
+ const SECRET_MSG = `Refused: orchestrators never read secrets (env files, keys, credential stores). If a step needs one, ${T.ask_owner}.`;
542
542
 
543
543
  // Orchestrators may read tokenvault's folders (and run `tv`); writing them
544
544
  // is still refused by the write guard.
@@ -118,7 +118,7 @@ export const { followAskVerdicts } = await import("./ask-verdict");
118
118
  export const { raiseHeldAsks, HOLD_MIN_MS, HOLD_MAX_MS } =
119
119
  await import("./held-asks");
120
120
 
121
- // The one ask a task's escalation leaves on Saad's list.
121
+ // The one ask a task's escalation leaves on the owner's list.
122
122
  export function oneGateAsk(w: string, task: string, sha: string) {
123
123
  const asks = openAsks(w);
124
124
  expect(asks).toHaveLength(1);
@@ -58,6 +58,7 @@ import { repoHasCI } from "./repo-ci";
58
58
  import { DIFF_CAP, review } from "./review";
59
59
  import { workspaceStack, workspaceTask } from "./targets";
60
60
  import { ciSettleIn, waitingState } from "./task-state";
61
+ import { owner, Owner } from "../owner";
61
62
 
62
63
  const short = (sha: string) => sha.slice(0, 7);
63
64
 
@@ -214,7 +215,7 @@ export async function gateVerdict(
214
215
  }
215
216
  if (approval?.sha === sha) {
216
217
  const i = await inputs();
217
- const approved = `${work.name} (PR #${pr.number} at ${short(sha)}): Saad approved this commit, but`;
218
+ const approved = `${work.name} (PR #${pr.number} at ${short(sha)}): ${owner()} approved this commit, but`;
218
219
  const stack =
219
220
  approval.raised_by === "orchestrator" || !held.length
220
221
  ? i.stackRefusal
@@ -268,10 +269,10 @@ export async function gateVerdict(
268
269
  { fast }
269
270
  );
270
271
  const head = `${work.name} (PR #${pr.number} at ${short(sha)}) can't merge:`;
271
- const toSaad = outcomes.find((o) => o.state === "escalate");
272
- if (toSaad)
272
+ const toOwner = outcomes.find((o) => o.state === "escalate");
273
+ if (toOwner)
273
274
  return no(
274
- `${head}\n- ${escalate(workspaceId, work, toSaad.gate, toSaad.reason!, pr.url, sha)}`
275
+ `${head}\n- ${escalate(workspaceId, work, toOwner.gate, toOwner.reason!, pr.url, sha)}`
275
276
  );
276
277
  const failed = outcomes.filter((o) => o.state === "fail");
277
278
  const waiting = outcomes.filter((o) => o.state === "wait");
@@ -354,7 +355,7 @@ export async function mergeJudged(
354
355
  opts: { wait?: boolean } = {}
355
356
  ): Promise<string> {
356
357
  if (verdict.approval && !spendApproval(verdict.approval))
357
- throw new Error(`Saad's approval for ${task.name} was already used`);
358
+ throw new Error(`${Owner()}'s approval for ${task.name} was already used`);
358
359
  let method: MergeMethod | null = null;
359
360
  await refundIfRefused(task.id, verdict.approval, async () => {
360
361
  method = await signOffTask(task.id, { head: verdict.sha, wait: opts.wait });
@@ -366,7 +367,7 @@ export async function mergeJudged(
366
367
  verdict.fast
367
368
  ? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on the fast lane: no CI wait or review.`
368
369
  : verdict.approval
369
- ? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on Saad's approval of that commit.`
370
+ ? `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}) on ${owner()}'s approval of that commit.`
370
371
  : `Merged ${task.name} (PR #${verdict.pr}, ${short(verdict.sha)}): CI green, review passed, in scope.`
371
372
  );
372
373
  return `Merged ${task.name}: PR #${verdict.pr} ${method ? MERGED_AS[method] : "merged"} at ${short(verdict.sha)}.`;
@@ -408,7 +409,9 @@ export function landGate(
408
409
  const verdict = await judge(workspaceId, task, true);
409
410
  if (verdict.ok && verdict.approval) {
410
411
  if (!spendApproval(verdict.approval))
411
- return { stop: `Saad's approval for ${task.name} was already used` };
412
+ return {
413
+ stop: `${Owner()}'s approval for ${task.name} was already used`,
414
+ };
412
415
  claimed.set(sessionId, verdict.approval);
413
416
  }
414
417
  if (verdict.ok) return { head: verdict.sha };
@@ -24,6 +24,7 @@ import type { SweepRun } from "../worktree-sweep/types";
24
24
  import { addNote } from "./notes";
25
25
  import { PAUSED_REFUSAL } from "./pause";
26
26
  import { getWorkspace } from "../workspaces";
27
+ import { owner } from "../owner";
27
28
 
28
29
  export const DRY_RUN_FRESH_MS = 60 * 60 * 1000;
29
30
 
@@ -165,7 +166,7 @@ export async function sweepArm(
165
166
 
166
167
  export async function sweepDryRun(machine: string): Promise<string> {
167
168
  const m = machineOf(machine);
168
- const started = `Dry run started on ${m.name}; it takes a few minutes and removes nothing. When Saad has said to turn the full clean-up on, call sweep_arm: it reads this run.`;
169
+ const started = `Dry run started on ${m.name}; it takes a few minutes and removes nothing. When ${owner()} has said to turn the full clean-up on, call sweep_arm: it reads this run.`;
169
170
  if (m.link) {
170
171
  await hostApi(m.link, "/api/worktree-sweep/run", {
171
172
  body: { dryRun: true },
@@ -58,7 +58,7 @@ export const TOOL_NAMES = {
58
58
  recipe_update: name("recipe_update"),
59
59
  reminders: name("reminders"),
60
60
  cancel_reminder: name("cancel_reminder"),
61
- ask_saad: name("ask_saad"),
61
+ ask_owner: name("ask_owner"),
62
62
  } as const;
63
63
 
64
64
  // What it may do, fixed by its role. It acts through its own tools, which
@@ -29,7 +29,7 @@ const group = z
29
29
  .max(60)
30
30
  .optional()
31
31
  .describe(
32
- "One of Saad's groups in this workspace, by name (see sessions): file it there when he asked, or when it continues work already in that group. It must exist; you can't create groups"
32
+ "One of the owner's groups in this workspace, by name (see sessions): file it there when he asked, or when it continues work already in that group. It must exist; you can't create groups"
33
33
  );
34
34
 
35
35
  const sweepMachine = z
@@ -96,7 +96,7 @@ export const TOOL_SHAPES = {
96
96
  .boolean()
97
97
  .optional()
98
98
  .describe(
99
- "The message changes the task's scope (Saad added, dropped or changed what it must do): it's recorded on the task's brief, and its review judges the PR against the brief as amended. Only for a change Saad made, never one the task asked for itself"
99
+ "The message changes the task's scope (the owner added, dropped or changed what it must do): it's recorded on the task's brief, and its review judges the PR against the brief as amended. Only for a change the owner made, never one the task asked for itself"
100
100
  ),
101
101
  },
102
102
  orchestrators: {},
@@ -139,7 +139,7 @@ export const TOOL_SHAPES = {
139
139
  .enum(PRIORITIES)
140
140
  .optional()
141
141
  .describe(
142
- "Where it stands in the queue: urgent (a bug Saad hit), blocking (something waits on it), normal (the default), background"
142
+ "Where it stands in the queue: urgent (a bug the owner hit), blocking (something waits on it), normal (the default), background"
143
143
  ),
144
144
  machine: z
145
145
  .enum(["mac", "any"])
@@ -159,7 +159,7 @@ export const TOOL_SHAPES = {
159
159
  .min(1)
160
160
  .max(300)
161
161
  .optional()
162
- .describe("With now: why it can't wait for a slot; logged for Saad"),
162
+ .describe("With now: why it can't wait for a slot; logged for the owner"),
163
163
  ignore_claims: z
164
164
  .boolean()
165
165
  .optional()
@@ -195,7 +195,7 @@ export const TOOL_SHAPES = {
195
195
  .max(200)
196
196
  .optional()
197
197
  .describe(
198
- 'Why, in a few words Saad reads on the task card: "touches 6 files across gates and UI", "one-line default change"'
198
+ 'Why, in a few words the owner reads on the task card: "touches 6 files across gates and UI", "one-line default change"'
199
199
  ),
200
200
  },
201
201
  set_task_model: {
@@ -238,7 +238,7 @@ export const TOOL_SHAPES = {
238
238
  .max(60)
239
239
  .nullable()
240
240
  .describe(
241
- "One of Saad's groups in this workspace by name, or null to take the session out of its group"
241
+ "One of the owner's groups in this workspace by name, or null to take the session out of its group"
242
242
  ),
243
243
  },
244
244
  quick_change: {
@@ -282,7 +282,7 @@ export const TOOL_SHAPES = {
282
282
  .max(8000)
283
283
  .optional()
284
284
  .describe(
285
- "Amends the plan: recorded on the task's brief as a scope change, so its review judges the PR against it. Only for a change Saad made"
285
+ "Amends the plan: recorded on the task's brief as a scope change, so its review judges the PR against it. Only for a change the owner made"
286
286
  ),
287
287
  },
288
288
  plan_pieces: {
@@ -317,7 +317,7 @@ export const TOOL_SHAPES = {
317
317
  .trim()
318
318
  .min(1)
319
319
  .max(500)
320
- .describe("What Saad said, and when; it goes in the decision log"),
320
+ .describe("What the owner said, and when; it goes in the decision log"),
321
321
  },
322
322
  set_priority: {
323
323
  task: ref("A queued or running task: name, project/name or id"),
@@ -330,14 +330,14 @@ export const TOOL_SHAPES = {
330
330
  .trim()
331
331
  .min(1)
332
332
  .max(300)
333
- .describe("Why it can't wait for a slot; logged for Saad"),
333
+ .describe("Why it can't wait for a slot; logged for the owner"),
334
334
  },
335
335
  pause_task: {
336
336
  task,
337
337
  for: z
338
338
  .enum(["1h", "2h", "tomorrow", "until_resumed"])
339
339
  .describe(
340
- "How long: 1h, 2h, tomorrow (9 AM, Saad's time) or until_resumed (until you or Saad resume it)"
340
+ "How long: 1h, 2h, tomorrow (9 AM, the owner's time) or until_resumed (until you or the owner resume it)"
341
341
  ),
342
342
  now: z
343
343
  .boolean()
@@ -353,7 +353,7 @@ export const TOOL_SHAPES = {
353
353
  archive: { session },
354
354
  resurrect: {
355
355
  session: ref(
356
- "A dropped, done or archived session: its name, project/name or id (from the decision log, or as Saad named it)"
356
+ "A dropped, done or archived session: its name, project/name or id (from the decision log, or as the owner named it)"
357
357
  ),
358
358
  reason: z.string().trim().min(1).max(2000),
359
359
  },
@@ -481,13 +481,13 @@ export const TOOL_SHAPES = {
481
481
  },
482
482
  arm: { task },
483
483
  disarm: { task },
484
- ask_saad: {
484
+ ask_owner: {
485
485
  title: z
486
486
  .string()
487
487
  .trim()
488
488
  .min(1)
489
489
  .max(200)
490
- .describe("What Saad decides, as a short question"),
490
+ .describe("What the owner decides, as a short question"),
491
491
  detail: z
492
492
  .string()
493
493
  .trim()