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 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`. That clone is the only
77
- part of the repo the container sees:
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
- | `<repo>/.git/objects` (read-only, same absolute path) | your repo's object store | Base history, borrowed by the clone through its `alternates`. The real `.git` is never mounted. |
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(["-v", `${dir.ghDir}:${CONTAINER_GH_CONFIG_DIR}:ro`], env),
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 writeFileSync7
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
- writeFileSync7(`${path}.tmp`, `${JSON.stringify(record, null, 2)}
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: objects, readOnly: true },
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
- "-v",
1986
- `${host}:${container}${readOnly ? ":ro" : ""}`
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 spawnSync2 } from "node:child_process";
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 = spawnSync2("git", ["-C", root, "push", "origin", `${tip}:refs/heads/${branch}`], { stdio: "inherit" });
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.1",
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": {