omp-conductor 0.5.2 → 0.5.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 +11 -7
- package/package.json +1 -1
- package/src/config.ts +1 -1
- package/src/daemon.ts +4 -2
- package/src/gitops.ts +13 -10
- package/src/omp.ts +6 -5
- package/src/orchestrator.ts +1 -7
- package/src/release-policy.ts +6 -5
- package/src/tracker/github.ts +10 -11
- package/src/types.ts +3 -3
- package/src/verbs/actions.ts +12 -12
- package/src/verbs/client.ts +5 -6
- package/src/verbs/server.ts +7 -7
- package/src/verbs/socket.ts +2 -2
- package/src/worktree.ts +16 -14
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ tiers: first to an orchestrator session that can re-brief the worker, then to yo
|
|
|
8
8
|
|
|
9
9
|
You label an issue. Within one tick the conductor claims it on the tracker, cuts a
|
|
10
10
|
worktree, hands one omp worker a self-contained brief, and watches it to a green
|
|
11
|
-
PR. The worker then stops: it
|
|
11
|
+
PR. The worker then stops: it does not merge, tag, publish or deploy.
|
|
12
12
|
|
|
13
13
|
### Scope
|
|
14
14
|
|
|
@@ -2028,7 +2028,7 @@ dispatcher. The brief is explicit about the boundary:
|
|
|
2028
2028
|
| Add or update tests for behaviour it introduced. | Suppress a warning, delete an assertion, or special-case an input to make a check pass. |
|
|
2029
2029
|
| Run the repo's configured cheap gates, each from its listed `cwd`, over the whole tree. | Run docker or image builds, production builds, browser/e2e suites, or the full test suite on the shared host — CI owns the heavy gates. |
|
|
2030
2030
|
| Review its whole diff, then commit and publish once with `conductor_push`. One corrective push if CI is red. | Force-push, `git add -f`, or add AI/co-author attribution. There is no force path to reach: `conductor_push` publishes that run's branch fast-forward only and takes no other ref. Red twice means stop and report, not push a third time. |
|
|
2031
|
-
| Open a PR with `conductor_pr_create`, and poll CI to a verdict with `conductor_pr_status`. | Run `gh pr merge` — or reach `conductor_pr_merge`, which refuses a worker session mechanically. **A worker never
|
|
2031
|
+
| Open a PR with `conductor_pr_create`, and poll CI to a verdict with `conductor_pr_status`. | Run `gh pr merge` — or reach `conductor_pr_merge`, which refuses a worker session mechanically. **A worker is never authorised to merge**, whoever else holds the authority, so PRs land one at a time with a freshness re-check; two workers merging concurrently is how agent PRs clobber each other. Who *may* merge is the [`authority`](#configuration) answer, and it is never the worker. The verb refusal is mechanical; reaching for `gh` instead is a prohibition, and one that shows up as a merge absent from the [ledger](#the-ledger). |
|
|
2032
2032
|
| Escalate: ambiguity, a cross-repo contract, a needed credential, a product or data-migration decision, a blocking existing test, CI red twice, or most of the wall-clock budget burned. | Cut a release, push a tag, publish to npm, edit a deployment pin, deploy, or touch infrastructure or secrets. `conductor_release` and `conductor_label` refuse a worker whatever `releasePolicy` says, because the check compares the caller against the configured holder rather than ruling one value out. The in-session tripwire still blocks recognised release/deploy tool calls early and audits the attempt, but it is [defence in depth](#the-mediated-verbs-126), not the gate. |
|
|
2033
2033
|
|
|
2034
2034
|
The worker ends with a seven-line evidence report (issue, PR, observed head SHA,
|
|
@@ -2150,8 +2150,9 @@ every release shape under the most permissive config there is.
|
|
|
2150
2150
|
|
|
2151
2151
|
Identity is never an argument. `project`, `run`, `issue` and the caller's role
|
|
2152
2152
|
come from **which socket the call arrived on** plus the verified peer uid, and a
|
|
2153
|
-
request carrying any of those field names is refused outright, named.
|
|
2154
|
-
on run X
|
|
2153
|
+
request carrying any of those field names is refused outright, named. So a worker
|
|
2154
|
+
on run X cannot *ask* to merge run Y's PR: on its own channel that request is
|
|
2155
|
+
unexpressible, and a merge is refused for a worker session on any channel.
|
|
2155
2156
|
|
|
2156
2157
|
```
|
|
2157
2158
|
<state dir>/verbs/ daemon-owned, mode 0711
|
|
@@ -2294,9 +2295,12 @@ Known and deliberate in this version:
|
|
|
2294
2295
|
than a silently dropped page. `delivered` means Telegram accepted an attempt,
|
|
2295
2296
|
not that exactly one message exists. See
|
|
2296
2297
|
[Report delivery](#report-delivery-the-outbox).
|
|
2297
|
-
- **Workers stop at green PRs.** They never merge, release or
|
|
2298
|
-
actions default to a human,
|
|
2299
|
-
`authority` never grants them to a worker or the dispatch
|
|
2298
|
+
- **Workers stop at green PRs.** They are never authorised to merge, release or
|
|
2299
|
+
deploy: those actions default to a human, and while setup may grant either to
|
|
2300
|
+
the orchestrator, `authority` never grants them to a worker or the dispatch
|
|
2301
|
+
daemon. The verbs refuse a worker mechanically. A session that shells out
|
|
2302
|
+
instead is violating its brief rather than defeating a gate — sessions inherit
|
|
2303
|
+
the daemon's credentials — and the missing ledger entry is the evidence.
|
|
2300
2304
|
- **The worker gate is partial, and the orchestrator has none.** A worker's
|
|
2301
2305
|
structured `write` / `edit` / `read` / `grep` / `glob` calls are gated to its
|
|
2302
2306
|
worktree by an inline harness extension; `bash` is not, so a shell one-liner
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omp-conductor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "A 24/7 dispatcher that takes ready GitHub issues to green, mergeable PRs using omp coding sessions, with tiered escalation first to an orchestrator session and then to a human.",
|
package/src/config.ts
CHANGED
|
@@ -816,7 +816,7 @@ function normalizeRepos(parsed: unknown, label: string, problems: string[]): Rec
|
|
|
816
816
|
const credentialInUrl = cloneUrlCredentialProblem(cloneUrl);
|
|
817
817
|
if (credentialInUrl !== undefined) {
|
|
818
818
|
problems.push(
|
|
819
|
-
`${label}: routing.repos.${key}.cloneUrl ${credentialInUrl}
|
|
819
|
+
`${label}: routing.repos.${key}.cloneUrl ${credentialInUrl}. Use an SSH URL, or an https URL ` +
|
|
820
820
|
`backed by the daemon's own credential helper.`,
|
|
821
821
|
);
|
|
822
822
|
continue;
|
package/src/daemon.ts
CHANGED
|
@@ -140,7 +140,9 @@ interface Deps {
|
|
|
140
140
|
* Reads the connecting uid off a verb socket (#126). Resolved once at startup
|
|
141
141
|
* so the mechanism is logged before the first socket exists; absent on a host
|
|
142
142
|
* exposing no peer-credential call, where the `0600` socket under the
|
|
143
|
-
* daemon-owned `0711` parent is
|
|
143
|
+
* daemon-owned `0711` parent is all that keeps other local accounts out, and
|
|
144
|
+
* `status` says so. It never told two sessions apart — they share the daemon's
|
|
145
|
+
* uid.
|
|
144
146
|
*/
|
|
145
147
|
verbPeerReader?: PeerReader;
|
|
146
148
|
/**
|
|
@@ -564,7 +566,7 @@ export async function settleWorktree(
|
|
|
564
566
|
branch: string;
|
|
565
567
|
/**
|
|
566
568
|
* Publishes the run branch on the privileged side. Required rather than
|
|
567
|
-
* optional:
|
|
569
|
+
* optional: the run's commits live in a repository of its own,
|
|
568
570
|
* so a removal that did not publish first would delete the only copy —
|
|
569
571
|
* which is #121's data loss with one extra step. `undefined` is a visible
|
|
570
572
|
* decision at the call site, never an omission.
|
package/src/gitops.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The daemon's own git and `gh` operations.
|
|
3
3
|
*
|
|
4
|
-
* Everything here runs as the daemon, with the operator's credentials, and it
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
7
|
-
* answered by this file's call sites
|
|
8
|
-
* in the tree.
|
|
4
|
+
* Everything here runs as the daemon, with the operator's credentials, and it is
|
|
5
|
+
* the only place this package **constructs** credential material for a git or
|
|
6
|
+
* `gh` call of its own. That is the organising idea: "what does the conductor
|
|
7
|
+
* itself do with the operator's credential" is answered by this file's call sites
|
|
8
|
+
* rather than by auditing every `Bun.spawn` in the tree.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* daemon and
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* It is emphatically **not** a claim that nothing else can reach that credential.
|
|
11
|
+
* Sessions are child processes of the daemon and inherit its environment, so a
|
|
12
|
+
* session can reach whatever the daemon can — `omp.ts` spawns them and does not
|
|
13
|
+
* scrub anything. What keeps a run's publication accountable is that the
|
|
14
|
+
* dispatcher performs it: a push or a pull request the daemon did not make is a
|
|
15
|
+
* run it cannot account for, and is absent from the verb ledger. The mediated
|
|
16
|
+
* verbs route a worker's intent through here so the settlement record and the
|
|
17
|
+
* branch cannot disagree.
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
20
|
import { join } from "node:path";
|
package/src/omp.ts
CHANGED
|
@@ -111,12 +111,13 @@ const disposers = new WeakMap<AgentSessionLike, () => Promise<void>>();
|
|
|
111
111
|
* Start one omp coding session **in this process**, rooted at `cwd`, with a
|
|
112
112
|
* private agent registry and a file-backed transcript of its own.
|
|
113
113
|
*
|
|
114
|
-
* This is the far side of the boundary, not the dispatcher's entry point.
|
|
115
114
|
* Production callers want {@link createSession}, which runs this same function
|
|
116
|
-
* inside a child process
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
115
|
+
* inside a **child process**; the only caller of this one is `session-host.ts`,
|
|
116
|
+
* plus the tests that pin what it builds. Calling it directly from the daemon
|
|
117
|
+
* puts the harness back inside the dispatcher, where a crash, a hang or an
|
|
118
|
+
* exhausted heap takes the whole fleet down with it and no `MemoryMax` bounds
|
|
119
|
+
* it — that is what the child process buys. It is not a security boundary: the
|
|
120
|
+
* child runs as the daemon's own user.
|
|
120
121
|
*
|
|
121
122
|
* `sessionDir` chooses the directory the harness writes that transcript into;
|
|
122
123
|
* omitted, the harness picks its default location for `cwd`. Either way the
|
package/src/orchestrator.ts
CHANGED
|
@@ -84,13 +84,7 @@ export interface OrchestratorOpts {
|
|
|
84
84
|
model?: string;
|
|
85
85
|
releaseGrants?: ResolvedGrants;
|
|
86
86
|
onReleaseBlocked?: (shape: ReleaseShape) => void;
|
|
87
|
-
/**
|
|
88
|
-
* The OS principal this session runs as (#125). Its own, distinct from every
|
|
89
|
-
* worker slot: the orchestrator reads the state directory and its briefs, and
|
|
90
|
-
* must have no read or write access to any run checkout — a property the
|
|
91
|
-
* adversarial probe asserts rather than assumes.
|
|
92
|
-
*/
|
|
93
|
-
/** Control socket for the session child. See {@link OrchestratorOpts.boundary}. */
|
|
87
|
+
/** Control socket for the session child, beside its own working directory. */
|
|
94
88
|
socketPath?: string;
|
|
95
89
|
/**
|
|
96
90
|
* The orchestrator's own verb socket (#126) — a third, distinct one, never
|
package/src/release-policy.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* It was the only mechanism there was, and it was never the one anybody should
|
|
9
9
|
* have had to rely on.
|
|
10
10
|
*
|
|
11
|
-
* What actually stops a release now is that the
|
|
12
|
-
*
|
|
13
|
-
* verbs,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
11
|
+
* What actually stops a release now is that the daemon performs every one, and
|
|
12
|
+
* checks it first: the mediated verbs run their checks against the configured
|
|
13
|
+
* holder (`verbs/server.ts`), on the daemon's side of the socket. A worker that
|
|
14
|
+
* talks its way past every regex below has still not cut a release — and if it
|
|
15
|
+
* reaches for `gh` directly instead, the release is not in the ledger, which is
|
|
16
|
+
* the discrepancy an operator reads.
|
|
16
17
|
*
|
|
17
18
|
* It stays installed anyway, for the two things a tripwire is good at and a
|
|
18
19
|
* boundary is not: it refuses *early*, in the session, with an explanation the
|
package/src/tracker/github.ts
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
* daemon never handles a token itself: credentials stay in the user's keychain
|
|
6
6
|
* or `gh` config and are never passed as argv, written to a file, or logged.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* The environment this adapter hands `gh` comes from `credentialedEnv()` — the
|
|
9
|
+
* one named construction site of credential material in the daemon — so "what in
|
|
10
|
+
* this package reaches the operator's GitHub credential" is answered by that
|
|
11
|
+
* function's call sites rather than by auditing every spawn in the tree. It is a
|
|
12
|
+
* legibility property, not a boundary: sessions run as the daemon's own user and
|
|
13
|
+
* can reach the same credential. What keeps a run's mutations accountable is
|
|
14
|
+
* that the daemon performs them, and records them.
|
|
15
15
|
*
|
|
16
16
|
* ponytail: shelling out to `gh` is the deliberate simplification. The ceiling
|
|
17
17
|
* is per-call cost (one process spawn plus one TLS handshake per operation,
|
|
@@ -151,10 +151,9 @@ async function gh(argv: string[], stdin?: string): Promise<string> {
|
|
|
151
151
|
stdin: new Blob([stdin ?? ""]),
|
|
152
152
|
stdout: "pipe",
|
|
153
153
|
stderr: "pipe",
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// `credentialedEnv` finds this call.
|
|
154
|
+
// Explicit rather than inherited, so that a future change to this process's
|
|
155
|
+
// own environment could never silently break the tracker, and so that the
|
|
156
|
+
// grep for `credentialedEnv` finds this call.
|
|
158
157
|
env: credentialedEnv(),
|
|
159
158
|
});
|
|
160
159
|
|
package/src/types.ts
CHANGED
|
@@ -1324,9 +1324,9 @@ export const VERB_NAMES = [
|
|
|
1324
1324
|
"conductor_pr_merge",
|
|
1325
1325
|
"conductor_label",
|
|
1326
1326
|
"conductor_release",
|
|
1327
|
-
/** The one read verb.
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1327
|
+
/** The one read verb. It answers with the merge gate's own verdict, so
|
|
1328
|
+
* "pushed-green" is the dispatcher's reading of the PR rather than a claim the
|
|
1329
|
+
* worker makes about itself from whatever it happened to run. */
|
|
1330
1330
|
"conductor_pr_status",
|
|
1331
1331
|
] as const;
|
|
1332
1332
|
|
package/src/verbs/actions.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The privileged half of the verbs, wired to real commands (#126).
|
|
3
3
|
*
|
|
4
|
-
* This is the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* This is the only module in the verb path that performs a mutation, and it runs
|
|
5
|
+
* in the daemon. Push and pull-request creation are delegated to `gitops.ts`
|
|
6
|
+
* rather than re-shelled here, so there is one implementation of "how does a
|
|
7
|
+
* run's work reach the remote" — fast-forward only, no force path. The remaining
|
|
8
|
+
* verbs shell `gh` through {@link credentialedEnv} for the same reason: one
|
|
9
|
+
* named construction site, so its call sites are the audit.
|
|
10
10
|
*
|
|
11
11
|
* Kept apart from `server.ts` deliberately: every policy check above it is
|
|
12
12
|
* testable against the {@link VerbActions} interface with no repository, no
|
|
@@ -23,8 +23,8 @@ import type { ActionOutcome, ReleaseExecution, VerbActions } from "./server.ts";
|
|
|
23
23
|
*
|
|
24
24
|
* The three GitHub-shaped ones, and deliberately not `package-publish` or
|
|
25
25
|
* `deploy`: the daemon holds a GitHub credential and nothing else — no npm
|
|
26
|
-
* token, no registry login, no deploy key — and
|
|
27
|
-
*
|
|
26
|
+
* token, no registry login, no deploy key — and that is deliberate. A shape
|
|
27
|
+
* outside this list is refused by name in `server.ts` rather than
|
|
28
28
|
* attempted and reported as a command failure, because "we tried and it did not
|
|
29
29
|
* work" and "we were never able to do this" are different answers and only one
|
|
30
30
|
* of them tells an operator to go and do it themselves.
|
|
@@ -87,10 +87,10 @@ export function githubVerbActions(project: ProjectConfig, run: CommandRunner = s
|
|
|
87
87
|
return {
|
|
88
88
|
releasableShapes: GITHUB_RELEASABLE_SHAPES,
|
|
89
89
|
|
|
90
|
-
// Both
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
90
|
+
// Both live in `gitops.ts`, not here: they know the run's own repository
|
|
91
|
+
// layout, and they are the two paths whose fast-forward-only rule must have
|
|
92
|
+
// exactly one implementation. Re-shelling `git push` here would be a second
|
|
93
|
+
// one, and the second one is where a `--force` eventually appears.
|
|
94
94
|
push: (target) => pushRunBranch(project, target),
|
|
95
95
|
|
|
96
96
|
createPr: (target, opts) => openRunPr(project, target, opts),
|
package/src/verbs/client.ts
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* The child-side half of the mutation verbs (#126): a thin client, and nothing
|
|
3
3
|
* else.
|
|
4
4
|
*
|
|
5
|
-
* This module runs **inside the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* can read, and on a bad day rewrite — so it decides nothing:
|
|
5
|
+
* This module runs **inside the session process**, as the daemon's own user, so
|
|
6
|
+
* it is code the model can read and on a bad day rewrite — and it can reach the
|
|
7
|
+
* same credentials the daemon can. That is exactly why it decides nothing:
|
|
9
8
|
*
|
|
10
9
|
* - **No policy branch.** It does not know what `authority.merge` says, which
|
|
11
10
|
* release shapes are granted, or whether the fleet is paused. Every answer
|
|
@@ -15,8 +14,8 @@
|
|
|
15
14
|
* There is nothing here to steal.
|
|
16
15
|
* - **No local fallback.** No socket means the verb fails, loudly, with an
|
|
17
16
|
* explanation. A client that fell back to `git push` when the daemon was
|
|
18
|
-
* unreachable would
|
|
19
|
-
*
|
|
17
|
+
* unreachable would leave the settlement record silently incomplete, and it
|
|
18
|
+
* would do so precisely when something had already gone wrong.
|
|
20
19
|
*
|
|
21
20
|
* The identity fields are absent by construction rather than stripped: this
|
|
22
21
|
* client has no project, run or role to send, because it was never told one.
|
package/src/verbs/server.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* The privileged half of the mutation verbs (#126): every check, in the daemon.
|
|
3
3
|
*
|
|
4
4
|
* The child-side tool handler forwards arguments and renders an answer. It
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* other side of the socket.
|
|
5
|
+
* evaluates no policy and writes no ledger entry, because it runs inside the
|
|
6
|
+
* session process — anything it decided would be decided by code the model can
|
|
7
|
+
* rewrite, and a session runs as the daemon's own user. Everything below runs on
|
|
8
|
+
* the other side of the socket, which is what makes the checks real.
|
|
9
9
|
*
|
|
10
10
|
* Four rules shape the order of the checks, and each is here because prose
|
|
11
11
|
* alone did not hold it on `main`:
|
|
@@ -819,7 +819,7 @@ async function releaseVerb(
|
|
|
819
819
|
return refuse(
|
|
820
820
|
"release-shape-not-executable",
|
|
821
821
|
`refused: this daemon cannot cut a ${shape}. It holds a GitHub credential and nothing else — no npm token, ` +
|
|
822
|
-
"no deploy key — and
|
|
822
|
+
"no deploy key — and that is deliberate. It can do " +
|
|
823
823
|
`${deps.actions.releasableShapes.join(", ")}. Escalate this one to your operator.`,
|
|
824
824
|
);
|
|
825
825
|
}
|
|
@@ -843,8 +843,8 @@ async function releaseVerb(
|
|
|
843
843
|
|
|
844
844
|
|
|
845
845
|
/**
|
|
846
|
-
* The one read verb, and the reason a
|
|
847
|
-
*
|
|
846
|
+
* The one read verb, and the reason a worker reports a PR's real state rather
|
|
847
|
+
* than its own recollection.
|
|
848
848
|
*
|
|
849
849
|
* Reuses `Tracker.verifyPr`, so the answer is literally the merge gate's own
|
|
850
850
|
* verdict rather than a second opinion assembled here — including the
|
package/src/verbs/socket.ts
CHANGED
|
@@ -361,14 +361,14 @@ export function peerVerdict(
|
|
|
361
361
|
return {
|
|
362
362
|
ok: true,
|
|
363
363
|
basis: "socket-ownership",
|
|
364
|
-
why: "no uid was named for this socket, so the 0600 mode under the daemon-owned 0711 parent is
|
|
364
|
+
why: "no uid was named for this socket, so the 0600 mode under the daemon-owned 0711 parent is all that keeps other local accounts out",
|
|
365
365
|
};
|
|
366
366
|
}
|
|
367
367
|
if (peer === undefined) {
|
|
368
368
|
return {
|
|
369
369
|
ok: true,
|
|
370
370
|
basis: "socket-ownership",
|
|
371
|
-
why: "this host exposes no peer
|
|
371
|
+
why: "this host exposes no peer-credential call, so the 0600 socket under the daemon-owned 0711 parent is all that keeps other local accounts out",
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
374
|
if (peer.uid !== expected.uid) {
|
package/src/worktree.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
* once and refreshed, rather than a fresh full clone per issue: a module repo's
|
|
7
7
|
* history is fetched one time and every later run pays only for the delta.
|
|
8
8
|
*
|
|
9
|
-
* **A run gets its own repository, not a linked worktree of the mirror, and
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* **A run gets its own repository, not a linked worktree of the mirror, and that
|
|
10
|
+
* is load-bearing rather than a preference.** A linked worktree shares the
|
|
11
|
+
* mirror's common dir — refs, objects, index locks — so every run would be
|
|
12
|
+
* writing into one store: concurrent runs contend on the same index lock, a
|
|
13
|
+
* `git gc` in one can pull objects from under another, and a run's refs are one
|
|
14
|
+
* `git update-ref` away from a sibling's. Here the objects arrive read-only through git's alternates
|
|
14
15
|
* mechanism and the run's refs, index and commits live in its own git dir. The
|
|
15
16
|
* documented residual is that a run can still *read* another run's objects out
|
|
16
17
|
* of the shared store: bounded, same source, no write path, no credential, and
|
|
@@ -61,9 +62,9 @@ async function runGit(
|
|
|
61
62
|
stdin: "ignore",
|
|
62
63
|
stdout: "pipe",
|
|
63
64
|
stderr: "pipe",
|
|
64
|
-
// Every git call in this module
|
|
65
|
-
//
|
|
66
|
-
//
|
|
65
|
+
// Every git call in this module is the daemon's own — provisioning,
|
|
66
|
+
// salvaging or publishing — so it goes through the one named construction
|
|
67
|
+
// site of credential material. It also carries
|
|
67
68
|
// GIT_TERMINAL_PROMPT=0, because an unattended dispatcher must fail loudly
|
|
68
69
|
// rather than block forever on a prompt nobody is there to answer.
|
|
69
70
|
env: credentialedEnv(),
|
|
@@ -287,8 +288,8 @@ export async function ensureMirror(
|
|
|
287
288
|
* git's own read-only borrowing mechanism — so provisioning still costs one
|
|
288
289
|
* fetch of the delta rather than a full clone, exactly as the linked worktree
|
|
289
290
|
* it replaces did. What changed is who can write what: the run's refs, index
|
|
290
|
-
* and commits are its own, and
|
|
291
|
-
* or the mirror
|
|
291
|
+
* and commits are its own, and no ordinary git operation in the checkout reaches
|
|
292
|
+
* a sibling run's refs or the mirror.
|
|
292
293
|
*
|
|
293
294
|
* `origin` is deliberately the real clone URL and not the mirror path. Pointing
|
|
294
295
|
* it at the mirror would make a worker's ordinary `git push origin HEAD`
|
|
@@ -553,9 +554,9 @@ function parseCachedNameStatus(raw: string): { files: string[]; newPaths: string
|
|
|
553
554
|
* commit and re-runs its checks. That is the price of work outliving its host.
|
|
554
555
|
*
|
|
555
556
|
* `publish` is a **required** parameter rather than an optional one with a
|
|
556
|
-
* no-op default, and that is the whole design. The salvage commit
|
|
557
|
-
*
|
|
558
|
-
*
|
|
557
|
+
* no-op default, and that is the whole design. The salvage commit does not push
|
|
558
|
+
* for itself: the network hop is the daemon's, through `pushRunBranch`, so the
|
|
559
|
+
* sha that reaches the remote is the sha the dispatcher recorded. A defaulted parameter
|
|
559
560
|
* is how a future call site silently stops publishing salvaged work and nobody
|
|
560
561
|
* finds out until the host that held it is gone — which is #121's failure with
|
|
561
562
|
* one extra step. Passing `undefined` is allowed, and is a visible decision at
|
|
@@ -676,7 +677,8 @@ export async function removeWorktree(
|
|
|
676
677
|
if (existsSync(worktreePath)) {
|
|
677
678
|
// A run repo is a plain directory now, so removal is a plain removal — but
|
|
678
679
|
// the `worktree remove` is still attempted first, because a fleet upgrading
|
|
679
|
-
//
|
|
680
|
+
// from a release that used linked worktrees has live trees that ARE linked
|
|
681
|
+
// worktrees of this mirror, and
|
|
680
682
|
// deleting one of those without deregistering it leaves the mirror
|
|
681
683
|
// believing the branch is still checked out. It refuses `worktree add` and,
|
|
682
684
|
// worse, refuses to delete the branch at cleanup. The failure is expected
|