maesttro 0.4.1 → 0.4.3
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 +4 -3
- package/dist/maesttro.js +51 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,8 +73,9 @@ maesttro update # bump the package + rebuild image with
|
|
|
73
73
|
|
|
74
74
|
Run `maesttro` from the root of a repository (git 2.48+ required). Each run is a
|
|
75
75
|
**session** with its own isolated clone, `.worktrees/maesttro-<id>`, detached at
|
|
76
|
-
`origin/<default branch>` and mounted at `/workspace`.
|
|
77
|
-
|
|
76
|
+
`origin/<default branch>` and mounted at `/workspace`. The runner container is
|
|
77
|
+
named `maesttro-<id>` too, so `docker ps` and `docker logs maesttro-<id>` line up
|
|
78
|
+
with the session. That clone is the only part of the repo the container sees:
|
|
78
79
|
|
|
79
80
|
- Your real `.git` is never mounted, and neither is your main checkout or any
|
|
80
81
|
uncommitted work there. The container works in a `git clone --shared` that
|
|
@@ -188,7 +189,7 @@ Declining only skips it for that run; the next start asks again.
|
|
|
188
189
|
|---|---|---|
|
|
189
190
|
| `/home/agent` | `maesttro-home` named volume | Per-client session state. Survives across runs, lives only in Docker. Holds no GitHub credential. |
|
|
190
191
|
| `/home/agent/.config/gh-maesttro` (read-only) | per-session temp dir on host | The session's GitHub token (`hosts.yml`). The CLI refreshes it before its 1 h expiry and revokes it on exit. Not mounted in `--strict` sessions, or when GitHub access is unavailable. |
|
|
191
|
-
|
|
|
192
|
+
| `/opt/maesttro-objects` (read-only) | your repo's `.git/objects` | Base history, borrowed by the clone through its `alternates`, which is pinned to this fixed path so it resolves the same on every host OS. The real `.git` is never mounted. |
|
|
192
193
|
| `/workspace` | the session clone (`.worktrees/<id>`) | The working directory: a `git clone --shared` detached at the default branch. |
|
|
193
194
|
| `/opt/maesttro`, `/opt/maesttro-host` (read-only) | this package; your instruction files | Used by `maesttro configure` at container start. |
|
|
194
195
|
| `/home/agent/.claude/skills` (read-only) | `~/agent-shared/skills` on host, if present | Reusable skills/house-style files shared across sessions. Read-only. |
|
package/dist/maesttro.js
CHANGED
|
@@ -1279,7 +1279,7 @@ Usage:
|
|
|
1279
1279
|
`;
|
|
1280
1280
|
|
|
1281
1281
|
// src/launch.ts
|
|
1282
|
-
import { execSync as execSync3, spawn } from "node:child_process";
|
|
1282
|
+
import { execSync as execSync3, spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
1283
1283
|
import { existsSync as existsSync4, rmSync as rmSync6 } from "node:fs";
|
|
1284
1284
|
import { constants, homedir as homedir4 } from "node:os";
|
|
1285
1285
|
import { join as join11 } from "node:path";
|
|
@@ -1611,7 +1611,10 @@ async function openGitHubSession({
|
|
|
1611
1611
|
...identityEnv(author, initial.committer)
|
|
1612
1612
|
};
|
|
1613
1613
|
return {
|
|
1614
|
-
dockerArgs: withEnvFlags([
|
|
1614
|
+
dockerArgs: withEnvFlags([
|
|
1615
|
+
"--mount",
|
|
1616
|
+
`type=bind,source=${dir.ghDir},target=${CONTAINER_GH_CONFIG_DIR},readonly`
|
|
1617
|
+
], env),
|
|
1615
1618
|
env,
|
|
1616
1619
|
close: async () => {
|
|
1617
1620
|
refresher.stop();
|
|
@@ -1642,7 +1645,8 @@ import {
|
|
|
1642
1645
|
lstatSync,
|
|
1643
1646
|
mkdirSync as mkdirSync9,
|
|
1644
1647
|
readdirSync as readdirSync3,
|
|
1645
|
-
readFileSync as readFileSync6
|
|
1648
|
+
readFileSync as readFileSync6,
|
|
1649
|
+
writeFileSync as writeFileSync7
|
|
1646
1650
|
} from "node:fs";
|
|
1647
1651
|
import { join as join9 } from "node:path";
|
|
1648
1652
|
|
|
@@ -1837,6 +1841,31 @@ function copyObjectFiles(fromDir, intoDir) {
|
|
|
1837
1841
|
copyFileSync(join9(fromDir, entry.name), dest);
|
|
1838
1842
|
}
|
|
1839
1843
|
}
|
|
1844
|
+
function pinAlternates(clonePath, containerObjects) {
|
|
1845
|
+
const gitDir = cloneGitDir(clonePath);
|
|
1846
|
+
if (!gitDir)
|
|
1847
|
+
return;
|
|
1848
|
+
const objects = join9(gitDir, "objects");
|
|
1849
|
+
const info = join9(objects, "info");
|
|
1850
|
+
if (!realDir(objects) || !realDir(info)) {
|
|
1851
|
+
throw new LaunchError(`Unsafe objects tree in the session clone: ${clonePath}`);
|
|
1852
|
+
}
|
|
1853
|
+
const alternates = join9(info, "alternates");
|
|
1854
|
+
let stat;
|
|
1855
|
+
try {
|
|
1856
|
+
stat = lstatSync(alternates);
|
|
1857
|
+
} catch {
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (!stat.isFile()) {
|
|
1861
|
+
throw new LaunchError(`Unsafe alternates file in the session clone: ${clonePath}`);
|
|
1862
|
+
}
|
|
1863
|
+
const desired = `${containerObjects}
|
|
1864
|
+
`;
|
|
1865
|
+
if (readFileSync6(alternates, "utf8") !== desired) {
|
|
1866
|
+
writeFileSync7(alternates, desired);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1840
1869
|
function importObjects(run, root, clonePath, tip) {
|
|
1841
1870
|
const gitDir = cloneGitDir(clonePath);
|
|
1842
1871
|
if (!gitDir)
|
|
@@ -1864,7 +1893,7 @@ import {
|
|
|
1864
1893
|
readFileSync as readFileSync7,
|
|
1865
1894
|
renameSync as renameSync4,
|
|
1866
1895
|
rmSync as rmSync5,
|
|
1867
|
-
writeFileSync as
|
|
1896
|
+
writeFileSync as writeFileSync8
|
|
1868
1897
|
} from "node:fs";
|
|
1869
1898
|
import { join as join10 } from "node:path";
|
|
1870
1899
|
function newSessionId() {
|
|
@@ -1876,7 +1905,7 @@ function sessionsDir(home = maesttroHome()) {
|
|
|
1876
1905
|
function saveSession(record, dir = sessionsDir()) {
|
|
1877
1906
|
mkdirSync10(dir, { recursive: true, mode: 448 });
|
|
1878
1907
|
const path = join10(dir, `${record.id}.json`);
|
|
1879
|
-
|
|
1908
|
+
writeFileSync8(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
|
|
1880
1909
|
`);
|
|
1881
1910
|
renameSync4(`${path}.tmp`, path);
|
|
1882
1911
|
}
|
|
@@ -1929,6 +1958,7 @@ function workspaceAuthor(run, workspace) {
|
|
|
1929
1958
|
// src/launch.ts
|
|
1930
1959
|
var CONTAINER_REPO = "/workspace";
|
|
1931
1960
|
var CONTAINER_PACKAGE = "/opt/maesttro";
|
|
1961
|
+
var CONTAINER_GIT_OBJECTS = "/opt/maesttro-objects";
|
|
1932
1962
|
function resolveCommand(args) {
|
|
1933
1963
|
const [first, ...rest] = args;
|
|
1934
1964
|
if (first === undefined)
|
|
@@ -1951,8 +1981,8 @@ function sessionMounts({
|
|
|
1951
1981
|
}) {
|
|
1952
1982
|
const objects = join11(repoRoot, ".git", "objects");
|
|
1953
1983
|
const mounts = [
|
|
1954
|
-
{ host: VOLUME, container: "/home/agent" },
|
|
1955
|
-
{ host: objects, container:
|
|
1984
|
+
{ host: VOLUME, container: "/home/agent", type: "volume" },
|
|
1985
|
+
{ host: objects, container: CONTAINER_GIT_OBJECTS, readOnly: true },
|
|
1956
1986
|
{ host: sessionDir, container: CONTAINER_REPO },
|
|
1957
1987
|
{ host: packageRoot, container: CONTAINER_PACKAGE, readOnly: true },
|
|
1958
1988
|
...stagedPaths.map((path) => ({
|
|
@@ -1979,16 +2009,18 @@ function dockerRunArgs({
|
|
|
1979
2009
|
command,
|
|
1980
2010
|
mounts,
|
|
1981
2011
|
workdir,
|
|
1982
|
-
extra = []
|
|
2012
|
+
extra = [],
|
|
2013
|
+
name
|
|
1983
2014
|
}) {
|
|
1984
|
-
const volumes = mounts.flatMap(({ host, container, readOnly }) => [
|
|
1985
|
-
"
|
|
1986
|
-
|
|
2015
|
+
const volumes = mounts.flatMap(({ host, container, readOnly, type = "bind" }) => [
|
|
2016
|
+
"--mount",
|
|
2017
|
+
`type=${type},source=${host},target=${container}${readOnly ? ",readonly" : ""}`
|
|
1987
2018
|
]);
|
|
1988
2019
|
return [
|
|
1989
2020
|
"run",
|
|
1990
2021
|
"-it",
|
|
1991
2022
|
"--rm",
|
|
2023
|
+
...name ? ["--name", name] : [],
|
|
1992
2024
|
...volumes,
|
|
1993
2025
|
...extra,
|
|
1994
2026
|
"-w",
|
|
@@ -2016,6 +2048,9 @@ function ensureReady() {
|
|
|
2016
2048
|
throw new LaunchError("Runner isn't set up yet. Run `maesttro setup` first.");
|
|
2017
2049
|
}
|
|
2018
2050
|
}
|
|
2051
|
+
function clearStaleContainer(id) {
|
|
2052
|
+
spawnSync2("docker", ["rm", "-f", id], { stdio: "ignore" });
|
|
2053
|
+
}
|
|
2019
2054
|
function sharedSkillsDir() {
|
|
2020
2055
|
const dir = process.env.AGENT_SHARED_SKILLS ?? join11(homedir4(), "agent-shared", "skills");
|
|
2021
2056
|
return existsSync4(dir) ? dir : undefined;
|
|
@@ -2216,10 +2251,13 @@ async function launch({
|
|
|
2216
2251
|
"MAESTTRO_HOST_UID",
|
|
2217
2252
|
"MAESTTRO_HOST_GID"
|
|
2218
2253
|
].concat(repo ? ["MAESTTRO_REPO"] : []).flatMap((name) => ["-e", name]);
|
|
2254
|
+
clearStaleContainer(saved.id);
|
|
2255
|
+
pinAlternates(saved.worktree, CONTAINER_GIT_OBJECTS);
|
|
2219
2256
|
const child = spawn("docker", dockerRunArgs({
|
|
2220
2257
|
command: containerCommand(resolveCommand(args)),
|
|
2221
2258
|
mounts,
|
|
2222
2259
|
workdir,
|
|
2260
|
+
name: saved.id,
|
|
2223
2261
|
extra: [...github.dockerArgs, ...envFlags]
|
|
2224
2262
|
}), { stdio: "inherit", env: { ...process.env, ...sessionEnv } });
|
|
2225
2263
|
const forward = (signal) => child.kill(signal);
|
|
@@ -2238,7 +2276,7 @@ async function launch({
|
|
|
2238
2276
|
}
|
|
2239
2277
|
|
|
2240
2278
|
// src/push.ts
|
|
2241
|
-
import { spawnSync as
|
|
2279
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
2242
2280
|
function pushCandidates(sessions) {
|
|
2243
2281
|
const candidates = new Map;
|
|
2244
2282
|
for (const session of sessions) {
|
|
@@ -2349,7 +2387,7 @@ Push? [y/N] `);
|
|
|
2349
2387
|
return;
|
|
2350
2388
|
}
|
|
2351
2389
|
}
|
|
2352
|
-
const pushed =
|
|
2390
|
+
const pushed = spawnSync3("git", ["-C", root, "push", "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
|
|
2353
2391
|
if (pushed.status === 0) {
|
|
2354
2392
|
console.log(`Pushed ${branch}. Base for comparison was ${baseSha.slice(0, 8)}.`);
|
|
2355
2393
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maesttro",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Isolated Docker runner for local coding agents (opencode, claude, ...) under a dedicated bot GitHub identity, with curated configs baked in per client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|