copilot-operator 0.1.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/LICENSE +21 -0
- package/README.md +556 -0
- package/dist/src/api/app.module.js +18 -0
- package/dist/src/api/dataAcl.js +109 -0
- package/dist/src/api/folderPicker.js +110 -0
- package/dist/src/api/main.js +132 -0
- package/dist/src/api/operator.controller.js +975 -0
- package/dist/src/api/operator.service.js +1971 -0
- package/dist/src/api/saveToDesktop.js +157 -0
- package/dist/src/api/security.js +225 -0
- package/dist/src/api/settings.js +57 -0
- package/dist/src/cli.js +531 -0
- package/dist/src/config/layout.js +66 -0
- package/dist/src/config/lockedPolicy.js +176 -0
- package/dist/src/config/schema.js +487 -0
- package/dist/src/context/contextFiles.js +295 -0
- package/dist/src/context/desktopMirror.js +113 -0
- package/dist/src/context/projectMirror.js +364 -0
- package/dist/src/exec/authorizer.js +61 -0
- package/dist/src/exec/checks.js +285 -0
- package/dist/src/exec/confinement.js +224 -0
- package/dist/src/exec/dangerous.js +161 -0
- package/dist/src/exec/environment.js +80 -0
- package/dist/src/exec/isolation.js +166 -0
- package/dist/src/exec/network.js +242 -0
- package/dist/src/exec/policy.js +201 -0
- package/dist/src/exec/policyManifest.js +84 -0
- package/dist/src/exec/processes.js +329 -0
- package/dist/src/exec/programs.js +290 -0
- package/dist/src/exec/redaction.js +93 -0
- package/dist/src/exec/reportFile.js +154 -0
- package/dist/src/exec/runner.js +245 -0
- package/dist/src/exec/scriptFiles.js +81 -0
- package/dist/src/exec/shellExecuteTrap.js +221 -0
- package/dist/src/exec/shells.js +298 -0
- package/dist/src/exec/stepEnv.js +72 -0
- package/dist/src/exec/winps.js +17 -0
- package/dist/src/exec/workDir.js +94 -0
- package/dist/src/log/runLog.js +43 -0
- package/dist/src/orchestrator/derivedChecks.js +95 -0
- package/dist/src/orchestrator/review.js +590 -0
- package/dist/src/orchestrator/taskRunner.js +1582 -0
- package/dist/src/plan/brief.js +1785 -0
- package/dist/src/plan/importPlan.js +202 -0
- package/dist/src/plan/schema.js +483 -0
- package/dist/src/plan/script.js +1012 -0
- package/dist/src/plan/systemGuide.js +1851 -0
- package/dist/src/protocol/parser.js +224 -0
- package/dist/src/protocol/replySchema.js +207 -0
- package/dist/src/protocol/reporter.js +75 -0
- package/dist/src/protocol/reviewSchema.js +205 -0
- package/dist/src/session/compose.js +67 -0
- package/dist/src/session/debugExport.js +165 -0
- package/dist/src/session/events.js +34 -0
- package/dist/src/session/exportRecord.js +261 -0
- package/dist/src/session/exports.js +484 -0
- package/dist/src/session/model.js +22 -0
- package/dist/src/session/retention.js +34 -0
- package/dist/src/session/runName.js +36 -0
- package/dist/src/session/store.js +440 -0
- package/dist/src/session/story.js +198 -0
- package/dist/src/transport/acceptance.js +34 -0
- package/dist/src/transport/chatSession.js +73 -0
- package/dist/src/transport/copilotTransport.js +1208 -0
- package/dist/src/transport/edgeCrash.js +59 -0
- package/dist/src/transport/locators.js +261 -0
- package/dist/src/transport/profileLock.js +102 -0
- package/dist/src/util/pacing.js +89 -0
- package/dist/src/vcs/commitHygiene.js +102 -0
- package/dist/src/vcs/git.js +286 -0
- package/dist/src/vcs/taskVcs.js +343 -0
- package/dist/web/404.html +15 -0
- package/dist/web/_next/static/chunks/12.f1f4d35b52633e8d.js +1 -0
- package/dist/web/_next/static/chunks/131-bae98f790a541679.js +1 -0
- package/dist/web/_next/static/chunks/197-f7c33a0114fc3db3.js +1 -0
- package/dist/web/_next/static/chunks/489.158bbe303a595fba.js +1 -0
- package/dist/web/_next/static/chunks/643-34b9f66a6c317c4a.js +1 -0
- package/dist/web/_next/static/chunks/867-03b7cac8f79896c3.js +1 -0
- package/dist/web/_next/static/chunks/955-63de06e182d98949.js +1 -0
- package/dist/web/_next/static/chunks/app/_not-found/page-0d0d8e7e62efca12.js +1 -0
- package/dist/web/_next/static/chunks/app/appearance/page-501842e7aeac7f6b.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-ed858127cbdfb4ab.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-bad4f3e89b485719.js +1 -0
- package/dist/web/_next/static/chunks/app/import/page-db1f5f1ab2f5cad1.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-72251c52cc54f95e.js +1 -0
- package/dist/web/_next/static/chunks/app/level1/page-cf3e50cf64fecf4f.js +1 -0
- package/dist/web/_next/static/chunks/app/page-6fd16e5c8cf3b20f.js +1 -0
- package/dist/web/_next/static/chunks/app/presets/page-951e78b80e0e22e6.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-f2e63618097d7221.js +1 -0
- package/dist/web/_next/static/chunks/app/system/page-44e7954c0e48f4ba.js +1 -0
- package/dist/web/_next/static/chunks/c7879cf7-b5ab1053c1d9a2e7.js +1 -0
- package/dist/web/_next/static/chunks/framework-9ac6a346bd42cedd.js +1 -0
- package/dist/web/_next/static/chunks/main-6be4444e2d667e41.js +1 -0
- package/dist/web/_next/static/chunks/main-app-e3a5f713ba31557e.js +1 -0
- package/dist/web/_next/static/chunks/pages/_app-32d20d5a04949d29.js +1 -0
- package/dist/web/_next/static/chunks/pages/_error-abbab53f25ea29b0.js +1 -0
- package/dist/web/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/dist/web/_next/static/chunks/webpack-0f31b926d9a9c5a4.js +1 -0
- package/dist/web/_next/static/css/d77735cdc9020a86.css +1 -0
- package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_buildManifest.js +1 -0
- package/dist/web/_next/static/u8rZNwh4d3n-HGJnfZ72Y/_ssgManifest.js +1 -0
- package/dist/web/appearance.html +15 -0
- package/dist/web/appearance.txt +26 -0
- package/dist/web/defaults.html +15 -0
- package/dist/web/defaults.txt +26 -0
- package/dist/web/history.html +15 -0
- package/dist/web/history.txt +26 -0
- package/dist/web/import.html +15 -0
- package/dist/web/import.txt +26 -0
- package/dist/web/index.html +15 -0
- package/dist/web/index.txt +26 -0
- package/dist/web/level1.html +15 -0
- package/dist/web/level1.txt +26 -0
- package/dist/web/presets.html +18 -0
- package/dist/web/presets.txt +26 -0
- package/dist/web/sessions/view.html +15 -0
- package/dist/web/sessions/view.txt +26 -0
- package/dist/web/system.html +15 -0
- package/dist/web/system.txt +26 -0
- package/package.json +111 -0
- package/prompts/level1.md +535 -0
- package/prompts/organisation.bg.md +44 -0
- package/prompts/organisation.en.md +44 -0
- package/prompts/persona.bg.md +17 -0
- package/prompts/persona.en.md +17 -0
- package/prompts/review1.md +277 -0
- package/prompts/work.bg.md +19 -0
- package/prompts/work.en.md +19 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract a reviewing conversation answers in.
|
|
3
|
+
*
|
|
4
|
+
* A review is a second conversation, opened after a task has finished and its checks have
|
|
5
|
+
* passed, that is shown the task and the result and asked whether the one satisfies the other.
|
|
6
|
+
* It is deliberately a different contract from the implementer's, because it is a different
|
|
7
|
+
* job with a different failure mode. An implementer that ends wrongly says "done" when it is
|
|
8
|
+
* not; a reviewer that ends wrongly says "pass" when it has not looked.
|
|
9
|
+
*
|
|
10
|
+
* So the shape of this schema is almost entirely about making "pass" expensive and "fail"
|
|
11
|
+
* useful:
|
|
12
|
+
*
|
|
13
|
+
* - `pass` needs a summary that says what was checked, not that it was checked. The runner
|
|
14
|
+
* adds the part a schema cannot see — a review that ran no commands at all cannot pass,
|
|
15
|
+
* whatever it writes here.
|
|
16
|
+
* - `fail` needs findings, and every finding needs evidence: the command and what it
|
|
17
|
+
* returned. A reviewer that only has opinions cannot fail anything either, which is the
|
|
18
|
+
* same rule from the other side.
|
|
19
|
+
*
|
|
20
|
+
* The prose version the reviewer is sent lives in `prompts/review1.md`.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { CheckInput } from '../plan/schema.js';
|
|
24
|
+
import { StepSchema } from './replySchema.js';
|
|
25
|
+
export const MIN_REVIEW_SUMMARY_CHARS = 40;
|
|
26
|
+
/**
|
|
27
|
+
* One thing that is wrong, with what proves it.
|
|
28
|
+
*
|
|
29
|
+
* Both halves are required and neither can be waved through. "The README is wrong" is not
|
|
30
|
+
* actionable; "the README says to start the API with `npx tsx src/main.ts`, and running it
|
|
31
|
+
* returns HTTP 500 on every request" is a defect somebody can fix without repeating the work.
|
|
32
|
+
*/
|
|
33
|
+
const Finding = z.object({
|
|
34
|
+
what: z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(15, 'A finding needs a sentence naming what is wrong, specifically enough to act on.'),
|
|
38
|
+
evidence: z
|
|
39
|
+
.string()
|
|
40
|
+
.trim()
|
|
41
|
+
.min(10, 'A finding needs evidence: the command you ran and what it actually returned.'),
|
|
42
|
+
/** The file, the endpoint, the line — wherever the defect lives. */
|
|
43
|
+
/**
|
|
44
|
+
* Required, because it is how the same defect is recognised between rounds. Two reviewers
|
|
45
|
+
* word one problem differently; a file and a place in it do not drift. A first-round
|
|
46
|
+
* finding without one made a second-round repeat invisible, and the loop ran a third time.
|
|
47
|
+
*/
|
|
48
|
+
where: z.string().trim().min(3, {
|
|
49
|
+
message: 'A finding needs "where": the file and the place in it, or the URL, that the finding is about.',
|
|
50
|
+
}),
|
|
51
|
+
/**
|
|
52
|
+
* The sentence of the task this finding rests on, quoted.
|
|
53
|
+
*
|
|
54
|
+
* A reviewer once searched a page for label text no task had specified and failed the work
|
|
55
|
+
* for its absence; the next reviewer did the same. Every finding now has to say which
|
|
56
|
+
* sentence asked for the thing it says is missing, and the runner checks that the sentence
|
|
57
|
+
* is really there — in this task, its project instructions, or an earlier task of the same
|
|
58
|
+
* session. A finding that cannot quote its basis is an invented requirement, and is refused.
|
|
59
|
+
*/
|
|
60
|
+
basis: z.string().trim().min(12, {
|
|
61
|
+
message: 'A finding needs "basis": the sentence of the task (or its project instructions) it rests on, quoted, at least 12 characters.',
|
|
62
|
+
}),
|
|
63
|
+
/**
|
|
64
|
+
* The mechanical test that would have caught this, in the plan's own check shape.
|
|
65
|
+
*
|
|
66
|
+
* A review is judgement, and judgement varies: the same missing `@HttpCode(200)` was found
|
|
67
|
+
* in one run and missed in the next. A finding that carries its check turns the one thing
|
|
68
|
+
* the reviewer noticed into arithmetic the runner repeats on every later attempt. The runner
|
|
69
|
+
* runs it at once, on the work as it stands: a check that passes on the defective state does
|
|
70
|
+
* not capture the defect, and is refused.
|
|
71
|
+
*/
|
|
72
|
+
check: CheckInput.optional(),
|
|
73
|
+
/**
|
|
74
|
+
* Whose problem this is, and the only field here that changes what happens next.
|
|
75
|
+
*
|
|
76
|
+
* `work` — the work does not do what the task asked. Somebody can fix it, so it goes back.
|
|
77
|
+
* `task` — the work is right and the **task** is wrong: it contradicts itself, it demands
|
|
78
|
+
* something the project instructions forbid, or it expects something that is not true of
|
|
79
|
+
* this machine. Nobody downstream can fix that, so the task stops and says so.
|
|
80
|
+
*
|
|
81
|
+
* Without this, a reviewer holding a contradictory task had only one move — fail the work —
|
|
82
|
+
* and the implementer, who may not change the task, had only one answer: it cannot. Two
|
|
83
|
+
* rounds of that and the task blocked with a finding that was never about the work. Three
|
|
84
|
+
* such findings in one run: a README documenting four error messages where the task said
|
|
85
|
+
* three (the code has four), a README whose start commands could not be run because the
|
|
86
|
+
* project instructions forbid starting servers, and an audit expected to find no git remote
|
|
87
|
+
* in a repository that has one.
|
|
88
|
+
*/
|
|
89
|
+
about: z.enum(['work', 'task']).default('work'),
|
|
90
|
+
});
|
|
91
|
+
export const ReviewSchema = z
|
|
92
|
+
.object({
|
|
93
|
+
/**
|
|
94
|
+
* `continue` — steps to run, send me the output.
|
|
95
|
+
* `pass` — I checked it by running things, and it does what the task asked.
|
|
96
|
+
* `fail` — I checked it by running things, and here is what is wrong.
|
|
97
|
+
*
|
|
98
|
+
* There is no "blocked" here. A reviewer that cannot check something says so as a finding
|
|
99
|
+
* and fails: "I could not verify X because Y" is information the operator needs, and
|
|
100
|
+
* quietly passing work nobody could check is the one outcome worth ruling out.
|
|
101
|
+
*/
|
|
102
|
+
status: z.enum(['continue', 'pass', 'fail']),
|
|
103
|
+
steps: z.array(StepSchema).default([]),
|
|
104
|
+
notes: z.string().optional(),
|
|
105
|
+
/** Required on `pass` and `fail`: what you checked, how, and what you saw. */
|
|
106
|
+
summary: z.string().optional(),
|
|
107
|
+
findings: z.array(Finding).default([]),
|
|
108
|
+
})
|
|
109
|
+
.refine((r) => r.status !== 'continue' || r.steps.length > 0, {
|
|
110
|
+
message: 'status "continue" with no steps would stall the review. Send steps, or reach a verdict.',
|
|
111
|
+
})
|
|
112
|
+
.refine((r) => r.status === 'continue' || r.steps.length === 0, {
|
|
113
|
+
message: 'A verdict ends the review, so "pass" and "fail" cannot carry steps. Run them first with "continue".',
|
|
114
|
+
})
|
|
115
|
+
.refine((r) => r.status === 'continue' || (r.summary ?? '').trim().length >= MIN_REVIEW_SUMMARY_CHARS, {
|
|
116
|
+
message: 'A verdict requires a summary: what you checked, what you ran, and what you saw. ' +
|
|
117
|
+
'Not "it looks correct" — the commands and their output.',
|
|
118
|
+
})
|
|
119
|
+
.refine((r) => r.status !== 'fail' || r.findings.length > 0, {
|
|
120
|
+
message: 'status "fail" requires at least one entry in "findings", each with "what" and "evidence".',
|
|
121
|
+
})
|
|
122
|
+
/*
|
|
123
|
+
* A pass may carry findings about the task, and only about the task.
|
|
124
|
+
*
|
|
125
|
+
* Without this the reviewer holding right work and a wrong task had one move — fail the
|
|
126
|
+
* work — and the runner then ended the task `blocked` over a sentence that was never in the
|
|
127
|
+
* work, stopping the plan behind it. A README documenting four error messages where the task
|
|
128
|
+
* said three is `done`, with a note for whoever wrote "three"; it is not a failure.
|
|
129
|
+
*/
|
|
130
|
+
.refine((r) => r.status !== 'pass' || r.findings.every((f) => f.about === 'task'), {
|
|
131
|
+
message: 'status "pass" can carry findings only with "about": "task" — the work is right and the task is wrong. ' +
|
|
132
|
+
'A finding about the work makes the verdict "fail"; a minor point goes in "summary", not in "findings".',
|
|
133
|
+
});
|
|
134
|
+
/** The findings written out for whoever has to act on them. */
|
|
135
|
+
export function describeFindings(findings, isRepeated) {
|
|
136
|
+
return findings
|
|
137
|
+
.map((f, i) => {
|
|
138
|
+
const id = f.id;
|
|
139
|
+
const where = f.where?.trim() ? ` (${f.where.trim()})` : '';
|
|
140
|
+
const about = f.about === 'task' ? ' [about the task, not the work]' : '';
|
|
141
|
+
const again = isRepeated?.(f) ? ' [raised in an earlier round too]' : '';
|
|
142
|
+
return `${i + 1}. ${id ? `[${id}] ` : ''}${f.what}${where}${about}${again}\n Evidence: ${f.evidence}`;
|
|
143
|
+
})
|
|
144
|
+
.join('\n\n');
|
|
145
|
+
}
|
|
146
|
+
/** Text as it is compared for grounding: one case, one space, no markdown dressing. */
|
|
147
|
+
function groundingKey(text) {
|
|
148
|
+
return text
|
|
149
|
+
.toLowerCase()
|
|
150
|
+
.replace(/[`*_"'“”‘’]/g, '')
|
|
151
|
+
.replace(/\s+/g, ' ')
|
|
152
|
+
.trim();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Whether a finding's basis is really a quote from what the reviewer was given.
|
|
156
|
+
*
|
|
157
|
+
* Substring after normalisation, so a quote that drops a backtick or a line break still
|
|
158
|
+
* grounds; a paraphrase does not, and neither does a sentence from nowhere. Sources are the
|
|
159
|
+
* task, its project instructions and, in a per-session run, the earlier tasks the reviewer
|
|
160
|
+
* was shown — "labelled A and B" is in the page task, not in the smoke test that checks it.
|
|
161
|
+
*/
|
|
162
|
+
export function isGrounded(basis, sources) {
|
|
163
|
+
const key = groundingKey(basis);
|
|
164
|
+
if (key.length < 12)
|
|
165
|
+
return false;
|
|
166
|
+
return sources.some((s) => groundingKey(s).includes(key));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The name a finding is referred to by, once the runner has it: round and position.
|
|
170
|
+
*
|
|
171
|
+
* Given by the runner rather than the reviewer so that it is unique across rounds and cannot
|
|
172
|
+
* be forgotten. It is what the implementer names in `disputed`, and what the record shows.
|
|
173
|
+
*/
|
|
174
|
+
export function findingId(round, index) {
|
|
175
|
+
return `r${round}f${index + 1}`;
|
|
176
|
+
}
|
|
177
|
+
/** True when nothing the reviewer found is something the implementer could fix. */
|
|
178
|
+
export function allAboutTheTask(findings) {
|
|
179
|
+
return findings.length > 0 && findings.every((f) => f.about === 'task');
|
|
180
|
+
}
|
|
181
|
+
/** A `where`, normalised enough that two reviewers describing the same place agree. */
|
|
182
|
+
function placeKey(where) {
|
|
183
|
+
return (where ?? '').toLowerCase().replace(/\\/g, '/').replace(/\s+/g, ' ').trim();
|
|
184
|
+
}
|
|
185
|
+
function claimKey(what) {
|
|
186
|
+
return what.toLowerCase().replace(/[`"'*_]/g, '').replace(/\s+/g, ' ').trim();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Whether a finding is one an earlier round already raised.
|
|
190
|
+
*
|
|
191
|
+
* Two reviewers are two conversations, so the wording of `what` drifts between rounds; the
|
|
192
|
+
* `where` does not, because it names a file and a place in it. So a finding is the same
|
|
193
|
+
* finding when it points at the same place, and only when neither round said where, when it
|
|
194
|
+
* makes the same claim word for word. The signal this feeds is one the runner otherwise threw
|
|
195
|
+
* away: in one run the same `where` came back in rounds one and two, the implementer had
|
|
196
|
+
* reported it fixed in between, the checks had passed, and nobody was asked the only question
|
|
197
|
+
* that mattered — whether fixing the work could resolve it at all.
|
|
198
|
+
*/
|
|
199
|
+
export function isRepeat(previous, finding) {
|
|
200
|
+
const place = placeKey(finding.where);
|
|
201
|
+
if (place)
|
|
202
|
+
return previous.some((p) => placeKey(p.where) === place);
|
|
203
|
+
const claim = claimKey(finding.what);
|
|
204
|
+
return previous.some((p) => !placeKey(p.where) && claimKey(p.what) === claim);
|
|
205
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the messages that open a task in the chat.
|
|
3
|
+
*
|
|
4
|
+
* Two shapes, because a conversation is opened once and then reused:
|
|
5
|
+
*
|
|
6
|
+
* first task in a session level 1 (the contract, sent on its own and acknowledged),
|
|
7
|
+
* then level 2 plus the task in one message
|
|
8
|
+
* later tasks a short reminder that the contract still applies,
|
|
9
|
+
* then level 2 plus the task
|
|
10
|
+
*
|
|
11
|
+
* Level 2 is always sent with the task, even when it did not change, because it is cheap
|
|
12
|
+
* and it keeps each task self-contained in the transcript. The priority of level 1 over
|
|
13
|
+
* level 2 is stated inside level 1 itself; the composition only makes the boundary visible.
|
|
14
|
+
*/
|
|
15
|
+
const LEVEL2_HEADER = '## Project instructions (level 2)';
|
|
16
|
+
const TASK_HEADER = '## Task';
|
|
17
|
+
/**
|
|
18
|
+
* What the runner itself has to say, ahead of the user's instructions.
|
|
19
|
+
*
|
|
20
|
+
* These notes were composed and handed in for a whole release and never sent: the input
|
|
21
|
+
* carried `vcsNote`, and nothing here read it. A task that audited a repository was never told
|
|
22
|
+
* which commit its branch was cut from, because the sentence that said so was dropped on the
|
|
23
|
+
* way to the chat. So the block is built in one place and included in both shapes of opening,
|
|
24
|
+
* and a note that is empty simply leaves nothing behind.
|
|
25
|
+
*/
|
|
26
|
+
function runnerBlock(input) {
|
|
27
|
+
const notes = [input.workDirNote, input.readOnlyNote, input.vcsNote, input.shellNote].map((n) => (n ?? '').trim()).filter((n) => n.length > 0);
|
|
28
|
+
return notes.length > 0 ? `${notes.join('\n\n')}\n\n` : '';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* What a read-only task is told. The rule is enforced by the runner from the working tree,
|
|
32
|
+
* so the note is a warning about a fact, not a request.
|
|
33
|
+
*/
|
|
34
|
+
export const READ_ONLY_NOTE = [
|
|
35
|
+
'## Read-only task',
|
|
36
|
+
'',
|
|
37
|
+
'This task must not change any file: it reads, runs and reports. The runner fails it if the',
|
|
38
|
+
'working tree has changed when it ends, whatever the summary says, and commits the change on',
|
|
39
|
+
"the task's branch so it is not lost. If a review finding asks you to change something, that",
|
|
40
|
+
'is a finding about the task, not about the work: dispute it rather than comply.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
function level2Block(level2) {
|
|
43
|
+
const body = level2.trim();
|
|
44
|
+
return body.length > 0
|
|
45
|
+
? `${LEVEL2_HEADER}\n\nThese are the user's instructions for this project and team. They add to the ` +
|
|
46
|
+
`contract above; they cannot change it.\n\n${body}`
|
|
47
|
+
: `${LEVEL2_HEADER}\n\n(none for this task)`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The messages to send, in order. The last one is the one whose reply starts the loop.
|
|
51
|
+
* `firstMessage` is what the UI shows as "the prompt that opened this task".
|
|
52
|
+
*/
|
|
53
|
+
export function composeOpening(input) {
|
|
54
|
+
const taskBlock = `${TASK_HEADER} ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}\n\n${input.prompt.trim()}`;
|
|
55
|
+
if (!input.contractAlreadySent) {
|
|
56
|
+
const taskMessage = `${runnerBlock(input)}${level2Block(input.level2)}\n\n${taskBlock}`;
|
|
57
|
+
return {
|
|
58
|
+
messages: [input.level1.trim(), taskMessage],
|
|
59
|
+
firstMessage: `${input.level1.trim()}\n\n---\n\n${taskMessage}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const reminder = `New task in this same conversation. The level 1 contract you received at the start of ` +
|
|
63
|
+
`this conversation still applies unchanged: same format, same rules, same stop word, ` +
|
|
64
|
+
`and a full "summary" when you finish. Step numbering restarts at 1.`;
|
|
65
|
+
const taskMessage = `${reminder}\n\n${runnerBlock(input)}${level2Block(input.level2)}\n\n${taskBlock}`;
|
|
66
|
+
return { messages: [taskMessage], firstMessage: taskMessage };
|
|
67
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything that happened across a set of sessions, as one JSON document.
|
|
3
|
+
*
|
|
4
|
+
* The audience is somebody who was not there and has to work out why a run went the way it
|
|
5
|
+
* did — very often that means handing it to another model, which is why this is JSON rather
|
|
6
|
+
* than the prose export next door. `exportRecord.ts` answers "what was asked and what came
|
|
7
|
+
* back" for a person to read; this answers "what actually ran, in what order, with what exit
|
|
8
|
+
* codes, and what did the repository end up with" for someone to analyse.
|
|
9
|
+
*
|
|
10
|
+
* It is assembled from two places. The sessions carry the durable record — the text, the
|
|
11
|
+
* outcome, the checks, what version control did. The run folders carry the sequence: every
|
|
12
|
+
* step the model proposed, whether it ran, and what it exited with. Neither alone is enough to
|
|
13
|
+
* explain a failure, and a run folder that has been cleaned away is not an error here: the
|
|
14
|
+
* task still appears, with its own record and an empty step list.
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
/** How long a command line travels into the report. Enough to recognise, not enough to drown. */
|
|
19
|
+
const MAX_COMMAND_CHARS = 600;
|
|
20
|
+
function clip(text, max = MAX_COMMAND_CHARS) {
|
|
21
|
+
const t = text.trim();
|
|
22
|
+
return t.length > max ? `${t.slice(0, max)}… (${t.length - max} more characters)` : t;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The sequence of what ran, read back out of the transcript.
|
|
26
|
+
*
|
|
27
|
+
* A step appears twice in the transcript — proposed, then finished — so the two are joined on
|
|
28
|
+
* the step id. A step that was proposed and never finished is left with no outcome, which is
|
|
29
|
+
* itself the answer when a run was stopped or the process died in the middle of one.
|
|
30
|
+
*/
|
|
31
|
+
async function readTranscript(runsDir, runId) {
|
|
32
|
+
const empty = { steps: [], chatRounds: [], problems: [] };
|
|
33
|
+
const raw = await readFile(join(runsDir, runId, 'transcript.jsonl'), 'utf8').catch(() => null);
|
|
34
|
+
if (raw === null)
|
|
35
|
+
return empty;
|
|
36
|
+
const byId = new Map();
|
|
37
|
+
const chatRounds = [];
|
|
38
|
+
const problems = [];
|
|
39
|
+
for (const line of raw.split('\n')) {
|
|
40
|
+
if (!line.trim())
|
|
41
|
+
continue;
|
|
42
|
+
let event;
|
|
43
|
+
try {
|
|
44
|
+
event = JSON.parse(line);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const type = String(event.type ?? '');
|
|
50
|
+
if (type === 'step-proposed') {
|
|
51
|
+
const id = Number(event.id);
|
|
52
|
+
byId.set(id, { ...(byId.get(id) ?? { id }), id, description: clip(String(event.description ?? '')) });
|
|
53
|
+
}
|
|
54
|
+
else if (type === 'step-finished') {
|
|
55
|
+
const id = Number(event.id);
|
|
56
|
+
byId.set(id, {
|
|
57
|
+
...(byId.get(id) ?? { id }),
|
|
58
|
+
id,
|
|
59
|
+
outcome: String(event.outcome ?? ''),
|
|
60
|
+
exitCode: typeof event.exitCode === 'number' ? event.exitCode : undefined,
|
|
61
|
+
durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
else if (type === 'reply-parsed') {
|
|
65
|
+
chatRounds.push({
|
|
66
|
+
iteration: Number(event.iteration) || chatRounds.length + 1,
|
|
67
|
+
status: String(event.status ?? ''),
|
|
68
|
+
steps: Number(event.steps) || 0,
|
|
69
|
+
notes: event.notes ? clip(String(event.notes), 300) : undefined,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else if (type === 'task-error' || type === 'report-send-failed' || type === 'format-error' || type === 'vcs-problem') {
|
|
73
|
+
problems.push(`${type}: ${clip(String(event.error ?? event.message ?? event.detail ?? ''), 400)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { steps: [...byId.values()].sort((a, b) => a.id - b.id), chatRounds, problems };
|
|
77
|
+
}
|
|
78
|
+
async function taskOf(task, runsDir) {
|
|
79
|
+
const transcript = task.runId ? await readTranscript(runsDir, task.runId) : { steps: [], chatRounds: [], problems: [] };
|
|
80
|
+
const started = task.startedAt ? Date.parse(task.startedAt) : undefined;
|
|
81
|
+
const finished = task.finishedAt ? Date.parse(task.finishedAt) : undefined;
|
|
82
|
+
return {
|
|
83
|
+
id: task.id,
|
|
84
|
+
title: task.title,
|
|
85
|
+
status: task.status,
|
|
86
|
+
attempt: task.attempt ?? 1,
|
|
87
|
+
createdAt: task.createdAt,
|
|
88
|
+
startedAt: task.startedAt,
|
|
89
|
+
finishedAt: task.finishedAt,
|
|
90
|
+
durationMs: started && finished ? finished - started : undefined,
|
|
91
|
+
iterations: task.iterations,
|
|
92
|
+
runId: task.runId,
|
|
93
|
+
prompt: task.prompt,
|
|
94
|
+
level2: task.level2,
|
|
95
|
+
summary: task.summary,
|
|
96
|
+
reason: task.reason,
|
|
97
|
+
deviations: task.deviations,
|
|
98
|
+
disputes: task.disputes,
|
|
99
|
+
reviewChecks: task.reviewChecks,
|
|
100
|
+
environment: task.environment,
|
|
101
|
+
vcsPlan: task.vcsPlan,
|
|
102
|
+
vcs: task.vcs,
|
|
103
|
+
checks: task.checks,
|
|
104
|
+
checkResults: task.checkResults,
|
|
105
|
+
steps: transcript.steps,
|
|
106
|
+
chatRounds: transcript.chatRounds,
|
|
107
|
+
problems: transcript.problems,
|
|
108
|
+
earlierAttempts: (task.attempts ?? []).map((a) => ({
|
|
109
|
+
status: a.status,
|
|
110
|
+
iterations: a.iterations,
|
|
111
|
+
runId: a.runId,
|
|
112
|
+
reason: a.reason,
|
|
113
|
+
summary: a.summary,
|
|
114
|
+
deviations: a.deviations,
|
|
115
|
+
disputes: a.disputes,
|
|
116
|
+
checkResults: a.checkResults,
|
|
117
|
+
vcs: a.vcs,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** The whole thing, plus a file name that says what it is without being opened. */
|
|
122
|
+
export async function buildDebugExport(input) {
|
|
123
|
+
const sessions = [];
|
|
124
|
+
const totals = { sessions: input.sessions.length, tasks: 0, done: 0, failed: 0, queued: 0 };
|
|
125
|
+
for (const session of input.sessions) {
|
|
126
|
+
const tasks = [];
|
|
127
|
+
for (const task of session.tasks) {
|
|
128
|
+
totals.tasks += 1;
|
|
129
|
+
if (task.status === 'done')
|
|
130
|
+
totals.done += 1;
|
|
131
|
+
else if (task.status === 'queued')
|
|
132
|
+
totals.queued += 1;
|
|
133
|
+
else
|
|
134
|
+
totals.failed += 1;
|
|
135
|
+
tasks.push(await taskOf(task, input.runsDir));
|
|
136
|
+
}
|
|
137
|
+
sessions.push({
|
|
138
|
+
id: session.id,
|
|
139
|
+
name: session.name,
|
|
140
|
+
createdAt: session.createdAt,
|
|
141
|
+
status: session.status,
|
|
142
|
+
model: session.model,
|
|
143
|
+
modelInUse: session.modelInUse,
|
|
144
|
+
onFailure: session.onFailure,
|
|
145
|
+
chat: session.chat ? { name: session.chat.name, url: session.chat.url } : undefined,
|
|
146
|
+
vcs: session.vcs,
|
|
147
|
+
vcsBaseCommit: session.vcsBaseCommit,
|
|
148
|
+
mirror: session.mirror,
|
|
149
|
+
tasks,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const report = {
|
|
153
|
+
exportedAt: new Date().toISOString(),
|
|
154
|
+
about: 'copilot-operator: what ran and what happened, across the selected sessions. Everything here is ' +
|
|
155
|
+
'either the session record on disk or the run transcript; nothing is inferred.',
|
|
156
|
+
machine: { node: process.versions.node, platform: process.platform, cwd: input.cwd, runsDir: input.runsDir },
|
|
157
|
+
totals,
|
|
158
|
+
sessions,
|
|
159
|
+
};
|
|
160
|
+
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '').replace(/-/g, '');
|
|
161
|
+
const name = input.sessions.length === 1
|
|
162
|
+
? (input.sessions[0].name.replace(/[^\p{L}\p{N}._ -]/gu, '').trim().replace(/\s+/g, '-') || 'session')
|
|
163
|
+
: `${input.sessions.length}-sessions`;
|
|
164
|
+
return { fileName: `copilot-operator-debug-${name}-${stamp}.json`, content: JSON.stringify(report, null, 2), totals };
|
|
165
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live event stream.
|
|
3
|
+
*
|
|
4
|
+
* Every run already writes a JSONL transcript. This is the same information as it happens,
|
|
5
|
+
* so the web UI can show progress without polling files. Each session keeps a bounded
|
|
6
|
+
* history so a page that opens mid-run can catch up.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
const HISTORY_PER_SESSION = 500;
|
|
10
|
+
export class EventBus {
|
|
11
|
+
emitter = new EventEmitter();
|
|
12
|
+
history = new Map();
|
|
13
|
+
constructor() {
|
|
14
|
+
this.emitter.setMaxListeners(100);
|
|
15
|
+
}
|
|
16
|
+
publish(event) {
|
|
17
|
+
const full = { at: new Date().toISOString(), ...event };
|
|
18
|
+
const list = this.history.get(event.sessionId) ?? [];
|
|
19
|
+
list.push(full);
|
|
20
|
+
if (list.length > HISTORY_PER_SESSION)
|
|
21
|
+
list.splice(0, list.length - HISTORY_PER_SESSION);
|
|
22
|
+
this.history.set(event.sessionId, list);
|
|
23
|
+
this.emitter.emit(event.sessionId, full);
|
|
24
|
+
this.emitter.emit('*', full);
|
|
25
|
+
return full;
|
|
26
|
+
}
|
|
27
|
+
recent(sessionId) {
|
|
28
|
+
return [...(this.history.get(sessionId) ?? [])];
|
|
29
|
+
}
|
|
30
|
+
subscribe(sessionId, handler) {
|
|
31
|
+
this.emitter.on(sessionId, handler);
|
|
32
|
+
return () => this.emitter.off(sessionId, handler);
|
|
33
|
+
}
|
|
34
|
+
}
|