@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
package/dist/adapters/types.d.ts
CHANGED
|
@@ -31,6 +31,16 @@ export interface ModelOption {
|
|
|
31
31
|
label: string;
|
|
32
32
|
description?: string;
|
|
33
33
|
isDefault?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The wire model this row's `id` resolves to — `sonnet` → `claude-sonnet-5`
|
|
36
|
+
* (ticket #111).
|
|
37
|
+
*
|
|
38
|
+
* The catalogue is written in ALIASES, but the agent reports the RESOLVED id
|
|
39
|
+
* once a turn starts. Matching the two is the only way the dashboard can tell
|
|
40
|
+
* which row is selected, and a row it cannot find is a row whose effort levels
|
|
41
|
+
* it cannot offer — which is exactly how the effort control disappeared.
|
|
42
|
+
*/
|
|
43
|
+
resolvedModel?: string;
|
|
34
44
|
/** Empty/absent when the model has no reasoning-effort dial. */
|
|
35
45
|
efforts?: EffortOption[];
|
|
36
46
|
/** The level the agent uses when none is chosen. */
|
|
@@ -51,6 +61,46 @@ export interface McpServerStatusInfo {
|
|
|
51
61
|
name: string;
|
|
52
62
|
status: string;
|
|
53
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* One unit of work the agent is running beside the conversation (ticket #113):
|
|
66
|
+
* a subagent, a background shell command, a dynamic workflow.
|
|
67
|
+
*
|
|
68
|
+
* The dashboard used to know only the session's own status — «Waiting for you»
|
|
69
|
+
* — while the agent had twenty subagents in flight and a deploy running in the
|
|
70
|
+
* background. None of that was anywhere on the page.
|
|
71
|
+
*/
|
|
72
|
+
export interface AgentTask {
|
|
73
|
+
id: string;
|
|
74
|
+
/**
|
|
75
|
+
* What kind of work this is, as the agent named it: `local_agent`,
|
|
76
|
+
* `local_bash`, `local_workflow`, … Kept as free text, not an enum — the set
|
|
77
|
+
* grows with every CLI release, and an unknown kind must degrade to a generic
|
|
78
|
+
* row rather than drop the task from the tray.
|
|
79
|
+
*/
|
|
80
|
+
kind: string;
|
|
81
|
+
/** One line the user can read. Truncated by the adapter. */
|
|
82
|
+
title: string;
|
|
83
|
+
status: 'running' | 'done' | 'failed';
|
|
84
|
+
/** Epoch ms the runner first saw it, on the RUNNER's clock. */
|
|
85
|
+
startedAt: number;
|
|
86
|
+
/**
|
|
87
|
+
* How long it has been running, measured on the runner at publish time.
|
|
88
|
+
*
|
|
89
|
+
* The tray renders this rather than `now - startedAt`: the two are different
|
|
90
|
+
* machines' clocks, and subtracting across them put the dev server's skew
|
|
91
|
+
* straight into the number a human reads.
|
|
92
|
+
*/
|
|
93
|
+
ageMs?: number;
|
|
94
|
+
/** `general-purpose`, `code-reviewer`, … for subagent tasks. */
|
|
95
|
+
subagentType?: string;
|
|
96
|
+
/** `meta.name` of a dynamic workflow, for workflow tasks. */
|
|
97
|
+
workflowName?: string;
|
|
98
|
+
tokens?: number;
|
|
99
|
+
toolUses?: number;
|
|
100
|
+
durationMs?: number;
|
|
101
|
+
/** Present only with `agentProgressSummaries`: «Analyzing the auth module». */
|
|
102
|
+
summary?: string;
|
|
103
|
+
}
|
|
54
104
|
/** Everything the dashboard needs to render agent controls, live from the agent. */
|
|
55
105
|
export interface AgentCapabilities {
|
|
56
106
|
models: ModelOption[];
|
|
@@ -75,7 +125,23 @@ export interface SessionSpec {
|
|
|
75
125
|
* first message instead of starting a turn.
|
|
76
126
|
*/
|
|
77
127
|
prompt?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Extra system-prompt material composed by the supervisor (session 13):
|
|
130
|
+
* which branch this session is on, where its plan/log belongs, and the
|
|
131
|
+
* project's own `CLAUDE.md` / `AGENTS.md`.
|
|
132
|
+
*
|
|
133
|
+
* System prompt and not the first user message on purpose — a free session
|
|
134
|
+
* has no first message, and the rules have to hold on every turn, not just
|
|
135
|
+
* the one that started the session.
|
|
136
|
+
*/
|
|
137
|
+
workspaceContext?: string;
|
|
78
138
|
trustMode: TrustMode;
|
|
139
|
+
/**
|
|
140
|
+
* Session 15: the project's «let the agent commit on its own» switch. Rides
|
|
141
|
+
* with `trustMode` because it lands in the same `PolicyContext` and is asked
|
|
142
|
+
* the same question — may this Bash call go through.
|
|
143
|
+
*/
|
|
144
|
+
agentAutoCommit?: boolean;
|
|
79
145
|
mode?: AgentMode;
|
|
80
146
|
model?: string;
|
|
81
147
|
effort?: string;
|
|
@@ -83,6 +149,63 @@ export interface SessionSpec {
|
|
|
83
149
|
mcp?: McpConfig;
|
|
84
150
|
maxBudgetUsd?: number;
|
|
85
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* One question inside an agent's question call (session 12).
|
|
154
|
+
*
|
|
155
|
+
* Both agents can ask several at once — Claude's `AskUserQuestion` takes 1–4,
|
|
156
|
+
* a Codex elicitation carries a list — and until session 12 we kept only the
|
|
157
|
+
* first one and threw the rest away.
|
|
158
|
+
*/
|
|
159
|
+
export interface AgentQuestion {
|
|
160
|
+
/** Stable inside this ask: Claude `q0`…`q3`, Codex the elicitation's own id. */
|
|
161
|
+
id: string;
|
|
162
|
+
text: string;
|
|
163
|
+
/** Short chip label (Claude caps its own at 12 chars). */
|
|
164
|
+
header?: string;
|
|
165
|
+
multiSelect: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* The user may write an answer of their own. Always true for Claude — its
|
|
168
|
+
* own tool description says the client is expected to provide "Other" — and
|
|
169
|
+
* for Codex when the elicitation marks the QUESTION `isOther` (that is where
|
|
170
|
+
* the live probe of 0.135.0 put the flag; an option-level one is accepted
|
|
171
|
+
* too, in case a later build moves it).
|
|
172
|
+
*/
|
|
173
|
+
allowsCustom: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* The answer is a secret (Codex marks an elicitation `isSecret`). The card
|
|
176
|
+
* masks the field and the runner keeps the value out of the feed — an event
|
|
177
|
+
* payload is stored for 90 days, which is no place for a token.
|
|
178
|
+
*/
|
|
179
|
+
secret?: boolean;
|
|
180
|
+
options: Array<{
|
|
181
|
+
label: string;
|
|
182
|
+
description?: string;
|
|
183
|
+
preview?: string;
|
|
184
|
+
}>;
|
|
185
|
+
}
|
|
186
|
+
/** What the user picked (and/or typed) for one question. */
|
|
187
|
+
export interface AgentQuestionAnswer {
|
|
188
|
+
questionId: string;
|
|
189
|
+
/** Chosen option labels; empty when the user only wrote their own answer. */
|
|
190
|
+
values: string[];
|
|
191
|
+
/** Free text the user typed instead of, or alongside, the options. */
|
|
192
|
+
custom?: string;
|
|
193
|
+
notes?: string;
|
|
194
|
+
}
|
|
195
|
+
/** Why a question was taken away from the user without them answering it. */
|
|
196
|
+
export type QuestionInvalidationReason = 'session_stopped' | 'session_parked' | 'runner_restarted' | 'turn_aborted' | 'agent_cancelled' | 'budget_spent';
|
|
197
|
+
/** The dashboard's answer to one open ask. */
|
|
198
|
+
export interface QuestionReply {
|
|
199
|
+
askId: string;
|
|
200
|
+
/**
|
|
201
|
+
* `answer` — the picked options / typed answer go back through the tool.
|
|
202
|
+
* `discuss` — the user chose to reply in words instead; the agent gets the
|
|
203
|
+
* text as the tool result and keeps its turn.
|
|
204
|
+
*/
|
|
205
|
+
action: 'answer' | 'discuss';
|
|
206
|
+
answers?: AgentQuestionAnswer[];
|
|
207
|
+
text?: string;
|
|
208
|
+
}
|
|
86
209
|
export type AgentEvent = {
|
|
87
210
|
type: 'message';
|
|
88
211
|
role: 'assistant' | 'user';
|
|
@@ -91,11 +214,35 @@ export type AgentEvent = {
|
|
|
91
214
|
type: 'thinking';
|
|
92
215
|
text: string;
|
|
93
216
|
}
|
|
94
|
-
/**
|
|
217
|
+
/**
|
|
218
|
+
* The agent needs a decision from the user before it can continue. The tool
|
|
219
|
+
* call is PARKED until the answer arrives (session 12) — it is no longer
|
|
220
|
+
* denied, which used to read to the model like a pressed Escape and let it
|
|
221
|
+
* carry on answering for the user.
|
|
222
|
+
*/
|
|
95
223
|
| {
|
|
96
224
|
type: 'question';
|
|
225
|
+
/** Issued by the adapter per ask; the answer is matched on it. */
|
|
226
|
+
askId: string;
|
|
227
|
+
questions: AgentQuestion[];
|
|
228
|
+
/**
|
|
229
|
+
* Mirror of the pre-session-12 shape, kept for one runner release so a
|
|
230
|
+
* dashboard or API that has not been redeployed yet still draws a card.
|
|
231
|
+
* Remove in 0.15.0 (recorded as debt in devreport #111).
|
|
232
|
+
*/
|
|
97
233
|
text: string;
|
|
98
234
|
options?: string[];
|
|
235
|
+
}
|
|
236
|
+
/** An open question stopped being open — answered, discussed or withdrawn. */
|
|
237
|
+
| {
|
|
238
|
+
type: 'question_resolved';
|
|
239
|
+
askId: string;
|
|
240
|
+
outcome: 'answered' | 'discussed' | 'invalidated';
|
|
241
|
+
source: 'user' | 'runner';
|
|
242
|
+
reason?: QuestionInvalidationReason;
|
|
243
|
+
answers?: AgentQuestionAnswer[];
|
|
244
|
+
/** One short line for the card: «Postgres». */
|
|
245
|
+
summary?: string;
|
|
99
246
|
} | {
|
|
100
247
|
type: 'tool';
|
|
101
248
|
phase: 'use' | 'result';
|
|
@@ -116,7 +263,12 @@ export type AgentEvent = {
|
|
|
116
263
|
type: 'permission_resolved';
|
|
117
264
|
requestId: string;
|
|
118
265
|
allow: boolean;
|
|
119
|
-
|
|
266
|
+
/**
|
|
267
|
+
* `runner` (session 12) is the honest answer for a card the runner took
|
|
268
|
+
* away — parked, stopped, restarted. Those used to be reported as a plain
|
|
269
|
+
* `deny`, so the feed said "denied" about a decision no human ever made.
|
|
270
|
+
*/
|
|
271
|
+
source: 'user' | 'policy' | 'abort' | 'runner';
|
|
120
272
|
reason?: string;
|
|
121
273
|
} | {
|
|
122
274
|
type: 'provider_session';
|
|
@@ -134,6 +286,20 @@ export type AgentEvent = {
|
|
|
134
286
|
type: 'context_usage';
|
|
135
287
|
usedTokens: number;
|
|
136
288
|
maxTokens: number;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Everything running beside the conversation, right now (ticket #113).
|
|
292
|
+
*
|
|
293
|
+
* A LEVEL signal with REPLACE semantics, not a stream of start/stop edges:
|
|
294
|
+
* the agent publishes its whole live set on every membership change, so a
|
|
295
|
+
* dropped frame can never wedge a task in the tray forever. `done`/`total`
|
|
296
|
+
* count the current turn, which is what «14 of 20 done» means.
|
|
297
|
+
*/
|
|
298
|
+
| {
|
|
299
|
+
type: 'agent_tasks';
|
|
300
|
+
tasks: AgentTask[];
|
|
301
|
+
done: number;
|
|
302
|
+
total: number;
|
|
137
303
|
} | {
|
|
138
304
|
type: 'notice';
|
|
139
305
|
level: 'info' | 'warn';
|
|
@@ -163,6 +329,18 @@ export interface AgentSession {
|
|
|
163
329
|
/** Ends when the underlying agent process is gone. */
|
|
164
330
|
events: AsyncIterable<AgentEvent>;
|
|
165
331
|
answerPermission(requestId: string, allow: boolean, note?: string): void;
|
|
332
|
+
/**
|
|
333
|
+
* Answer (or discuss) an open question. Returns false when the ask is no
|
|
334
|
+
* longer open — a card from a previous life of the session, or one the user
|
|
335
|
+
* answered twice — so the caller can say so instead of failing silently.
|
|
336
|
+
*/
|
|
337
|
+
answerQuestion(reply: QuestionReply): boolean;
|
|
338
|
+
/**
|
|
339
|
+
* Withdraw every open ask without killing the session — what «Stop the turn»
|
|
340
|
+
* needs. Codex's elicitation blocks its own rpc, so leaving it parked would
|
|
341
|
+
* make the NEXT message be consumed as the answer to a turn that is gone.
|
|
342
|
+
*/
|
|
343
|
+
cancelQuestions(reason: QuestionInvalidationReason): void;
|
|
166
344
|
/** Follow-up user input into the live session. */
|
|
167
345
|
send(text: string): void;
|
|
168
346
|
/** Switch model mid-session (VS-Code-extension parity). */
|
|
@@ -171,10 +349,28 @@ export interface AgentSession {
|
|
|
171
349
|
setEffort(effort: string | null): Promise<void>;
|
|
172
350
|
/** Switch interaction mode mid-session. */
|
|
173
351
|
setMode(mode: AgentMode): Promise<void>;
|
|
352
|
+
/**
|
|
353
|
+
* Project-level policy changed while this session is running (session 15).
|
|
354
|
+
*
|
|
355
|
+
* `trustMode` and `agentAutoCommit` are read on EVERY tool call, so updating
|
|
356
|
+
* them here takes effect on the next one. Without this they were fixed at
|
|
357
|
+
* launch: a manager tightening trust to STRICT — or switching the agent's
|
|
358
|
+
* own commits off — changed a row in the database and nothing else, while
|
|
359
|
+
* the card next to the control reported the new value as a fact. The
|
|
360
|
+
* dangerous direction is the tightening one.
|
|
361
|
+
*/
|
|
362
|
+
setWorkspacePolicy(policy: {
|
|
363
|
+
trustMode?: TrustMode;
|
|
364
|
+
agentAutoCommit?: boolean;
|
|
365
|
+
}): void;
|
|
174
366
|
/** Interrupt the current turn (session stays resumable). */
|
|
175
367
|
interrupt(): Promise<void>;
|
|
176
|
-
/**
|
|
177
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Tear the session down (kills the agent process). The reason travels so
|
|
370
|
+
* anything the user was still being asked is withdrawn with a cause they can
|
|
371
|
+
* read, instead of quietly turning into a denial.
|
|
372
|
+
*/
|
|
373
|
+
stop(reason?: QuestionInvalidationReason): void;
|
|
178
374
|
}
|
|
179
375
|
export interface AgentAdapter {
|
|
180
376
|
readonly id: 'claude' | 'codex';
|
package/dist/attachments.d.ts
CHANGED
|
@@ -35,12 +35,19 @@ export interface SavedAttachment {
|
|
|
35
35
|
*/
|
|
36
36
|
export declare function safeAttachmentName(id: string, fileName: string): string;
|
|
37
37
|
/**
|
|
38
|
-
* Keep
|
|
38
|
+
* Keep downloaded attachments out of git for this repository.
|
|
39
39
|
*
|
|
40
40
|
* Uses `info/exclude` rather than `.gitignore`: it is local-only, so we never
|
|
41
41
|
* add a line to a file the user commits. `--git-common-dir` matters because a
|
|
42
42
|
* linked worktree's `.git` is a file, and the shared exclude list is what both
|
|
43
43
|
* the worktree and the main checkout consult.
|
|
44
|
+
*
|
|
45
|
+
* Session 14 narrowed the line from `/.devbridge/` to `/.devbridge/attachments/`,
|
|
46
|
+
* and migrates repositories that already carry the wide one. The wide version
|
|
47
|
+
* excluded `.devbridge/project.json` — the recipe file this release asks people
|
|
48
|
+
* to commit — so `git add` on it would have been refused without `-f` and
|
|
49
|
+
* `git add -A` would have skipped it silently. Only our own exact line is
|
|
50
|
+
* rewritten; anything else in the file is left alone.
|
|
44
51
|
*/
|
|
45
52
|
export declare function ensureGitExclude(worktreePath: string): Promise<void>;
|
|
46
53
|
/**
|
package/dist/attachments.js
CHANGED
|
@@ -37,13 +37,24 @@ export function safeAttachmentName(id, fileName) {
|
|
|
37
37
|
const trimmed = base.slice(-80) || 'file';
|
|
38
38
|
return `${id.slice(0, 8)}-${trimmed}`;
|
|
39
39
|
}
|
|
40
|
+
/** What session 10 wrote — the whole directory. Narrowed in session 14. */
|
|
41
|
+
const LEGACY_EXCLUDE_LINE = '/.devbridge/';
|
|
42
|
+
/** Only the attachments, so `.devbridge/project.json` stays a normal file. */
|
|
43
|
+
const EXCLUDE_LINE = '/.devbridge/attachments/';
|
|
40
44
|
/**
|
|
41
|
-
* Keep
|
|
45
|
+
* Keep downloaded attachments out of git for this repository.
|
|
42
46
|
*
|
|
43
47
|
* Uses `info/exclude` rather than `.gitignore`: it is local-only, so we never
|
|
44
48
|
* add a line to a file the user commits. `--git-common-dir` matters because a
|
|
45
49
|
* linked worktree's `.git` is a file, and the shared exclude list is what both
|
|
46
50
|
* the worktree and the main checkout consult.
|
|
51
|
+
*
|
|
52
|
+
* Session 14 narrowed the line from `/.devbridge/` to `/.devbridge/attachments/`,
|
|
53
|
+
* and migrates repositories that already carry the wide one. The wide version
|
|
54
|
+
* excluded `.devbridge/project.json` — the recipe file this release asks people
|
|
55
|
+
* to commit — so `git add` on it would have been refused without `-f` and
|
|
56
|
+
* `git add -A` would have skipped it silently. Only our own exact line is
|
|
57
|
+
* rewritten; anything else in the file is left alone.
|
|
47
58
|
*/
|
|
48
59
|
export async function ensureGitExclude(worktreePath) {
|
|
49
60
|
try {
|
|
@@ -54,7 +65,6 @@ export async function ensureGitExclude(worktreePath) {
|
|
|
54
65
|
const commonDir = path.resolve(worktreePath, stdout.trim());
|
|
55
66
|
const infoDir = path.join(commonDir, 'info');
|
|
56
67
|
const excludeFile = path.join(infoDir, 'exclude');
|
|
57
|
-
const line = '/.devbridge/';
|
|
58
68
|
let current = '';
|
|
59
69
|
try {
|
|
60
70
|
current = fs.readFileSync(excludeFile, 'utf8');
|
|
@@ -62,11 +72,19 @@ export async function ensureGitExclude(worktreePath) {
|
|
|
62
72
|
catch {
|
|
63
73
|
/* no exclude file yet */
|
|
64
74
|
}
|
|
65
|
-
|
|
75
|
+
const lines = current.split('\n');
|
|
76
|
+
const legacyIndex = lines.findIndex((entry) => entry.trim() === LEGACY_EXCLUDE_LINE);
|
|
77
|
+
if (legacyIndex !== -1) {
|
|
78
|
+
lines[legacyIndex] = EXCLUDE_LINE;
|
|
79
|
+
fs.mkdirSync(infoDir, { recursive: true });
|
|
80
|
+
fs.writeFileSync(excludeFile, lines.join('\n'));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (lines.some((entry) => entry.trim() === EXCLUDE_LINE))
|
|
66
84
|
return;
|
|
67
85
|
fs.mkdirSync(infoDir, { recursive: true });
|
|
68
86
|
const prefix = current.length === 0 || current.endsWith('\n') ? '' : '\n';
|
|
69
|
-
fs.appendFileSync(excludeFile, `${prefix}# DevBridge: files you attach to a session live here\n${
|
|
87
|
+
fs.appendFileSync(excludeFile, `${prefix}# DevBridge: files you attach to a session live here\n${EXCLUDE_LINE}\n`);
|
|
70
88
|
}
|
|
71
89
|
catch (error) {
|
|
72
90
|
// Not fatal: the worst case is that attachments show up as untracked files.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* May this session's interrupted turn be picked up automatically right now?
|
|
3
|
+
*
|
|
4
|
+
* Records the attempt when it says yes — the caller does not have to remember to,
|
|
5
|
+
* and a caller that forgot would silently remove the ceiling.
|
|
6
|
+
*/
|
|
7
|
+
export declare function claimAutoResume(sessionId: string, now?: number): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Forget a session's history — called when a HUMAN sends a message.
|
|
10
|
+
*
|
|
11
|
+
* Without this, two automatic continuations early in a long session would use up
|
|
12
|
+
* the allowance for the rest of its life. A person typing into the session is the
|
|
13
|
+
* clearest possible signal that the work is on track again.
|
|
14
|
+
*/
|
|
15
|
+
export declare function clearAutoResume(sessionId: string): void;
|
|
16
|
+
/** Drop entries for sessions that no longer exist, and anything past the window. */
|
|
17
|
+
export declare function pruneAutoResume(liveSessionIds: Set<string>, now?: number): void;
|
|
18
|
+
//# sourceMappingURL=auto-resume.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { stateDir } from './paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* How often a session is allowed to have its interrupted turn picked up again
|
|
6
|
+
* automatically.
|
|
7
|
+
*
|
|
8
|
+
* Session 18 (QA-112). When the runner process dies mid-turn, everything the
|
|
9
|
+
* agent was doing is abandoned and the session is parked with «send a message to
|
|
10
|
+
* continue». On 2026-07-30 that cost the owner two full runs of
|
|
11
|
+
* `timeout 900 pnpm -r typecheck`: the process died, they typed «продолжай», and
|
|
12
|
+
* the fifteen-minute command started from zero — twice, four minutes apart.
|
|
13
|
+
*
|
|
14
|
+
* Continuing by itself is only safe with a ceiling. A runner stuck in a crash
|
|
15
|
+
* loop would otherwise relaunch the same agent on every restart, five seconds
|
|
16
|
+
* apart, forever — and each relaunch costs real tokens and may repeat a
|
|
17
|
+
* side effect. So: at most `MAX_ATTEMPTS` inside `WINDOW_MS`, per session,
|
|
18
|
+
* recorded on disk because the whole point is that the process does not survive.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ATTEMPTS = 2;
|
|
21
|
+
const WINDOW_MS = 30 * 60_000;
|
|
22
|
+
function ledgerPath() {
|
|
23
|
+
return path.join(stateDir(), 'auto-resume.json');
|
|
24
|
+
}
|
|
25
|
+
function read() {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(fs.readFileSync(ledgerPath(), 'utf8'));
|
|
28
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
29
|
+
return {};
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function write(ledger) {
|
|
37
|
+
try {
|
|
38
|
+
const dir = stateDir();
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
const tmp = path.join(dir, `.auto-resume.json.${process.pid}.tmp`);
|
|
41
|
+
fs.writeFileSync(tmp, JSON.stringify(ledger), { mode: 0o600 });
|
|
42
|
+
fs.renameSync(tmp, ledgerPath());
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* Failing to record an attempt must not stop the session from continuing:
|
|
46
|
+
the ceiling is a safety net, not a precondition. */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* May this session's interrupted turn be picked up automatically right now?
|
|
51
|
+
*
|
|
52
|
+
* Records the attempt when it says yes — the caller does not have to remember to,
|
|
53
|
+
* and a caller that forgot would silently remove the ceiling.
|
|
54
|
+
*/
|
|
55
|
+
export function claimAutoResume(sessionId, now = Date.now()) {
|
|
56
|
+
const ledger = read();
|
|
57
|
+
const recent = (ledger[sessionId]?.at ?? []).filter((at) => now - at < WINDOW_MS);
|
|
58
|
+
if (recent.length >= MAX_ATTEMPTS) {
|
|
59
|
+
// Keep the pruned list so the window slides instead of being reset by a
|
|
60
|
+
// refusal, then let the caller fall back to asking the human.
|
|
61
|
+
ledger[sessionId] = { at: recent };
|
|
62
|
+
write(ledger);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
ledger[sessionId] = { at: [...recent, now] };
|
|
66
|
+
write(ledger);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Forget a session's history — called when a HUMAN sends a message.
|
|
71
|
+
*
|
|
72
|
+
* Without this, two automatic continuations early in a long session would use up
|
|
73
|
+
* the allowance for the rest of its life. A person typing into the session is the
|
|
74
|
+
* clearest possible signal that the work is on track again.
|
|
75
|
+
*/
|
|
76
|
+
export function clearAutoResume(sessionId) {
|
|
77
|
+
const ledger = read();
|
|
78
|
+
if (!(sessionId in ledger))
|
|
79
|
+
return;
|
|
80
|
+
// Rebuilt rather than `delete`d: the ledger is a plain record keyed by session
|
|
81
|
+
// id, and a dynamic delete on it is exactly the pattern the lint rule is for.
|
|
82
|
+
write(Object.fromEntries(Object.entries(ledger).filter(([id]) => id !== sessionId)));
|
|
83
|
+
}
|
|
84
|
+
/** Drop entries for sessions that no longer exist, and anything past the window. */
|
|
85
|
+
export function pruneAutoResume(liveSessionIds, now = Date.now()) {
|
|
86
|
+
const ledger = read();
|
|
87
|
+
const kept = {};
|
|
88
|
+
let changed = false;
|
|
89
|
+
for (const [sessionId, entry] of Object.entries(ledger)) {
|
|
90
|
+
const recent = entry.at.filter((at) => now - at < WINDOW_MS);
|
|
91
|
+
// Nothing recent AND the session is gone → forget it entirely. A live
|
|
92
|
+
// session keeps its (possibly empty) entry so its budget is honest.
|
|
93
|
+
if (recent.length === 0 && !liveSessionIds.has(sessionId)) {
|
|
94
|
+
changed = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (recent.length !== entry.at.length)
|
|
98
|
+
changed = true;
|
|
99
|
+
kept[sessionId] = { at: recent };
|
|
100
|
+
}
|
|
101
|
+
if (changed)
|
|
102
|
+
write(kept);
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=auto-resume.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
export interface ProposeCommitMessageInput {
|
|
3
|
+
worktreePath: string;
|
|
4
|
+
workspacePath: string;
|
|
5
|
+
branch: string;
|
|
6
|
+
baseBranch?: string;
|
|
7
|
+
/** Ticket numbers to reference; the API substitutes them, never the model. */
|
|
8
|
+
tickets?: number[];
|
|
9
|
+
/** Subjects of the commits already on the branch, newest first. */
|
|
10
|
+
commitSubjects?: string[];
|
|
11
|
+
/** Project settings, straight from the workspace row. */
|
|
12
|
+
language?: string;
|
|
13
|
+
convention?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProposeCommitMessageResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
message?: string;
|
|
18
|
+
/** Files removed from the prompt because they are on the secret denylist. */
|
|
19
|
+
redactedFiles?: number;
|
|
20
|
+
diffTruncated?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Drop whole file sections that are on the secret denylist.
|
|
25
|
+
*
|
|
26
|
+
* `gitDiff` throws in this situation; here that would mean «no proposal at all
|
|
27
|
+
* because the branch touches `.env.example`». Removing the file and saying so
|
|
28
|
+
* is both safer (the bytes never leave) and more useful.
|
|
29
|
+
*/
|
|
30
|
+
export declare function stripProtectedFiles(diff: string): {
|
|
31
|
+
diff: string;
|
|
32
|
+
redacted: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* The one-shot itself.
|
|
36
|
+
*
|
|
37
|
+
* `maxTurns: 1` and every tool refused: this run must read nothing, write
|
|
38
|
+
* nothing and call nothing. It gets the diff in its prompt and answers with
|
|
39
|
+
* text. `settingSources: []` for the same reason the session adapter uses it —
|
|
40
|
+
* a `.claude/settings.json` inside the worktree is a file the agent can write.
|
|
41
|
+
*/
|
|
42
|
+
export declare function proposeCommitMessage(input: ProposeCommitMessageInput, queryFn?: typeof query): Promise<ProposeCommitMessageResult>;
|
|
43
|
+
/**
|
|
44
|
+
* What comes back is text a model wrote, so it is treated as such: fences off,
|
|
45
|
+
* an opening «Here is the commit message:» off, masked, capped.
|
|
46
|
+
*
|
|
47
|
+
* The trailer and signature filtering lives on the API side, where it also
|
|
48
|
+
* catches what a human pastes into the box — one filter, one place.
|
|
49
|
+
*/
|
|
50
|
+
export declare function cleanProposal(text: string): string;
|
|
51
|
+
//# sourceMappingURL=commit-message.d.ts.map
|