copilot-operator 0.1.3 → 0.1.5
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/README.md +8 -2
- package/dist/src/api/operator.controller.js +6 -1
- package/dist/src/api/operator.service.js +69 -22
- package/dist/src/exec/authorizer.js +2 -2
- package/dist/src/exec/commandHeads.js +799 -0
- package/dist/src/exec/policy.js +1 -1
- package/dist/src/exec/programs.js +22 -82
- package/dist/src/exec/reportFile.js +5 -2
- package/dist/src/orchestrator/review.js +4 -3
- package/dist/src/orchestrator/taskRunner.js +33 -8
- package/dist/src/plan/brief.js +1248 -1221
- package/dist/src/plan/schema.js +19 -0
- package/dist/src/plan/systemGuide.js +40 -0
- package/dist/src/protocol/reporter.js +8 -6
- package/dist/src/session/exports.js +131 -1
- package/dist/src/session/store.js +2 -0
- package/dist/src/vcs/taskVcs.js +153 -11
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/867-0cd2c3cfd0ff0a45.js +1 -0
- package/dist/web/_next/static/chunks/89-382e9b29486d2747.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-1747c931adc746fd.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-83543937cecde5de.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-4631bdf5113a73b2.js +1 -0
- 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 +4 -3
- package/dist/web/_next/static/chunks/867-55627c5122ff6336.js +0 -1
- package/dist/web/_next/static/chunks/89-5585315f51e4c481.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-4ff25cafe6de779c.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-2232949a2ff8559e.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-9dda275f4b1b0423.js +0 -1
- /package/dist/web/_next/static/{QwjyyBjkO5xlKFU5O6Wux → 7TE2rEuHqGdGx14If8TEb}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{QwjyyBjkO5xlKFU5O6Wux → 7TE2rEuHqGdGx14If8TEb}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -132,8 +132,14 @@ Six rules decide what a step may do, and each is recorded with the run that it g
|
|
|
132
132
|
- **Only the declared toolchain runs.** `execution.allowedPrograms` names the programs a command
|
|
133
133
|
may start — the shells, the JavaScript, .NET, Java, Python, Go and Rust tools, `git`, and a few
|
|
134
134
|
Windows utilities. A command that starts anything else is refused and sent back with the reason.
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
The programs are read the way PowerShell reads a line (`src/exec/commandHeads.ts`): strings,
|
|
136
|
+
operators, array values, method arguments and hashtable keys are not programs, and a command in a
|
|
137
|
+
script block, a subexpression, a string's `$( )` or after an assignment is still found. A line that
|
|
138
|
+
cannot be read with certainty — an unclosed quote or bracket, a program called through a variable
|
|
139
|
+
(`& $exe`) — is refused rather than guessed at. `cmd` steps are read by `cmd`'s own rules. A refused
|
|
140
|
+
step is reported to the chat as refused and never executed, with no exit code, apart from a
|
|
141
|
+
command that ran and failed. An empty list turns the gate off. This is a floor against the unknown
|
|
142
|
+
binary, not a boundary: allowing `node` allows `node -e`.
|
|
137
143
|
- **Autonomy is graded.** An unattended run carries strictly more restrictions than a watched one,
|
|
138
144
|
because the thing that makes a watched run safe is a person reading each line. Unattended
|
|
139
145
|
requires a non-empty allowlist, and refuses an allowed program used to evaluate a string
|
|
@@ -251,7 +251,12 @@ let OperatorController = class OperatorController {
|
|
|
251
251
|
*/
|
|
252
252
|
async exportBundle(res, body) {
|
|
253
253
|
const pairs = (body?.tasks ?? [])
|
|
254
|
-
.map((x) => ({
|
|
254
|
+
.map((x) => ({
|
|
255
|
+
sessionId: (x?.sessionId ?? '').trim(),
|
|
256
|
+
taskId: (x?.taskId ?? '').trim(),
|
|
257
|
+
// One attempt of the task rather than the task as it is now; see `exportBundle`.
|
|
258
|
+
...(Number.isInteger(x?.attempt) ? { attempt: x.attempt } : {}),
|
|
259
|
+
}))
|
|
255
260
|
.filter((x) => x.sessionId && x.taskId);
|
|
256
261
|
try {
|
|
257
262
|
const { fileName, content } = await this.ops.exportBundle(pairs);
|
|
@@ -23,7 +23,7 @@ import { newId, tidyVcsPlan } from '../session/model.js';
|
|
|
23
23
|
import { runSession, openBrowser } from '../orchestrator/taskRunner.js';
|
|
24
24
|
import { buildExport } from '../session/exportRecord.js';
|
|
25
25
|
import { buildDebugExport } from '../session/debugExport.js';
|
|
26
|
-
import { buildPlanExport, buildDomainExport, buildBotExport, buildBundleExport, exportFileName } from '../session/exports.js';
|
|
26
|
+
import { buildPlanExport, buildDomainExport, buildBotExport, buildBundleExport, exportFileName, exportMachine, taskAtAttempt, withTask, writeAttemptRecord } from '../session/exports.js';
|
|
27
27
|
import { suggestRunName } from '../session/runName.js';
|
|
28
28
|
import { mirrorAllProjects, removeProjectMirrors, contextRoot } from '../context/desktopMirror.js';
|
|
29
29
|
import { describeMirror } from '../context/projectMirror.js';
|
|
@@ -36,7 +36,7 @@ function sameFolder(a, b) {
|
|
|
36
36
|
return a.trim() !== '' && norm(a) === norm(b);
|
|
37
37
|
}
|
|
38
38
|
import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
|
|
39
|
-
import { vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
|
|
39
|
+
import { commitInterrupted, vcsPreflight, restorePreview, restoreToBase, sessionBranches } from '../vcs/taskVcs.js';
|
|
40
40
|
import { gitAvailable, repoUnusableReason } from '../vcs/git.js';
|
|
41
41
|
import { makeAuthorizer } from '../exec/authorizer.js';
|
|
42
42
|
import { listSelectableDirs, collectFiles, findSelectionConflicts, describeConflicts, DEFAULT_IGNORE_DIRS } from '../context/projectMirror.js';
|
|
@@ -81,6 +81,9 @@ let OperatorService = class OperatorService {
|
|
|
81
81
|
level: 'warn',
|
|
82
82
|
message: `"${r.title}" was left unfinished by an earlier run and has been marked aborted`,
|
|
83
83
|
});
|
|
84
|
+
// What it had changed goes onto its branch, and its record into its run folder, as a
|
|
85
|
+
// finished task's would. See `commitInterrupted` and `writeAttemptRecord`.
|
|
86
|
+
await this.settleRecovered(r.sessionId, r.taskId).catch((e) => console.warn(`[api] could not settle "${r.title}": ${e.message}`));
|
|
84
87
|
}
|
|
85
88
|
if (recovered.length > 0) {
|
|
86
89
|
console.log(`[api] closed ${recovered.length} task(s) left unfinished by an earlier run`);
|
|
@@ -88,6 +91,31 @@ let OperatorService = class OperatorService {
|
|
|
88
91
|
})();
|
|
89
92
|
return this.ready;
|
|
90
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Closes the loose ends of one task the previous process left unfinished: commits its work on its
|
|
96
|
+
* branch, says so on the task, and writes the attempt's plan, work and runner into its run folder.
|
|
97
|
+
*/
|
|
98
|
+
async settleRecovered(sessionId, taskId) {
|
|
99
|
+
const session = await this.store.getSession(sessionId);
|
|
100
|
+
const task = session?.tasks.find((t) => t.id === taskId);
|
|
101
|
+
if (!session || !task)
|
|
102
|
+
return;
|
|
103
|
+
const vcs = await commitInterrupted(session, task, task.reason ?? 'interrupted', this.bus);
|
|
104
|
+
const settled = vcs?.commit
|
|
105
|
+
? await this.store.updateSession(sessionId, (s) => {
|
|
106
|
+
const t = s.tasks.find((x) => x.id === taskId);
|
|
107
|
+
if (!t)
|
|
108
|
+
return;
|
|
109
|
+
t.vcs = vcs;
|
|
110
|
+
t.reason = `${t.reason ?? ''} What it had changed is committed on ${vcs.branch} as ${vcs.commit?.slice(0, 8)}.`.trim();
|
|
111
|
+
})
|
|
112
|
+
: session;
|
|
113
|
+
const ended = settled.tasks.find((t) => t.id === taskId);
|
|
114
|
+
if (!ended)
|
|
115
|
+
return;
|
|
116
|
+
const cfg = await this.settings.load();
|
|
117
|
+
await writeAttemptRecord(settled, ended, cfg.resolved.runsDir, exportMachine(cfg));
|
|
118
|
+
}
|
|
91
119
|
/** Runs the startup work now, so it does not wait for the first request. */
|
|
92
120
|
bootstrap() {
|
|
93
121
|
return this.init();
|
|
@@ -221,6 +249,20 @@ let OperatorService = class OperatorService {
|
|
|
221
249
|
if (reason)
|
|
222
250
|
throw new Error(reason);
|
|
223
251
|
}
|
|
252
|
+
if (merged.baseBranch !== undefined)
|
|
253
|
+
merged.baseBranch = merged.baseBranch.trim();
|
|
254
|
+
/*
|
|
255
|
+
* Where the session starts is fixed at its first run. A new choice clears that record, so
|
|
256
|
+
* the next task to run is cut from what was chosen now; the tasks that already ran keep
|
|
257
|
+
* their own base commits, which is what a re-run of them goes back to.
|
|
258
|
+
*/
|
|
259
|
+
const startChanged = (merged.startFrom ?? 'head') !== (s.vcs?.startFrom ?? 'head') ||
|
|
260
|
+
(merged.baseBranch || 'main') !== (s.vcs?.baseBranch || 'main') ||
|
|
261
|
+
merged.repoDir.toLowerCase() !== (s.vcs?.repoDir ?? '').trim().toLowerCase();
|
|
262
|
+
if (startChanged) {
|
|
263
|
+
s.vcsBaseCommit = undefined;
|
|
264
|
+
s.vcsStart = undefined;
|
|
265
|
+
}
|
|
224
266
|
s.vcs = merged;
|
|
225
267
|
}
|
|
226
268
|
});
|
|
@@ -498,12 +540,7 @@ let OperatorService = class OperatorService {
|
|
|
498
540
|
? buildPlanExport(scope)
|
|
499
541
|
: kind === 'domain'
|
|
500
542
|
? await buildDomainExport(scope, cfg.resolved.runsDir)
|
|
501
|
-
: await buildBotExport(scope, cfg.resolved.runsDir,
|
|
502
|
-
node: process.versions.node,
|
|
503
|
-
platform: process.platform,
|
|
504
|
-
cwd: cfg.resolved.cwd,
|
|
505
|
-
limits: { ...cfg.limits, execution: { commandTimeoutSec: cfg.execution.commandTimeoutSec, idleTimeoutSec: cfg.execution.idleTimeoutSec, mode: cfg.execution.mode } },
|
|
506
|
-
});
|
|
543
|
+
: await buildBotExport(scope, cfg.resolved.runsDir, exportMachine(cfg));
|
|
507
544
|
return { fileName: exportFileName(kind, scope.label), content: JSON.stringify(document, null, 2) };
|
|
508
545
|
}
|
|
509
546
|
/**
|
|
@@ -525,29 +562,39 @@ let OperatorService = class OperatorService {
|
|
|
525
562
|
set.add(taskId);
|
|
526
563
|
wanted.set(sessionId, set);
|
|
527
564
|
}
|
|
565
|
+
/*
|
|
566
|
+
* A pair may name one attempt of its task — the button beside an earlier attempt does. That
|
|
567
|
+
* task is then exported as it stood for that attempt (`taskAtAttempt`): its text, its outcome,
|
|
568
|
+
* its run folder, and only the attempts before it.
|
|
569
|
+
*/
|
|
528
570
|
const sessions = [];
|
|
529
571
|
for (const sessionId of wanted.keys()) {
|
|
530
|
-
|
|
531
|
-
if (session)
|
|
532
|
-
|
|
572
|
+
let session = await this.store.getSession(sessionId);
|
|
573
|
+
if (!session)
|
|
574
|
+
continue;
|
|
575
|
+
for (const pair of pairs.filter((x) => x.sessionId === sessionId && x.attempt !== undefined)) {
|
|
576
|
+
const task = session.tasks.find((t) => t.id === pair.taskId);
|
|
577
|
+
const asItWas = task ? taskAtAttempt(task, pair.attempt) : null;
|
|
578
|
+
if (task && !asItWas)
|
|
579
|
+
throw new Error(`"${task.title}" has no attempt ${pair.attempt}.`);
|
|
580
|
+
if (asItWas)
|
|
581
|
+
session = withTask(session, asItWas);
|
|
582
|
+
}
|
|
583
|
+
sessions.push(session);
|
|
533
584
|
}
|
|
534
585
|
const found = sessions.flatMap((s) => s.tasks.filter((t) => wanted.get(s.id)?.has(t.id)));
|
|
535
586
|
if (found.length === 0)
|
|
536
587
|
throw new Error('None of the chosen tasks still exists.');
|
|
537
588
|
// Named after what was chosen rather than after a run, because a selection is not a run: it
|
|
538
589
|
// may be one task of one, or a task from each of three.
|
|
590
|
+
const one = pairs.length === 1 ? pairs[0] : undefined;
|
|
539
591
|
const label = found.length === 1 && sessions.length === 1
|
|
540
|
-
? `${sessions[0].name}-${found[0].title}`
|
|
592
|
+
? `${sessions[0].name}-${found[0].title}${one?.attempt !== undefined ? `-attempt-${one.attempt}` : ''}`
|
|
541
593
|
: sessions.length === 1
|
|
542
594
|
? `${sessions[0].name}-${found.length}-tasks`
|
|
543
595
|
: `${found.length}-tasks-${sessions.length}-sessions`;
|
|
544
596
|
const cfg = await this.settings.load();
|
|
545
|
-
const document = await buildBundleExport({ sessions, taskFilter: (s, t) => !!wanted.get(s.id)?.has(t.id), label }, cfg.resolved.runsDir,
|
|
546
|
-
node: process.versions.node,
|
|
547
|
-
platform: process.platform,
|
|
548
|
-
cwd: cfg.resolved.cwd,
|
|
549
|
-
limits: { ...cfg.limits, execution: { commandTimeoutSec: cfg.execution.commandTimeoutSec, idleTimeoutSec: cfg.execution.idleTimeoutSec, mode: cfg.execution.mode } },
|
|
550
|
-
});
|
|
597
|
+
const document = await buildBundleExport({ sessions, taskFilter: (s, t) => !!wanted.get(s.id)?.has(t.id), label }, cfg.resolved.runsDir, exportMachine(cfg));
|
|
551
598
|
return { fileName: exportFileName('bundle', label), content: JSON.stringify(document, null, 2) };
|
|
552
599
|
}
|
|
553
600
|
// --- running --------------------------------------------------------------------------
|
|
@@ -718,7 +765,7 @@ let OperatorService = class OperatorService {
|
|
|
718
765
|
r.controller.abort();
|
|
719
766
|
for (const [id, w] of this.waiting) {
|
|
720
767
|
if (w.approval.sessionId === sessionId) {
|
|
721
|
-
w.resolve({ action: 'abort', reason: 'stopped by the operator' });
|
|
768
|
+
w.resolve({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
722
769
|
this.waiting.delete(id);
|
|
723
770
|
}
|
|
724
771
|
}
|
|
@@ -1275,8 +1322,8 @@ let OperatorService = class OperatorService {
|
|
|
1275
1322
|
const decision = action === 'run' || action === 'run-all'
|
|
1276
1323
|
? { action: 'run' }
|
|
1277
1324
|
: action === 'skip'
|
|
1278
|
-
? { action: 'skip', reason: 'skipped by the operator' }
|
|
1279
|
-
: { action: 'abort', reason: 'aborted by the operator' };
|
|
1325
|
+
? { action: 'skip', reason: 'skipped by the operator', by: 'operator' }
|
|
1326
|
+
: { action: 'abort', reason: 'aborted by the operator', by: 'operator' };
|
|
1280
1327
|
w.resolve(decision);
|
|
1281
1328
|
this.bus.publish({ sessionId: w.approval.sessionId, taskId: w.approval.taskId, type: 'approval-decided', level: 'info',
|
|
1282
1329
|
message: `step ${w.approval.stepId}: ${action}`, data: { approvalId, action } });
|
|
@@ -1359,7 +1406,7 @@ let OperatorService = class OperatorService {
|
|
|
1359
1406
|
...(held ? { network: held.network } : {}),
|
|
1360
1407
|
};
|
|
1361
1408
|
if (signal.aborted) {
|
|
1362
|
-
resolvePromise({ action: 'abort', reason: 'stopped by the operator' });
|
|
1409
|
+
resolvePromise({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
1363
1410
|
return;
|
|
1364
1411
|
}
|
|
1365
1412
|
this.waiting.set(approval.id, { approval, resolve: resolvePromise });
|
|
@@ -47,9 +47,9 @@ export function terminalAuthorizer(cfg, print) {
|
|
|
47
47
|
try {
|
|
48
48
|
const answer = (await rl.question(' [Enter] run [s] skip [q] abort > ')).trim().toLowerCase();
|
|
49
49
|
if (answer === 'q')
|
|
50
|
-
return { action: 'abort', reason: 'aborted by the operator' };
|
|
50
|
+
return { action: 'abort', reason: 'aborted by the operator', by: 'operator' };
|
|
51
51
|
if (answer === 's')
|
|
52
|
-
return { action: 'skip', reason: 'skipped by the operator' };
|
|
52
|
+
return { action: 'skip', reason: 'skipped by the operator', by: 'operator' };
|
|
53
53
|
return { action: 'run' };
|
|
54
54
|
}
|
|
55
55
|
finally {
|