@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
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { db } from "../db";
|
|
14
|
+
import type { CodeReviewGate } from "../tasks/code-review";
|
|
14
15
|
import type { ChecksVerdict } from "../tasks/state";
|
|
15
16
|
import type { CheckRow } from "./checks";
|
|
16
17
|
import { APPROVAL_GATES, mergeApprovalsOn } from "./merge-approvals";
|
|
@@ -45,11 +46,9 @@ export interface GateInput {
|
|
|
45
46
|
fromCard: boolean;
|
|
46
47
|
scope: CheckRow | null;
|
|
47
48
|
stackRefusal: string | null;
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// of this commit stands in for the section).
|
|
52
|
-
codeReviewWait?: string | null;
|
|
49
|
+
// Whether a passing independent review covers this commit, and the body
|
|
50
|
+
// carries no AI attribution (tasks/code-review.ts).
|
|
51
|
+
codeReview: CodeReviewGate;
|
|
53
52
|
// A PR no task owns has no session to read: a draft isn't ready yet, and
|
|
54
53
|
// a label saying it's blocked holds it.
|
|
55
54
|
prDraft?: string | null;
|
|
@@ -147,11 +146,12 @@ export function evaluateGates(i: GateInput): GateOutcome[] {
|
|
|
147
146
|
const stack: GateOutcome = i.stackRefusal
|
|
148
147
|
? { gate: "stack", state: "fail", reason: i.stackRefusal }
|
|
149
148
|
: { gate: "stack", state: "pass" };
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
// Its wait is the review gate's own (no passing review of this exact
|
|
150
|
+
// head), which already holds the merge: only paths without that gate (an
|
|
151
|
+
// approval, a stack's land) read it.
|
|
152
|
+
const codeReview: GateOutcome = i.codeReview.refusal
|
|
153
|
+
? { gate: "code-review", state: "fail", reason: i.codeReview.refusal }
|
|
154
|
+
: { gate: "code-review", state: "pass" };
|
|
155
155
|
return [
|
|
156
156
|
ciGate(i),
|
|
157
157
|
checkGate("review", i.review, i.sha),
|
|
@@ -272,7 +272,7 @@ export function checkMessage(
|
|
|
272
272
|
: pr.failingUrl
|
|
273
273
|
? `\n\nIts details: ${untrusted("CI", pr.failingUrl)}`
|
|
274
274
|
: "";
|
|
275
|
-
return `CI check ${untrusted("CI", pr.failing ?? "")} failed on your PR #${pr.number} at ${short(pr.head)}${step}.${tail}\n\n${UNTRUSTED_RULE}\n\nFix the root cause (not the check), re-run your checks,
|
|
275
|
+
return `CI check ${untrusted("CI", pr.failing ?? "")} failed on your PR #${pr.number} at ${short(pr.head)}${step}.${tail}\n\n${UNTRUSTED_RULE}\n\nFix the root cause (not the check), re-run your checks, then push.`;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
278
|
// True once the PR's activity at this fingerprint has been handled.
|
|
@@ -374,7 +374,7 @@ export function reviewMessage(pr: TaskPR, events: PREvent[]): string {
|
|
|
374
374
|
: "";
|
|
375
375
|
return `- ${e.author} ${what}${body}${notes}${more}`;
|
|
376
376
|
});
|
|
377
|
-
return `New review activity on your PR #${pr.number}:\n${items.join("\n")}\n\n${UNTRUSTED_RULE} Address each point: fix what's right, re-run your checks,
|
|
377
|
+
return `New review activity on your PR #${pr.number}:\n${items.join("\n")}\n\n${UNTRUSTED_RULE} Address each point: fix what's right, re-run your checks, then push. Where you disagree, reply on the PR (\`gh pr comment ${pr.number}\`) saying why.`;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
// Tests only.
|
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
* only while its agent is idle with nothing uncommitted, and pushed to the
|
|
9
9
|
* task's own branch with a lease on the commit fetched (the restack's code,
|
|
10
10
|
* stacks/git.ts). The base itself and every other branch are never written.
|
|
11
|
-
* The heads are recorded
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* head carries over; otherwise a fresh one starts.
|
|
11
|
+
* The heads are recorded, and when the PR's own patch is unchanged (same
|
|
12
|
+
* `git patch-id` against the base before and after), the orchestrator's
|
|
13
|
+
* review of the old head carries over to the new one (tasks/code-review.ts);
|
|
14
|
+
* otherwise a fresh one starts.
|
|
16
15
|
*
|
|
17
16
|
* Not clean, or not rebasable (uncommitted work, commits only on origin):
|
|
18
17
|
* the task is asked once, per head, to rebase by hand. The orchestrator
|
|
@@ -28,7 +27,6 @@ import { hostLink } from "../hosts/remote-api";
|
|
|
28
27
|
import type { TaskPR } from "../tasks/state";
|
|
29
28
|
import { run } from "../tasks/gh";
|
|
30
29
|
import { git as runGit, gitFetch, revParse } from "../git-exec";
|
|
31
|
-
import { withRestackedLine } from "../tasks/code-review";
|
|
32
30
|
import { outputOf, type Runner } from "../stacks/git";
|
|
33
31
|
import { restackedHeads, stackNotice } from "../stacks/restack";
|
|
34
32
|
import { getCheck, putCheck } from "./checks";
|
|
@@ -53,37 +51,6 @@ export interface RebaseDeps {
|
|
|
53
51
|
pr: TaskPR,
|
|
54
52
|
sha: string
|
|
55
53
|
) => Promise<unknown>;
|
|
56
|
-
// Add the Restacked line to the PR body's Code review section.
|
|
57
|
-
editBody: (
|
|
58
|
-
repo: string,
|
|
59
|
-
pr: number,
|
|
60
|
-
from: string,
|
|
61
|
-
to: string
|
|
62
|
-
) => Promise<void>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async function editBody(repo: string, pr: number, from: string, to: string) {
|
|
66
|
-
const body = await run(
|
|
67
|
-
"gh",
|
|
68
|
-
["pr", "view", String(pr), "--json", "body", "--jq", ".body"],
|
|
69
|
-
repo,
|
|
70
|
-
15000
|
|
71
|
-
);
|
|
72
|
-
const next = withRestackedLine(body.replace(/\n$/, ""), from, to);
|
|
73
|
-
if (!next) return;
|
|
74
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-body-"));
|
|
75
|
-
const file = path.join(dir, "body.md");
|
|
76
|
-
try {
|
|
77
|
-
fs.writeFileSync(file, next);
|
|
78
|
-
await run(
|
|
79
|
-
"gh",
|
|
80
|
-
["pr", "edit", String(pr), "--body-file", file],
|
|
81
|
-
repo,
|
|
82
|
-
30000
|
|
83
|
-
);
|
|
84
|
-
} finally {
|
|
85
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
86
|
-
}
|
|
87
54
|
}
|
|
88
55
|
|
|
89
56
|
const defaults: RebaseDeps = {
|
|
@@ -94,7 +61,6 @@ const defaults: RebaseDeps = {
|
|
|
94
61
|
idle: async (task) => (await statusOf(task)).status === "idle",
|
|
95
62
|
review: (w, task, pr, sha) =>
|
|
96
63
|
reviewAt(w, taskReviewTarget(task, { ...pr, head: sha }), sha),
|
|
97
|
-
editBody,
|
|
98
64
|
};
|
|
99
65
|
|
|
100
66
|
// The (head, base tip) pairs already rebased or found not to need it, so a
|
|
@@ -263,11 +229,6 @@ async function rebaseOne(
|
|
|
263
229
|
deps
|
|
264
230
|
);
|
|
265
231
|
}
|
|
266
|
-
if (heads.restacked_from && heads.restacked_to)
|
|
267
|
-
await deps
|
|
268
|
-
.editBody(repo, pr.number, heads.restacked_from, heads.restacked_to)
|
|
269
|
-
.catch(() => {});
|
|
270
|
-
|
|
271
232
|
// The task's worktree follows while it's still idle and untouched;
|
|
272
233
|
// otherwise the task is told how to catch up.
|
|
273
234
|
const follows =
|
|
@@ -277,7 +238,7 @@ async function rebaseOne(
|
|
|
277
238
|
if (!follows)
|
|
278
239
|
await deps.send(
|
|
279
240
|
task.id,
|
|
280
|
-
`AgentOS rebased your branch ${task.branch_name} onto ${branch} on origin (now ${short(to)}), because PR #${pr.number} conflicted with it. Your worktree is still on the old head: before changing anything, run \`git fetch origin && git reset --keep origin/${task.branch_name}\` (commit your work first).
|
|
241
|
+
`AgentOS rebased your branch ${task.branch_name} onto ${branch} on origin (now ${short(to)}), because PR #${pr.number} conflicted with it. Your worktree is still on the old head: before changing anything, run \`git fetch origin && git reset --keep origin/${task.branch_name}\` (commit your work first). Its review carries over when its own patch didn't change.`
|
|
281
242
|
);
|
|
282
243
|
const reviewed = await carryReview(workspaceId, task, pr, onto, to, deps);
|
|
283
244
|
queueEvent(
|
|
@@ -422,6 +383,6 @@ async function askToRebase(
|
|
|
422
383
|
);
|
|
423
384
|
await deps.send(
|
|
424
385
|
task.id,
|
|
425
|
-
`Your PR #${pr.number} conflicts with ${base}, and AgentOS couldn't rebase it for you: ${why.slice(0, 300)}. At your next stopping point: commit your work, run \`git fetch origin && git rebase origin/${base}\`, resolve the conflicts, re-run your checks, push with \`git push --force-with-lease
|
|
386
|
+
`Your PR #${pr.number} conflicts with ${base}, and AgentOS couldn't rebase it for you: ${why.slice(0, 300)}. At your next stopping point: commit your work, run \`git fetch origin && git rebase origin/${base}\`, resolve the conflicts, re-run your checks, push with \`git push --force-with-lease\`. The orchestrator reviews the new head.`
|
|
426
387
|
);
|
|
427
388
|
}
|
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
toVerdict,
|
|
39
39
|
} from "./review-prompt";
|
|
40
40
|
import { checkScope } from "./scope";
|
|
41
|
+
import {
|
|
42
|
+
checkRunRecorder,
|
|
43
|
+
checkRunTotals,
|
|
44
|
+
describeCheckRuns,
|
|
45
|
+
} from "../usage/check-runs";
|
|
41
46
|
import { workspaceTask } from "./targets";
|
|
42
47
|
import { untrusted } from "./untrusted";
|
|
43
48
|
|
|
@@ -52,10 +57,8 @@ export function describeReview(row: CheckRow): string {
|
|
|
52
57
|
if (row.status === "running") return `The review of ${at} is still running.`;
|
|
53
58
|
if (row.status === "error")
|
|
54
59
|
return `The review of ${at} couldn't run: ${row.detail}`;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
? `Review of ${at}: pass.`
|
|
58
|
-
: `Review of ${at}: blocking findings.`;
|
|
60
|
+
const cost = checkRunTotals(row.session_id, row.sha);
|
|
61
|
+
const head = `${row.status === "pass" ? `Review of ${at}: pass.` : `Review of ${at}: blocking findings.`}${cost ? ` (${describeCheckRuns(cost)})` : ""}`;
|
|
59
62
|
return row.detail ? `${head}\n${untrusted("reviewer", row.detail)}` : head;
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -115,6 +118,14 @@ async function reviewJob(
|
|
|
115
118
|
const amendments = task ? amendmentsOf(task.id).map((a) => a.text) : [];
|
|
116
119
|
const scope = task ? { amendments, scopeClaim: pr.scopeChange ?? null } : {};
|
|
117
120
|
const dir = await checkout(repo, sha);
|
|
121
|
+
const onUsage = checkRunRecorder({
|
|
122
|
+
workspaceId,
|
|
123
|
+
sessionId: target.id,
|
|
124
|
+
label: target.label,
|
|
125
|
+
prNumber: pr.number,
|
|
126
|
+
sha,
|
|
127
|
+
kind: "review",
|
|
128
|
+
});
|
|
118
129
|
try {
|
|
119
130
|
const verdicts = [];
|
|
120
131
|
for (const [i, part] of parts.entries()) {
|
|
@@ -143,6 +154,7 @@ async function reviewJob(
|
|
|
143
154
|
schema: REVIEW_SCHEMA,
|
|
144
155
|
tools: ["Read", "Grep", "Glob"],
|
|
145
156
|
allow: [`Read(/${dir}/**)`, `Grep(/${dir}/**)`, `Glob(/${dir}/**)`],
|
|
157
|
+
onUsage,
|
|
146
158
|
});
|
|
147
159
|
verdicts.push(toVerdict(answer));
|
|
148
160
|
}
|
|
@@ -246,11 +258,12 @@ export async function reviewAt(
|
|
|
246
258
|
: done.status === "block"
|
|
247
259
|
? "found blocking issues"
|
|
248
260
|
: "couldn't run";
|
|
261
|
+
const cost = checkRunTotals(target.id, sha);
|
|
249
262
|
queueEvent(
|
|
250
263
|
workspaceId,
|
|
251
264
|
`review:${target.id}:${sha}`,
|
|
252
265
|
target.id,
|
|
253
|
-
`${target.label}: review of ${short(sha)} ${said}${countVerdicts(workspaceId, target, sha)}`
|
|
266
|
+
`${target.label}: review of ${short(sha)} ${said}${cost ? ` (${describeCheckRuns(cost)})` : ""}${countVerdicts(workspaceId, target, sha)}`
|
|
254
267
|
);
|
|
255
268
|
return done;
|
|
256
269
|
});
|
|
@@ -8,6 +8,7 @@ import { connectedClient } from "../lumifyhub/connection";
|
|
|
8
8
|
import { promptFromCard } from "../lumifyhub/task-cards";
|
|
9
9
|
import { putCheck, type CheckRow } from "./checks";
|
|
10
10
|
import type { ClaudeRunner } from "./claude-cli";
|
|
11
|
+
import { checkRunRecorder } from "../usage/check-runs";
|
|
11
12
|
import type { ChangedFile } from "./diff";
|
|
12
13
|
import { fence } from "./review-prompt";
|
|
13
14
|
|
|
@@ -78,6 +79,14 @@ export async function checkScope(input: {
|
|
|
78
79
|
prompt: `The card, as data:\n${fence("card", card)}${amended}\n\nFiles changed:\n${list}\n\n${label}\n${fence("diff", part)}`,
|
|
79
80
|
schema: SCOPE_SCHEMA,
|
|
80
81
|
tools: [],
|
|
82
|
+
onUsage: checkRunRecorder({
|
|
83
|
+
workspaceId,
|
|
84
|
+
sessionId: task.id,
|
|
85
|
+
label: `task ${task.name}`,
|
|
86
|
+
prNumber: task.pr_number ?? null,
|
|
87
|
+
sha,
|
|
88
|
+
kind: "scope",
|
|
89
|
+
}),
|
|
81
90
|
})) as { within?: boolean; reason?: string };
|
|
82
91
|
if (answer.within !== true) break;
|
|
83
92
|
}
|
|
@@ -15,6 +15,7 @@ import { sessionFacts } from "./facts";
|
|
|
15
15
|
import { describeExternal, externalSessions } from "./external-sessions";
|
|
16
16
|
import { resolveStaleAsks } from "./ask-settle";
|
|
17
17
|
import { addNote } from "./notes";
|
|
18
|
+
import { addProject } from "./add-project";
|
|
18
19
|
import { addRule, removeRule } from "./rules";
|
|
19
20
|
import { askSaad } from "./ask-tool";
|
|
20
21
|
import { listOrchestratorPeers, messageOrchestrator } from "./cross";
|
|
@@ -41,6 +42,7 @@ const ACTING = new Set<ToolName>([
|
|
|
41
42
|
"review",
|
|
42
43
|
"sign_off",
|
|
43
44
|
"arm",
|
|
45
|
+
"add_project",
|
|
44
46
|
]);
|
|
45
47
|
|
|
46
48
|
// The orchestrator's tools, each scoped to its workspace and its arguments
|
|
@@ -120,6 +122,10 @@ export async function runTool(
|
|
|
120
122
|
return stop(w, p(tool, raw).session);
|
|
121
123
|
case "done":
|
|
122
124
|
return done(w, p(tool, raw).session);
|
|
125
|
+
case "add_project": {
|
|
126
|
+
const a = p(tool, raw);
|
|
127
|
+
return addProject(w, a.path, a.name);
|
|
128
|
+
}
|
|
123
129
|
case "note":
|
|
124
130
|
addNote(w, p(tool, raw).text);
|
|
125
131
|
return "Noted.";
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { MERGED_AS, type MergeMethod } from "../tasks/merge-policy";
|
|
11
11
|
import { prFor } from "../tasks/session";
|
|
12
|
-
import {
|
|
12
|
+
import { codeReviewGate, signOffTask } from "../tasks";
|
|
13
13
|
import { signOffRefusal } from "../stacks/guard";
|
|
14
14
|
import { getStack, landInBackground } from "../stacks";
|
|
15
15
|
import { LAND_DEFAULTS } from "../stacks/land";
|
|
@@ -91,11 +91,12 @@ export async function judge(
|
|
|
91
91
|
fromCard: !!task.lh_card_id,
|
|
92
92
|
scope: getCheck(task.id, sha, "scope"),
|
|
93
93
|
stackRefusal: landing ? null : signOffRefusal(task.id),
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
codeReview: codeReviewGate({
|
|
95
|
+
head: sha,
|
|
96
|
+
attribution: pr.attribution,
|
|
97
|
+
review: (at) => getCheck(task.id, at, "review"),
|
|
98
|
+
restacked: restackedOf(task),
|
|
99
|
+
}),
|
|
99
100
|
}),
|
|
100
101
|
opts
|
|
101
102
|
);
|
|
@@ -129,7 +130,8 @@ export async function gateVerdict(
|
|
|
129
130
|
const held = holdingEscalations(work.id);
|
|
130
131
|
// Saad approved merging this exact commit. On an ask the orchestrator
|
|
131
132
|
// raised (ask_saad with its task and sha), held or not, it merges once
|
|
132
|
-
// whatever the gates say, but only
|
|
133
|
+
// whatever the gates say, but only once an independent review of that
|
|
134
|
+
// commit finished (its findings are his to override, not that it ran) and
|
|
133
135
|
// its stack parent merged: the orchestrator wrote that ask, and no answer
|
|
134
136
|
// to it stands in for those. An approval of the gates' own ask on a held
|
|
135
137
|
// task merges as it always has (heldVerdict).
|
|
@@ -139,7 +141,10 @@ export async function gateVerdict(
|
|
|
139
141
|
(approval.raised_by === "orchestrator" || !held.length)
|
|
140
142
|
) {
|
|
141
143
|
const i = await inputs();
|
|
142
|
-
const refusal =
|
|
144
|
+
const refusal =
|
|
145
|
+
i.codeReview.refusal ??
|
|
146
|
+
(i.codeReview.ran ? null : i.codeReview.wait) ??
|
|
147
|
+
i.stackRefusal;
|
|
143
148
|
if (refusal)
|
|
144
149
|
return no(
|
|
145
150
|
`${work.name} (PR #${pr.number} at ${short(sha)}): Saad approved this commit, but ${refusal}`
|
|
@@ -21,6 +21,7 @@ export const TOOL_NAMES = {
|
|
|
21
21
|
drop: name("drop"),
|
|
22
22
|
stop: name("stop"),
|
|
23
23
|
done: name("done"),
|
|
24
|
+
add_project: name("add_project"),
|
|
24
25
|
note: name("note"),
|
|
25
26
|
rule_add: name("rule_add"),
|
|
26
27
|
rule_remove: name("rule_remove"),
|
|
@@ -32,11 +33,13 @@ export const TOOL_NAMES = {
|
|
|
32
33
|
} as const;
|
|
33
34
|
|
|
34
35
|
// What it may do, fixed by its role: it reads other sessions' text, so it
|
|
35
|
-
// never gets a general shell
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
36
|
+
// never gets a general shell. Anything not listed is refused without asking.
|
|
37
|
+
// It acts through its own tools, which are scoped to its workspace and
|
|
38
|
+
// braked; the shell keeps only `aos` commands that read, and notify, which
|
|
39
|
+
// reaches only Saad, so `aos send`, `task`, `spawn` and `stack` can't go
|
|
40
|
+
// around them. It writes files, but never in a project repository, a task
|
|
41
|
+
// worktree, AgentOS's own state or a secret file: its worker checks each
|
|
42
|
+
// write first (write-guard.ts).
|
|
40
43
|
export const ORCHESTRATOR_PERMISSIONS: {
|
|
41
44
|
permissionMode: PermissionMode;
|
|
42
45
|
allowedTools: string[];
|
|
@@ -48,6 +51,10 @@ export const ORCHESTRATOR_PERMISSIONS: {
|
|
|
48
51
|
"Read",
|
|
49
52
|
"Grep",
|
|
50
53
|
"Glob",
|
|
54
|
+
"Write",
|
|
55
|
+
"Edit",
|
|
56
|
+
"MultiEdit",
|
|
57
|
+
"NotebookEdit",
|
|
51
58
|
"ToolSearch",
|
|
52
59
|
"TodoWrite",
|
|
53
60
|
"Bash(aos peers:*)",
|
|
@@ -60,5 +67,5 @@ export const ORCHESTRATOR_PERMISSIONS: {
|
|
|
60
67
|
"Bash(aos docs:*)",
|
|
61
68
|
"Bash(aos doc:*)",
|
|
62
69
|
],
|
|
63
|
-
disallowedTools: [
|
|
70
|
+
disallowedTools: [],
|
|
64
71
|
};
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { BRANCH_NAME } from "../git";
|
|
6
6
|
import { RAISED_KINDS } from "./asks";
|
|
7
7
|
import { MAX_RULE_CHARS } from "./rules";
|
|
8
|
+
import { PROJECT_NAME } from "../project-add/name";
|
|
8
9
|
|
|
9
10
|
const ref = (what: string) => z.string().trim().min(1).max(300).describe(what);
|
|
10
11
|
const session = ref("Session name, project/name, or id from sessions");
|
|
@@ -100,6 +101,20 @@ export const TOOL_SHAPES = {
|
|
|
100
101
|
drop: { task, reason: z.string().trim().min(1).max(2000) },
|
|
101
102
|
stop: { session },
|
|
102
103
|
done: { session },
|
|
104
|
+
add_project: {
|
|
105
|
+
path: z
|
|
106
|
+
.string()
|
|
107
|
+
.trim()
|
|
108
|
+
.min(1)
|
|
109
|
+
.max(1000)
|
|
110
|
+
.describe("The git repository's absolute path on this machine"),
|
|
111
|
+
name: z
|
|
112
|
+
.string()
|
|
113
|
+
.trim()
|
|
114
|
+
.regex(PROJECT_NAME, "use letters, numbers, dots, dashes or underscores")
|
|
115
|
+
.optional()
|
|
116
|
+
.describe("A new project's name (default: the folder's)"),
|
|
117
|
+
},
|
|
103
118
|
note: { text: z.string().trim().min(1).max(2000) },
|
|
104
119
|
rule_add: {
|
|
105
120
|
text: z
|
|
@@ -71,6 +71,8 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
71
71
|
drop: "Reject a task: close its PR and remove its worktree. Logged with the reason.",
|
|
72
72
|
stop: "Stop a session's agent, keeping its worktree and branch.",
|
|
73
73
|
done: "Finish a session whose work is complete: a task's open PR merges through the sign-off gates first (refused naming the failing gate), then its agent stops, its worktree is removed if nothing would be lost, and it's archived. Not a rejection.",
|
|
74
|
+
add_project:
|
|
75
|
+
"Make a git repository on this machine a project of this workspace, by its absolute path, or move the project already at that path into this workspace when it's in no workspace. Says its default branch, detected from git. Refuses a folder that isn't a git repository or that holds secrets, and a project another workspace has.",
|
|
74
76
|
note: "Add a line to this workspace's decision log; it shows in your chat.",
|
|
75
77
|
rule_add:
|
|
76
78
|
'Keep a standing rule for this workspace, in your brief on every start from now on ("never merge #111", "briefs must not name third-party products"). It restricts or informs; it can never approve anything.',
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
} from "./tools";
|
|
11
11
|
import type { RoleExtras } from "../chat/worker/extras";
|
|
12
12
|
import { refreshSettings } from "../chat/refresh";
|
|
13
|
+
import { orchestratorToolRefusal } from "./write-guard";
|
|
13
14
|
|
|
14
15
|
// What an orchestrator's chat worker starts with on top of any chat: its
|
|
15
|
-
// brief, its tools,
|
|
16
|
+
// brief, its tools, permissions fixed by its role, and the guard on where
|
|
17
|
+
// it may write.
|
|
16
18
|
export async function orchestratorExtras(
|
|
17
19
|
session: Session
|
|
18
20
|
): Promise<RoleExtras> {
|
|
@@ -29,6 +31,7 @@ export async function orchestratorExtras(
|
|
|
29
31
|
),
|
|
30
32
|
},
|
|
31
33
|
...ORCHESTRATOR_PERMISSIONS,
|
|
34
|
+
toolGuard: (tool, input) => orchestratorToolRefusal(session, tool, input),
|
|
32
35
|
toolsDigest: orchestratorToolsDigest(),
|
|
33
36
|
refresh: {
|
|
34
37
|
atTokens: refreshSettings().orchestratorTokens,
|