@bridge4dev/runner 0.31.0 → 0.34.0
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.js +190 -47
- package/dist/adapters/codex.js +40 -14
- package/dist/adapters/types.d.ts +16 -1
- package/dist/index.js +13 -0
- package/dist/policy.d.ts +52 -1
- package/dist/policy.js +387 -53
- package/dist/protocol.d.ts +148 -18
- package/dist/protocol.js +38 -0
- package/dist/supervisor.d.ts +24 -0
- package/dist/supervisor.js +162 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +9 -9
package/dist/supervisor.js
CHANGED
|
@@ -22,6 +22,34 @@ import { availableModes, MODE_REFUSED_TEXT } from './adapters/types.js';
|
|
|
22
22
|
/** Refusals shared by every checkpoint command (ticket #126). */
|
|
23
23
|
const CHECKPOINTS_OFF = 'Restore points are switched off on this server ([checkpoints] enabled = false)';
|
|
24
24
|
const AGENT_BUSY = 'The agent is still working — stop the turn first';
|
|
25
|
+
/**
|
|
26
|
+
* Adapter events that exist only because the agent is producing output right
|
|
27
|
+
* now (ticket #185). `message` is deliberately absent — a user echo carries the
|
|
28
|
+
* same type and proves nothing about the agent — and is handled at the call site.
|
|
29
|
+
*/
|
|
30
|
+
const AGENT_OUTPUT_EVENTS = new Set(['tool', 'thinking']);
|
|
31
|
+
/**
|
|
32
|
+
* The project's git policy, lifted out of a session descriptor (session 18).
|
|
33
|
+
*
|
|
34
|
+
* One function so that «which descriptor fields are the git policy» is answered
|
|
35
|
+
* once. Absent fields are passed through as absent — `resolveGitPolicy` in
|
|
36
|
+
* `policy.ts` is the single place that decides what absent MEANS, and inventing
|
|
37
|
+
* a default here would put a second such place in the codebase, facing the
|
|
38
|
+
* other way.
|
|
39
|
+
*/
|
|
40
|
+
function gitPolicyOf(descriptor) {
|
|
41
|
+
const w = descriptor.workspace;
|
|
42
|
+
return {
|
|
43
|
+
...(w.agentPushBan !== undefined ? { agentPushBan: w.agentPushBan } : {}),
|
|
44
|
+
...(w.agentProtectedBranches !== undefined
|
|
45
|
+
? { agentProtectedBranches: w.agentProtectedBranches }
|
|
46
|
+
: {}),
|
|
47
|
+
...(w.agentAllowForcePush !== undefined ? { agentAllowForcePush: w.agentAllowForcePush } : {}),
|
|
48
|
+
...(w.agentAllowDestructiveGit !== undefined
|
|
49
|
+
? { agentAllowDestructiveGit: w.agentAllowDestructiveGit }
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
25
53
|
export class Supervisor {
|
|
26
54
|
ws;
|
|
27
55
|
opts;
|
|
@@ -131,11 +159,43 @@ export class Supervisor {
|
|
|
131
159
|
if (frame.agentAutoCommit !== undefined) {
|
|
132
160
|
running.descriptor.workspace.agentAutoCommit = frame.agentAutoCommit;
|
|
133
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Session 18: the git policy, applied live for the same reason as the
|
|
164
|
+
* two above — it is read on every tool call, so «switch push back
|
|
165
|
+
* off» must not mean «stop the agent first».
|
|
166
|
+
*
|
|
167
|
+
* The four fields move together, and the API sends all four in every
|
|
168
|
+
* frame that touches any of them, so a partial update cannot leave
|
|
169
|
+
* the runner with «push is allowed now» beside a protected list from
|
|
170
|
+
* before the change. `!== undefined` on the FRAME is what tells a
|
|
171
|
+
* policy change apart from a trust-only frame; inside the object,
|
|
172
|
+
* `undefined` keeps its restrictive meaning as everywhere else.
|
|
173
|
+
*/
|
|
174
|
+
const policyChanged = frame.agentPushBan !== undefined ||
|
|
175
|
+
frame.agentProtectedBranches !== undefined ||
|
|
176
|
+
frame.agentAllowForcePush !== undefined ||
|
|
177
|
+
frame.agentAllowDestructiveGit !== undefined;
|
|
178
|
+
if (policyChanged) {
|
|
179
|
+
const next = {
|
|
180
|
+
...(frame.agentPushBan !== undefined ? { agentPushBan: frame.agentPushBan } : {}),
|
|
181
|
+
...(frame.agentProtectedBranches !== undefined
|
|
182
|
+
? { agentProtectedBranches: frame.agentProtectedBranches }
|
|
183
|
+
: {}),
|
|
184
|
+
...(frame.agentAllowForcePush !== undefined
|
|
185
|
+
? { agentAllowForcePush: frame.agentAllowForcePush }
|
|
186
|
+
: {}),
|
|
187
|
+
...(frame.agentAllowDestructiveGit !== undefined
|
|
188
|
+
? { agentAllowDestructiveGit: frame.agentAllowDestructiveGit }
|
|
189
|
+
: {}),
|
|
190
|
+
};
|
|
191
|
+
Object.assign(running.descriptor.workspace, next);
|
|
192
|
+
}
|
|
134
193
|
running.session?.setWorkspacePolicy({
|
|
135
194
|
...(frame.trustMode !== undefined ? { trustMode: frame.trustMode } : {}),
|
|
136
195
|
...(frame.agentAutoCommit !== undefined
|
|
137
196
|
? { agentAutoCommit: frame.agentAutoCommit }
|
|
138
197
|
: {}),
|
|
198
|
+
...(policyChanged ? { gitPolicy: gitPolicyOf(running.descriptor) } : {}),
|
|
139
199
|
});
|
|
140
200
|
}
|
|
141
201
|
break;
|
|
@@ -406,6 +466,11 @@ export class Supervisor {
|
|
|
406
466
|
...(descriptor.workspace.agentAutoCommit === undefined
|
|
407
467
|
? {}
|
|
408
468
|
: { agentAutoCommit: descriptor.workspace.agentAutoCommit }),
|
|
469
|
+
// Session 18. Always present, even when every field inside it is absent:
|
|
470
|
+
// an absent OBJECT and an object of absent fields resolve identically
|
|
471
|
+
// (`resolveGitPolicy` gives both the restrictive reading), and passing it
|
|
472
|
+
// unconditionally keeps one code path instead of two.
|
|
473
|
+
gitPolicy: gitPolicyOf(descriptor),
|
|
409
474
|
mode: running.mode,
|
|
410
475
|
...(running.model ? { model: running.model } : {}),
|
|
411
476
|
...(running.effort ? { effort: running.effort } : {}),
|
|
@@ -987,8 +1052,47 @@ export class Supervisor {
|
|
|
987
1052
|
}
|
|
988
1053
|
running.session.stop('session_parked');
|
|
989
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* The agent is producing output while this runner still says a human is
|
|
1057
|
+
* expected — put `lastReported` back to RUNNING (ticket #185).
|
|
1058
|
+
*
|
|
1059
|
+
* This is not cosmetics on top of the API's own repair. While `lastReported`
|
|
1060
|
+
* lies, two things break here and only here:
|
|
1061
|
+
*
|
|
1062
|
+
* - `isParkable()` reads WAITING_INPUT with no open question as «idle», so
|
|
1063
|
+
* `ensureCapacity` may KILL a live agent process to hand its slot to
|
|
1064
|
+
* another session;
|
|
1065
|
+
* - `isBillable()` counts only RUNNING/STARTING, so `agentActiveMs` stops
|
|
1066
|
+
* growing and the time budget never fires for work that is really happening.
|
|
1067
|
+
*
|
|
1068
|
+
* It happens for real: a background subagent's report wakes a new turn inside
|
|
1069
|
+
* the agent process (SDK 0.3.226 emits a second `system:init` and a `result`
|
|
1070
|
+
* carrying `origin: {kind:'task-notification'}`), and nothing here reports a
|
|
1071
|
+
* turn the runner did not start. Compaction does the same, and so does a
|
|
1072
|
+
* question this runner withdraws by itself.
|
|
1073
|
+
*
|
|
1074
|
+
* An open question is the one thing that must survive: there, WAITING_INPUT
|
|
1075
|
+
* means a tool call is parked on a person, and the agent narrating around its
|
|
1076
|
+
* own question must not take the card off the screen.
|
|
1077
|
+
*/
|
|
1078
|
+
noteAgentIsWorking(running) {
|
|
1079
|
+
if (running.openQuestions.size > 0)
|
|
1080
|
+
return;
|
|
1081
|
+
if (running.stopRequested || running.parkRequested || running.budgetSpent)
|
|
1082
|
+
return;
|
|
1083
|
+
if (running.lastReported !== 'WAITING_INPUT' && running.lastReported !== 'REVIEW')
|
|
1084
|
+
return;
|
|
1085
|
+
running.lastReported = 'RUNNING';
|
|
1086
|
+
this.syncBudgetClock(running);
|
|
1087
|
+
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
1088
|
+
}
|
|
990
1089
|
forwardEvent(running, event) {
|
|
991
1090
|
const { descriptor } = running;
|
|
1091
|
+
// Anything below that is the agent talking means the agent is working. Read
|
|
1092
|
+
// before the switch so every such case gets it, including the ones added
|
|
1093
|
+
// after this line was written.
|
|
1094
|
+
if (event.type === 'message' ? event.role === 'assistant' : AGENT_OUTPUT_EVENTS.has(event.type))
|
|
1095
|
+
this.noteAgentIsWorking(running);
|
|
992
1096
|
switch (event.type) {
|
|
993
1097
|
case 'provider_session': {
|
|
994
1098
|
running.descriptor = { ...descriptor, providerSessionId: event.providerSessionId };
|
|
@@ -2372,6 +2476,21 @@ export class Supervisor {
|
|
|
2372
2476
|
error: 'The agent could not compact right now — wait for the current turn to finish',
|
|
2373
2477
|
});
|
|
2374
2478
|
}
|
|
2479
|
+
// Ticket #185. Compaction is a whole agent turn — `/compact` goes in
|
|
2480
|
+
// as an ordinary user message — and until now nobody said so: the
|
|
2481
|
+
// session read «Ваш ход» for the entire squeeze, and the dashboard
|
|
2482
|
+
// papered over it with `compactingSince`, a variable that exists only
|
|
2483
|
+
// in the tab that pressed the button. A second tab, a teammate or a
|
|
2484
|
+
// reload saw a session waiting for a person who had nothing to do.
|
|
2485
|
+
//
|
|
2486
|
+
// It is also not only cosmetic here: a session reading WAITING_INPUT
|
|
2487
|
+
// with no open question is parkable, and parking it would kill the
|
|
2488
|
+
// compaction mid-flight.
|
|
2489
|
+
if (running.lastReported !== 'RUNNING') {
|
|
2490
|
+
running.lastReported = 'RUNNING';
|
|
2491
|
+
this.syncBudgetClock(running);
|
|
2492
|
+
this.reportStatus(running.descriptor.id, 'RUNNING', {});
|
|
2493
|
+
}
|
|
2375
2494
|
return void reply({ ok: true, result: { started: true } });
|
|
2376
2495
|
}
|
|
2377
2496
|
case 'git_status': {
|
|
@@ -3358,16 +3477,58 @@ export function composeInitialPrompt(descriptor) {
|
|
|
3358
3477
|
export function composeWorkspaceContext(descriptor, agentPrompt) {
|
|
3359
3478
|
const sections = [];
|
|
3360
3479
|
const plan = descriptor.branchPlan;
|
|
3480
|
+
/**
|
|
3481
|
+
* What this session may do about push, in one sentence (session 18).
|
|
3482
|
+
*
|
|
3483
|
+
* `!== false` — silence refuses, exactly as `resolveGitPolicy` reads it. This
|
|
3484
|
+
* text and the rule in `policy.ts` have to agree; an agent told it may push
|
|
3485
|
+
* and then refused has been lied to, and an agent told it may not while the
|
|
3486
|
+
* project allows it wastes the freedom the owner paid for.
|
|
3487
|
+
*/
|
|
3488
|
+
const pushLine = (() => {
|
|
3489
|
+
// `?.` and not `.`: this function is exported and called with descriptors
|
|
3490
|
+
// assembled by hand in tests and by older code paths. A missing `workspace`
|
|
3491
|
+
// must land on the refusing branch like every other unknown here, not throw
|
|
3492
|
+
// and take the whole session start with it.
|
|
3493
|
+
if (descriptor.workspace?.agentPushBan !== false) {
|
|
3494
|
+
return '- You cannot push: `git push` is refused for this project. A human presses «Push» in DevBridge when they want the branch on the remote.';
|
|
3495
|
+
}
|
|
3496
|
+
const guarded = descriptor.workspace?.agentProtectedBranches ?? ['main', 'master'];
|
|
3497
|
+
return guarded.length > 0
|
|
3498
|
+
? `- You may push, except to these protected branches: ${guarded.join(', ')}. Name the branch explicitly — \`git push <remote> <branch>\`.`
|
|
3499
|
+
: '- You may push.';
|
|
3500
|
+
})();
|
|
3361
3501
|
if (plan) {
|
|
3362
3502
|
const forked = plan.baseBranch ? ` It was branched from \`${plan.baseBranch}\`.` : '';
|
|
3363
3503
|
sections.push([
|
|
3364
3504
|
'Git in this session:',
|
|
3365
3505
|
`- You are in a dedicated worktree on branch \`${plan.branch}\`.${forked}`,
|
|
3366
3506
|
'- Commit to this branch. Do not switch branches and do not merge into the base branch yourself — a human presses «Apply» in DevBridge, which squash-merges your branch for them.',
|
|
3367
|
-
|
|
3507
|
+
pushLine,
|
|
3368
3508
|
`- Put a plan or working notes in \`docs/devbridge/${plan.branch.replace(/\//g, '-')}.md\`, unless this repository already has its own convention for where such documents live — if it does, follow that.`,
|
|
3369
3509
|
].join('\n'));
|
|
3370
3510
|
}
|
|
3511
|
+
else {
|
|
3512
|
+
/**
|
|
3513
|
+
* A DIRECT session got NO «Git in this session» block at all, and that is
|
|
3514
|
+
* the older half of this bug (session 16 shipped DIRECT as the default;
|
|
3515
|
+
* this block stayed gated on `branchPlan`, which only a BRANCH session
|
|
3516
|
+
* has). So the arrangement that writes into the person's OWN project
|
|
3517
|
+
* folder was the one told nothing about where it was — while the adapter's
|
|
3518
|
+
* static line went on claiming «a dedicated git worktree on a session
|
|
3519
|
+
* branch», which in DIRECT is simply false.
|
|
3520
|
+
*
|
|
3521
|
+
* Being in somebody's working copy is the case that needs saying MORE, not
|
|
3522
|
+
* less: there is no «Apply» to undo it, and uncommitted changes in that
|
|
3523
|
+
* folder may not be the agent's.
|
|
3524
|
+
*/
|
|
3525
|
+
sections.push([
|
|
3526
|
+
'Git in this session:',
|
|
3527
|
+
'- You are working in the PROJECT FOLDER ITSELF, on the branch it is already checked out on — not in a worktree of your own. There is no session branch and nothing to «apply» afterwards: your commits are immediately on the branch the person works on.',
|
|
3528
|
+
'- Uncommitted changes you find here may be a human’s, or another session’s. Never discard or reset what you did not write.',
|
|
3529
|
+
pushLine,
|
|
3530
|
+
].join('\n'));
|
|
3531
|
+
}
|
|
3371
3532
|
// The ticket convention lives HERE, not in the first chat message (session
|
|
3372
3533
|
// 16). It is a standing rule of the project — true on the twentieth turn as
|
|
3373
3534
|
// much as the first — and putting it in the visible prompt only trained the
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.34.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge4dev/runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
|
|
5
5
|
"homepage": "https://bridge4.dev",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"prepublishOnly": "tsc"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
41
|
-
"smol-toml": "^1.7.
|
|
42
|
-
"ws": "^8.21.
|
|
43
|
-
"zod": "^3.
|
|
40
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.226",
|
|
41
|
+
"smol-toml": "^1.7.1",
|
|
42
|
+
"ws": "^8.21.3",
|
|
43
|
+
"zod": "^3.25.76"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "^22.
|
|
46
|
+
"@types/node": "^22.20.1",
|
|
47
47
|
"@types/ws": "^8.18.1",
|
|
48
|
-
"tsx": "^4.
|
|
49
|
-
"typescript": "~5.7.
|
|
50
|
-
"vitest": "^2.
|
|
48
|
+
"tsx": "^4.23.11",
|
|
49
|
+
"typescript": "~5.7.3",
|
|
50
|
+
"vitest": "^3.2.7"
|
|
51
51
|
}
|
|
52
52
|
}
|