@bridge4dev/runner 0.30.0 → 0.31.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 +2 -0
- package/dist/adapters/codex.js +2 -0
- package/dist/adapters/types.d.ts +9 -0
- package/dist/agent-prompt.d.ts +75 -0
- package/dist/agent-prompt.js +252 -0
- package/dist/index.js +10 -0
- package/dist/policy.d.ts +21 -0
- package/dist/policy.js +11 -0
- package/dist/protocol.d.ts +92 -12
- package/dist/protocol.js +18 -0
- package/dist/supervisor.d.ts +29 -1
- package/dist/supervisor.js +84 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -873,6 +873,7 @@ class ClaudeSession {
|
|
|
873
873
|
...(this.spec.agentAutoCommit === undefined
|
|
874
874
|
? {}
|
|
875
875
|
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
876
|
+
...(this.spec.agentPromptFile ? { agentPromptFile: this.spec.agentPromptFile } : {}),
|
|
876
877
|
worktreePath: this.spec.cwd,
|
|
877
878
|
});
|
|
878
879
|
if (verdict.decision !== 'allow')
|
|
@@ -1300,6 +1301,7 @@ class ClaudeSession {
|
|
|
1300
1301
|
...(this.spec.agentAutoCommit === undefined
|
|
1301
1302
|
? {}
|
|
1302
1303
|
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
1304
|
+
...(this.spec.agentPromptFile ? { agentPromptFile: this.spec.agentPromptFile } : {}),
|
|
1303
1305
|
worktreePath: this.spec.cwd,
|
|
1304
1306
|
});
|
|
1305
1307
|
if (verdict.decision === 'allow') {
|
package/dist/adapters/codex.js
CHANGED
|
@@ -669,6 +669,7 @@ class CodexSession {
|
|
|
669
669
|
...(this.spec.agentAutoCommit === undefined
|
|
670
670
|
? {}
|
|
671
671
|
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
672
|
+
...(this.spec.agentPromptFile ? { agentPromptFile: this.spec.agentPromptFile } : {}),
|
|
672
673
|
worktreePath: this.spec.cwd,
|
|
673
674
|
});
|
|
674
675
|
if (verdict.decision !== 'allow')
|
|
@@ -788,6 +789,7 @@ class CodexSession {
|
|
|
788
789
|
...(this.spec.agentAutoCommit === undefined
|
|
789
790
|
? {}
|
|
790
791
|
: { agentAutoCommit: this.spec.agentAutoCommit }),
|
|
792
|
+
...(this.spec.agentPromptFile ? { agentPromptFile: this.spec.agentPromptFile } : {}),
|
|
791
793
|
worktreePath: this.spec.cwd,
|
|
792
794
|
});
|
|
793
795
|
if (verdict.decision === 'allow') {
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -176,6 +176,15 @@ export interface SessionSpec {
|
|
|
176
176
|
* the same question — may this Bash call go through.
|
|
177
177
|
*/
|
|
178
178
|
agentAutoCommit?: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Absolute path of the project's prompt file, set only when it was actually
|
|
181
|
+
* read into `workspaceContext` for this process (session 17).
|
|
182
|
+
*
|
|
183
|
+
* Rides down to `PolicyContext` so layer 1 can refuse writes to it: in
|
|
184
|
+
* `workMode: DIRECT` the project folder is the agent's own working directory,
|
|
185
|
+
* so without this the agent could rewrite its own next system prompt.
|
|
186
|
+
*/
|
|
187
|
+
agentPromptFile?: string;
|
|
179
188
|
mode?: AgentMode;
|
|
180
189
|
model?: string;
|
|
181
190
|
effort?: string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project's own prompt file — the one thing on this machine that a project
|
|
3
|
+
* can put into an agent's SYSTEM prompt.
|
|
4
|
+
*
|
|
5
|
+
* ## Why a system prompt and not a file the agent reads
|
|
6
|
+
*
|
|
7
|
+
* Measured on a live request (2026-08-05), not assumed. `CLAUDE.md`, `AGENTS.md`
|
|
8
|
+
* and `SessionStart` hook output all arrive inside the first USER message —
|
|
9
|
+
* Claude Code's own prompt says so in as many words: «Treat feedback from hooks
|
|
10
|
+
* … as coming from the user». `systemPrompt.append` arrives in `system[]`,
|
|
11
|
+
* after the CLI's own text and last. Only the second survives a compaction, and
|
|
12
|
+
* only the second can stand next to a rule the CLI itself states.
|
|
13
|
+
*
|
|
14
|
+
* That difference is the whole feature. An Opus-5 system prompt carries
|
|
15
|
+
* «Do not call the AgentTool unless the user requested it»; a project whose
|
|
16
|
+
* process REQUIRES an independent review round has to be able to say so at the
|
|
17
|
+
* same level, or it is simply outranked and nobody can see why.
|
|
18
|
+
*
|
|
19
|
+
* ## Why this file is paranoid
|
|
20
|
+
*
|
|
21
|
+
* Its contents become system-prompt text verbatim, and the path comes off the
|
|
22
|
+
* wire. So every rule is re-derived here rather than trusted from the API: the
|
|
23
|
+
* runner is the process that opens the file, and it is the only side that can
|
|
24
|
+
* see what the path actually resolves to on this disk.
|
|
25
|
+
*/
|
|
26
|
+
/** Big enough for a real process document; small enough to stay a prompt. */
|
|
27
|
+
export declare const AGENT_PROMPT_MAX_BYTES: number;
|
|
28
|
+
export type AgentPromptResult = {
|
|
29
|
+
ok: true;
|
|
30
|
+
text: string;
|
|
31
|
+
relPath: string;
|
|
32
|
+
absPath: string;
|
|
33
|
+
bytes: number;
|
|
34
|
+
sha: string;
|
|
35
|
+
}
|
|
36
|
+
/** Already phrased for a human and safe to show — no raw paths beyond the one they typed. */
|
|
37
|
+
| {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Read the project's prompt file, or explain why it cannot be read.
|
|
43
|
+
*
|
|
44
|
+
* @param projectRoot Absolute path of the PROJECT FOLDER (`workspace.path`) —
|
|
45
|
+
* not the session worktree. Deliberate, for two reasons that always hold: the
|
|
46
|
+
* file may be uncommitted (a worktree cut from the base branch would not have
|
|
47
|
+
* it), and every session of the project then reads the same rules whatever
|
|
48
|
+
* branch it is on.
|
|
49
|
+
*
|
|
50
|
+
* A third reason used to be written here and was wrong, so it is worth saying
|
|
51
|
+
* plainly (QA-130 MAJOR-3): this does NOT stop a session from rewriting the
|
|
52
|
+
* file that becomes its own next system prompt. It stops it in `BRANCH` mode,
|
|
53
|
+
* where writes outside the worktree are refused — but in `workMode: DIRECT`,
|
|
54
|
+
* which is the default, the project folder IS the session's working
|
|
55
|
+
* directory. What guards it there is the layer-1 rule in `policy.ts`
|
|
56
|
+
* (`agentPromptFile`), and that rule is a guard rather than a guarantee: it
|
|
57
|
+
* covers the file-writing tools, not a shell redirect, and `full` mode
|
|
58
|
+
* bypasses layer 1 altogether by design. The honest backstop is the `sha` in
|
|
59
|
+
* the session feed, which changes when the file does.
|
|
60
|
+
* @param relPath The configured path, relative to `projectRoot`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readAgentPrompt(projectRoot: string, relPath: string): AgentPromptResult;
|
|
63
|
+
/** How the prompt is announced in the session feed and in the journal. */
|
|
64
|
+
export declare function agentPromptSizeLabel(bytes: number): string;
|
|
65
|
+
/**
|
|
66
|
+
* A configured path, safe to put in a line a person reads.
|
|
67
|
+
*
|
|
68
|
+
* The session feed renders a notice as plain text, so backticks would be shown
|
|
69
|
+
* literally rather than as code — and this string can be a REFUSED path, which
|
|
70
|
+
* means it never passed any of the checks above and is only as clean as the API
|
|
71
|
+
* schema made it. Quoted with guillemets, stripped of anything that could break
|
|
72
|
+
* a line, and bounded (QA-130 NIT-13).
|
|
73
|
+
*/
|
|
74
|
+
export declare function quotePath(value: string): string;
|
|
75
|
+
//# sourceMappingURL=agent-prompt.d.ts.map
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { isGitInternalPath, isSecretPath } from './policy.js';
|
|
5
|
+
/**
|
|
6
|
+
* The project's own prompt file — the one thing on this machine that a project
|
|
7
|
+
* can put into an agent's SYSTEM prompt.
|
|
8
|
+
*
|
|
9
|
+
* ## Why a system prompt and not a file the agent reads
|
|
10
|
+
*
|
|
11
|
+
* Measured on a live request (2026-08-05), not assumed. `CLAUDE.md`, `AGENTS.md`
|
|
12
|
+
* and `SessionStart` hook output all arrive inside the first USER message —
|
|
13
|
+
* Claude Code's own prompt says so in as many words: «Treat feedback from hooks
|
|
14
|
+
* … as coming from the user». `systemPrompt.append` arrives in `system[]`,
|
|
15
|
+
* after the CLI's own text and last. Only the second survives a compaction, and
|
|
16
|
+
* only the second can stand next to a rule the CLI itself states.
|
|
17
|
+
*
|
|
18
|
+
* That difference is the whole feature. An Opus-5 system prompt carries
|
|
19
|
+
* «Do not call the AgentTool unless the user requested it»; a project whose
|
|
20
|
+
* process REQUIRES an independent review round has to be able to say so at the
|
|
21
|
+
* same level, or it is simply outranked and nobody can see why.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this file is paranoid
|
|
24
|
+
*
|
|
25
|
+
* Its contents become system-prompt text verbatim, and the path comes off the
|
|
26
|
+
* wire. So every rule is re-derived here rather than trusted from the API: the
|
|
27
|
+
* runner is the process that opens the file, and it is the only side that can
|
|
28
|
+
* see what the path actually resolves to on this disk.
|
|
29
|
+
*/
|
|
30
|
+
/** Big enough for a real process document; small enough to stay a prompt. */
|
|
31
|
+
export const AGENT_PROMPT_MAX_BYTES = 32 * 1024;
|
|
32
|
+
function deny(reason) {
|
|
33
|
+
return { ok: false, reason };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read the project's prompt file, or explain why it cannot be read.
|
|
37
|
+
*
|
|
38
|
+
* @param projectRoot Absolute path of the PROJECT FOLDER (`workspace.path`) —
|
|
39
|
+
* not the session worktree. Deliberate, for two reasons that always hold: the
|
|
40
|
+
* file may be uncommitted (a worktree cut from the base branch would not have
|
|
41
|
+
* it), and every session of the project then reads the same rules whatever
|
|
42
|
+
* branch it is on.
|
|
43
|
+
*
|
|
44
|
+
* A third reason used to be written here and was wrong, so it is worth saying
|
|
45
|
+
* plainly (QA-130 MAJOR-3): this does NOT stop a session from rewriting the
|
|
46
|
+
* file that becomes its own next system prompt. It stops it in `BRANCH` mode,
|
|
47
|
+
* where writes outside the worktree are refused — but in `workMode: DIRECT`,
|
|
48
|
+
* which is the default, the project folder IS the session's working
|
|
49
|
+
* directory. What guards it there is the layer-1 rule in `policy.ts`
|
|
50
|
+
* (`agentPromptFile`), and that rule is a guard rather than a guarantee: it
|
|
51
|
+
* covers the file-writing tools, not a shell redirect, and `full` mode
|
|
52
|
+
* bypasses layer 1 altogether by design. The honest backstop is the `sha` in
|
|
53
|
+
* the session feed, which changes when the file does.
|
|
54
|
+
* @param relPath The configured path, relative to `projectRoot`.
|
|
55
|
+
*/
|
|
56
|
+
export function readAgentPrompt(projectRoot, relPath) {
|
|
57
|
+
const wanted = relPath.trim();
|
|
58
|
+
if (wanted === '')
|
|
59
|
+
return deny('the path is empty');
|
|
60
|
+
if (wanted.length > 300)
|
|
61
|
+
return deny('the path is too long');
|
|
62
|
+
// A NUL truncates the string every syscall below sees, so it must never get
|
|
63
|
+
// as far as a syscall.
|
|
64
|
+
// eslint-disable-next-line no-control-regex -- a control character in a path is never legitimate
|
|
65
|
+
if (/[\u0000-\u001f\u007f]/.test(wanted))
|
|
66
|
+
return deny('the path contains control characters');
|
|
67
|
+
if (path.isAbsolute(wanted) || wanted.startsWith('~')) {
|
|
68
|
+
return deny('the path must be relative to the project folder');
|
|
69
|
+
}
|
|
70
|
+
if (wanted.includes('\\'))
|
|
71
|
+
return deny('the path must use "/" separators');
|
|
72
|
+
if (wanted.endsWith('/'))
|
|
73
|
+
return deny('the path must name a file, not a directory');
|
|
74
|
+
if (wanted.split('/').includes('..'))
|
|
75
|
+
return deny('the path must not contain ".." segments');
|
|
76
|
+
// The project folder itself has to exist and be a directory before anything
|
|
77
|
+
// can be resolved against it — otherwise `realpath` failures below would all
|
|
78
|
+
// read as «no such file», blaming the prompt for a moved project.
|
|
79
|
+
let rootReal;
|
|
80
|
+
try {
|
|
81
|
+
rootReal = fs.realpathSync(path.resolve(projectRoot));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return deny('the project folder is not readable');
|
|
85
|
+
}
|
|
86
|
+
const absolute = path.resolve(rootReal, wanted);
|
|
87
|
+
// `lstat` first, and on the path as written: a SYMLINK is refused outright
|
|
88
|
+
// rather than followed. Following it would mean the setting says one file and
|
|
89
|
+
// the agent's system prompt comes from another, which is exactly the sort of
|
|
90
|
+
// indirection that makes a security rule unreadable.
|
|
91
|
+
let link;
|
|
92
|
+
try {
|
|
93
|
+
link = fs.lstatSync(absolute);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return deny(describeFsError(error));
|
|
97
|
+
}
|
|
98
|
+
if (link.isSymbolicLink())
|
|
99
|
+
return deny('the path is a symlink');
|
|
100
|
+
if (link.isDirectory())
|
|
101
|
+
return deny('the path is a directory');
|
|
102
|
+
if (!link.isFile())
|
|
103
|
+
return deny('the path is not a regular file');
|
|
104
|
+
// Containment is checked AFTER symlinks are resolved, because a symlinked
|
|
105
|
+
// PARENT directory would otherwise pass a purely textual check and land the
|
|
106
|
+
// read outside the project entirely (the QA-96 F13 lesson, applied here).
|
|
107
|
+
let realPath;
|
|
108
|
+
try {
|
|
109
|
+
realPath = fs.realpathSync(absolute);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return deny(describeFsError(error));
|
|
113
|
+
}
|
|
114
|
+
const rel = path.relative(rootReal, realPath);
|
|
115
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
116
|
+
return deny('the path leaves the project folder');
|
|
117
|
+
}
|
|
118
|
+
// The same two lists layer 1 uses for every tool call. A setting must not be
|
|
119
|
+
// a way around them: «read .env into the system prompt» is precisely the
|
|
120
|
+
// shape of request this would otherwise grant.
|
|
121
|
+
if (isSecretPath(realPath))
|
|
122
|
+
return deny('that file is on the protected list');
|
|
123
|
+
if (isGitInternalPath(realPath))
|
|
124
|
+
return deny('that file is inside .git');
|
|
125
|
+
// Read with a hard ceiling on BYTES ACTUALLY READ rather than trusting the
|
|
126
|
+
// size from `stat`: a file can grow between the two calls, and «we checked
|
|
127
|
+
// and then read whatever was there» is not a limit.
|
|
128
|
+
//
|
|
129
|
+
// The open re-checks the file through its own descriptor rather than trusting
|
|
130
|
+
// the path a second time, and that closes two holes the path checks cannot
|
|
131
|
+
// (QA-130 MINOR-4):
|
|
132
|
+
//
|
|
133
|
+
// - **the window between `realpath` and `open`.** Every rule above was
|
|
134
|
+
// decided about a path; `open` resolves the name again. Replacing the last
|
|
135
|
+
// component with a symlink in between would hand back a file nothing here
|
|
136
|
+
// ever looked at. `O_NOFOLLOW` refuses that outright, and comparing
|
|
137
|
+
// dev/ino catches a plain swap.
|
|
138
|
+
// - **hard links**, which are the same capability under another name and
|
|
139
|
+
// which `realpath` cannot see: a link has no target to resolve. Without
|
|
140
|
+
// this, `ln /etc/hostname docs/prompt.md` walks past both «inside the
|
|
141
|
+
// project folder» and «not on the protected list», because both were
|
|
142
|
+
// decided about the NAME.
|
|
143
|
+
let buffer;
|
|
144
|
+
try {
|
|
145
|
+
buffer = readCapped(realPath, AGENT_PROMPT_MAX_BYTES + 1, link);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof PromptFileRefused)
|
|
149
|
+
return deny(error.reason);
|
|
150
|
+
return deny(describeFsError(error));
|
|
151
|
+
}
|
|
152
|
+
if (buffer.length > AGENT_PROMPT_MAX_BYTES) {
|
|
153
|
+
return deny(`the file is larger than ${Math.floor(AGENT_PROMPT_MAX_BYTES / 1024)} KB`);
|
|
154
|
+
}
|
|
155
|
+
if (buffer.includes(0))
|
|
156
|
+
return deny('the file is not text');
|
|
157
|
+
const sha = crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 12);
|
|
158
|
+
// Strip a UTF-8 BOM: it is invisible in every editor and would otherwise be
|
|
159
|
+
// the first character of the agent's system prompt.
|
|
160
|
+
const text = buffer
|
|
161
|
+
.toString('utf8')
|
|
162
|
+
.replace(/^\uFEFF/, '')
|
|
163
|
+
.trim();
|
|
164
|
+
if (text === '')
|
|
165
|
+
return deny('the file is empty');
|
|
166
|
+
return { ok: true, text, relPath: wanted, absPath: realPath, bytes: buffer.length, sha };
|
|
167
|
+
}
|
|
168
|
+
/** A refusal decided after the file was already open — carried out by throwing. */
|
|
169
|
+
class PromptFileRefused extends Error {
|
|
170
|
+
reason;
|
|
171
|
+
constructor(reason) {
|
|
172
|
+
super(reason);
|
|
173
|
+
this.reason = reason;
|
|
174
|
+
this.name = 'PromptFileRefused';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Read at most `limit` bytes from a file that is proved, through its own
|
|
179
|
+
* descriptor, to be the same plain file the checks above approved.
|
|
180
|
+
*
|
|
181
|
+
* @param expected The `lstat` taken before the path checks — the identity every
|
|
182
|
+
* rule above was decided about.
|
|
183
|
+
*/
|
|
184
|
+
function readCapped(file, limit, expected) {
|
|
185
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
186
|
+
try {
|
|
187
|
+
const opened = fs.fstatSync(fd);
|
|
188
|
+
if (!opened.isFile())
|
|
189
|
+
throw new PromptFileRefused('the path is not a regular file');
|
|
190
|
+
// A second name for the same inode is a second way in, and it is the one
|
|
191
|
+
// `realpath` cannot see. One link is the only shape whose name and contents
|
|
192
|
+
// are the same fact.
|
|
193
|
+
if (opened.nlink !== 1)
|
|
194
|
+
throw new PromptFileRefused('the file has more than one name');
|
|
195
|
+
if (opened.dev !== expected.dev || opened.ino !== expected.ino) {
|
|
196
|
+
throw new PromptFileRefused('the file changed while it was being opened');
|
|
197
|
+
}
|
|
198
|
+
const buffer = Buffer.alloc(limit);
|
|
199
|
+
let read = 0;
|
|
200
|
+
while (read < limit) {
|
|
201
|
+
const n = fs.readSync(fd, buffer, read, limit - read, null);
|
|
202
|
+
if (n === 0)
|
|
203
|
+
break;
|
|
204
|
+
read += n;
|
|
205
|
+
}
|
|
206
|
+
return buffer.subarray(0, read);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
fs.closeSync(fd);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Turn an fs error into a sentence a human can act on.
|
|
214
|
+
*
|
|
215
|
+
* Never echoes the error's own message: it carries the absolute path, and this
|
|
216
|
+
* text is shown in the session feed of a dashboard that may be open on somebody
|
|
217
|
+
* else's screen.
|
|
218
|
+
*/
|
|
219
|
+
function describeFsError(error) {
|
|
220
|
+
const code = error?.code;
|
|
221
|
+
if (code === 'ENOENT')
|
|
222
|
+
return 'no such file in the project folder';
|
|
223
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
224
|
+
return 'the runner may not read that file';
|
|
225
|
+
if (code === 'EISDIR')
|
|
226
|
+
return 'the path is a directory';
|
|
227
|
+
if (code === 'ELOOP')
|
|
228
|
+
return 'the path is a symlink loop';
|
|
229
|
+
if (code === 'ENAMETOOLONG')
|
|
230
|
+
return 'the path is too long';
|
|
231
|
+
return 'the file could not be read';
|
|
232
|
+
}
|
|
233
|
+
/** How the prompt is announced in the session feed and in the journal. */
|
|
234
|
+
export function agentPromptSizeLabel(bytes) {
|
|
235
|
+
return bytes < 1024 ? `${bytes} B` : `${(bytes / 1024).toFixed(1)} KB`;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* A configured path, safe to put in a line a person reads.
|
|
239
|
+
*
|
|
240
|
+
* The session feed renders a notice as plain text, so backticks would be shown
|
|
241
|
+
* literally rather than as code — and this string can be a REFUSED path, which
|
|
242
|
+
* means it never passed any of the checks above and is only as clean as the API
|
|
243
|
+
* schema made it. Quoted with guillemets, stripped of anything that could break
|
|
244
|
+
* a line, and bounded (QA-130 NIT-13).
|
|
245
|
+
*/
|
|
246
|
+
export function quotePath(value) {
|
|
247
|
+
const clean = Array.from(value)
|
|
248
|
+
.filter((ch) => ch >= ' ' && ch !== '\u007f' && ch !== '`')
|
|
249
|
+
.join('');
|
|
250
|
+
return `«${clean.length > 120 ? `${clean.slice(0, 117)}…` : clean}»`;
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=agent-prompt.js.map
|
package/dist/index.js
CHANGED
|
@@ -249,6 +249,16 @@ function runnerCapabilities(apiUrlOverride) {
|
|
|
249
249
|
*/
|
|
250
250
|
contextRewind: true,
|
|
251
251
|
contextCompaction: true,
|
|
252
|
+
/**
|
|
253
|
+
* Reads the project's own prompt file and hands it to the agent as
|
|
254
|
+
* SYSTEM-prompt text, for both Claude and Codex.
|
|
255
|
+
*
|
|
256
|
+
* Announced so the dashboard can refuse to offer the setting on a server
|
|
257
|
+
* that would ignore it. That refusal is the point of the flag: this is the
|
|
258
|
+
* one setting whose value is that it cannot be silently outranked, so a
|
|
259
|
+
* version of it that is silently ignored would be worse than none.
|
|
260
|
+
*/
|
|
261
|
+
agentPrompt: true,
|
|
252
262
|
/**
|
|
253
263
|
* Session 15: `git_status` reports whether the PROJECT FOLDER is clean, so
|
|
254
264
|
* the panel can say what is blocking an Apply instead of offering a button
|
package/dist/policy.d.ts
CHANGED
|
@@ -30,6 +30,27 @@ export interface PolicyContext {
|
|
|
30
30
|
* than this release — and there the answer stays what it has always been.
|
|
31
31
|
*/
|
|
32
32
|
agentAutoCommit?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Absolute path of the project's own prompt file, when this session was given
|
|
35
|
+
* one (session 17).
|
|
36
|
+
*
|
|
37
|
+
* Same shape and the same reason as the `.git` rule below it: this file has
|
|
38
|
+
* stopped being data. Its contents are the session's SYSTEM prompt, so a
|
|
39
|
+
* write to it is a rewrite of the rules the next process of this session will
|
|
40
|
+
* be given — including the rules that say what the agent may do.
|
|
41
|
+
*
|
|
42
|
+
* It only became reachable when `workMode: DIRECT` made the project folder
|
|
43
|
+
* the agent's own working directory (session 16, and the default). In
|
|
44
|
+
* `BRANCH` mode «writes outside the worktree are not allowed» already covered
|
|
45
|
+
* it, which is why the first version of session 17 believed it was safe
|
|
46
|
+
* everywhere (QA-130 MAJOR-3).
|
|
47
|
+
*
|
|
48
|
+
* A guard, not a guarantee, and the difference is worth stating: it covers
|
|
49
|
+
* the file-writing tools, not `sh -c 'echo … > prompt.md'`, and `full` mode
|
|
50
|
+
* does not consult layer 1 at all. The `sha` printed in the session feed is
|
|
51
|
+
* what makes a change visible when this cannot prevent it.
|
|
52
|
+
*/
|
|
53
|
+
agentPromptFile?: string;
|
|
33
54
|
}
|
|
34
55
|
export interface PolicyDecision {
|
|
35
56
|
decision: 'allow' | 'deny' | 'ask';
|
package/dist/policy.js
CHANGED
|
@@ -706,6 +706,17 @@ export function evaluateToolUse(toolName, input, ctx) {
|
|
|
706
706
|
if (WRITE_TOOLS.has(toolName) && !isInsideWorktree(resolved, ctx.worktreePath)) {
|
|
707
707
|
return { decision: 'deny', reason: 'writes outside the session worktree are not allowed' };
|
|
708
708
|
}
|
|
709
|
+
// The project's own prompt file — the same rule as `.git` above, for the
|
|
710
|
+
// same reason: what is written here is not data, it is the instructions the
|
|
711
|
+
// next process of this session will be started with.
|
|
712
|
+
if (WRITE_TOOLS.has(toolName) &&
|
|
713
|
+
ctx.agentPromptFile &&
|
|
714
|
+
resolved === path.resolve(ctx.agentPromptFile)) {
|
|
715
|
+
return {
|
|
716
|
+
decision: 'deny',
|
|
717
|
+
reason: 'this file is the system prompt of this session — a human edits it, not the agent it instructs',
|
|
718
|
+
};
|
|
719
|
+
}
|
|
709
720
|
if (trust === 'STRICT')
|
|
710
721
|
return { decision: 'ask', reason: 'strict mode' };
|
|
711
722
|
if (READ_TOOLS.has(toolName) && !isInsideWorktree(resolved, ctx.worktreePath)) {
|
package/dist/protocol.d.ts
CHANGED
|
@@ -26,6 +26,18 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
26
26
|
* every project the moment a runner updated ahead of its API.
|
|
27
27
|
*/
|
|
28
28
|
agentAutoCommit: z.ZodOptional<z.ZodBoolean>;
|
|
29
|
+
/**
|
|
30
|
+
* The project's own prompt file, relative to `path` — its contents reach
|
|
31
|
+
* the agent as SYSTEM-prompt text (see `agent-prompt.ts` for why the level
|
|
32
|
+
* matters).
|
|
33
|
+
*
|
|
34
|
+
* `.catch(undefined)` for the same reason as `branchHint` and `branchPlan`:
|
|
35
|
+
* this field travels inside `hello_ack`, which carries EVERY session of the
|
|
36
|
+
* server, so a malformed value must cost its own session's prompt at most —
|
|
37
|
+
* never the frame (QA-100 MAJOR-1). The runner re-validates the path from
|
|
38
|
+
* scratch anyway; the bounds here only keep nonsense off the wire.
|
|
39
|
+
*/
|
|
40
|
+
agentPromptPath: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
29
41
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
30
42
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
31
43
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -36,6 +48,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
36
48
|
budgetUsd: number | null;
|
|
37
49
|
budgetMinutes: number | null;
|
|
38
50
|
agentAutoCommit?: boolean | undefined;
|
|
51
|
+
agentPromptPath?: string | null | undefined;
|
|
39
52
|
}, {
|
|
40
53
|
id: string;
|
|
41
54
|
path: string;
|
|
@@ -44,7 +57,14 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
44
57
|
budgetUsd: number | null;
|
|
45
58
|
budgetMinutes: number | null;
|
|
46
59
|
agentAutoCommit?: boolean | undefined;
|
|
60
|
+
agentPromptPath?: unknown;
|
|
47
61
|
}>;
|
|
62
|
+
/**
|
|
63
|
+
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
64
|
+
* because an older API never sends it and because that is the default a human
|
|
65
|
+
* gets when they do not touch the checkbox.
|
|
66
|
+
*/
|
|
67
|
+
skipAgentPrompt: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
48
68
|
tickets: z.ZodArray<z.ZodObject<{
|
|
49
69
|
id: z.ZodString;
|
|
50
70
|
number: z.ZodNumber;
|
|
@@ -119,8 +139,8 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
119
139
|
}>>;
|
|
120
140
|
}, "strip", z.ZodTypeAny, {
|
|
121
141
|
mode: "ask" | "plan" | "auto" | "full";
|
|
122
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
123
142
|
id: string;
|
|
143
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
124
144
|
agent: "CLAUDE" | "CODEX";
|
|
125
145
|
kind: "TICKET" | "CHAT";
|
|
126
146
|
model: string | null;
|
|
@@ -140,6 +160,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
140
160
|
budgetUsd: number | null;
|
|
141
161
|
budgetMinutes: number | null;
|
|
142
162
|
agentAutoCommit?: boolean | undefined;
|
|
163
|
+
agentPromptPath?: string | null | undefined;
|
|
143
164
|
};
|
|
144
165
|
tickets: {
|
|
145
166
|
number: number;
|
|
@@ -150,6 +171,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
150
171
|
url: string;
|
|
151
172
|
token: string;
|
|
152
173
|
} | undefined;
|
|
174
|
+
skipAgentPrompt?: boolean | undefined;
|
|
153
175
|
branchHint?: string | undefined;
|
|
154
176
|
branchPlan?: {
|
|
155
177
|
branch: string;
|
|
@@ -159,8 +181,8 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
159
181
|
} | undefined;
|
|
160
182
|
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
161
183
|
}, {
|
|
162
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
163
184
|
id: string;
|
|
185
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
164
186
|
agent: "CLAUDE" | "CODEX";
|
|
165
187
|
kind: "TICKET" | "CHAT";
|
|
166
188
|
prompt: string;
|
|
@@ -173,6 +195,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
173
195
|
budgetUsd: number | null;
|
|
174
196
|
budgetMinutes: number | null;
|
|
175
197
|
agentAutoCommit?: boolean | undefined;
|
|
198
|
+
agentPromptPath?: unknown;
|
|
176
199
|
};
|
|
177
200
|
tickets: {
|
|
178
201
|
number: number;
|
|
@@ -191,6 +214,7 @@ export declare const SessionDescriptorSchema: z.ZodObject<{
|
|
|
191
214
|
costUsd?: number | undefined;
|
|
192
215
|
activeMsBase?: number | undefined;
|
|
193
216
|
extraBudgetMinutes?: number | null | undefined;
|
|
217
|
+
skipAgentPrompt?: unknown;
|
|
194
218
|
branchHint?: unknown;
|
|
195
219
|
branchPlan?: unknown;
|
|
196
220
|
workMode?: unknown;
|
|
@@ -228,6 +252,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
228
252
|
* every project the moment a runner updated ahead of its API.
|
|
229
253
|
*/
|
|
230
254
|
agentAutoCommit: z.ZodOptional<z.ZodBoolean>;
|
|
255
|
+
/**
|
|
256
|
+
* The project's own prompt file, relative to `path` — its contents reach
|
|
257
|
+
* the agent as SYSTEM-prompt text (see `agent-prompt.ts` for why the level
|
|
258
|
+
* matters).
|
|
259
|
+
*
|
|
260
|
+
* `.catch(undefined)` for the same reason as `branchHint` and `branchPlan`:
|
|
261
|
+
* this field travels inside `hello_ack`, which carries EVERY session of the
|
|
262
|
+
* server, so a malformed value must cost its own session's prompt at most —
|
|
263
|
+
* never the frame (QA-100 MAJOR-1). The runner re-validates the path from
|
|
264
|
+
* scratch anyway; the bounds here only keep nonsense off the wire.
|
|
265
|
+
*/
|
|
266
|
+
agentPromptPath: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
231
267
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
232
268
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
233
269
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -238,6 +274,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
238
274
|
budgetUsd: number | null;
|
|
239
275
|
budgetMinutes: number | null;
|
|
240
276
|
agentAutoCommit?: boolean | undefined;
|
|
277
|
+
agentPromptPath?: string | null | undefined;
|
|
241
278
|
}, {
|
|
242
279
|
id: string;
|
|
243
280
|
path: string;
|
|
@@ -246,7 +283,14 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
246
283
|
budgetUsd: number | null;
|
|
247
284
|
budgetMinutes: number | null;
|
|
248
285
|
agentAutoCommit?: boolean | undefined;
|
|
286
|
+
agentPromptPath?: unknown;
|
|
249
287
|
}>;
|
|
288
|
+
/**
|
|
289
|
+
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
290
|
+
* because an older API never sends it and because that is the default a human
|
|
291
|
+
* gets when they do not touch the checkbox.
|
|
292
|
+
*/
|
|
293
|
+
skipAgentPrompt: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
250
294
|
tickets: z.ZodArray<z.ZodObject<{
|
|
251
295
|
id: z.ZodString;
|
|
252
296
|
number: z.ZodNumber;
|
|
@@ -321,8 +365,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
321
365
|
}>>;
|
|
322
366
|
}, "strip", z.ZodTypeAny, {
|
|
323
367
|
mode: "ask" | "plan" | "auto" | "full";
|
|
324
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
325
368
|
id: string;
|
|
369
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
326
370
|
agent: "CLAUDE" | "CODEX";
|
|
327
371
|
kind: "TICKET" | "CHAT";
|
|
328
372
|
model: string | null;
|
|
@@ -342,6 +386,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
342
386
|
budgetUsd: number | null;
|
|
343
387
|
budgetMinutes: number | null;
|
|
344
388
|
agentAutoCommit?: boolean | undefined;
|
|
389
|
+
agentPromptPath?: string | null | undefined;
|
|
345
390
|
};
|
|
346
391
|
tickets: {
|
|
347
392
|
number: number;
|
|
@@ -352,6 +397,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
352
397
|
url: string;
|
|
353
398
|
token: string;
|
|
354
399
|
} | undefined;
|
|
400
|
+
skipAgentPrompt?: boolean | undefined;
|
|
355
401
|
branchHint?: string | undefined;
|
|
356
402
|
branchPlan?: {
|
|
357
403
|
branch: string;
|
|
@@ -361,8 +407,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
361
407
|
} | undefined;
|
|
362
408
|
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
363
409
|
}, {
|
|
364
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
365
410
|
id: string;
|
|
411
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
366
412
|
agent: "CLAUDE" | "CODEX";
|
|
367
413
|
kind: "TICKET" | "CHAT";
|
|
368
414
|
prompt: string;
|
|
@@ -375,6 +421,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
375
421
|
budgetUsd: number | null;
|
|
376
422
|
budgetMinutes: number | null;
|
|
377
423
|
agentAutoCommit?: boolean | undefined;
|
|
424
|
+
agentPromptPath?: unknown;
|
|
378
425
|
};
|
|
379
426
|
tickets: {
|
|
380
427
|
number: number;
|
|
@@ -393,6 +440,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
393
440
|
costUsd?: number | undefined;
|
|
394
441
|
activeMsBase?: number | undefined;
|
|
395
442
|
extraBudgetMinutes?: number | null | undefined;
|
|
443
|
+
skipAgentPrompt?: unknown;
|
|
396
444
|
branchHint?: unknown;
|
|
397
445
|
branchPlan?: unknown;
|
|
398
446
|
workMode?: unknown;
|
|
@@ -403,8 +451,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
403
451
|
serverId: string;
|
|
404
452
|
sessions: {
|
|
405
453
|
mode: "ask" | "plan" | "auto" | "full";
|
|
406
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
407
454
|
id: string;
|
|
455
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
408
456
|
agent: "CLAUDE" | "CODEX";
|
|
409
457
|
kind: "TICKET" | "CHAT";
|
|
410
458
|
model: string | null;
|
|
@@ -424,6 +472,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
424
472
|
budgetUsd: number | null;
|
|
425
473
|
budgetMinutes: number | null;
|
|
426
474
|
agentAutoCommit?: boolean | undefined;
|
|
475
|
+
agentPromptPath?: string | null | undefined;
|
|
427
476
|
};
|
|
428
477
|
tickets: {
|
|
429
478
|
number: number;
|
|
@@ -434,6 +483,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
434
483
|
url: string;
|
|
435
484
|
token: string;
|
|
436
485
|
} | undefined;
|
|
486
|
+
skipAgentPrompt?: boolean | undefined;
|
|
437
487
|
branchHint?: string | undefined;
|
|
438
488
|
branchPlan?: {
|
|
439
489
|
branch: string;
|
|
@@ -449,8 +499,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
449
499
|
serverName: string;
|
|
450
500
|
serverId: string;
|
|
451
501
|
sessions: {
|
|
452
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
453
502
|
id: string;
|
|
503
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
454
504
|
agent: "CLAUDE" | "CODEX";
|
|
455
505
|
kind: "TICKET" | "CHAT";
|
|
456
506
|
prompt: string;
|
|
@@ -463,6 +513,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
463
513
|
budgetUsd: number | null;
|
|
464
514
|
budgetMinutes: number | null;
|
|
465
515
|
agentAutoCommit?: boolean | undefined;
|
|
516
|
+
agentPromptPath?: unknown;
|
|
466
517
|
};
|
|
467
518
|
tickets: {
|
|
468
519
|
number: number;
|
|
@@ -481,6 +532,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
481
532
|
costUsd?: number | undefined;
|
|
482
533
|
activeMsBase?: number | undefined;
|
|
483
534
|
extraBudgetMinutes?: number | null | undefined;
|
|
535
|
+
skipAgentPrompt?: unknown;
|
|
484
536
|
branchHint?: unknown;
|
|
485
537
|
branchPlan?: unknown;
|
|
486
538
|
workMode?: unknown;
|
|
@@ -542,6 +594,18 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
542
594
|
* every project the moment a runner updated ahead of its API.
|
|
543
595
|
*/
|
|
544
596
|
agentAutoCommit: z.ZodOptional<z.ZodBoolean>;
|
|
597
|
+
/**
|
|
598
|
+
* The project's own prompt file, relative to `path` — its contents reach
|
|
599
|
+
* the agent as SYSTEM-prompt text (see `agent-prompt.ts` for why the level
|
|
600
|
+
* matters).
|
|
601
|
+
*
|
|
602
|
+
* `.catch(undefined)` for the same reason as `branchHint` and `branchPlan`:
|
|
603
|
+
* this field travels inside `hello_ack`, which carries EVERY session of the
|
|
604
|
+
* server, so a malformed value must cost its own session's prompt at most —
|
|
605
|
+
* never the frame (QA-100 MAJOR-1). The runner re-validates the path from
|
|
606
|
+
* scratch anyway; the bounds here only keep nonsense off the wire.
|
|
607
|
+
*/
|
|
608
|
+
agentPromptPath: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodString>>>;
|
|
545
609
|
budgetUsd: z.ZodNullable<z.ZodNumber>;
|
|
546
610
|
budgetMinutes: z.ZodNullable<z.ZodNumber>;
|
|
547
611
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -552,6 +616,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
552
616
|
budgetUsd: number | null;
|
|
553
617
|
budgetMinutes: number | null;
|
|
554
618
|
agentAutoCommit?: boolean | undefined;
|
|
619
|
+
agentPromptPath?: string | null | undefined;
|
|
555
620
|
}, {
|
|
556
621
|
id: string;
|
|
557
622
|
path: string;
|
|
@@ -560,7 +625,14 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
560
625
|
budgetUsd: number | null;
|
|
561
626
|
budgetMinutes: number | null;
|
|
562
627
|
agentAutoCommit?: boolean | undefined;
|
|
628
|
+
agentPromptPath?: unknown;
|
|
563
629
|
}>;
|
|
630
|
+
/**
|
|
631
|
+
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
632
|
+
* because an older API never sends it and because that is the default a human
|
|
633
|
+
* gets when they do not touch the checkbox.
|
|
634
|
+
*/
|
|
635
|
+
skipAgentPrompt: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
|
|
564
636
|
tickets: z.ZodArray<z.ZodObject<{
|
|
565
637
|
id: z.ZodString;
|
|
566
638
|
number: z.ZodNumber;
|
|
@@ -635,8 +707,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
635
707
|
}>>;
|
|
636
708
|
}, "strip", z.ZodTypeAny, {
|
|
637
709
|
mode: "ask" | "plan" | "auto" | "full";
|
|
638
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
639
710
|
id: string;
|
|
711
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
640
712
|
agent: "CLAUDE" | "CODEX";
|
|
641
713
|
kind: "TICKET" | "CHAT";
|
|
642
714
|
model: string | null;
|
|
@@ -656,6 +728,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
656
728
|
budgetUsd: number | null;
|
|
657
729
|
budgetMinutes: number | null;
|
|
658
730
|
agentAutoCommit?: boolean | undefined;
|
|
731
|
+
agentPromptPath?: string | null | undefined;
|
|
659
732
|
};
|
|
660
733
|
tickets: {
|
|
661
734
|
number: number;
|
|
@@ -666,6 +739,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
666
739
|
url: string;
|
|
667
740
|
token: string;
|
|
668
741
|
} | undefined;
|
|
742
|
+
skipAgentPrompt?: boolean | undefined;
|
|
669
743
|
branchHint?: string | undefined;
|
|
670
744
|
branchPlan?: {
|
|
671
745
|
branch: string;
|
|
@@ -675,8 +749,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
675
749
|
} | undefined;
|
|
676
750
|
workMode?: "DIRECT" | "BRANCH" | undefined;
|
|
677
751
|
}, {
|
|
678
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
679
752
|
id: string;
|
|
753
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
680
754
|
agent: "CLAUDE" | "CODEX";
|
|
681
755
|
kind: "TICKET" | "CHAT";
|
|
682
756
|
prompt: string;
|
|
@@ -689,6 +763,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
689
763
|
budgetUsd: number | null;
|
|
690
764
|
budgetMinutes: number | null;
|
|
691
765
|
agentAutoCommit?: boolean | undefined;
|
|
766
|
+
agentPromptPath?: unknown;
|
|
692
767
|
};
|
|
693
768
|
tickets: {
|
|
694
769
|
number: number;
|
|
@@ -707,6 +782,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
707
782
|
costUsd?: number | undefined;
|
|
708
783
|
activeMsBase?: number | undefined;
|
|
709
784
|
extraBudgetMinutes?: number | null | undefined;
|
|
785
|
+
skipAgentPrompt?: unknown;
|
|
710
786
|
branchHint?: unknown;
|
|
711
787
|
branchPlan?: unknown;
|
|
712
788
|
workMode?: unknown;
|
|
@@ -715,8 +791,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
715
791
|
type: "session_start";
|
|
716
792
|
session: {
|
|
717
793
|
mode: "ask" | "plan" | "auto" | "full";
|
|
718
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
719
794
|
id: string;
|
|
795
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
720
796
|
agent: "CLAUDE" | "CODEX";
|
|
721
797
|
kind: "TICKET" | "CHAT";
|
|
722
798
|
model: string | null;
|
|
@@ -736,6 +812,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
736
812
|
budgetUsd: number | null;
|
|
737
813
|
budgetMinutes: number | null;
|
|
738
814
|
agentAutoCommit?: boolean | undefined;
|
|
815
|
+
agentPromptPath?: string | null | undefined;
|
|
739
816
|
};
|
|
740
817
|
tickets: {
|
|
741
818
|
number: number;
|
|
@@ -746,6 +823,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
746
823
|
url: string;
|
|
747
824
|
token: string;
|
|
748
825
|
} | undefined;
|
|
826
|
+
skipAgentPrompt?: boolean | undefined;
|
|
749
827
|
branchHint?: string | undefined;
|
|
750
828
|
branchPlan?: {
|
|
751
829
|
branch: string;
|
|
@@ -758,8 +836,8 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
758
836
|
}, {
|
|
759
837
|
type: "session_start";
|
|
760
838
|
session: {
|
|
761
|
-
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
762
839
|
id: string;
|
|
840
|
+
status: "RUNNING" | "FAILED" | "STARTING" | "WAITING_INPUT" | "WAITING_PERMISSION" | "REVIEW" | "DONE" | "STOPPED";
|
|
763
841
|
agent: "CLAUDE" | "CODEX";
|
|
764
842
|
kind: "TICKET" | "CHAT";
|
|
765
843
|
prompt: string;
|
|
@@ -772,6 +850,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
772
850
|
budgetUsd: number | null;
|
|
773
851
|
budgetMinutes: number | null;
|
|
774
852
|
agentAutoCommit?: boolean | undefined;
|
|
853
|
+
agentPromptPath?: unknown;
|
|
775
854
|
};
|
|
776
855
|
tickets: {
|
|
777
856
|
number: number;
|
|
@@ -790,6 +869,7 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
790
869
|
costUsd?: number | undefined;
|
|
791
870
|
activeMsBase?: number | undefined;
|
|
792
871
|
extraBudgetMinutes?: number | null | undefined;
|
|
872
|
+
skipAgentPrompt?: unknown;
|
|
793
873
|
branchHint?: unknown;
|
|
794
874
|
branchPlan?: unknown;
|
|
795
875
|
workMode?: unknown;
|
|
@@ -815,9 +895,9 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
815
895
|
fileSize: number;
|
|
816
896
|
}>, "many">>>;
|
|
817
897
|
}, "strip", z.ZodTypeAny, {
|
|
898
|
+
text: string;
|
|
818
899
|
sessionId: string;
|
|
819
900
|
type: "session_message";
|
|
820
|
-
text: string;
|
|
821
901
|
attachments?: {
|
|
822
902
|
id: string;
|
|
823
903
|
fileName: string;
|
|
@@ -825,9 +905,9 @@ export declare const GatewayFrameSchema: z.ZodDiscriminatedUnion<"type", [z.ZodO
|
|
|
825
905
|
fileSize: number;
|
|
826
906
|
}[] | undefined;
|
|
827
907
|
}, {
|
|
908
|
+
text: string;
|
|
828
909
|
sessionId: string;
|
|
829
910
|
type: "session_message";
|
|
830
|
-
text: string;
|
|
831
911
|
attachments?: unknown;
|
|
832
912
|
}>, z.ZodObject<{
|
|
833
913
|
type: z.ZodLiteral<"permission_answer">;
|
package/dist/protocol.js
CHANGED
|
@@ -52,9 +52,27 @@ export const SessionDescriptorSchema = z.object({
|
|
|
52
52
|
* every project the moment a runner updated ahead of its API.
|
|
53
53
|
*/
|
|
54
54
|
agentAutoCommit: z.boolean().optional(),
|
|
55
|
+
/**
|
|
56
|
+
* The project's own prompt file, relative to `path` — its contents reach
|
|
57
|
+
* the agent as SYSTEM-prompt text (see `agent-prompt.ts` for why the level
|
|
58
|
+
* matters).
|
|
59
|
+
*
|
|
60
|
+
* `.catch(undefined)` for the same reason as `branchHint` and `branchPlan`:
|
|
61
|
+
* this field travels inside `hello_ack`, which carries EVERY session of the
|
|
62
|
+
* server, so a malformed value must cost its own session's prompt at most —
|
|
63
|
+
* never the frame (QA-100 MAJOR-1). The runner re-validates the path from
|
|
64
|
+
* scratch anyway; the bounds here only keep nonsense off the wire.
|
|
65
|
+
*/
|
|
66
|
+
agentPromptPath: z.string().max(300).nullable().optional().catch(undefined),
|
|
55
67
|
budgetUsd: z.number().nullable(),
|
|
56
68
|
budgetMinutes: z.number().nullable(),
|
|
57
69
|
}),
|
|
70
|
+
/**
|
|
71
|
+
* «Run this one without the project's agent prompt.» Absent means no — both
|
|
72
|
+
* because an older API never sends it and because that is the default a human
|
|
73
|
+
* gets when they do not touch the checkbox.
|
|
74
|
+
*/
|
|
75
|
+
skipAgentPrompt: z.boolean().optional().catch(undefined),
|
|
58
76
|
tickets: z.array(z.object({ id: z.string().uuid(), number: z.number(), title: z.string() })),
|
|
59
77
|
// Branch the API would like this session to use (derived from its ticket
|
|
60
78
|
// group). Optional: an older API omits it and the runner keeps its
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -118,6 +118,26 @@ export declare class Supervisor {
|
|
|
118
118
|
* stay queued rather than be marked delivered (session 9).
|
|
119
119
|
*/
|
|
120
120
|
private launchAgent;
|
|
121
|
+
/**
|
|
122
|
+
* The project's own prompt file, read fresh for THIS agent process.
|
|
123
|
+
*
|
|
124
|
+
* Read per launch rather than per session on purpose: a system prompt only
|
|
125
|
+
* ever changes when the process restarts, so «edit the file, then press
|
|
126
|
+
* Continue» is the honest contract, and re-reading is what makes it true.
|
|
127
|
+
*
|
|
128
|
+
* Every outcome is said out loud. The whole point of moving the project's
|
|
129
|
+
* rules into the system prompt is that they can no longer be quietly
|
|
130
|
+
* outranked — so «the prompt did not load» must never be indistinguishable
|
|
131
|
+
* from «the prompt loaded». Supervisor notices repeat on every launch (only
|
|
132
|
+
* ADAPTER notices are de-duplicated — gotcha #148), which is exactly what is
|
|
133
|
+
* wanted here: each agent process either has the prompt or does not.
|
|
134
|
+
*
|
|
135
|
+
* Returns the text AND the absolute file, because layer 1 needs the second to
|
|
136
|
+
* refuse writes to it: in `workMode: DIRECT` the project folder is the
|
|
137
|
+
* agent's own working directory, so without that rule a session could rewrite
|
|
138
|
+
* the prompt it will itself be started with next time (QA-130 MAJOR-3).
|
|
139
|
+
*/
|
|
140
|
+
private resolveAgentPrompt;
|
|
121
141
|
/** Warn the user when this share of the budget is gone. */
|
|
122
142
|
private static readonly BUDGET_WARN_RATIO;
|
|
123
143
|
/** Is the agent actually working right now (i.e. should the clock run)? */
|
|
@@ -408,6 +428,14 @@ export declare function composeInitialPrompt(descriptor: SessionDescriptor): str
|
|
|
408
428
|
* A repository that wants both agents equipped ships both files, or symlinks
|
|
409
429
|
* one to the other. That is a repository convention and not something a runner
|
|
410
430
|
* should paper over.
|
|
431
|
+
*
|
|
432
|
+
* `agentPrompt` is the one exception, and it is an exception for a reason this
|
|
433
|
+
* function cannot do anything about: `CLAUDE.md` and `AGENTS.md` arrive at the
|
|
434
|
+
* USER level, and a project's standing process rules have to sit at the system
|
|
435
|
+
* level to survive both a compaction and a rule the CLI states about itself
|
|
436
|
+
* (`agent-prompt.ts`). It arrives here already read and already checked — this
|
|
437
|
+
* function still opens no files — and it goes LAST, so a project overrides us
|
|
438
|
+
* exactly the way `CLAUDE.md` does by being read last.
|
|
411
439
|
*/
|
|
412
|
-
export declare function composeWorkspaceContext(descriptor: SessionDescriptor): string;
|
|
440
|
+
export declare function composeWorkspaceContext(descriptor: SessionDescriptor, agentPrompt?: string): string;
|
|
413
441
|
//# sourceMappingURL=supervisor.d.ts.map
|
package/dist/supervisor.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { log } from './log.js';
|
|
4
4
|
import { claimAutoResume, clearAutoResume, pruneAutoResume } from './auto-resume.js';
|
|
5
5
|
import { evaluateRecipeCommand, maskSecrets, maskString } from './policy.js';
|
|
6
|
+
import { agentPromptSizeLabel, quotePath, readAgentPrompt } from './agent-prompt.js';
|
|
6
7
|
import { JournalStore } from './journal.js';
|
|
7
8
|
import { deleteSessionBranch, ensurePreviewWorktree, ensureSessionWorktree, prepareDirectWorkspace, previewWorktreePath, removePreviewWorktree, removeSessionWorktree, repoKeyFor, sessionWorktreePath, validateWorkspacePath, } from './git.js';
|
|
8
9
|
import { readRecipeProposal } from './recipe.js';
|
|
@@ -380,9 +381,11 @@ export class Supervisor {
|
|
|
380
381
|
return false;
|
|
381
382
|
}
|
|
382
383
|
running.lastPrompt = prompt;
|
|
383
|
-
// Facts about this session only
|
|
384
|
-
// each agent itself — see
|
|
385
|
-
|
|
384
|
+
// Facts about this session only, plus the one file the project named. The
|
|
385
|
+
// rest of the project's documentation is read by each agent itself — see
|
|
386
|
+
// `composeWorkspaceContext`.
|
|
387
|
+
const agentPrompt = this.resolveAgentPrompt(running);
|
|
388
|
+
const workspaceContext = composeWorkspaceContext(descriptor, agentPrompt?.text);
|
|
386
389
|
const rewind = running.rewindAnchor;
|
|
387
390
|
delete running.rewindAnchor;
|
|
388
391
|
// A rewind resumes the conversation the POINT names, which is not always
|
|
@@ -396,6 +399,9 @@ export class Supervisor {
|
|
|
396
399
|
cwd: running.worktreePath,
|
|
397
400
|
...(prompt ? { prompt } : {}),
|
|
398
401
|
...(workspaceContext ? { workspaceContext } : {}),
|
|
402
|
+
// Only when it was actually read: layer 1 must refuse writes to the file
|
|
403
|
+
// this process was given, not to a path it was merely told about.
|
|
404
|
+
...(agentPrompt ? { agentPromptFile: agentPrompt.absPath } : {}),
|
|
399
405
|
trustMode: descriptor.workspace.trustMode,
|
|
400
406
|
...(descriptor.workspace.agentAutoCommit === undefined
|
|
401
407
|
? {}
|
|
@@ -426,6 +432,70 @@ export class Supervisor {
|
|
|
426
432
|
void this.pumpEvents(running);
|
|
427
433
|
return true;
|
|
428
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* The project's own prompt file, read fresh for THIS agent process.
|
|
437
|
+
*
|
|
438
|
+
* Read per launch rather than per session on purpose: a system prompt only
|
|
439
|
+
* ever changes when the process restarts, so «edit the file, then press
|
|
440
|
+
* Continue» is the honest contract, and re-reading is what makes it true.
|
|
441
|
+
*
|
|
442
|
+
* Every outcome is said out loud. The whole point of moving the project's
|
|
443
|
+
* rules into the system prompt is that they can no longer be quietly
|
|
444
|
+
* outranked — so «the prompt did not load» must never be indistinguishable
|
|
445
|
+
* from «the prompt loaded». Supervisor notices repeat on every launch (only
|
|
446
|
+
* ADAPTER notices are de-duplicated — gotcha #148), which is exactly what is
|
|
447
|
+
* wanted here: each agent process either has the prompt or does not.
|
|
448
|
+
*
|
|
449
|
+
* Returns the text AND the absolute file, because layer 1 needs the second to
|
|
450
|
+
* refuse writes to it: in `workMode: DIRECT` the project folder is the
|
|
451
|
+
* agent's own working directory, so without that rule a session could rewrite
|
|
452
|
+
* the prompt it will itself be started with next time (QA-130 MAJOR-3).
|
|
453
|
+
*/
|
|
454
|
+
resolveAgentPrompt(running) {
|
|
455
|
+
const { descriptor } = running;
|
|
456
|
+
const configured = descriptor.workspace.agentPromptPath?.trim();
|
|
457
|
+
if (!configured)
|
|
458
|
+
return undefined;
|
|
459
|
+
// The session was started with «without the project's agent prompt». Said
|
|
460
|
+
// out loud too: a session behaving unlike every other session on the
|
|
461
|
+
// project should carry the reason in its own feed.
|
|
462
|
+
if (descriptor.skipAgentPrompt) {
|
|
463
|
+
this.sendEvent(running, 'notice', {
|
|
464
|
+
level: 'info',
|
|
465
|
+
text: 'Project prompt is switched off for this session. DevBridge rules still apply.',
|
|
466
|
+
});
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
const result = readAgentPrompt(descriptor.workspace.path, configured);
|
|
470
|
+
if (!result.ok) {
|
|
471
|
+
log.warn('agent prompt not loaded', {
|
|
472
|
+
sessionId: descriptor.id,
|
|
473
|
+
path: configured,
|
|
474
|
+
reason: result.reason,
|
|
475
|
+
});
|
|
476
|
+
this.sendEvent(running, 'notice', {
|
|
477
|
+
level: 'warn',
|
|
478
|
+
text: `Project prompt ${quotePath(configured)} was NOT loaded: ${result.reason}. The agent is running without it.`,
|
|
479
|
+
});
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
log.info('agent prompt loaded', {
|
|
483
|
+
sessionId: descriptor.id,
|
|
484
|
+
path: result.relPath,
|
|
485
|
+
bytes: result.bytes,
|
|
486
|
+
sha: result.sha,
|
|
487
|
+
});
|
|
488
|
+
// The `sha` is in the line a person reads, not only in journald. It is the
|
|
489
|
+
// one signal that the file behind an unchanged path has changed — which is
|
|
490
|
+
// exactly what an agent editing its own rules in DIRECT mode looks like
|
|
491
|
+
// (QA-130 MAJOR-3), and layer 1 cannot promise to prevent that in every
|
|
492
|
+
// mode.
|
|
493
|
+
this.sendEvent(running, 'notice', {
|
|
494
|
+
level: 'info',
|
|
495
|
+
text: `Project prompt loaded from ${quotePath(result.relPath)} — ${agentPromptSizeLabel(result.bytes)}, sha ${result.sha}.`,
|
|
496
|
+
});
|
|
497
|
+
return { text: result.text, absPath: result.absPath };
|
|
498
|
+
}
|
|
429
499
|
// ─── Time budget (session 7) ───────────────────────────────────────
|
|
430
500
|
//
|
|
431
501
|
// The budget measures the AGENT's working time, not the calendar. Everything
|
|
@@ -3276,8 +3346,16 @@ export function composeInitialPrompt(descriptor) {
|
|
|
3276
3346
|
* A repository that wants both agents equipped ships both files, or symlinks
|
|
3277
3347
|
* one to the other. That is a repository convention and not something a runner
|
|
3278
3348
|
* should paper over.
|
|
3349
|
+
*
|
|
3350
|
+
* `agentPrompt` is the one exception, and it is an exception for a reason this
|
|
3351
|
+
* function cannot do anything about: `CLAUDE.md` and `AGENTS.md` arrive at the
|
|
3352
|
+
* USER level, and a project's standing process rules have to sit at the system
|
|
3353
|
+
* level to survive both a compaction and a rule the CLI states about itself
|
|
3354
|
+
* (`agent-prompt.ts`). It arrives here already read and already checked — this
|
|
3355
|
+
* function still opens no files — and it goes LAST, so a project overrides us
|
|
3356
|
+
* exactly the way `CLAUDE.md` does by being read last.
|
|
3279
3357
|
*/
|
|
3280
|
-
export function composeWorkspaceContext(descriptor) {
|
|
3358
|
+
export function composeWorkspaceContext(descriptor, agentPrompt) {
|
|
3281
3359
|
const sections = [];
|
|
3282
3360
|
const plan = descriptor.branchPlan;
|
|
3283
3361
|
if (plan) {
|
|
@@ -3305,6 +3383,8 @@ export function composeWorkspaceContext(descriptor) {
|
|
|
3305
3383
|
else if (descriptor.tickets.length > 0) {
|
|
3306
3384
|
sections.push('DevBridge tickets are attached to this chat for CONTEXT only. Read them with the DevBridge MCP tools; do not change their status — this session is not assigned to them.');
|
|
3307
3385
|
}
|
|
3386
|
+
if (agentPrompt)
|
|
3387
|
+
sections.push(agentPrompt);
|
|
3308
3388
|
return sections.join('\n\n');
|
|
3309
3389
|
}
|
|
3310
3390
|
//# sourceMappingURL=supervisor.js.map
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.31.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED