@saadnvd1/agent-os 0.3.537 → 0.3.539
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 +8 -3
- package/lib/orchestrator/armed.ts +18 -8
- package/lib/orchestrator/ask-tool.ts +1 -1
- package/lib/orchestrator/brief.ts +2 -2
- package/lib/orchestrator/gates.ts +13 -2
- package/lib/orchestrator/held.ts +2 -4
- package/lib/orchestrator/signoff.ts +49 -29
- package/lib/orchestrator/tools.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,10 +122,12 @@ conversation in **Messages** and can join in. [More](#agent-network)
|
|
|
122
122
|
|
|
123
123
|

|
|
124
124
|
|
|
125
|
-
**Guides:** step-by-step how-tos are in [`docs/guides/`](docs/guides/), starting with [
|
|
125
|
+
**Guides:** step-by-step how-tos are in [`docs/guides/`](docs/guides/), starting with [installing AgentOS](docs/guides/install.md) and [using it from your phone](docs/guides/phone.md).
|
|
126
126
|
|
|
127
127
|
## Install
|
|
128
128
|
|
|
129
|
+
The [install guide](docs/guides/install.md) walks through this step by step.
|
|
130
|
+
|
|
129
131
|
You need Node.js 20+, tmux, and at least one agent CLI:
|
|
130
132
|
[Claude Code](https://github.com/anthropics/claude-code),
|
|
131
133
|
[Codex](https://github.com/openai/codex),
|
|
@@ -1590,8 +1592,11 @@ approved`). An approval covers that one item only: a held task's approval
|
|
|
1590
1592
|
pass, by giving `ask_saad` the task and its head sha: the ask is pinned to
|
|
1591
1593
|
that commit, opens with what AgentOS knows about it (files changed,
|
|
1592
1594
|
sensitive files, the review's verdict) and links the PR, and your approval
|
|
1593
|
-
lets `sign_off` merge it once, at that commit
|
|
1594
|
-
|
|
1595
|
+
lets `sign_off` merge it once, at that commit. It never rewrites an ask the
|
|
1596
|
+
gates raised. An approved merge (either kind, and an armed task's) needs no
|
|
1597
|
+
review or other gate of that commit: only an AI-attributed PR body, failing
|
|
1598
|
+
CI (running CI waits), a head that moved off the commit, and on the
|
|
1599
|
+
orchestrator's own ask a stack parent not merged yet, still stop it.
|
|
1595
1600
|
An ask closes itself when its task is merged or dropped, or the brakes
|
|
1596
1601
|
lift. At most 10 are open per workspace, titles that say the same thing
|
|
1597
1602
|
fold into one, a declined subject isn't asked again for 6 hours, and a
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* The orchestrator hears one event: merged, or a failure. A failure is
|
|
9
9
|
* counted against the task once per head, as sign_off would count it, and
|
|
10
10
|
* reported once; the task stays armed for its fix. One that goes to Saad
|
|
11
|
-
* disarms it, since only he merges it from then on
|
|
11
|
+
* disarms it, since only he merges it from then on, or approves its head
|
|
12
|
+
* for it to merge on that approval. The flag is in the
|
|
12
13
|
* database, so it survives a restart.
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -85,17 +86,19 @@ export function arm(workspaceId: string, ref: string): string {
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// Arms a task, or says why it can't be: only a running task, in a
|
|
88
|
-
// workspace whose orchestrator hears how it went, that isn't with Saad
|
|
89
|
+
// workspace whose orchestrator hears how it went, that isn't with Saad
|
|
90
|
+
// (or that he approved a commit of: it merges on that, at that commit only).
|
|
89
91
|
export function armTask(task: Session): void {
|
|
90
92
|
if (task.task_status !== "running")
|
|
91
93
|
throw new Error(`${task.name} is already ${task.task_status}`);
|
|
92
|
-
const
|
|
94
|
+
const w = armingWorkspace(task);
|
|
95
|
+
const approved = !!w && !!latestApproval(w, workSubject(task.id))?.sha;
|
|
96
|
+
const held = approved ? [] : holdingEscalations(task.id);
|
|
93
97
|
if (held.length)
|
|
94
98
|
throw new Error(
|
|
95
99
|
`${task.name} is with Saad (${held.map((h) => h.gate).join(", ")}); only he merges it`
|
|
96
100
|
);
|
|
97
|
-
|
|
98
|
-
if (w && openBySubject(w, workSubject(task.id)))
|
|
101
|
+
if (w && !approved && openBySubject(w, workSubject(task.id)))
|
|
99
102
|
throw new Error(
|
|
100
103
|
`${task.name} has an open ask with Saad; it merges on his answer`
|
|
101
104
|
);
|
|
@@ -209,9 +212,16 @@ async function mergeOne(
|
|
|
209
212
|
return;
|
|
210
213
|
}
|
|
211
214
|
// With Saad, on a gate or on a question about it: he decides, so it
|
|
212
|
-
// doesn't merge by itself meanwhile.
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
+
// doesn't merge by itself meanwhile, unless he approved this head.
|
|
216
|
+
const approved =
|
|
217
|
+
!!pr.head &&
|
|
218
|
+
latestApproval(workspaceId, workSubject(task.id))?.sha === pr.head;
|
|
219
|
+
const held = approved ? [] : holdingEscalations(task.id).map((h) => h.gate);
|
|
220
|
+
if (
|
|
221
|
+
!approved &&
|
|
222
|
+
!held.length &&
|
|
223
|
+
openBySubject(workspaceId, workSubject(task.id))
|
|
224
|
+
)
|
|
215
225
|
held.push("an open ask");
|
|
216
226
|
if (held.length) {
|
|
217
227
|
setArmed(task.id, false);
|
|
@@ -122,7 +122,7 @@ export async function askSaad(
|
|
|
122
122
|
}
|
|
123
123
|
const { ask, created } = raised;
|
|
124
124
|
const once = merge
|
|
125
|
-
? ` If he approves, sign_off merges it once, at ${merge.sha.slice(0, 7)} only.`
|
|
125
|
+
? ` If he approves, sign_off merges it once, at ${merge.sha.slice(0, 7)} only, without waiting on a review.`
|
|
126
126
|
: "";
|
|
127
127
|
if (!created)
|
|
128
128
|
return `Already on Saad's list as ask ${ask.id}; updated it.${once} Carry on with everything else.`;
|
|
@@ -86,11 +86,11 @@ ${brakesEnabled() ? BRAKES_ON : BRAKES_OFF}
|
|
|
86
86
|
- Credentials and account security.
|
|
87
87
|
- Choosing what gets built when it's a product call he hasn't made.
|
|
88
88
|
|
|
89
|
-
When one of these comes up, park it with \`ask_saad\` (kind: the hard line it crosses) and carry on with everything else. Use it too for a decision that's his (kind: decision). Escalated gates and brakes become asks on their own; don't ask again for those. To ask whether to merge a PR the gates won't pass, give \`ask_saad\` the task and the PR's head sha: if he approves, \`sign_off\` merges it once, at that commit only.
|
|
89
|
+
When one of these comes up, park it with \`ask_saad\` (kind: the hard line it crosses) and carry on with everything else. Use it too for a decision that's his (kind: decision). Escalated gates and brakes become asks on their own; don't ask again for those. To ask whether to merge a PR the gates won't pass, give \`ask_saad\` the task and the PR's head sha: if he approves, \`sign_off\` merges it once, at that commit only, without the review gates.
|
|
90
90
|
|
|
91
91
|
## Asks and pause
|
|
92
92
|
|
|
93
|
-
Saad answers an ask on his list, and the answer reaches you as an event: \`ask "<title>": approved\`, \`declined\`, or \`reply: <text>\`. Act on it. An approval covers that one item only, never standing permission: the next item like it is a new ask. Approving a held task lets \`sign_off
|
|
93
|
+
Saad answers an ask on his list, and the answer reaches you as an event: \`ask "<title>": approved\`, \`declined\`, or \`reply: <text>\`. Act on it. An approval covers that one item only, never standing permission: the next item like it is a new ask. Approving a merge (a held task's or your own ask's) lets \`sign_off\`, or an armed task, merge it once at the commit he approved, with no review, scope or other gate in the way: only an AI-attributed PR body, failing CI (running CI is a wait), an unmerged stack parent on your own ask, or a head that moved off that commit still stop it; approving a brake lets one start through, once.
|
|
94
94
|
|
|
95
95
|
Anything you need from Saad in chat (a question, a choice, a confirmation) goes through \`question\` in the same turn you ask it, and you still write the question plainly in your reply. It stays pinned on his Needs-you strip until it is resolved: when he answers (in chat, or with the strip's Reply) or it stops mattering, call \`resolve_question\`. Don't leave one open once he has answered. \`ask_saad\` is for hard lines and calls that need Approve or Decline; \`question\` is for everything conversational. Don't open a question for something that is already an ask. Updates AgentOS sends you fold into one-line rows in his chat, so a question buried in a long reply to an event is easy to miss: that is what \`question\` is for.
|
|
96
96
|
|
|
@@ -65,7 +65,18 @@ export interface GateInput {
|
|
|
65
65
|
|
|
66
66
|
const short = (sha: string) => sha.slice(0, 7);
|
|
67
67
|
|
|
68
|
-
function ciGate(
|
|
68
|
+
export function ciGate(
|
|
69
|
+
i: Pick<
|
|
70
|
+
GateInput,
|
|
71
|
+
| "sha"
|
|
72
|
+
| "checks"
|
|
73
|
+
| "failing"
|
|
74
|
+
| "settleIn"
|
|
75
|
+
| "mergeable"
|
|
76
|
+
| "baseName"
|
|
77
|
+
| "repoCI"
|
|
78
|
+
>
|
|
79
|
+
): GateOutcome {
|
|
69
80
|
const gate = "ci";
|
|
70
81
|
const at = short(i.sha);
|
|
71
82
|
if (i.checks === "pending")
|
|
@@ -88,7 +99,7 @@ function ciGate(i: GateInput): GateOutcome {
|
|
|
88
99
|
|
|
89
100
|
// A head with no checks. Only a repository with no CI at all has nothing to
|
|
90
101
|
// gate on; in one with CI the head is not yet, whatever the reason.
|
|
91
|
-
function noChecks(i:
|
|
102
|
+
function noChecks(i: Parameters<typeof ciGate>[0], at: string): GateOutcome {
|
|
92
103
|
const gate = "ci";
|
|
93
104
|
const wait = (reason: string): GateOutcome => ({
|
|
94
105
|
gate,
|
package/lib/orchestrator/held.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A task that's with Saad merges only on his approval of its exact head
|
|
2
|
-
// commit, once. A new commit since he approved goes back to him.
|
|
2
|
+
// commit, once (signoff.ts). A new commit since he approved goes back to him.
|
|
3
3
|
|
|
4
4
|
import { AskRefused, raiseAsk, workSubject } from "./asks";
|
|
5
5
|
import { latestApproval } from "./ask-approvals";
|
|
@@ -17,10 +17,8 @@ export function heldVerdict(
|
|
|
17
17
|
pr: number,
|
|
18
18
|
moved?: string
|
|
19
19
|
): Verdict {
|
|
20
|
-
//
|
|
20
|
+
// An approval of this head was judged before this (signoff.ts).
|
|
21
21
|
const approval = latestApproval(workspaceId, workSubject(task.id));
|
|
22
|
-
if (approval?.sha === sha)
|
|
23
|
-
return { ok: true, sha, pr, approval: approval.id };
|
|
24
22
|
if (approval) {
|
|
25
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}` : ""}`;
|
|
26
24
|
try {
|
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
sensitiveFiles,
|
|
26
26
|
} from "./diff";
|
|
27
27
|
import { countFailure, escalate } from "./escalate";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
ciGate,
|
|
30
|
+
evaluateGates,
|
|
31
|
+
holdingEscalations,
|
|
32
|
+
type GateInput,
|
|
33
|
+
} from "./gates";
|
|
29
34
|
import { addNote } from "./notes";
|
|
30
35
|
import { latestApproval, refundApproval, spendApproval } from "./ask-approvals";
|
|
31
36
|
import { workSubject } from "./asks";
|
|
@@ -96,7 +101,11 @@ export async function judge(
|
|
|
96
101
|
restackedOf(task)?.to === sha
|
|
97
102
|
)
|
|
98
103
|
: null;
|
|
99
|
-
|
|
104
|
+
// Saad's approval of this head stands in for that review (gateVerdict).
|
|
105
|
+
if (
|
|
106
|
+
quick?.wait &&
|
|
107
|
+
latestApproval(workspaceId, workSubject(task.id))?.sha !== sha
|
|
108
|
+
)
|
|
100
109
|
return {
|
|
101
110
|
ok: false,
|
|
102
111
|
wait: true,
|
|
@@ -164,13 +173,12 @@ export async function gateVerdict(
|
|
|
164
173
|
});
|
|
165
174
|
const { pr, sha, repo } = work;
|
|
166
175
|
const held = holdingEscalations(work.id);
|
|
167
|
-
// Saad approved merging this exact commit
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// task merges as it always has (heldVerdict).
|
|
176
|
+
// Saad approved merging this exact commit: held or not, it merges once,
|
|
177
|
+
// without a review or any other gate approval stands in for. Approval
|
|
178
|
+
// doesn't cover what's left: an AI-attributed body, failing CI (running
|
|
179
|
+
// CI is a wait), and, on an ask the orchestrator raised or a task the
|
|
180
|
+
// gates don't hold, a stack parent not merged yet. On a held task, an
|
|
181
|
+
// approval of an earlier commit goes back to him (heldVerdict).
|
|
174
182
|
let approval = latestApproval(workspaceId, workSubject(work.id));
|
|
175
183
|
// The head moved since he approved: his approval follows it when the PR's
|
|
176
184
|
// own patch is unchanged (a rebase), else the re-ask says what changed.
|
|
@@ -185,19 +193,19 @@ export async function gateVerdict(
|
|
|
185
193
|
if (carried.approval) approval = carried.approval;
|
|
186
194
|
else moved = carried.note;
|
|
187
195
|
}
|
|
188
|
-
if (
|
|
189
|
-
approval?.sha === sha &&
|
|
190
|
-
(approval.raised_by === "orchestrator" || !held.length)
|
|
191
|
-
) {
|
|
196
|
+
if (approval?.sha === sha) {
|
|
192
197
|
const i = await inputs();
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
const approved = `${work.name} (PR #${pr.number} at ${short(sha)}): Saad approved this commit, but`;
|
|
199
|
+
const stack =
|
|
200
|
+
approval.raised_by === "orchestrator" || !held.length
|
|
201
|
+
? i.stackRefusal
|
|
202
|
+
: null;
|
|
203
|
+
const refusal = i.codeReview.refusal ?? stack;
|
|
204
|
+
if (refusal) return no(`${approved} ${refusal}`);
|
|
205
|
+
const ci = ciGate({ sha, ...i, repoCI: await repoCIFor(repo, work, i) });
|
|
206
|
+
if (ci.state === "fail") return no(`${approved} ${ci.reason}`);
|
|
207
|
+
if (ci.state === "wait") return no(`${approved} ${ci.reason}`, true);
|
|
208
|
+
// "escalate" is a repository with no CI: Saad is who that goes to.
|
|
201
209
|
return { ok: true, sha, pr: pr.number, approval: approval.id };
|
|
202
210
|
}
|
|
203
211
|
if (held.length)
|
|
@@ -223,14 +231,7 @@ export async function gateVerdict(
|
|
|
223
231
|
|
|
224
232
|
const ruled = ruleBreaks(files, await addedLines(repo, base, sha));
|
|
225
233
|
const live = await inputs();
|
|
226
|
-
|
|
227
|
-
// isn't settling or conflicting.
|
|
228
|
-
const repoCI =
|
|
229
|
-
live.checks === "none" &&
|
|
230
|
-
live.settleIn === 0 &&
|
|
231
|
-
live.mergeable !== "CONFLICTING"
|
|
232
|
-
? await repoHasCI(repo, base)
|
|
233
|
-
: undefined;
|
|
234
|
+
const repoCI = await repoCIFor(repo, base, live);
|
|
234
235
|
const outcomes = evaluateGates({ sha, ruleBreaks: ruled, ...live, repoCI });
|
|
235
236
|
const head = `${work.name} (PR #${pr.number} at ${short(sha)}) can't merge:`;
|
|
236
237
|
const toSaad = outcomes.find((o) => o.state === "escalate");
|
|
@@ -253,6 +254,25 @@ export async function gateVerdict(
|
|
|
253
254
|
return no(`${head}\n${lines.join("\n")}`, !failed.length);
|
|
254
255
|
}
|
|
255
256
|
|
|
257
|
+
// Asked only when it can change the answer: a head with no checks that
|
|
258
|
+
// isn't settling or conflicting.
|
|
259
|
+
async function repoCIFor(
|
|
260
|
+
repo: string,
|
|
261
|
+
base: string | GateWork,
|
|
262
|
+
i: Pick<GateInput, "checks" | "settleIn" | "mergeable">
|
|
263
|
+
) {
|
|
264
|
+
if (i.checks !== "none" || i.settleIn !== 0 || i.mergeable === "CONFLICTING")
|
|
265
|
+
return undefined;
|
|
266
|
+
if (typeof base !== "string") {
|
|
267
|
+
try {
|
|
268
|
+
base = await fetchRefs(repo, base.refs);
|
|
269
|
+
} catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return repoHasCI(repo, base);
|
|
274
|
+
}
|
|
275
|
+
|
|
256
276
|
export async function signOff(
|
|
257
277
|
workspaceId: string,
|
|
258
278
|
ref: string
|
|
@@ -116,7 +116,7 @@ const DESCRIPTIONS: Record<ToolName, string> = {
|
|
|
116
116
|
resolve_question:
|
|
117
117
|
"Clear an open question once Saad has answered it in chat, or it no longer matters.",
|
|
118
118
|
ask_saad:
|
|
119
|
-
"Park an item on Saad's asks list (a decision, or anything crossing a hard line) and carry on; it never waits. One open ask per title. His answer reaches you as an event. To ask whether to merge a PR the gates refuse, give task and sha (its head): his approval lets sign_off merge it once, at that commit.",
|
|
119
|
+
"Park an item on Saad's asks list (a decision, or anything crossing a hard line) and carry on; it never waits. One open ask per title. His answer reaches you as an event. To ask whether to merge a PR the gates refuse, give task and sha (its head): his approval lets sign_off merge it once, at that commit, without a review (an AI-attributed body or failing CI still refuses).",
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
export function orchestratorTools(
|