@bridge4dev/runner 0.11.0 → 0.22.1
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/dist/adapters/claude.d.ts +15 -7
- package/dist/adapters/claude.js +1024 -70
- package/dist/adapters/codex.d.ts +18 -3
- package/dist/adapters/codex.js +224 -65
- package/dist/adapters/questions.d.ts +42 -0
- package/dist/adapters/questions.js +86 -0
- package/dist/adapters/types.d.ts +200 -4
- package/dist/attachments.d.ts +8 -1
- package/dist/attachments.js +22 -4
- package/dist/auto-resume.d.ts +18 -0
- package/dist/auto-resume.js +104 -0
- package/dist/commit-message.d.ts +51 -0
- package/dist/commit-message.js +224 -0
- package/dist/config.d.ts +29 -6
- package/dist/config.js +15 -0
- package/dist/crash-note.d.ts +54 -0
- package/dist/crash-note.js +105 -0
- package/dist/git.d.ts +71 -0
- package/dist/git.js +207 -10
- package/dist/gitops.d.ts +489 -12
- package/dist/gitops.js +1717 -96
- package/dist/index.js +435 -32
- package/dist/paths.d.ts +26 -0
- package/dist/paths.js +34 -0
- package/dist/policy.d.ts +63 -0
- package/dist/policy.js +412 -10
- package/dist/protocol.d.ts +382 -60
- package/dist/protocol.js +104 -1
- package/dist/recipe-schema.d.ts +310 -0
- package/dist/recipe-schema.js +103 -0
- package/dist/recipe.d.ts +94 -0
- package/dist/recipe.js +238 -0
- package/dist/self-update.d.ts +7 -0
- package/dist/self-update.js +171 -23
- package/dist/service-unit.d.ts +79 -0
- package/dist/service-unit.js +211 -0
- package/dist/supervisor.d.ts +108 -1
- package/dist/supervisor.js +1010 -56
- package/dist/verify-queue.d.ts +17 -0
- package/dist/verify-queue.js +100 -0
- package/dist/verify.d.ts +203 -0
- package/dist/verify.js +788 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { systemdUserHome } from './paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* The systemd user unit, and the one decision inside it that matters: what to
|
|
7
|
+
* exec.
|
|
8
|
+
*
|
|
9
|
+
* The first version baked `realpathSync(process.argv[1])` — the resolved file
|
|
10
|
+
* inside the installed package directory. That pins the service to a directory
|
|
11
|
+
* that a package RENAME deletes: `@devbridge/runner` → `@bridge4dev/runner` moved
|
|
12
|
+
* the file, the unit kept pointing at the old path, and `systemctl restart`
|
|
13
|
+
* failed with ENOENT. The runner simply never came back and the server went
|
|
14
|
+
* offline until someone logged in — the one outcome an update must never produce.
|
|
15
|
+
*
|
|
16
|
+
* So the unit execs the COMMAND (`<prefix>/bin/devbridge-runner`), which npm
|
|
17
|
+
* re-creates on every install whatever the package is called. A source checkout
|
|
18
|
+
* has no such symlink, and there the resolved script is the honest answer.
|
|
19
|
+
*/
|
|
20
|
+
export const SERVICE_NAME = 'devbridge-runner';
|
|
21
|
+
const COMMAND_NAME = 'devbridge-runner';
|
|
22
|
+
/** `<prefix>/bin/devbridge-runner` for an installed package, else the script. */
|
|
23
|
+
export function unitExecTarget(argv1 = process.argv[1] ?? '') {
|
|
24
|
+
// Invoked through the command itself: that is already the stable path.
|
|
25
|
+
try {
|
|
26
|
+
if (fs.lstatSync(argv1).isSymbolicLink()) {
|
|
27
|
+
return { execStart: path.resolve(argv1), viaCommand: true };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* fall through to the resolved script */
|
|
32
|
+
}
|
|
33
|
+
let script;
|
|
34
|
+
try {
|
|
35
|
+
script = fs.realpathSync(argv1);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { execStart: argv1, viaCommand: false };
|
|
39
|
+
}
|
|
40
|
+
// Invoked as the script inside a global install: find the prefix that owns the
|
|
41
|
+
// command. `<prefix>/lib/node_modules/@scope/runner/dist/index.js` → `<prefix>`.
|
|
42
|
+
if (script.split(path.sep).includes('node_modules')) {
|
|
43
|
+
let dir = path.dirname(script);
|
|
44
|
+
for (let i = 0; i < 6; i++) {
|
|
45
|
+
const candidate = path.join(dir, 'bin', COMMAND_NAME);
|
|
46
|
+
if (fs.existsSync(candidate))
|
|
47
|
+
return { execStart: candidate, viaCommand: true };
|
|
48
|
+
const parent = path.dirname(dir);
|
|
49
|
+
if (parent === dir)
|
|
50
|
+
break;
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { execStart: script, viaCommand: false };
|
|
55
|
+
}
|
|
56
|
+
export function unitPath(home = systemdUserHome()) {
|
|
57
|
+
return path.join(home, '.config', 'systemd', 'user', `${SERVICE_NAME}.service`);
|
|
58
|
+
}
|
|
59
|
+
export function buildUnit(execStart) {
|
|
60
|
+
const target = execStart ?? unitExecTarget().execStart;
|
|
61
|
+
// The command carries a `#!/usr/bin/env node` shebang, so it is exec'd directly;
|
|
62
|
+
// a bare script path needs the interpreter spelled out.
|
|
63
|
+
const command = target.endsWith('.js')
|
|
64
|
+
? `${process.execPath} ${target} daemon`
|
|
65
|
+
: `${target} daemon`;
|
|
66
|
+
return ([
|
|
67
|
+
'[Unit]',
|
|
68
|
+
'Description=DevBridge Dev Runner',
|
|
69
|
+
'After=network-online.target',
|
|
70
|
+
'',
|
|
71
|
+
'[Service]',
|
|
72
|
+
`ExecStart=${command}`,
|
|
73
|
+
'Restart=always',
|
|
74
|
+
'RestartSec=5',
|
|
75
|
+
'',
|
|
76
|
+
'[Install]',
|
|
77
|
+
'WantedBy=default.target',
|
|
78
|
+
].join('\n') + '\n');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resource policy for the service, and why it does not live in the unit above.
|
|
82
|
+
*
|
|
83
|
+
* The unit is written ONCE, at install time. Until 0.21.0 it carried
|
|
84
|
+
* `CPUQuota=80%` and `MemoryMax=2G` — and those two lines cost a production
|
|
85
|
+
* server every session it was running (QA-112 BLOCKER-1, 2026-07-30):
|
|
86
|
+
*
|
|
87
|
+
* - every agent the runner starts, and every `pnpm build`/`tsc`/`vitest` that
|
|
88
|
+
* agent runs, is a CHILD of this service and therefore inside this cgroup.
|
|
89
|
+
* Three measured `claude` processes are 512/577/646 MB and one workspace
|
|
90
|
+
* `pnpm -r typecheck` peaks at 1571 MB, against a ceiling of 2048 MB;
|
|
91
|
+
* - `OOMPolicy` defaults to `stop`, so the kernel killing ONE of those children
|
|
92
|
+
* tore down the whole service — and with it every other session on the
|
|
93
|
+
* machine. `Restart=always` then brought the daemon back with an empty
|
|
94
|
+
* session map, which is what the user sees as
|
|
95
|
+
* «Runner reconnected. The session was resumed»;
|
|
96
|
+
* - `CPUQuota=80%` is 0.8 of ONE core for all of the above, which is separately
|
|
97
|
+
* what starves the event loop until the gateway's heartbeat gives up on it.
|
|
98
|
+
*
|
|
99
|
+
* So the policy is versioned and shipped as a drop-in instead. A drop-in can
|
|
100
|
+
* RESET a directive the main unit set (`MemoryMax=` with no value clears it),
|
|
101
|
+
* which is the only way to fix the servers that already have the bad numbers
|
|
102
|
+
* baked in — and it never overwrites a unit the operator edited by hand.
|
|
103
|
+
*/
|
|
104
|
+
export const LIMITS_VERSION = 2;
|
|
105
|
+
const LIMITS_MARKER = '# devbridge-limits-version:';
|
|
106
|
+
/** `zz-` so it sorts last: an operator's own drop-in should still win. */
|
|
107
|
+
const LIMITS_FILE = 'zz-devbridge-limits.conf';
|
|
108
|
+
export function limitsOverridePath(home = systemdUserHome()) {
|
|
109
|
+
return path.join(home, '.config', 'systemd', 'user', `${SERVICE_NAME}.service.d`, LIMITS_FILE);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* `CPUQuota` worth keeping: enough headroom that a runaway build cannot make the
|
|
113
|
+
* box unreachable, but never so little that ordinary work is throttled.
|
|
114
|
+
*
|
|
115
|
+
* One core is reserved for the system (sshd, journald, the runner's own event
|
|
116
|
+
* loop). Below four cores there is nothing to reserve without crippling the
|
|
117
|
+
* agents, so no quota is set at all — an unusable dev server is a worse failure
|
|
118
|
+
* than a busy one.
|
|
119
|
+
*/
|
|
120
|
+
export function cpuQuotaPercent(cpuCount = os.cpus().length) {
|
|
121
|
+
return cpuCount >= 4 ? (cpuCount - 1) * 100 : null;
|
|
122
|
+
}
|
|
123
|
+
export function buildLimitsOverride(cpuCount = os.cpus().length) {
|
|
124
|
+
const quota = cpuQuotaPercent(cpuCount);
|
|
125
|
+
return ([
|
|
126
|
+
`${LIMITS_MARKER} ${LIMITS_VERSION}`,
|
|
127
|
+
'# Managed by devbridge-runner. Put your own overrides in a file that sorts',
|
|
128
|
+
'# after this one, or edit the unit itself — neither is touched by updates.',
|
|
129
|
+
'',
|
|
130
|
+
'[Unit]',
|
|
131
|
+
// Five fast failures used to leave the service in `failed` and the server
|
|
132
|
+
// offline until someone logged in. A dev runner must always come back;
|
|
133
|
+
// crash loops are surfaced through `lastExit` in hello, not by giving up.
|
|
134
|
+
'StartLimitIntervalSec=0',
|
|
135
|
+
'StartLimitBurst=0',
|
|
136
|
+
'',
|
|
137
|
+
'[Service]',
|
|
138
|
+
// The whole point of the change: one child's OOM must not take the fleet.
|
|
139
|
+
'OOMPolicy=continue',
|
|
140
|
+
// Clears `MemoryMax=2G` from units written before 0.21.0.
|
|
141
|
+
'MemoryMax=',
|
|
142
|
+
// Soft pressure instead of a hard ceiling: the kernel reclaims and
|
|
143
|
+
// throttles rather than killing, and the machine keeps a fifth of its
|
|
144
|
+
// memory for everything that is not this service.
|
|
145
|
+
'MemoryHigh=80%',
|
|
146
|
+
// Either a computed quota, or an explicit reset — both of which clear the
|
|
147
|
+
// `CPUQuota=80%` baked into units written before 0.21.0.
|
|
148
|
+
quota === null ? 'CPUQuota=' : `CPUQuota=${quota}%`,
|
|
149
|
+
// Needed for `doctor` and for the memory/CPU figures the runner reports.
|
|
150
|
+
'MemoryAccounting=yes',
|
|
151
|
+
'CPUAccounting=yes',
|
|
152
|
+
// An agent running a monorepo build forks a lot; the default is per-user.
|
|
153
|
+
'TasksMax=8192',
|
|
154
|
+
].join('\n') + '\n');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Is the shipped resource policy missing or from an older runner?
|
|
158
|
+
*
|
|
159
|
+
* Deliberately version-based rather than content-based: an operator may add
|
|
160
|
+
* their own directives to our file, and re-writing on every start would fight
|
|
161
|
+
* them. Only the version number decides.
|
|
162
|
+
*/
|
|
163
|
+
export function limitsOverrideIsOutdated(readFile = (p) => fs.readFileSync(p, 'utf8'), home = systemdUserHome()) {
|
|
164
|
+
let contents;
|
|
165
|
+
try {
|
|
166
|
+
contents = readFile(limitsOverridePath(home));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return true; // never written — every server that predates 0.21.0
|
|
170
|
+
}
|
|
171
|
+
const line = contents.split('\n').find((l) => l.startsWith(LIMITS_MARKER));
|
|
172
|
+
if (!line)
|
|
173
|
+
return true;
|
|
174
|
+
const version = Number.parseInt(line.slice(LIMITS_MARKER.length).trim(), 10);
|
|
175
|
+
return !Number.isFinite(version) || version < LIMITS_VERSION;
|
|
176
|
+
}
|
|
177
|
+
/** Write the drop-in. Returns false when nothing needed doing. */
|
|
178
|
+
export function writeLimitsOverride(force = false, home = systemdUserHome()) {
|
|
179
|
+
if (!force && !limitsOverrideIsOutdated(undefined, home))
|
|
180
|
+
return false;
|
|
181
|
+
const target = limitsOverridePath(home);
|
|
182
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
183
|
+
fs.writeFileSync(target, buildLimitsOverride(), { mode: 0o644 });
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Does the installed unit point at something that no longer exists?
|
|
188
|
+
*
|
|
189
|
+
* Used after an update: if the package moved (a rename), a unit pinned to the old
|
|
190
|
+
* directory would kill the runner on the very restart the update asks for. Only a
|
|
191
|
+
* missing target counts — a unit the user edited on purpose is left alone.
|
|
192
|
+
*/
|
|
193
|
+
export function unitIsBroken(readFile = (p) => fs.readFileSync(p, 'utf8')) {
|
|
194
|
+
let contents;
|
|
195
|
+
try {
|
|
196
|
+
contents = readFile(unitPath());
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false; // no unit of ours — nothing to repair
|
|
200
|
+
}
|
|
201
|
+
const line = contents.split('\n').find((l) => l.startsWith('ExecStart='));
|
|
202
|
+
if (!line)
|
|
203
|
+
return false;
|
|
204
|
+
const parts = line.slice('ExecStart='.length).trim().split(/\s+/);
|
|
205
|
+
// `ExecStart=/usr/bin/node /path/index.js daemon` or `ExecStart=/path/cmd daemon`
|
|
206
|
+
const target = parts[0]?.endsWith('node') ? parts[1] : parts[0];
|
|
207
|
+
if (!target)
|
|
208
|
+
return false;
|
|
209
|
+
return !fs.existsSync(target);
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=service-unit.js.map
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JournalStore } from './journal.js';
|
|
2
|
+
import { proposeCommitMessage } from './commit-message.js';
|
|
2
3
|
import { selfUpdate, type SelfUpdateOutcome } from './self-update.js';
|
|
3
4
|
import type { RunnerWsClient } from './ws-client.js';
|
|
4
5
|
import type { SessionDescriptor } from './protocol.js';
|
|
@@ -30,6 +31,14 @@ export interface SupervisorOptions {
|
|
|
30
31
|
* machine's owner decides how much of their machine an agent fleet may take.
|
|
31
32
|
*/
|
|
32
33
|
maxSessionsLimit?: number;
|
|
34
|
+
/**
|
|
35
|
+
* `[verify] enabled` from the runner's own config (session 14). The machine's
|
|
36
|
+
* owner has the last word on whether project recipes run here at all;
|
|
37
|
+
* `false` means the capability is not announced and no run can be started.
|
|
38
|
+
*/
|
|
39
|
+
verifyEnabled?: boolean;
|
|
40
|
+
/** Test seam for the one-shot commit-message run. */
|
|
41
|
+
proposeCommitMessage?: typeof proposeCommitMessage;
|
|
33
42
|
}
|
|
34
43
|
export declare class Supervisor {
|
|
35
44
|
private readonly ws;
|
|
@@ -54,10 +63,32 @@ export declare class Supervisor {
|
|
|
54
63
|
private readonly repoLocks;
|
|
55
64
|
/** An update is installing right now — a second one would fight it. */
|
|
56
65
|
private selfUpdateInFlight;
|
|
66
|
+
/** Session 14: one project-recipe run per machine, and its verdict queue. */
|
|
67
|
+
private readonly verify;
|
|
68
|
+
private readonly verifyReports;
|
|
57
69
|
constructor(ws: RunnerWsClient, opts: SupervisorOptions);
|
|
70
|
+
/**
|
|
71
|
+
* Push every unacked verdict at the API.
|
|
72
|
+
*
|
|
73
|
+
* Called on each reconnect and whenever a run finishes. Sending a report the
|
|
74
|
+
* API already has is harmless — it is keyed by `runId` and stored idempotently
|
|
75
|
+
* — while not sending one is a verdict that never existed.
|
|
76
|
+
*/
|
|
77
|
+
private flushVerifyReports;
|
|
58
78
|
get activeSessionIds(): string[];
|
|
59
79
|
private onFrame;
|
|
60
80
|
private startSession;
|
|
81
|
+
/**
|
|
82
|
+
* Where this session is going to work — a worktree of its own, or the project
|
|
83
|
+
* folder itself (session 16).
|
|
84
|
+
*
|
|
85
|
+
* The BRANCH path takes the repo lock because `worktree add` writes into the
|
|
86
|
+
* shared `.git` (registration + prune), exactly like commit/apply/revert. The
|
|
87
|
+
* DIRECT path takes none: it only READS which branch a folder is on, and
|
|
88
|
+
* queueing every session start behind whatever merge happens to be running
|
|
89
|
+
* would be a lock bought for nothing.
|
|
90
|
+
*/
|
|
91
|
+
private prepareWorkspace;
|
|
61
92
|
/**
|
|
62
93
|
* Spin the adapter up — for a fresh session, a resume-on-next-message, or a
|
|
63
94
|
* free CHAT session with no prompt at all (the agent boots, reports its
|
|
@@ -100,6 +131,23 @@ export declare class Supervisor {
|
|
|
100
131
|
*/
|
|
101
132
|
private pauseForBudget;
|
|
102
133
|
private pumpEvents;
|
|
134
|
+
/**
|
|
135
|
+
* "…and N of them are waiting for an answer from you."
|
|
136
|
+
*
|
|
137
|
+
* An open question pins its slot deliberately (`isParkable`), so a message
|
|
138
|
+
* that blames a running turn sends the user to wait for something that will
|
|
139
|
+
* never happen. Empty when nothing is waiting.
|
|
140
|
+
*/
|
|
141
|
+
private waitingForAnswerSuffix;
|
|
142
|
+
/**
|
|
143
|
+
* Close out every ask this session still has open, with a stated cause.
|
|
144
|
+
*
|
|
145
|
+
* Idempotent: the adapter reports its own `question_resolved` when it can, and
|
|
146
|
+
* `forwardEvent` clears the id — so by the time this runs the set is usually
|
|
147
|
+
* already empty. What it catches is the path where the adapter never got the
|
|
148
|
+
* chance, and the alternative there is a card that stays clickable forever.
|
|
149
|
+
*/
|
|
150
|
+
private withdrawOpenQuestions;
|
|
103
151
|
/**
|
|
104
152
|
* Deliver messages that were held because every slot was taken.
|
|
105
153
|
*
|
|
@@ -140,10 +188,27 @@ export declare class Supervisor {
|
|
|
140
188
|
* returns false and the caller tells the user rather than thrashing.
|
|
141
189
|
*/
|
|
142
190
|
private ensureCapacity;
|
|
143
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Idle after a finished turn — safe to kill the process and resume later.
|
|
193
|
+
*
|
|
194
|
+
* An open question is the exception (session 12): WAITING_INPUT there does
|
|
195
|
+
* NOT mean "the turn is over", it means the agent's tool call is parked on a
|
|
196
|
+
* human. Parking such a session killed a live turn — and, worse, killed the
|
|
197
|
+
* card the user was about to answer — the moment another session wanted a
|
|
198
|
+
* slot.
|
|
199
|
+
*/
|
|
144
200
|
private isParkable;
|
|
145
201
|
private park;
|
|
146
202
|
private forwardEvent;
|
|
203
|
+
/**
|
|
204
|
+
* The dashboard's answer to a parked question (session 12).
|
|
205
|
+
*
|
|
206
|
+
* A miss is reported in the feed rather than swallowed: the three cases that
|
|
207
|
+
* get here — a card from a previous life of the session, a second click, an
|
|
208
|
+
* ask the runner already withdrew — all look identical to the user unless
|
|
209
|
+
* somebody says so.
|
|
210
|
+
*/
|
|
211
|
+
private onQuestionAnswer;
|
|
147
212
|
private onUserMessage;
|
|
148
213
|
/**
|
|
149
214
|
* Run delivery work for one session, strictly after whatever is already
|
|
@@ -212,5 +277,47 @@ export declare class Supervisor {
|
|
|
212
277
|
/** Graceful daemon shutdown: kill agents, keep sessions resumable server-side. */
|
|
213
278
|
shutdown(): void;
|
|
214
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* The first message the agent gets.
|
|
282
|
+
*
|
|
283
|
+
* Two shapes, and the difference is whether tickets were handed over
|
|
284
|
+
* (session 16, owner's decision):
|
|
285
|
+
*
|
|
286
|
+
* - **No tickets** — exactly what the human typed, and an empty prompt stays
|
|
287
|
+
* empty. That is how you get a session that boots and waits for you to talk
|
|
288
|
+
* first; it is a feature, not an oversight.
|
|
289
|
+
* - **Tickets handed to a TICKET session** — the assignment is always sent and
|
|
290
|
+
* cannot be removed: «Implement ticket #28.» Handing a ticket over IS the
|
|
291
|
+
* instruction, so making the human retype it was ceremony. Anything they
|
|
292
|
+
* typed follows underneath.
|
|
293
|
+
*
|
|
294
|
+
* What is NOT here any more: eight lines about which MCP tools to call and
|
|
295
|
+
* which statuses to move through. That is a standing convention of the project,
|
|
296
|
+
* true on the twentieth turn as much as the first, so it moved to the system
|
|
297
|
+
* prompt beside `CLAUDE.md` — which is read last and therefore overrides ours.
|
|
298
|
+
*/
|
|
215
299
|
export declare function composeInitialPrompt(descriptor: SessionDescriptor): string;
|
|
300
|
+
/**
|
|
301
|
+
* Extra system-prompt material: where this session is in git, and how tickets
|
|
302
|
+
* are meant to move (session 13).
|
|
303
|
+
*
|
|
304
|
+
* Facts about THIS SESSION, and nothing else. They exist in no file on disk —
|
|
305
|
+
* which branch the agent is on, that it must not push, where a plan belongs,
|
|
306
|
+
* what to do with the tickets it was given — so somebody has to say them, and
|
|
307
|
+
* that somebody is us.
|
|
308
|
+
*
|
|
309
|
+
* The project's own documentation is deliberately NOT here. Both agents read
|
|
310
|
+
* their own file natively, verified live: Claude picks up `CLAUDE.md` through
|
|
311
|
+
* its memory mechanism (since `settingSources` includes `'project'`), and Codex
|
|
312
|
+
* picks up `AGENTS.md` even under the runner's isolated `CODEX_HOME`. Pasting a
|
|
313
|
+
* copy on top of that was work we were doing for no one — and when it was
|
|
314
|
+
* switched off for Claude alone it briefly left repositories that carry only
|
|
315
|
+
* `AGENTS.md` with nothing at all, which is precisely the kind of hole a
|
|
316
|
+
* half-measure digs.
|
|
317
|
+
*
|
|
318
|
+
* A repository that wants both agents equipped ships both files, or symlinks
|
|
319
|
+
* one to the other. That is a repository convention and not something a runner
|
|
320
|
+
* should paper over.
|
|
321
|
+
*/
|
|
322
|
+
export declare function composeWorkspaceContext(descriptor: SessionDescriptor): string;
|
|
216
323
|
//# sourceMappingURL=supervisor.d.ts.map
|