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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns what a task did into one document the operator can keep.
|
|
3
|
+
*
|
|
4
|
+
* Everything here already exists on disk: the opening message is on the task, every reply is
|
|
5
|
+
* under `replies/`, every report that was sent back is under `reports/`. What was missing was
|
|
6
|
+
* a way to hand someone the record without explaining the folder layout first.
|
|
7
|
+
*
|
|
8
|
+
* Two shapes, because two questions get asked about a finished task:
|
|
9
|
+
*
|
|
10
|
+
* `full` the whole conversation, in order, both sides. For reading what actually
|
|
11
|
+
* happened, step by step, or for attaching to a bug report.
|
|
12
|
+
* `outcome` only the first message and the last one. The first says what the task was
|
|
13
|
+
* asked to do, the last says what was done: expected and actual, side by side,
|
|
14
|
+
* with nothing in between to wade through.
|
|
15
|
+
*
|
|
16
|
+
* A task whose run folder is gone still exports: the opening message, the summary and the
|
|
17
|
+
* final reply are kept on the session itself, so the record survives a cleaned `runs/`.
|
|
18
|
+
*/
|
|
19
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { describeDeviations, describeDisputes } from '../protocol/replySchema.js';
|
|
22
|
+
import { SHELL_ORDER, availableShells } from '../exec/shells.js';
|
|
23
|
+
const RULE = '='.repeat(78);
|
|
24
|
+
const THIN = '-'.repeat(78);
|
|
25
|
+
function heading(text) {
|
|
26
|
+
return `\n${RULE}\n${text}\n${RULE}\n`;
|
|
27
|
+
}
|
|
28
|
+
function when(iso) {
|
|
29
|
+
return iso ? new Date(iso).toISOString().replace('T', ' ').slice(0, 19) : '—';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The conversation of one task, reconstructed from the run folder.
|
|
33
|
+
*
|
|
34
|
+
* Replies are numbered by the transport as `NN-<label>.md`, so their names sort into the
|
|
35
|
+
* order they arrived. A reply labelled `iteration-K` was the answer to report K, which is why
|
|
36
|
+
* the report is emitted just before it: that pairing is what makes the document read like a
|
|
37
|
+
* conversation rather than two separate piles of text.
|
|
38
|
+
*/
|
|
39
|
+
async function conversationOf(task, runsDir) {
|
|
40
|
+
const out = [];
|
|
41
|
+
if (!task.runId)
|
|
42
|
+
return out;
|
|
43
|
+
const base = join(runsDir, task.runId);
|
|
44
|
+
const replies = (await readdir(join(base, 'replies')).catch(() => []))
|
|
45
|
+
.filter((n) => n.endsWith('.md'))
|
|
46
|
+
.sort();
|
|
47
|
+
let openingWritten = false;
|
|
48
|
+
for (const name of replies) {
|
|
49
|
+
const label = name.replace(/^\d+-/, '').replace(/\.md$/, '');
|
|
50
|
+
if (label.startsWith('opening')) {
|
|
51
|
+
if (!openingWritten && task.firstMessage) {
|
|
52
|
+
out.push(`${heading('SENT — the message that opened the task')}${task.firstMessage.trimEnd()}`);
|
|
53
|
+
openingWritten = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (label.startsWith('iteration')) {
|
|
57
|
+
const n = label.split('-')[1];
|
|
58
|
+
const report = await readFile(join(base, 'reports', `iteration-${n}.txt`), 'utf8').catch(() => null);
|
|
59
|
+
out.push(`${heading(`SENT — the report after iteration ${n}`)}${report ? report.trimEnd() : '(the report file is no longer in the run folder)'}`);
|
|
60
|
+
}
|
|
61
|
+
else if (label.startsWith('format-retry')) {
|
|
62
|
+
out.push(heading('SENT — a request to keep the agreed format').trimEnd());
|
|
63
|
+
}
|
|
64
|
+
const body = await readFile(join(base, 'replies', name), 'utf8').catch(() => '');
|
|
65
|
+
out.push(`${heading(`RECEIVED — ${label}`)}${body.trimEnd()}`);
|
|
66
|
+
}
|
|
67
|
+
if (!openingWritten && task.firstMessage) {
|
|
68
|
+
out.unshift(`${heading('SENT — the message that opened the task')}${task.firstMessage.trimEnd()}`);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What version control did for one task, as lines in the record.
|
|
74
|
+
*
|
|
75
|
+
* This belongs in the document rather than only on screen. The question someone asks weeks
|
|
76
|
+
* later is "where did this change go", and the answer — the branch, the commit it started
|
|
77
|
+
* from, the commits it produced — is not reconstructable from a summary. `commits` is the
|
|
78
|
+
* list the runner read back from the repository after committing, so the count is what is
|
|
79
|
+
* actually on that branch since it was cut, not what anybody assumed.
|
|
80
|
+
*/
|
|
81
|
+
function versionControlLines(task, session) {
|
|
82
|
+
const vcs = task.vcs;
|
|
83
|
+
if (!session.vcs?.enabled)
|
|
84
|
+
return ['version control : off for this session'];
|
|
85
|
+
if (!vcs || (!vcs.branch && !vcs.problem))
|
|
86
|
+
return ['version control : nothing was recorded for this task'];
|
|
87
|
+
if (!vcs.branch)
|
|
88
|
+
return [`version control : did not run — ${vcs.problem}`];
|
|
89
|
+
const commits = vcs.commits ?? [];
|
|
90
|
+
const lines = [
|
|
91
|
+
`branch : ${vcs.branch}`,
|
|
92
|
+
`started at : ${vcs.baseCommit ? vcs.baseCommit.slice(0, 8) : '—'} (the commit this task branched from)`,
|
|
93
|
+
`commit : ${vcs.commit ? vcs.commit.slice(0, 8) : 'none — the task changed no files'}`,
|
|
94
|
+
`commits : ${commits.length} on this branch since it was cut`,
|
|
95
|
+
];
|
|
96
|
+
for (const line of commits)
|
|
97
|
+
lines.push(` ${line}`);
|
|
98
|
+
const files = vcs.files ?? [];
|
|
99
|
+
if (files.length > 0) {
|
|
100
|
+
lines.push(`files : ${files.length} changed by this task's commit`);
|
|
101
|
+
for (const f of files) {
|
|
102
|
+
const counts = f.added < 0 || f.removed < 0 ? 'binary' : `+${f.added} -${f.removed}`;
|
|
103
|
+
lines.push(` ${counts.padEnd(12)} ${f.path}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (vcs.commit) {
|
|
107
|
+
lines.push('files : none recorded for this commit');
|
|
108
|
+
}
|
|
109
|
+
lines.push('pushed : no — this runner never pushes');
|
|
110
|
+
if (vcs.problem)
|
|
111
|
+
lines.push(`note : ${vcs.problem}`);
|
|
112
|
+
const earlier = (task.attempts ?? []).map((a) => a.vcs?.branch).filter(Boolean);
|
|
113
|
+
if (earlier.length > 0) {
|
|
114
|
+
lines.push(`earlier : ${earlier.join(', ')} (branches of the previous attempts, untouched)`);
|
|
115
|
+
}
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Which shells the machine had when the task ran, as lines in the record.
|
|
120
|
+
*
|
|
121
|
+
* Worth the four lines because of the failure it explains. A task whose commands were written
|
|
122
|
+
* for PowerShell 7 on a machine that has not got it fails in a way that reads like bad work —
|
|
123
|
+
* every command dead, nothing accomplished — and the only thing that says otherwise is what was
|
|
124
|
+
* installed at the time. Read weeks later, on a different machine, that is not recoverable from
|
|
125
|
+
* anywhere else.
|
|
126
|
+
*/
|
|
127
|
+
function shellLines(task) {
|
|
128
|
+
const shells = task.environment?.shells;
|
|
129
|
+
if (!shells)
|
|
130
|
+
return [];
|
|
131
|
+
const lines = SHELL_ORDER.map((shell) => `${shell.padEnd(11)}: ${shells.found[shell] ?? '(not installed)'}`);
|
|
132
|
+
const fallback = task.environment?.defaultShell ?? availableShells(shells)[0];
|
|
133
|
+
lines.push(`default : ${fallback ?? '(none — this machine could run nothing)'} for a step or check that named no shell`);
|
|
134
|
+
return lines;
|
|
135
|
+
}
|
|
136
|
+
/** One task's section of the document. */
|
|
137
|
+
async function sectionFor(task, index, input) {
|
|
138
|
+
const parts = [];
|
|
139
|
+
parts.push(heading(`TASK ${index}: ${task.title}`).trimStart());
|
|
140
|
+
parts.push([
|
|
141
|
+
`status : ${task.status}`,
|
|
142
|
+
`attempt : ${task.attempt ?? 1}${(task.attempts?.length ?? 0) > 0 ? ` (earlier attempts: ${task.attempts?.length})` : ''}`,
|
|
143
|
+
`started : ${when(task.startedAt)}`,
|
|
144
|
+
`finished : ${when(task.finishedAt)}`,
|
|
145
|
+
`iterations : ${task.iterations}`,
|
|
146
|
+
`run folder : ${task.runId ?? '(never started)'}`,
|
|
147
|
+
].join('\n'));
|
|
148
|
+
parts.push(`\n${THIN}\nVERSION CONTROL\n${THIN}`);
|
|
149
|
+
parts.push(versionControlLines(task, input.session).join('\n'));
|
|
150
|
+
const shells = shellLines(task);
|
|
151
|
+
if (shells.length > 0) {
|
|
152
|
+
parts.push(`\n${THIN}\nSHELLS ON THE MACHINE\n${THIN}`);
|
|
153
|
+
parts.push(shells.join('\n'));
|
|
154
|
+
}
|
|
155
|
+
/*
|
|
156
|
+
* What the second opinion concluded, kept in the record because it is the part a reader
|
|
157
|
+
* cannot reconstruct. A summary says what the implementer believes about its own work; this
|
|
158
|
+
* says what somebody who had no part in it found when they ran the thing.
|
|
159
|
+
*/
|
|
160
|
+
if (task.review && task.review.verdict !== 'skipped') {
|
|
161
|
+
const r = task.review;
|
|
162
|
+
parts.push(`\n${THIN}\nINDEPENDENT REVIEW\n${THIN}`);
|
|
163
|
+
parts.push([
|
|
164
|
+
`verdict : ${r.verdict}`,
|
|
165
|
+
`rounds : ${r.rounds}`,
|
|
166
|
+
`commands : ${r.stepsRun} run by the reviewer`,
|
|
167
|
+
r.model ? `model : ${r.model}` : '',
|
|
168
|
+
r.problem ? `problem : ${r.problem}` : '',
|
|
169
|
+
r.summary ? `\n${r.summary}` : '',
|
|
170
|
+
(r.findings ?? []).length > 0
|
|
171
|
+
? '\n' +
|
|
172
|
+
(r.findings ?? [])
|
|
173
|
+
.map((f, i) => `${i + 1}. ${f.what}${f.where ? ` (${f.where})` : ''}\n Evidence: ${f.evidence}`)
|
|
174
|
+
.join('\n\n')
|
|
175
|
+
: '',
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join('\n'));
|
|
179
|
+
}
|
|
180
|
+
/*
|
|
181
|
+
* The expectation is the task as it was written, not the whole first message.
|
|
182
|
+
*
|
|
183
|
+
* The first message of a session carries the level-1 contract in front of the task, which
|
|
184
|
+
* runs to several pages. Putting that under "EXPECTED" buries the one paragraph the reader
|
|
185
|
+
* is comparing against the outcome. The exact first message is not lost: the full variant
|
|
186
|
+
* prints it verbatim as the first thing in the conversation.
|
|
187
|
+
*/
|
|
188
|
+
parts.push(`\n${THIN}\nEXPECTED — the task as it was written\n${THIN}`);
|
|
189
|
+
parts.push(task.prompt.trimEnd() || '(the task had no text)');
|
|
190
|
+
if (task.level2.trim()) {
|
|
191
|
+
parts.push(`\n${THIN}\nEXPECTED — the project instructions sent with it (level 2)\n${THIN}`);
|
|
192
|
+
parts.push(task.level2.trimEnd());
|
|
193
|
+
}
|
|
194
|
+
if (task.firstMessage && task.firstMessage.includes('Level 1')) {
|
|
195
|
+
parts.push('\n(The level 1 contract was sent in front of this task. It is in the full variant.)');
|
|
196
|
+
}
|
|
197
|
+
parts.push(`\n${THIN}\nACTUAL — what came back\n${THIN}`);
|
|
198
|
+
if (task.summary)
|
|
199
|
+
parts.push(task.summary.trimEnd());
|
|
200
|
+
else if (task.reason)
|
|
201
|
+
parts.push(`The task did not finish with a summary. Reason: ${task.reason}`);
|
|
202
|
+
else
|
|
203
|
+
parts.push('(no closing summary)');
|
|
204
|
+
// Right under the outcome, because this is the part of it the reader is least likely to
|
|
205
|
+
// expect: the task said one thing, and the model — with a reason — did another.
|
|
206
|
+
if ((task.deviations ?? []).length > 0) {
|
|
207
|
+
parts.push(`\n${THIN}\nNOT AS THE TASK SAID — instructions the model could not follow as written\n${THIN}`);
|
|
208
|
+
parts.push(describeDeviations(task.deviations ?? []));
|
|
209
|
+
}
|
|
210
|
+
if ((task.disputes ?? []).length > 0) {
|
|
211
|
+
parts.push(`\n${THIN}\nDISPUTED — review findings the model said were wrong\n${THIN}`);
|
|
212
|
+
parts.push(describeDisputes(task.disputes ?? []));
|
|
213
|
+
}
|
|
214
|
+
if (task.finalReply && task.finalReply.trim() !== (task.summary ?? '').trim()) {
|
|
215
|
+
parts.push(`\n${THIN}\nACTUAL — the last message in full\n${THIN}`);
|
|
216
|
+
parts.push(task.finalReply.trimEnd());
|
|
217
|
+
}
|
|
218
|
+
if (input.variant === 'full') {
|
|
219
|
+
const conversation = await conversationOf(task, input.runsDir);
|
|
220
|
+
if (conversation.length > 0) {
|
|
221
|
+
parts.push(`\n${THIN}\nTHE WHOLE CONVERSATION, IN ORDER\n${THIN}`);
|
|
222
|
+
parts.push(conversation.join('\n'));
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
parts.push(`\n${THIN}\nTHE WHOLE CONVERSATION, IN ORDER\n${THIN}`);
|
|
226
|
+
parts.push('(the run folder is not on this machine any more, so only the record above is left)');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return `${parts.join('\n')}\n`;
|
|
230
|
+
}
|
|
231
|
+
/** The whole document, plus a file name that says what it is without being opened. */
|
|
232
|
+
export async function buildExport(input) {
|
|
233
|
+
const { session, tasks, variant } = input;
|
|
234
|
+
const header = [
|
|
235
|
+
`copilot-operator — ${variant === 'full' ? 'the whole conversation' : 'expected and actual'}`,
|
|
236
|
+
'',
|
|
237
|
+
`session : ${session.name} (${session.id})`,
|
|
238
|
+
`chat : ${session.chat?.url ?? '(no conversation was opened)'}`,
|
|
239
|
+
`model : ${session.model || 'whatever the chat was set to'}${session.modelInUse ? ` (ran on ${session.modelInUse})` : ''}`,
|
|
240
|
+
`repository : ${session.vcs?.enabled
|
|
241
|
+
? `${session.vcs.repoDir || session.mirror.rootDir || '(none set)'} — ${session.vcs.branchMode === 'per-session' ? 'one branch for the whole session' : 'a branch of its own for every task'}, prefix ${session.vcs.branchPrefix}${session.vcs.commitOnFinish ? '' : ', commits off'}`
|
|
242
|
+
: 'version control is off for this session'}`,
|
|
243
|
+
`tasks : ${tasks.length} of ${session.tasks.length}`,
|
|
244
|
+
`exported : ${when(new Date().toISOString())}`,
|
|
245
|
+
'',
|
|
246
|
+
variant === 'full'
|
|
247
|
+
? 'Every message of every selected task, in the order it was sent or received.'
|
|
248
|
+
: 'For each selected task: the message that opened it, and what came back at the end.',
|
|
249
|
+
].join('\n');
|
|
250
|
+
const sections = [];
|
|
251
|
+
for (const [i, task] of tasks.entries()) {
|
|
252
|
+
sections.push(await sectionFor(task, i + 1, input));
|
|
253
|
+
}
|
|
254
|
+
const safeName = session.name.replace(/[^\p{L}\p{N}._ -]/gu, '').trim().replace(/\s+/g, '-') || 'session';
|
|
255
|
+
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '').replace(/-/g, '');
|
|
256
|
+
const kind = variant === 'full' ? 'full' : 'expected-actual';
|
|
257
|
+
return {
|
|
258
|
+
fileName: `${safeName}-${kind}-${stamp}.txt`,
|
|
259
|
+
content: `${header}\n${sections.join('\n')}`,
|
|
260
|
+
};
|
|
261
|
+
}
|