copilot-operator 0.1.3 → 0.1.4
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 +55 -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 +32 -7
- package/dist/src/plan/schema.js +7 -0
- package/dist/src/plan/systemGuide.js +8 -0
- package/dist/src/protocol/reporter.js +8 -6
- package/dist/src/session/exports.js +128 -1
- package/dist/src/session/store.js +2 -0
- package/dist/src/vcs/taskVcs.js +33 -0
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/867-f0e3e152f6f67e45.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-2232949a2ff8559e.js → page-8d4f817e36986e3f.js} +1 -1
- package/dist/web/_next/static/chunks/app/sessions/view/{page-9dda275f4b1b0423.js → page-185f5fcc383e6619.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 +1 -1
- 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/{QwjyyBjkO5xlKFU5O6Wux → p9lexLS0u8W5N6VzLavHO}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{QwjyyBjkO5xlKFU5O6Wux → p9lexLS0u8W5N6VzLavHO}/_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();
|
|
@@ -498,12 +526,7 @@ let OperatorService = class OperatorService {
|
|
|
498
526
|
? buildPlanExport(scope)
|
|
499
527
|
: kind === 'domain'
|
|
500
528
|
? 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
|
-
});
|
|
529
|
+
: await buildBotExport(scope, cfg.resolved.runsDir, exportMachine(cfg));
|
|
507
530
|
return { fileName: exportFileName(kind, scope.label), content: JSON.stringify(document, null, 2) };
|
|
508
531
|
}
|
|
509
532
|
/**
|
|
@@ -525,29 +548,39 @@ let OperatorService = class OperatorService {
|
|
|
525
548
|
set.add(taskId);
|
|
526
549
|
wanted.set(sessionId, set);
|
|
527
550
|
}
|
|
551
|
+
/*
|
|
552
|
+
* A pair may name one attempt of its task — the button beside an earlier attempt does. That
|
|
553
|
+
* task is then exported as it stood for that attempt (`taskAtAttempt`): its text, its outcome,
|
|
554
|
+
* its run folder, and only the attempts before it.
|
|
555
|
+
*/
|
|
528
556
|
const sessions = [];
|
|
529
557
|
for (const sessionId of wanted.keys()) {
|
|
530
|
-
|
|
531
|
-
if (session)
|
|
532
|
-
|
|
558
|
+
let session = await this.store.getSession(sessionId);
|
|
559
|
+
if (!session)
|
|
560
|
+
continue;
|
|
561
|
+
for (const pair of pairs.filter((x) => x.sessionId === sessionId && x.attempt !== undefined)) {
|
|
562
|
+
const task = session.tasks.find((t) => t.id === pair.taskId);
|
|
563
|
+
const asItWas = task ? taskAtAttempt(task, pair.attempt) : null;
|
|
564
|
+
if (task && !asItWas)
|
|
565
|
+
throw new Error(`"${task.title}" has no attempt ${pair.attempt}.`);
|
|
566
|
+
if (asItWas)
|
|
567
|
+
session = withTask(session, asItWas);
|
|
568
|
+
}
|
|
569
|
+
sessions.push(session);
|
|
533
570
|
}
|
|
534
571
|
const found = sessions.flatMap((s) => s.tasks.filter((t) => wanted.get(s.id)?.has(t.id)));
|
|
535
572
|
if (found.length === 0)
|
|
536
573
|
throw new Error('None of the chosen tasks still exists.');
|
|
537
574
|
// Named after what was chosen rather than after a run, because a selection is not a run: it
|
|
538
575
|
// may be one task of one, or a task from each of three.
|
|
576
|
+
const one = pairs.length === 1 ? pairs[0] : undefined;
|
|
539
577
|
const label = found.length === 1 && sessions.length === 1
|
|
540
|
-
? `${sessions[0].name}-${found[0].title}`
|
|
578
|
+
? `${sessions[0].name}-${found[0].title}${one?.attempt !== undefined ? `-attempt-${one.attempt}` : ''}`
|
|
541
579
|
: sessions.length === 1
|
|
542
580
|
? `${sessions[0].name}-${found.length}-tasks`
|
|
543
581
|
: `${found.length}-tasks-${sessions.length}-sessions`;
|
|
544
582
|
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
|
-
});
|
|
583
|
+
const document = await buildBundleExport({ sessions, taskFilter: (s, t) => !!wanted.get(s.id)?.has(t.id), label }, cfg.resolved.runsDir, exportMachine(cfg));
|
|
551
584
|
return { fileName: exportFileName('bundle', label), content: JSON.stringify(document, null, 2) };
|
|
552
585
|
}
|
|
553
586
|
// --- running --------------------------------------------------------------------------
|
|
@@ -718,7 +751,7 @@ let OperatorService = class OperatorService {
|
|
|
718
751
|
r.controller.abort();
|
|
719
752
|
for (const [id, w] of this.waiting) {
|
|
720
753
|
if (w.approval.sessionId === sessionId) {
|
|
721
|
-
w.resolve({ action: 'abort', reason: 'stopped by the operator' });
|
|
754
|
+
w.resolve({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
722
755
|
this.waiting.delete(id);
|
|
723
756
|
}
|
|
724
757
|
}
|
|
@@ -1275,8 +1308,8 @@ let OperatorService = class OperatorService {
|
|
|
1275
1308
|
const decision = action === 'run' || action === 'run-all'
|
|
1276
1309
|
? { action: 'run' }
|
|
1277
1310
|
: action === 'skip'
|
|
1278
|
-
? { action: 'skip', reason: 'skipped by the operator' }
|
|
1279
|
-
: { action: 'abort', reason: 'aborted by the operator' };
|
|
1311
|
+
? { action: 'skip', reason: 'skipped by the operator', by: 'operator' }
|
|
1312
|
+
: { action: 'abort', reason: 'aborted by the operator', by: 'operator' };
|
|
1280
1313
|
w.resolve(decision);
|
|
1281
1314
|
this.bus.publish({ sessionId: w.approval.sessionId, taskId: w.approval.taskId, type: 'approval-decided', level: 'info',
|
|
1282
1315
|
message: `step ${w.approval.stepId}: ${action}`, data: { approvalId, action } });
|
|
@@ -1359,7 +1392,7 @@ let OperatorService = class OperatorService {
|
|
|
1359
1392
|
...(held ? { network: held.network } : {}),
|
|
1360
1393
|
};
|
|
1361
1394
|
if (signal.aborted) {
|
|
1362
|
-
resolvePromise({ action: 'abort', reason: 'stopped by the operator' });
|
|
1395
|
+
resolvePromise({ action: 'abort', reason: 'stopped by the operator', by: 'operator' });
|
|
1363
1396
|
return;
|
|
1364
1397
|
}
|
|
1365
1398
|
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 {
|