copilot-operator 0.1.23 → 0.1.26
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/src/api/operator.controller.js +1 -1
- package/dist/src/api/operator.service.js +337 -33
- package/dist/src/exec/confinement.js +85 -7
- package/dist/src/exec/programs.js +12 -3
- package/dist/src/exec/reportFile.js +5 -3
- package/dist/src/exec/shells.js +13 -2
- package/dist/src/log/runLog.js +6 -1
- package/dist/src/orchestrator/contract.js +29 -7
- package/dist/src/orchestrator/review.js +16 -3
- package/dist/src/orchestrator/taskRunner.js +391 -54
- package/dist/src/plan/schema.js +11 -1
- package/dist/src/plan/systemGuide.js +12 -4
- package/dist/src/protocol/parser.js +33 -5
- package/dist/src/protocol/reporter.js +2 -0
- package/dist/src/session/compose.js +18 -9
- package/dist/src/session/exportRecord.js +12 -3
- package/dist/src/session/exports.js +46 -9
- package/dist/src/session/handoff.js +5 -2
- package/dist/src/session/interruption.js +3 -1
- package/dist/src/session/runLog.js +42 -1
- package/dist/src/session/runName.js +3 -1
- package/dist/src/session/store.js +7 -2
- package/dist/src/transport/copilotTransport.js +100 -74
- package/dist/src/transport/modelMatch.js +60 -0
- package/dist/src/vcs/artifacts.js +20 -7
- package/dist/src/vcs/commitHygiene.js +7 -2
- package/dist/src/vcs/contentIntegrity.js +65 -6
- package/dist/src/vcs/git.js +42 -7
- package/dist/src/vcs/prepareFromRemote.js +21 -14
- package/dist/src/vcs/remoteAuth.js +31 -0
- package/dist/src/vcs/runPreflight.js +100 -31
- package/dist/src/vcs/scope.js +18 -2
- package/dist/src/vcs/snapshot.js +61 -16
- package/dist/src/vcs/syncCommand.js +3 -11
- package/dist/src/vcs/taskVcs.js +173 -24
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-7e35e4fdefadb05d.js +1 -0
- package/dist/web/_next/static/chunks/867-52e371cb1ec49089.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/{page-0e778dafc69afbd3.js → page-17dd586d1d6bd068.js} +1 -1
- package/dist/web/_next/static/chunks/app/page-4defcfd1eb482644.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/{page-9b66b729748595bb.js → page-54ab3cb89b84679d.js} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +10 -10
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +10 -10
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +10 -10
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +10 -10
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +10 -10
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +10 -10
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +10 -10
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +10 -10
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +10 -10
- package/package.json +3 -2
- package/prompts/level1.md +10 -3
- package/dist/web/_next/static/chunks/549-df7a4876917615da.js +0 -1
- package/dist/web/_next/static/chunks/867-a67ea8ba3f9b24aa.js +0 -1
- package/dist/web/_next/static/chunks/app/page-0a861b38da0a1c51.js +0 -1
- /package/dist/web/_next/static/{-dZtu-yqxc-HXzYTmqYSc → jYvSWuPPywbxckIuz6cNs}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{-dZtu-yqxc-HXzYTmqYSc → jYvSWuPPywbxckIuz6cNs}/_ssgManifest.js +0 -0
|
@@ -55,6 +55,19 @@ export function isWithin(path, roots) {
|
|
|
55
55
|
return target === root || target.startsWith(`${root}\\`);
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The folders a session's commands may reach: its own, and the projects registered in Settings when its
|
|
60
|
+
* own folder is one of them (or inside one) — a front end, a back end and their tests are one piece of
|
|
61
|
+
* work and a test suite starts the application next door. A session working anywhere else is confined to
|
|
62
|
+
* its own folder: found live on 2026-10-03, a fixture session elsewhere on the disk could write into every
|
|
63
|
+
* registered project, whose paths were also sent to the chat in each refusal, and nothing there would have
|
|
64
|
+
* been put back or committed.
|
|
65
|
+
*/
|
|
66
|
+
export function sessionRoots(cwd, registered) {
|
|
67
|
+
const projects = projectRoots(registered);
|
|
68
|
+
const inProject = projects.some((p) => isWithin(cwd, [p]));
|
|
69
|
+
return projectRoots([cwd, ...(inProject ? projects : [])]);
|
|
70
|
+
}
|
|
58
71
|
/** Roots as a set: resolved, de-duplicated, empty ones dropped. */
|
|
59
72
|
export function projectRoots(folders) {
|
|
60
73
|
const seen = new Map();
|
|
@@ -92,11 +105,74 @@ const TILDE = /(?:^|[\s"'=(,])~(?=[\\/]|$|[\s"'`;|&)])/;
|
|
|
92
105
|
const CD_TO_ROOT = /(?:^|[\s;|&({])(?:Set-Location|Push-Location|cd|chdir|sl|pushd)\s+["']?[\\/]["']?(?=$|[\s;|&)}])/i;
|
|
93
106
|
/** A path segment that climbs: `..` on its own, or between separators. */
|
|
94
107
|
const CLIMBS = /(?:^|[\\/])\.\.(?:$|[\\/])/;
|
|
108
|
+
/**
|
|
109
|
+
* A here-string, `@'…'@` or `@"…"@`: its lines are text, not part of the command line.
|
|
110
|
+
*/
|
|
111
|
+
const HERE_STRING = /@(['"])[ \t]*\r?\n[\s\S]*?\r?\n[ \t]*\1@/g;
|
|
112
|
+
/**
|
|
113
|
+
* A quoted PowerShell string, single- or double-quoted, over several lines too: a script's text kept in a
|
|
114
|
+
* variable over many lines was still read as paths when only one-line strings were matched (live run 2026-10-04).
|
|
115
|
+
*/
|
|
116
|
+
const STRING = String.raw `(?:'(?:[^']|'')*'|"(?:[^"` + '`' + String.raw `]|""|` + '`' + String.raw `[\s\S])*")`;
|
|
117
|
+
/** Text: a quoted string, or an array of them `@('a', "b")` or `('a', "b")`, which may be joined `-join "…"`. */
|
|
118
|
+
const TEXT_VALUE = String.raw `(?:${STRING}|@?\(\s*(?:${STRING}\s*,?\s*)*\)(?:\s*-join\s*${STRING})?)`;
|
|
119
|
+
/** Where what follows is the text written: `-Value`, `-InputObject`, `WriteAllText(path, …`, or Set-Content's second positional argument. */
|
|
120
|
+
const CONTENT_LEAD = String.raw `-(?:Value|InputObject)\s+|::(?:Write|Append)All(?:Text|Lines)\s*\([^,]*,\s*|\b(?:Set|Add)-Content\s+(?:-(?:Path|LiteralPath)\s+)?(?:${STRING}|[^\s;|&()"'$-][^\s;|&()"'$]*)\s+`;
|
|
121
|
+
/** The text after `-Value`/`-InputObject`, the content argument of `WriteAllText`/`AppendAllText`, or Set-Content's positional text. */
|
|
122
|
+
const CONTENT_ARGUMENT = new RegExp(`(${CONTENT_LEAD})${TEXT_VALUE}`, 'gi');
|
|
123
|
+
/** A command that runs text as code: then the text is the command, and is read as one. */
|
|
124
|
+
const RUNS_TEXT = /\b(?:Invoke-Expression|iex|Invoke-Command|ScriptBlock\]?::Create|Start-Job)\b|\|\s*&?\s*(?:pwsh|powershell|cmd|node|python3?|py|bash|sh|wsl|ruby|perl|php)(?:\.exe)?\b/i;
|
|
125
|
+
/**
|
|
126
|
+
* The command without the text it writes into a file.
|
|
127
|
+
*
|
|
128
|
+
* Found in a live run on 2026-10-03: a step writing `src/cart.mjs` with a here-string, as the contract
|
|
129
|
+
* recommends, was refused because the file's own `new URL('../config/limits.json', import.meta.url)`
|
|
130
|
+
* was read as a path, resolved against the working folder — where it climbs out — instead of against
|
|
131
|
+
* the file it belongs to, where it does not. The same text in double quotes passed, by an accident of
|
|
132
|
+
* splitting. What a step writes into a file is data; the path it writes to is still read, and so is
|
|
133
|
+
* the text of a command that runs it (`iex`, a pipe into an interpreter). A script written now and run
|
|
134
|
+
* later is read when it is run (see `scriptFiles.ts`).
|
|
135
|
+
*/
|
|
136
|
+
function withoutWrittenText(command) {
|
|
137
|
+
if (RUNS_TEXT.test(command))
|
|
138
|
+
return command;
|
|
139
|
+
let line = command.replace(HERE_STRING, ' ').replace(CONTENT_ARGUMENT, (_m, lead) => `${lead}''`);
|
|
140
|
+
/*
|
|
141
|
+
* The same text put in a variable first and then written: `$t = "…'../config/x'…"; $f::WriteAllText('a.mjs', $t, $e)`.
|
|
142
|
+
* Taken out only when every use of the variable is as what is written — never when it is used as a path —
|
|
143
|
+
* so `$p = '..\..\x'; Set-Content -Path $p` is still read (live run 2026-10-04, a second form of 2026-10-03's).
|
|
144
|
+
*/
|
|
145
|
+
// An array of lines `$c = @("…", "…")` too, and Set-Content's positional text `Set-Content a.js $c` (live run 2026-10-04).
|
|
146
|
+
// And lines joined into another variable first, `$t = ($lines -join "`n")`: content when that one is.
|
|
147
|
+
const joined = new Map();
|
|
148
|
+
for (const j of line.matchAll(new RegExp(String.raw `\$(\w+)\s*=\s*\(?\s*\$(\w+)\s+-join\s*${STRING}\s*\)?`, 'gi')))
|
|
149
|
+
joined.set(j[1].toLowerCase(), j[2].toLowerCase());
|
|
150
|
+
const contentOnly = (name, depth = 0) => {
|
|
151
|
+
if (depth > 3)
|
|
152
|
+
return false;
|
|
153
|
+
const uses = [...line.matchAll(new RegExp(`\\$${name}\\b`, 'gi'))].length - 1;
|
|
154
|
+
if (uses <= 0)
|
|
155
|
+
return false;
|
|
156
|
+
const direct = [
|
|
157
|
+
new RegExp(`(?:${CONTENT_LEAD})\\$${name}\\b`, 'gi'),
|
|
158
|
+
new RegExp(`\\$${name}\\s*\\|\\s*(?:Set-Content|Add-Content|Out-File)\\b`, 'gi'),
|
|
159
|
+
].reduce((n, re) => n + [...line.matchAll(re)].length, 0);
|
|
160
|
+
const viaJoin = [...joined].filter(([into, from]) => from === name.toLowerCase() && contentOnly(into, depth + 1)).length;
|
|
161
|
+
return direct + viaJoin === uses;
|
|
162
|
+
};
|
|
163
|
+
for (const m of [...line.matchAll(new RegExp(String.raw `\$(\w+)\s*=\s*${TEXT_VALUE}`, 'g'))]) {
|
|
164
|
+
const name = m[1];
|
|
165
|
+
if (contentOnly(name))
|
|
166
|
+
line = line.replace(m[0], () => `$${name} = ''`);
|
|
167
|
+
}
|
|
168
|
+
return line;
|
|
169
|
+
}
|
|
95
170
|
/** Every whitespace- or separator-delimited token, quoted content split the same way. */
|
|
96
171
|
function tokens(command) {
|
|
97
172
|
const out = [];
|
|
98
173
|
const rest = command.replace(QUOTED, (_whole, _q, inner) => {
|
|
99
|
-
|
|
174
|
+
// Split like the rest of the line: `URL('../x'` inside a double-quoted string is the token `../x`.
|
|
175
|
+
out.push(...inner.split(/[\s;|&<>(){},'"]+/));
|
|
100
176
|
return ' ';
|
|
101
177
|
});
|
|
102
178
|
out.push(...rest.split(/[\s;|&<>(){},]+/));
|
|
@@ -199,21 +275,23 @@ export function confinementRefusal(command, c) {
|
|
|
199
275
|
const pip = pipRefusal(command);
|
|
200
276
|
if (pip)
|
|
201
277
|
return pip;
|
|
202
|
-
|
|
278
|
+
// Paths are read from the command line without the text it writes into a file: see `withoutWrittenText`.
|
|
279
|
+
const line = withoutWrittenText(command);
|
|
280
|
+
const env = ENV_LOCATION.exec(line);
|
|
203
281
|
if (env)
|
|
204
282
|
return outside(`\`${env[0]}\` is a location on the machine — the operator's profile or the system — not in the project`, c);
|
|
205
|
-
if (HOME_VAR.test(
|
|
283
|
+
if (HOME_VAR.test(line) || TILDE.test(line)) {
|
|
206
284
|
return outside('`~` and `$HOME` are the operator\'s profile, not the project', c);
|
|
207
285
|
}
|
|
208
|
-
if (UNC.test(
|
|
286
|
+
if (UNC.test(line))
|
|
209
287
|
return outside('a network share is not the project', c);
|
|
210
|
-
if (CD_TO_ROOT.test(
|
|
288
|
+
if (CD_TO_ROOT.test(line))
|
|
211
289
|
return outside('this changes to the root of the drive', c);
|
|
212
|
-
for (const p of drivePaths(
|
|
290
|
+
for (const p of drivePaths(line)) {
|
|
213
291
|
if (!isWithin(p, c.roots))
|
|
214
292
|
return outside(`\`${p}\` is outside the project`, c);
|
|
215
293
|
}
|
|
216
|
-
for (const t of tokens(
|
|
294
|
+
for (const t of tokens(line)) {
|
|
217
295
|
if (!CLIMBS.test(t))
|
|
218
296
|
continue;
|
|
219
297
|
const resolved = win32.resolve(c.cwd, t);
|
|
@@ -126,7 +126,13 @@ export function externalProgram(head) {
|
|
|
126
126
|
* anywhere on the line refused honest work, which is the error this gate can least afford.
|
|
127
127
|
*/
|
|
128
128
|
const INLINE_CODE = [
|
|
129
|
-
{ what: 'node -e', pattern: /\bnode(\.exe)?\s+(-\S+\s+)*?-(e|-eval)\b/i },
|
|
129
|
+
{ what: 'node -e', pattern: /\bnode(\.exe)?\s+(-\S+\s+)*?-(e|-eval|p|-print)\b/i },
|
|
130
|
+
/*
|
|
131
|
+
* Code piped into an interpreter that has no script to run: the same thing as `-e`, spelled through
|
|
132
|
+
* stdin. Seen live on 2026-10-03: refused `node -e`, the chat sent `'…' | node` in its next step and
|
|
133
|
+
* it ran. An interpreter given a script file after the pipe (`| node tools/format.js`) is not this.
|
|
134
|
+
*/
|
|
135
|
+
{ what: 'code piped into an interpreter', pattern: /\|\s*&?\s*(node|deno|bun|python3?|py|ruby|perl|php)(\.exe)?(\s+-)?\s*($|[;|)&])/im },
|
|
130
136
|
{ what: 'deno eval', pattern: /\bdeno(\.exe)?\s+eval\b/i },
|
|
131
137
|
{ what: 'bun -e', pattern: /\bbun(\.exe)?\s+(-\S+\s+)*?-e\b/i },
|
|
132
138
|
{ what: 'python -c', pattern: /\b(python3?|py)(\.exe)?\s+(-\S+\s+)*?-c\b/i },
|
|
@@ -214,8 +220,11 @@ export function programRefusal(command, allowedPrograms, shell = 'pwsh') {
|
|
|
214
220
|
const scan = scanCommandHeads(command, shell);
|
|
215
221
|
if (scan.uncertain) {
|
|
216
222
|
return (`refused: the runner could not tell which programs this line starts (${scan.uncertain}). ` +
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
// The advice for the case at hand: a here-string with text on its header line got the general one, which does not say what to change (live run 2026-10-04).
|
|
224
|
+
(/here-string/.test(scan.uncertain)
|
|
225
|
+
? "PowerShell itself rejects it too: nothing may follow @' or @\" on its line — the text starts on the next line, and '@ or \"@ closes it at the very start of a line of its own."
|
|
226
|
+
: 'Write it so every program is named literally — close each quote and bracket, and call a program by its ' +
|
|
227
|
+
'name or a literal path rather than through a variable or an expression.'));
|
|
219
228
|
}
|
|
220
229
|
// What `Start-Process` is asked to start is a program this line starts, as much as a head is.
|
|
221
230
|
for (const head of [...scan.heads, ...startProcessTargets(command)]) {
|
|
@@ -44,9 +44,11 @@ export function clip(text, maxChars) {
|
|
|
44
44
|
function sectionFor(r, maxOutputChars) {
|
|
45
45
|
// A refused step never ran, so it is not given an exit code or a duration that could be read as
|
|
46
46
|
// a command that ran and failed; the header says what happened instead.
|
|
47
|
-
const head = r.outcome === 'refused'
|
|
48
|
-
? `--- step ${r.id} (${r.shell},
|
|
49
|
-
:
|
|
47
|
+
const head = r.outcome === 'refused' && r.skippedAfter !== undefined
|
|
48
|
+
? `--- step ${r.id} (${r.shell}, NOT RUN because step ${r.skippedAfter} was refused, no exit code)\n$ ${r.command}\n`
|
|
49
|
+
: r.outcome === 'refused'
|
|
50
|
+
? `--- step ${r.id} (${r.shell}, REFUSED by the runner, never executed, no exit code)\n$ ${r.command}\n`
|
|
51
|
+
: `--- step ${r.id} (${r.shell}, ${r.outcome}, exit ${r.exitCode}, ` + `${(r.durationMs / 1000).toFixed(1)}s)\n$ ${r.command}\n`;
|
|
50
52
|
const out = clip(r.stdout, maxOutputChars);
|
|
51
53
|
const err = r.stderr.trim().length > 0 ? `[stderr]\n${clip(r.stderr, maxOutputChars)}\n` : '';
|
|
52
54
|
const note = r.truncated ? `[note] output was truncated; the full stream is in ${r.logPath}\n` : '';
|
package/dist/src/exec/shells.js
CHANGED
|
@@ -276,6 +276,16 @@ export function shellNote(inventory = detectShells(), fallback) {
|
|
|
276
276
|
* `cmdArgument`, because nothing else quotes them any more. PowerShell is not touched: it reads its
|
|
277
277
|
* arguments by the C runtime's rules, and Node's quoting is the right quoting for it.
|
|
278
278
|
*/
|
|
279
|
+
/**
|
|
280
|
+
* Put in front of every PowerShell command: the console in UTF-8 both ways, and `$OutputEncoding` with it.
|
|
281
|
+
*
|
|
282
|
+
* Without it a step ran with the console's OEM code page (866 on this machine): what a native program
|
|
283
|
+
* printed and the step captured in a variable was decoded as 866 and written back mangled — node's "✔"
|
|
284
|
+
* saved as "тЬФ" in a kept artifact — and Cyrillic printed by a cmdlet reached the runner, which reads
|
|
285
|
+
* UTF-8, as "�" (live run 2026-10-03). Through a variable, because the line is a command line and the
|
|
286
|
+
* chat's own `[Type]::` restriction is about what survives the chat, not about what the runner runs.
|
|
287
|
+
*/
|
|
288
|
+
export const PWSH_UTF8 = 'try { $__u = New-Object System.Text.UTF8Encoding $false; [Console]::OutputEncoding = $__u; [Console]::InputEncoding = $__u; $OutputEncoding = $__u; Remove-Variable __u } catch { }; ';
|
|
279
289
|
export function invocationFor(resolved, command, scriptArgs) {
|
|
280
290
|
const isScript = Array.isArray(scriptArgs);
|
|
281
291
|
switch (resolved.shell) {
|
|
@@ -284,10 +294,11 @@ export function invocationFor(resolved, command, scriptArgs) {
|
|
|
284
294
|
const base = ['-NoProfile', '-NonInteractive'];
|
|
285
295
|
return isScript
|
|
286
296
|
? { file: resolved.path, args: [...base, '-File', command, ...(scriptArgs ?? [])], windowsVerbatimArguments: false }
|
|
287
|
-
: { file: resolved.path, args: [...base, '-Command', command], windowsVerbatimArguments: false };
|
|
297
|
+
: { file: resolved.path, args: [...base, '-Command', `${PWSH_UTF8}${command}`], windowsVerbatimArguments: false };
|
|
288
298
|
}
|
|
289
299
|
case 'cmd': {
|
|
290
|
-
|
|
300
|
+
// The code page to UTF-8 first, for the same reason as PowerShell's (see `PWSH_UTF8`).
|
|
301
|
+
const line = `chcp 65001 >nul & ${isScript ? [`"${command}"`, ...(scriptArgs ?? []).map(cmdArgument)].join(' ') : command}`;
|
|
291
302
|
return { file: resolved.path, args: ['/d', '/s', '/c', `"${line}"`], windowsVerbatimArguments: true };
|
|
292
303
|
}
|
|
293
304
|
}
|
package/dist/src/log/runLog.js
CHANGED
|
@@ -23,7 +23,12 @@ export class RunLog {
|
|
|
23
23
|
}
|
|
24
24
|
/** Structured record for the transcript; nothing is printed unless `human` is given. */
|
|
25
25
|
event(type, data = {}, human, level = 'info') {
|
|
26
|
-
|
|
26
|
+
/*
|
|
27
|
+
* The level and the sentence go into the transcript too. Without them the runner export's list of
|
|
28
|
+
* problems was always empty — it looks for warn and error — and an event read back said only its type
|
|
29
|
+
* (live run 2026-10-03).
|
|
30
|
+
*/
|
|
31
|
+
this.stream.write(JSON.stringify({ at: new Date().toISOString(), type, ...data, ...(human && data.message === undefined ? { message: human } : {}), ...(level !== 'info' ? { level } : {}) }) + '\n');
|
|
27
32
|
if (human && !this.quiet) {
|
|
28
33
|
const prefix = level === 'error' ? ' !! ' : level === 'warn' ? ' ! ' : ' ';
|
|
29
34
|
process.stdout.write(prefix + human + '\n');
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* chat could never have met. This names the contradiction instead, before the first message.
|
|
10
10
|
*
|
|
11
11
|
* Only what can be known without running anything is judged: the flags against each other, and
|
|
12
|
-
* the file checks against the tree
|
|
12
|
+
* the file checks against the tree the task starts from (see `ContractTree`). A file check that already passes needs no change;
|
|
13
13
|
* one that fails needs its file changed, which a read-only task, or a scope that leaves the file
|
|
14
14
|
* out, forbids. Command checks are not judged: whether a command will pass depends on work the
|
|
15
15
|
* task is allowed to do elsewhere.
|
|
@@ -17,15 +17,27 @@
|
|
|
17
17
|
import { readFile, stat } from 'node:fs/promises';
|
|
18
18
|
import { isAbsolute, relative, resolve } from 'node:path';
|
|
19
19
|
import { inScope } from '../vcs/scope.js';
|
|
20
|
-
|
|
20
|
+
import { gitBytes } from '../vcs/git.js';
|
|
21
|
+
async function fileCheckPassesNow(check, cwd, repoDir, at) {
|
|
21
22
|
const path = resolve(cwd, (check.cwd ?? '').trim() || '.', (check.file ?? '').trim());
|
|
22
|
-
const
|
|
23
|
-
const
|
|
23
|
+
const rel = relative(resolve(repoDir), path).replace(/\\/g, '/');
|
|
24
|
+
const inRepo = rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
25
|
+
let exists;
|
|
26
|
+
let text = '';
|
|
27
|
+
if (at !== 'tree' && inRepo) {
|
|
28
|
+
const shown = await gitBytes(repoDir, ['show', `${at.ref}:${rel}`]).catch(() => null);
|
|
29
|
+
exists = !!shown?.ok;
|
|
30
|
+
text = exists ? shown.stdout.toString('utf8') : '';
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const info = await stat(path).catch(() => null);
|
|
34
|
+
exists = !!info?.isFile();
|
|
35
|
+
text = exists && check.expect === 'file-contains' ? await readFile(path, 'utf8').catch(() => '') : '';
|
|
36
|
+
}
|
|
24
37
|
if (check.expect === 'file-exists')
|
|
25
38
|
return { passes: exists, path };
|
|
26
39
|
if (check.expect === 'file-missing')
|
|
27
40
|
return { passes: !exists, path };
|
|
28
|
-
const text = exists ? await readFile(path, 'utf8').catch(() => '') : '';
|
|
29
41
|
return { passes: exists && text.includes(check.value ?? ''), path };
|
|
30
42
|
}
|
|
31
43
|
/** `git` with the global options that may come before its subcommand. */
|
|
@@ -68,7 +80,7 @@ function branchCheckPasses(check, branch) {
|
|
|
68
80
|
}
|
|
69
81
|
/** Each contradiction in the task, as a sentence; empty when it can be satisfied. */
|
|
70
82
|
export async function contractConflicts(task, cwd, repoDir,
|
|
71
|
-
/** The branch version control put the task on, when it is on. */
|
|
83
|
+
/** The branch version control put the task on, when it is on; where its file checks are judged (see `ContractTree`). */
|
|
72
84
|
vcs = {}) {
|
|
73
85
|
const out = [];
|
|
74
86
|
const scope = task.scope ?? [];
|
|
@@ -96,12 +108,22 @@ vcs = {}) {
|
|
|
96
108
|
...(task.checks ?? []),
|
|
97
109
|
...(task.reviewChecks ?? []).filter((rc) => rc.state === 'active').map((rc) => rc.check),
|
|
98
110
|
].filter((c) => c.expect === 'file-exists' || c.expect === 'file-missing' || c.expect === 'file-contains');
|
|
111
|
+
const at = vcs.files ?? 'tree';
|
|
112
|
+
// Judged when the task starts, on its own tree, once that is known.
|
|
113
|
+
if (at === 'unknown')
|
|
114
|
+
return out;
|
|
99
115
|
for (const check of checks) {
|
|
100
|
-
const { passes, path } = await fileCheckPassesNow(check, cwd);
|
|
116
|
+
const { passes, path } = await fileCheckPassesNow(check, cwd, repoDir, at);
|
|
101
117
|
if (passes)
|
|
102
118
|
continue;
|
|
103
119
|
const rel = relative(resolve(repoDir), path).replace(/\\/g, '/');
|
|
104
120
|
const inside = rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
121
|
+
/*
|
|
122
|
+
* An input file the runner carries onto the task's start from its last capture is not in the tree
|
|
123
|
+
* judged here, and needs no change to be there: judged at the task's start (live run 2026-10-04).
|
|
124
|
+
*/
|
|
125
|
+
if (inside && (vcs.carried?.length ?? 0) > 0 && inScope(rel, vcs.carried))
|
|
126
|
+
continue;
|
|
105
127
|
if (task.readOnly) {
|
|
106
128
|
out.push(`The check "${check.name}" fails now and can only pass if ${rel || path} changes, but the task is read-only.`);
|
|
107
129
|
}
|
|
@@ -31,6 +31,7 @@ import { writeReport } from '../exec/reportFile.js';
|
|
|
31
31
|
import { mergeRedactions } from '../exec/redaction.js';
|
|
32
32
|
import { trackedRepoOf } from '../vcs/taskVcs.js';
|
|
33
33
|
import { buildCoveringMessage, assertSendable } from '../protocol/reporter.js';
|
|
34
|
+
import { inScope } from '../vcs/scope.js';
|
|
34
35
|
/** The sentence both sides get when a review left something listening. */
|
|
35
36
|
function leftoverNote(leftovers) {
|
|
36
37
|
const listening = leftovers.filter((l) => l.ports.length > 0);
|
|
@@ -54,7 +55,18 @@ export function deliverableFor(task, summary, tracked, changedFiles) {
|
|
|
54
55
|
export function reviewBrief(session, task, changedFiles, cwd, deviations = [], previous, disputes = [], derivedFailing = [], earlier = [], deliverable) {
|
|
55
56
|
// The same repository the runner branched and read the changed files from, however it is named.
|
|
56
57
|
const repo = trackedRepoOf(session);
|
|
57
|
-
|
|
58
|
+
// Each file marked against the task's scope, and the scope said: the reviewer was shown side effects as
|
|
59
|
+
// the task's changes, with no scope to judge them by (live run 2026-10-03).
|
|
60
|
+
const scope = task.scope ?? [];
|
|
61
|
+
const outside = (f) => !!task.readOnly || (scope.length > 0 && !inScope(f, scope));
|
|
62
|
+
const files = changedFiles.length > 0
|
|
63
|
+
? changedFiles.map((f) => `- ${f}${outside(f) ? " (outside what this task may change — a violation if it is the task's)" : ''}`).join('\n')
|
|
64
|
+
: '(version control recorded no file changes for this task)';
|
|
65
|
+
const rule = task.readOnly
|
|
66
|
+
? 'This task is read-only: it may change no file in the project.'
|
|
67
|
+
: scope.length > 0
|
|
68
|
+
? `This task may change only: ${scope.join(', ')}. The runner puts back what it changes outside that.`
|
|
69
|
+
: '';
|
|
58
70
|
return [
|
|
59
71
|
'## The task that was given',
|
|
60
72
|
'',
|
|
@@ -81,6 +93,7 @@ export function reviewBrief(session, task, changedFiles, cwd, deviations = [], p
|
|
|
81
93
|
`Working directory for your commands: ${cwd}`,
|
|
82
94
|
repo ? `Repository: ${repo}` : 'There is no repository for this work.',
|
|
83
95
|
'',
|
|
96
|
+
...(rule ? [rule, ''] : []),
|
|
84
97
|
'Files this task changed:',
|
|
85
98
|
files,
|
|
86
99
|
...(deliverable
|
|
@@ -227,8 +240,8 @@ export async function runReview(transport, session, task, deps) {
|
|
|
227
240
|
// The machine's shells, when they are not what the contract's example assumes — the same note
|
|
228
241
|
// the implementer's conversation opens with, for a conversation working to the same example.
|
|
229
242
|
const machineNote = shellNote(undefined, defaultShell);
|
|
230
|
-
let before = await transport.sendAndConfirm(machineNote ? `${contract}
|
|
231
|
-
|
|
243
|
+
let before = await transport.sendAndConfirm(machineNote ? `${contract}
|
|
244
|
+
|
|
232
245
|
${machineNote}` : contract);
|
|
233
246
|
await transport.waitForReply(before);
|
|
234
247
|
await pacer.throttleSend();
|