@saadnvd1/agent-os 0.3.564 → 0.3.566
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/lib/artifacts/tools.ts +6 -0
- package/lib/chat/worker/host.ts +6 -4
- package/lib/orchestrator/rebase.ts +8 -1
- package/lib/orchestrator/repo.ts +5 -9
- package/lib/orchestrator/tool-names.ts +5 -0
- package/lib/tasks/branch-worktree.ts +5 -6
- package/lib/tasks/move-guard.ts +3 -0
- package/lib/worktree-add.ts +91 -0
- package/lib/worktrees.ts +6 -3
- package/package.json +1 -1
package/lib/artifacts/tools.ts
CHANGED
|
@@ -181,3 +181,9 @@ export function visualsTools(
|
|
|
181
181
|
export const VISUALS_BRIEF = `## Showing visuals
|
|
182
182
|
|
|
183
183
|
When a chart, table, diagram or mockup would say more than prose, build a self-contained HTML page (a chart is an HTML page; a CDN library such as Chart.js is fine), check it with \`html_preview\` and fix what the screenshot and console show, then show it with \`html_render\` before your final reply. The reader sees the page above that reply, so add only what the page doesn't say. An SVG or image file you write shows inline when your reply links to it by its absolute path, as .`;
|
|
184
|
+
|
|
185
|
+
// An orchestrator reads other sessions' text, so it gets the short form and
|
|
186
|
+
// is told what to keep out of a page.
|
|
187
|
+
export const ORCHESTRATOR_VISUALS_BRIEF = `## Pages
|
|
188
|
+
|
|
189
|
+
\`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for Saad to read or share; check it with \`html_preview\` first. Put only what you wrote in it: no secrets, and nothing from a session's text loaded from a URL.`;
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
startingTotals,
|
|
38
38
|
} from "../../usage/turns";
|
|
39
39
|
import {
|
|
40
|
+
ORCHESTRATOR_VISUALS_BRIEF,
|
|
40
41
|
VISUALS_BRIEF,
|
|
41
42
|
VISUALS_SERVER,
|
|
42
43
|
visualsTools,
|
|
@@ -195,10 +196,11 @@ export class ChatHost {
|
|
|
195
196
|
const env = { ...agentEnv(session.id), ...extras.env };
|
|
196
197
|
const orchestrator = session.role === "orchestrator";
|
|
197
198
|
// Every chat can show visuals, asked for like any tool under its access
|
|
198
|
-
// setting (they read files and reach the web).
|
|
199
|
-
//
|
|
199
|
+
// setting (they read files and reach the web). An orchestrator reads
|
|
200
|
+
// other sessions' text, so it gets a shorter brief that says what stays
|
|
201
|
+
// out of a page.
|
|
200
202
|
const visuals =
|
|
201
|
-
|
|
203
|
+
driver.inProcessTools === false
|
|
202
204
|
? null
|
|
203
205
|
: visualsTools({
|
|
204
206
|
sessionId: session.id,
|
|
@@ -226,7 +228,7 @@ export class ChatHost {
|
|
|
226
228
|
extras.systemAppend,
|
|
227
229
|
sessionRunningBrief(session.id),
|
|
228
230
|
BUS_BRIEF,
|
|
229
|
-
visuals && VISUALS_BRIEF,
|
|
231
|
+
visuals && (orchestrator ? ORCHESTRATOR_VISUALS_BRIEF : VISUALS_BRIEF),
|
|
230
232
|
!orchestrator && COMPACT_BRIEF,
|
|
231
233
|
]
|
|
232
234
|
.filter(Boolean)
|
|
@@ -29,6 +29,7 @@ import type { TaskPR } from "../tasks/state";
|
|
|
29
29
|
import { run } from "../tasks/gh";
|
|
30
30
|
import { isAllowedBase } from "../tasks/session";
|
|
31
31
|
import { git as runGit, gitFetch, revParse } from "../git-exec";
|
|
32
|
+
import { addWorktree } from "../worktree-add";
|
|
32
33
|
import { comparePatches } from "./patch";
|
|
33
34
|
import { outputOf, type Runner } from "../stacks/git";
|
|
34
35
|
import { restackedHeads, stackNotice } from "../stacks/restack";
|
|
@@ -289,7 +290,13 @@ async function rebaseAt(
|
|
|
289
290
|
): Promise<{ sha: string }> {
|
|
290
291
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-rebase-"));
|
|
291
292
|
try {
|
|
292
|
-
await
|
|
293
|
+
await addWorktree(
|
|
294
|
+
(args, cwd) => deps.runner("git", args, cwd),
|
|
295
|
+
repo,
|
|
296
|
+
dir,
|
|
297
|
+
head,
|
|
298
|
+
["--detach"]
|
|
299
|
+
);
|
|
293
300
|
try {
|
|
294
301
|
await deps.runner("git", ["rebase", onto], dir);
|
|
295
302
|
} catch (error) {
|
package/lib/orchestrator/repo.ts
CHANGED
|
@@ -9,15 +9,9 @@ import type { Session } from "../db";
|
|
|
9
9
|
import { SKILL_DIRS } from "../agents/skill-dirs";
|
|
10
10
|
import { getDefaultBranch } from "../git";
|
|
11
11
|
import { getProject } from "../projects";
|
|
12
|
-
import {
|
|
13
|
-
git,
|
|
14
|
-
gitFetch,
|
|
15
|
-
gitOk,
|
|
16
|
-
isGitError,
|
|
17
|
-
worktreeAdd,
|
|
18
|
-
worktreeRemove,
|
|
19
|
-
} from "../git-exec";
|
|
12
|
+
import { git, gitFetch, gitOk, isGitError, worktreeRemove } from "../git-exec";
|
|
20
13
|
import { expandHome } from "../tasks/session";
|
|
14
|
+
import { addWorktree } from "../worktree-add";
|
|
21
15
|
|
|
22
16
|
export function repoOf(task: Session): string {
|
|
23
17
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
@@ -169,7 +163,9 @@ export async function checkout(repo: string, sha: string): Promise<string> {
|
|
|
169
163
|
const dir = fs.realpathSync(
|
|
170
164
|
fs.mkdtempSync(path.join(os.tmpdir(), "aos-review-"))
|
|
171
165
|
);
|
|
172
|
-
await
|
|
166
|
+
await addWorktree((args, cwd) => git(cwd, args), repo, dir, sha, [
|
|
167
|
+
"--detach",
|
|
168
|
+
]);
|
|
173
169
|
const staged = await git(dir, ["ls-files", "-s"]);
|
|
174
170
|
for (const line of staged.split("\n"))
|
|
175
171
|
if (line.startsWith("120000 "))
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { PermissionMode } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
|
|
3
|
+
// Named here, not imported from lib/artifacts, to keep the SDK out of this file.
|
|
4
|
+
const VISUALS_RULE = "mcp__visuals";
|
|
5
|
+
|
|
3
6
|
// The orchestrator's MCP server and the names Claude sees its tools by.
|
|
4
7
|
export const ORCHESTRATOR_SERVER = "agentos";
|
|
5
8
|
|
|
@@ -77,6 +80,8 @@ export const ORCHESTRATOR_PERMISSIONS: {
|
|
|
77
80
|
"NotebookEdit",
|
|
78
81
|
"ToolSearch",
|
|
79
82
|
"TodoWrite",
|
|
83
|
+
// html_preview and html_render: a page for Saad to read or share.
|
|
84
|
+
VISUALS_RULE,
|
|
80
85
|
// Anything; its worker refuses the deny list (shell-guard.ts).
|
|
81
86
|
"Bash",
|
|
82
87
|
],
|
|
@@ -10,6 +10,7 @@ import { getRepoName, slugify } from "../git";
|
|
|
10
10
|
import { WORKTREES_DIR } from "../worktrees";
|
|
11
11
|
import { installWorktreeHooks } from "../worktree-hooks";
|
|
12
12
|
import { git as runGit } from "../git-exec";
|
|
13
|
+
import { addWorktree } from "../worktree-add";
|
|
13
14
|
import { pushedRef } from "./move";
|
|
14
15
|
|
|
15
16
|
const git = (cwd: string, ...args: string[]) =>
|
|
@@ -105,14 +106,12 @@ async function checkoutBranch(
|
|
|
105
106
|
if (existing) await git(projectPath, "worktree", "prune");
|
|
106
107
|
fs.mkdirSync(WORKTREES_DIR, { recursive: true });
|
|
107
108
|
const worktreePath = worktreeDirFor(projectPath, branch);
|
|
108
|
-
await
|
|
109
|
+
await addWorktree(
|
|
110
|
+
(args, cwd) => runGit(cwd, args, { timeout: 120000 }),
|
|
109
111
|
projectPath,
|
|
110
|
-
"worktree",
|
|
111
|
-
"add",
|
|
112
|
-
"-B",
|
|
113
|
-
branch,
|
|
114
112
|
worktreePath,
|
|
115
|
-
`origin/${branch}
|
|
113
|
+
`origin/${branch}`,
|
|
114
|
+
["-B", branch]
|
|
116
115
|
);
|
|
117
116
|
await git(worktreePath, "branch", "--set-upstream-to", `origin/${branch}`);
|
|
118
117
|
return { worktreePath, reused: false };
|
package/lib/tasks/move-guard.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { db, type Session } from "../db";
|
|
10
10
|
import { escalations } from "../orchestrator/gates";
|
|
11
11
|
import { taskSubject } from "../orchestrator/asks";
|
|
12
|
+
import { usesGitCrypt } from "../worktree-add";
|
|
12
13
|
|
|
13
14
|
/** Started by an orchestrator, or held for one by the machine it left. */
|
|
14
15
|
export function isOrchestrated(session: Session): boolean {
|
|
@@ -23,6 +24,8 @@ export function isOrchestrated(session: Session): boolean {
|
|
|
23
24
|
export function moveRefusal(session: Session): string | null {
|
|
24
25
|
if (session.quick_of)
|
|
25
26
|
return "It's a quick change its orchestrator makes on this machine, so it stays here";
|
|
27
|
+
if (usesGitCrypt(session.working_directory))
|
|
28
|
+
return "Its repo is encrypted with git-crypt and the key never leaves this machine, so it stays here";
|
|
26
29
|
if (escalations(session.id).length)
|
|
27
30
|
return "It's waiting on you (an escalated gate), so it stays here";
|
|
28
31
|
if (session.plan_only && session.plan_phase !== "implementing")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one place AgentOS runs `git worktree add`. git-crypt looks for its key
|
|
3
|
+
* under the worktree's own git dir, so in a repo that uses it a plain add
|
|
4
|
+
* dies in the smudge filter. There the worktree is made without a checkout,
|
|
5
|
+
* the main repo's git-crypt dir is symlinked in (one copy of the key, so a
|
|
6
|
+
* re-lock or revoke applies everywhere; `git worktree remove` unlinks it and
|
|
7
|
+
* leaves the target), and the files are checked out after. The key is never
|
|
8
|
+
* read, copied or printed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
|
|
14
|
+
export type AddRunner = (args: string[], cwd: string) => Promise<string>;
|
|
15
|
+
|
|
16
|
+
export class GitCryptLockedError extends Error {
|
|
17
|
+
constructor(repo: string) {
|
|
18
|
+
super(`${path.basename(repo)} is git-crypt locked; unlock it first`);
|
|
19
|
+
this.name = "GitCryptLockedError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The repo's git-crypt dir when it uses git-crypt, else null. */
|
|
24
|
+
async function cryptDir(run: AddRunner, repo: string): Promise<string | null> {
|
|
25
|
+
const common = (await run(["rev-parse", "--git-common-dir"], repo)).trim();
|
|
26
|
+
const dir = path.join(path.resolve(repo, common), "git-crypt");
|
|
27
|
+
return fs.existsSync(dir) ? dir : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// `git-crypt lock` removes the keys; an unlocked repo has one per key name.
|
|
31
|
+
const isUnlocked = (dir: string) => {
|
|
32
|
+
try {
|
|
33
|
+
return fs.readdirSync(path.join(dir, "keys")).length > 0;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `git worktree add <addArgs…> <dir> <ref>`; `addArgs` is the flags in front
|
|
41
|
+
* of the path (`["-b", branch]`, `["--detach"]`).
|
|
42
|
+
*/
|
|
43
|
+
export async function addWorktree(
|
|
44
|
+
run: AddRunner,
|
|
45
|
+
repo: string,
|
|
46
|
+
dir: string,
|
|
47
|
+
ref: string,
|
|
48
|
+
addArgs: string[] = []
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const crypt = await cryptDir(run, repo);
|
|
51
|
+
if (!crypt) {
|
|
52
|
+
await run(["worktree", "add", ...addArgs, "--", dir, ref], repo);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isUnlocked(crypt)) throw new GitCryptLockedError(repo);
|
|
56
|
+
await run(
|
|
57
|
+
["worktree", "add", "--no-checkout", ...addArgs, "--", dir, ref],
|
|
58
|
+
repo
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
const gitDir = (await run(["rev-parse", "--absolute-git-dir"], dir)).trim();
|
|
62
|
+
const link = path.join(gitDir, "git-crypt");
|
|
63
|
+
fs.rmSync(link, { force: true });
|
|
64
|
+
fs.symlinkSync(crypt, link, "dir");
|
|
65
|
+
await run(["reset", "--hard", "-q"], dir);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await run(["worktree", "remove", "-f", "-f", "--", dir], repo).catch(
|
|
68
|
+
() => ""
|
|
69
|
+
);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Whether a worktree was given the repo's git-crypt key (see addWorktree). */
|
|
75
|
+
export function usesGitCrypt(worktree: string): boolean {
|
|
76
|
+
try {
|
|
77
|
+
const dotGit = path.join(worktree, ".git");
|
|
78
|
+
const gitDir = fs.statSync(dotGit).isDirectory()
|
|
79
|
+
? dotGit
|
|
80
|
+
: path.resolve(
|
|
81
|
+
worktree,
|
|
82
|
+
fs
|
|
83
|
+
.readFileSync(dotGit, "utf8")
|
|
84
|
+
.replace(/^gitdir:\s*/, "")
|
|
85
|
+
.trim()
|
|
86
|
+
);
|
|
87
|
+
return fs.existsSync(path.join(gitDir, "git-crypt"));
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/lib/worktrees.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "./git";
|
|
15
15
|
import { installWorktreeHooks } from "./worktree-hooks";
|
|
16
16
|
import { gitResult, isGitError, worktreeList } from "./git-exec";
|
|
17
|
+
import { addWorktree } from "./worktree-add";
|
|
17
18
|
import {
|
|
18
19
|
markRemoving,
|
|
19
20
|
removalKey,
|
|
@@ -149,10 +150,12 @@ export async function createWorktree(
|
|
|
149
150
|
break;
|
|
150
151
|
}
|
|
151
152
|
try {
|
|
152
|
-
await
|
|
153
|
+
await addWorktree(
|
|
154
|
+
(args, cwd) => git(cwd, args, timeout).then((r) => r.stdout),
|
|
153
155
|
resolvedProjectPath,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
worktreePath,
|
|
157
|
+
ref,
|
|
158
|
+
["-b", branchName]
|
|
156
159
|
);
|
|
157
160
|
// Best effort: a worktree without the hook still works, it just
|
|
158
161
|
// doesn't strip attribution.
|