copilot-operator 0.1.20 → 0.1.23
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 +49 -0
- package/dist/src/api/operator.service.js +181 -7
- package/dist/src/orchestrator/taskRunner.js +14 -2
- package/dist/src/plan/systemGuide.js +143 -15
- package/dist/src/session/exports.js +70 -1
- package/dist/src/session/runLog.js +60 -0
- package/dist/src/vcs/git.js +15 -3
- package/dist/src/vcs/inputs.js +26 -2
- package/dist/src/vcs/prepareFromRemote.js +293 -0
- package/dist/src/vcs/remoteAuth.js +61 -0
- package/dist/src/vcs/runPreflight.js +286 -0
- package/dist/src/vcs/taskVcs.js +42 -6
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-df7a4876917615da.js +1 -0
- package/dist/web/_next/static/chunks/867-a67ea8ba3f9b24aa.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-0e778dafc69afbd3.js +1 -0
- package/dist/web/_next/static/chunks/app/page-0a861b38da0a1c51.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-9b66b729748595bb.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 +150 -148
- package/dist/web/_next/static/chunks/549-5d7c845fb0a27dbe.js +0 -1
- package/dist/web/_next/static/chunks/867-d9c54f7a2b285b3f.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-fd819bbfa7f33982.js +0 -1
- package/dist/web/_next/static/chunks/app/page-409e37e208dc12e5.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-99f9153fd75078d0.js +0 -1
- /package/dist/web/_next/static/{P0lpNIJvdSO3yuWRpIPQf → -dZtu-yqxc-HXzYTmqYSc}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{P0lpNIJvdSO3yuWRpIPQf → -dZtu-yqxc-HXzYTmqYSc}/_ssgManifest.js +0 -0
|
@@ -496,6 +496,14 @@ let OperatorController = class OperatorController {
|
|
|
496
496
|
repoSync(dir) {
|
|
497
497
|
return this.ops.syncPlan(dir ?? '').catch(fail);
|
|
498
498
|
}
|
|
499
|
+
/** "Prepare the folder from the remote main branch": what it would do (fetches, changes nothing else). */
|
|
500
|
+
repoPreparePreview(body) {
|
|
501
|
+
return this.ops.preparePreview(body?.dir ?? '', body?.fetch !== false).catch(fail);
|
|
502
|
+
}
|
|
503
|
+
/** …and doing it, if the folder is still what the preview showed. */
|
|
504
|
+
repoPrepare(body) {
|
|
505
|
+
return this.ops.prepareProject(body?.dir ?? '', body?.fingerprint ?? '').catch(fail);
|
|
506
|
+
}
|
|
499
507
|
repo(dir) {
|
|
500
508
|
const problem = this.ops.repoProblem(dir ?? '');
|
|
501
509
|
return problem ? { ok: false, problem } : { ok: true };
|
|
@@ -504,6 +512,18 @@ let OperatorController = class OperatorController {
|
|
|
504
512
|
vcsStatus(id) {
|
|
505
513
|
return this.ops.vcsStatus(id).catch(fail);
|
|
506
514
|
}
|
|
515
|
+
/** "Prepare version control for this run": one group per repository of the sessions given. */
|
|
516
|
+
runVcs(ids, tasks) {
|
|
517
|
+
const only = (tasks ?? '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
518
|
+
return this.ops.runVcs((ids ?? '').split(',').map((s) => s.trim()).filter(Boolean), only.length > 0 ? new Set(only) : undefined).catch(fail);
|
|
519
|
+
}
|
|
520
|
+
/** One action of that panel, for one repository. */
|
|
521
|
+
runVcsPrepare(body) {
|
|
522
|
+
const actions = ['review-inputs', 'snapshot-on-base', 'snapshot-here', 'use-current-branch', 'allow-snapshot'];
|
|
523
|
+
if (!actions.includes(body?.action ?? ''))
|
|
524
|
+
return Promise.resolve({ ok: false, problem: 'unknown action' });
|
|
525
|
+
return this.ops.runVcsPrepare(body?.sessionIds ?? [], body?.repoDir ?? '', body.action, body?.choices ?? {}).catch(fail);
|
|
526
|
+
}
|
|
507
527
|
/** The operator's approval of the starting snapshot: one choice per file of the list they saw. */
|
|
508
528
|
vcsSnapshot(id, body) {
|
|
509
529
|
return this.ops.vcsSnapshot(id, body?.choices ?? {}).catch(fail);
|
|
@@ -1028,6 +1048,20 @@ __decorate([
|
|
|
1028
1048
|
__metadata("design:paramtypes", [String]),
|
|
1029
1049
|
__metadata("design:returntype", Promise)
|
|
1030
1050
|
], OperatorController.prototype, "repoSync", null);
|
|
1051
|
+
__decorate([
|
|
1052
|
+
Post('repo/prepare/preview'),
|
|
1053
|
+
__param(0, Body()),
|
|
1054
|
+
__metadata("design:type", Function),
|
|
1055
|
+
__metadata("design:paramtypes", [Object]),
|
|
1056
|
+
__metadata("design:returntype", Promise)
|
|
1057
|
+
], OperatorController.prototype, "repoPreparePreview", null);
|
|
1058
|
+
__decorate([
|
|
1059
|
+
Post('repo/prepare'),
|
|
1060
|
+
__param(0, Body()),
|
|
1061
|
+
__metadata("design:type", Function),
|
|
1062
|
+
__metadata("design:paramtypes", [Object]),
|
|
1063
|
+
__metadata("design:returntype", Promise)
|
|
1064
|
+
], OperatorController.prototype, "repoPrepare", null);
|
|
1031
1065
|
__decorate([
|
|
1032
1066
|
Get('repo'),
|
|
1033
1067
|
__param(0, Query('dir')),
|
|
@@ -1042,6 +1076,21 @@ __decorate([
|
|
|
1042
1076
|
__metadata("design:paramtypes", [String]),
|
|
1043
1077
|
__metadata("design:returntype", Promise)
|
|
1044
1078
|
], OperatorController.prototype, "vcsStatus", null);
|
|
1079
|
+
__decorate([
|
|
1080
|
+
Get('batch/vcs'),
|
|
1081
|
+
__param(0, Query('ids')),
|
|
1082
|
+
__param(1, Query('tasks')),
|
|
1083
|
+
__metadata("design:type", Function),
|
|
1084
|
+
__metadata("design:paramtypes", [String, String]),
|
|
1085
|
+
__metadata("design:returntype", Promise)
|
|
1086
|
+
], OperatorController.prototype, "runVcs", null);
|
|
1087
|
+
__decorate([
|
|
1088
|
+
Post('batch/vcs/prepare'),
|
|
1089
|
+
__param(0, Body()),
|
|
1090
|
+
__metadata("design:type", Function),
|
|
1091
|
+
__metadata("design:paramtypes", [Object]),
|
|
1092
|
+
__metadata("design:returntype", Promise)
|
|
1093
|
+
], OperatorController.prototype, "runVcsPrepare", null);
|
|
1045
1094
|
__decorate([
|
|
1046
1095
|
Post('sessions/:id/vcs/snapshot'),
|
|
1047
1096
|
__param(0, Param('id')),
|
|
@@ -45,7 +45,11 @@ import { unattendedPrecondition } from '../exec/policy.js';
|
|
|
45
45
|
import { createTransport } from '../transport/chatTransport.js';
|
|
46
46
|
import { composeHandoff } from '../session/handoff.js';
|
|
47
47
|
import { planSync } from '../vcs/syncCommand.js';
|
|
48
|
+
import { planPrepare, prepareFromRemote } from '../vcs/prepareFromRemote.js';
|
|
49
|
+
import { askpassProgram } from '../vcs/remoteAuth.js';
|
|
50
|
+
import { appendRunLog } from '../session/runLog.js';
|
|
48
51
|
import { dirtyPolicy, takeSnapshot } from '../vcs/snapshot.js';
|
|
52
|
+
import { runVcsPreflight, runVcsPrepare } from '../vcs/runPreflight.js';
|
|
49
53
|
import { normalisePatterns } from '../vcs/inputs.js';
|
|
50
54
|
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
51
55
|
import { botVersion } from '../config/version.js';
|
|
@@ -917,6 +921,12 @@ let OperatorService = class OperatorService {
|
|
|
917
921
|
// A run of one is still a run, and it records the same thing a batch does, so that going
|
|
918
922
|
// back and starting again from a task works the same whether one session was started or six.
|
|
919
923
|
// Recorded by `beginRun` once the start is accepted, not here: see `recordOnSession`.
|
|
924
|
+
// Version control first, before anything opens the browser: see `vcs/runPreflight.ts`.
|
|
925
|
+
const notReady = await this.runPreflight(runGroup.id, [sessionId]);
|
|
926
|
+
if (notReady) {
|
|
927
|
+
this.releaseBrowser(claim.holder);
|
|
928
|
+
return { started: false, reason: notReady };
|
|
929
|
+
}
|
|
920
930
|
const begun = await this.beginRun(sessionId, mode, undefined, runGroup, undefined, { recordOnSession: true });
|
|
921
931
|
if (!begun.started) {
|
|
922
932
|
this.releaseBrowser(claim.holder);
|
|
@@ -1026,8 +1036,10 @@ let OperatorService = class OperatorService {
|
|
|
1026
1036
|
transport,
|
|
1027
1037
|
runGroup,
|
|
1028
1038
|
onlyTasks,
|
|
1039
|
+
vcsGate: (s) => this.sessionVcsGate(s, onlyTasks),
|
|
1040
|
+
...(transport || !runGroup ? {} : { beforeBrowser: () => this.browserRequested(runGroup.id, [sessionId]) }),
|
|
1029
1041
|
})
|
|
1030
|
-
.then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused }))
|
|
1042
|
+
.then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused, ...(outcome.refused ? { error: outcome.refused } : {}) }))
|
|
1031
1043
|
.catch(async (e) => {
|
|
1032
1044
|
this.bus.publish({ sessionId, type: 'run-failed', level: 'error', message: e.message });
|
|
1033
1045
|
return { ...(await this.tally(sessionId, queuedIds)), error: e.message };
|
|
@@ -1097,7 +1109,12 @@ let OperatorService = class OperatorService {
|
|
|
1097
1109
|
* directory walk every few seconds, on every page, to light up a dot would be a bad trade.
|
|
1098
1110
|
*/
|
|
1099
1111
|
activity() {
|
|
1100
|
-
|
|
1112
|
+
/*
|
|
1113
|
+
* A run being started — its version control checked before the browser opens — is not running yet
|
|
1114
|
+
* and not idle either: `starting` says so, so "nothing is happening" is not read in that moment.
|
|
1115
|
+
*/
|
|
1116
|
+
const starting = this.running.size === 0 && !this.batch?.running && (this.browser?.kind === 'run' || this.browser?.kind === 'batch');
|
|
1117
|
+
return { running: this.running.size > 0, sessions: this.running.size, batch: this.batch?.running === true, starting };
|
|
1101
1118
|
}
|
|
1102
1119
|
/** The batch in progress, or the last one that ran, or null if none ever has. */
|
|
1103
1120
|
batchState() {
|
|
@@ -1156,7 +1173,13 @@ let OperatorService = class OperatorService {
|
|
|
1156
1173
|
await this.init();
|
|
1157
1174
|
if (wanted.length === 0)
|
|
1158
1175
|
return { started: false, reason: 'no sessions were selected' };
|
|
1159
|
-
|
|
1176
|
+
// Version control first, before anything opens the browser: see `vcs/runPreflight.ts`.
|
|
1177
|
+
// The batch's id first, so the preflight is recorded under the run it decides about.
|
|
1178
|
+
const batchId = newId('b-');
|
|
1179
|
+
const notReady = await this.runPreflight(batchId, wanted, onlyTasks);
|
|
1180
|
+
if (notReady)
|
|
1181
|
+
return { started: false, reason: notReady };
|
|
1182
|
+
const begun = await this.prepareBatch(wanted, onlyTasks, mode, onFailure, model, reviewModel, name, batchId);
|
|
1160
1183
|
if (!begun.batch)
|
|
1161
1184
|
return { started: false, reason: begun.reason };
|
|
1162
1185
|
this.batch = begun.batch;
|
|
@@ -1188,7 +1211,9 @@ let OperatorService = class OperatorService {
|
|
|
1188
1211
|
* an unattended run may begin, and the models the run panel chose. A refusal comes back as the
|
|
1189
1212
|
* reason; nothing is started here.
|
|
1190
1213
|
*/
|
|
1191
|
-
async prepareBatch(wanted, onlyTasks, mode, onFailure, model, reviewModel, name
|
|
1214
|
+
async prepareBatch(wanted, onlyTasks, mode, onFailure, model, reviewModel, name,
|
|
1215
|
+
/** The id its preflight was recorded under; a new one when there was none. */
|
|
1216
|
+
id = newId('b-')) {
|
|
1192
1217
|
// Worked out here rather than asked of the operator again: every way into this — the run
|
|
1193
1218
|
// panel, continuing after a failure, "Run again from here" — either has a field they may
|
|
1194
1219
|
// have left empty or has no field at all.
|
|
@@ -1252,7 +1277,7 @@ let OperatorService = class OperatorService {
|
|
|
1252
1277
|
}
|
|
1253
1278
|
return {
|
|
1254
1279
|
batch: {
|
|
1255
|
-
id
|
|
1280
|
+
id,
|
|
1256
1281
|
startedAt: new Date().toISOString(),
|
|
1257
1282
|
name: chosenName,
|
|
1258
1283
|
...(onlyTasks ? { onlyTasks: [...onlyTasks] } : {}),
|
|
@@ -1355,6 +1380,7 @@ let OperatorService = class OperatorService {
|
|
|
1355
1380
|
let browser = null;
|
|
1356
1381
|
try {
|
|
1357
1382
|
const cfg = await this.settings.load();
|
|
1383
|
+
await this.browserRequested(batch.id, batch.sessions.filter((x) => x.state === 'waiting').map((x) => x.sessionId));
|
|
1358
1384
|
browser = await openBrowser(cfg, this.bus, join(cfg.resolved.runsDir, '_browser'), batch.id);
|
|
1359
1385
|
this.bus.publish({
|
|
1360
1386
|
sessionId: batch.sessions[0]?.sessionId ?? batch.id,
|
|
@@ -1855,6 +1881,124 @@ let OperatorService = class OperatorService {
|
|
|
1855
1881
|
return this.bus.subscribe(sessionId, handler);
|
|
1856
1882
|
}
|
|
1857
1883
|
// --- version control ----------------------------------------------------------------------
|
|
1884
|
+
/**
|
|
1885
|
+
* Version control for the sessions of a run, one group per repository, with what can be done about
|
|
1886
|
+
* it from the run screen. See `vcs/runPreflight.ts`. Only sessions that have something queued count.
|
|
1887
|
+
*/
|
|
1888
|
+
async runVcs(sessionIds, onlyTasks) {
|
|
1889
|
+
await this.init();
|
|
1890
|
+
const sessions = [];
|
|
1891
|
+
for (const id of sessionIds) {
|
|
1892
|
+
const s = await this.store.getSession(id);
|
|
1893
|
+
if (s && queuedToRun(s, onlyTasks).length > 0)
|
|
1894
|
+
sessions.push(s);
|
|
1895
|
+
}
|
|
1896
|
+
return await runVcsPreflight(sessions, () => this.store.listSessions());
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* The run's version control preflight: why the run may not start, said before anything opens the
|
|
1900
|
+
* browser, creates a task attempt or emits `task-started`; null when it may.
|
|
1901
|
+
*
|
|
1902
|
+
* Recorded as it happens, in the run's own log (`session/runLog.ts`) and on each session's event
|
|
1903
|
+
* stream: `run-preflight-started`, one `repository-preflight` per repository, `baseline-created` for
|
|
1904
|
+
* a starting snapshot taken on the run screen, `snapshot-approval-required` where the operator has a
|
|
1905
|
+
* fix to press, then `run-preflight-passed` or `run-preflight-refused`. The browser is asked for
|
|
1906
|
+
* only after a pass (`browser-launch-requested`), so the export can show the order.
|
|
1907
|
+
*/
|
|
1908
|
+
async runPreflight(runId, sessionIds, onlyTasks) {
|
|
1909
|
+
const cfg = await this.settings.load();
|
|
1910
|
+
const log = [];
|
|
1911
|
+
const note = (type, message, data, ids, level = 'info') => {
|
|
1912
|
+
log.push({ at: new Date().toISOString(), type, message, data });
|
|
1913
|
+
for (const id of ids)
|
|
1914
|
+
this.bus.publish({ sessionId: id, type, level, message, data: { runId, ...data } });
|
|
1915
|
+
};
|
|
1916
|
+
note('run-preflight-started', `version control preflight for ${sessionIds.length} session(s), before the browser opens`, { sessions: sessionIds }, sessionIds);
|
|
1917
|
+
const groups = await this.runVcs(sessionIds, onlyTasks);
|
|
1918
|
+
for (const g of groups) {
|
|
1919
|
+
const ids = g.sessions.map((x) => x.id);
|
|
1920
|
+
const first = ids[0] ? await this.store.getSession(ids[0]) : null;
|
|
1921
|
+
const start = first?.vcsStart;
|
|
1922
|
+
const untouched = !!first && !first.tasks.some((t) => t.startedAt);
|
|
1923
|
+
if (start?.kind === 'snapshot' && start.snapshot?.approved && untouched) {
|
|
1924
|
+
note('baseline-created', `starting snapshot ${start.commit.slice(0, 8)} on ${start.branch ?? '?'}, approved ${start.snapshot.approvedAt ?? ''}${start.snapshot.onBase ? ` on top of "${start.snapshot.fromBranch}"` : ''}`, {
|
|
1925
|
+
repoDir: g.repoDir,
|
|
1926
|
+
commit: start.commit,
|
|
1927
|
+
branch: start.branch,
|
|
1928
|
+
approvedAt: start.snapshot.approvedAt,
|
|
1929
|
+
onBase: !!start.snapshot.onBase,
|
|
1930
|
+
included: start.snapshot.included.length,
|
|
1931
|
+
}, ids);
|
|
1932
|
+
}
|
|
1933
|
+
note('repository-preflight', `${g.repoDir}: ${g.ready ? 'ready' : g.problem ?? 'needs your approval'}`, {
|
|
1934
|
+
repoDir: g.repoDir,
|
|
1935
|
+
ready: g.ready,
|
|
1936
|
+
branch: g.branch,
|
|
1937
|
+
head: g.head,
|
|
1938
|
+
...(g.baseBranch ? { baseBranch: g.baseBranch, baseHead: g.baseHead } : {}),
|
|
1939
|
+
sessions: g.sessions.map((x) => ({ id: x.id, name: x.name, startFrom: x.startFrom, role: x.role })),
|
|
1940
|
+
inputPatterns: g.inputPatterns,
|
|
1941
|
+
inputs: g.inputs.map((e) => e.path),
|
|
1942
|
+
unrelated: g.unrelated.map((u) => u.path),
|
|
1943
|
+
...(g.problem ? { problem: g.problem } : {}),
|
|
1944
|
+
}, ids, g.ready ? 'info' : 'warn');
|
|
1945
|
+
const fixes = g.actions.filter((a) => a.available && a.id !== 'review-inputs').map((a) => a.id);
|
|
1946
|
+
if (!g.ready && fixes.length > 0) {
|
|
1947
|
+
note('snapshot-approval-required', `${g.repoDir}: nothing runs until one of these is pressed on the run screen: ${fixes.join(', ')}`, { repoDir: g.repoDir, actions: fixes }, ids, 'warn');
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const open = groups.filter((g) => !g.ready);
|
|
1951
|
+
const reason = open.length === 0
|
|
1952
|
+
? null
|
|
1953
|
+
: 'Version control is not ready for this run, so nothing was opened or sent. ' +
|
|
1954
|
+
open.map((g) => `${g.repoDir}: ${g.problem ?? 'it needs your approval'}`).join(' ') +
|
|
1955
|
+
' Fix it under "Prepare version control for this run" on the Sessions page.';
|
|
1956
|
+
if (reason)
|
|
1957
|
+
note('run-preflight-refused', reason, { repositories: open.map((g) => g.repoDir) }, sessionIds, 'warn');
|
|
1958
|
+
else
|
|
1959
|
+
note('run-preflight-passed', `version control is ready in ${groups.length} repositor${groups.length === 1 ? 'y' : 'ies'}; the browser may open`, { repositories: groups.map((g) => g.repoDir) }, sessionIds);
|
|
1960
|
+
await appendRunLog(cfg.resolved.runsDir, runId, log).catch(() => undefined);
|
|
1961
|
+
return reason;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* The same rule for one session that has not started, when its turn comes: see `RunDeps.vcsGate`.
|
|
1965
|
+
* A session that has started is past its first preparation; its tasks are prepared as they come.
|
|
1966
|
+
*/
|
|
1967
|
+
async sessionVcsGate(session, onlyTasks) {
|
|
1968
|
+
if (!session.vcs?.enabled || session.vcsBaseCommit)
|
|
1969
|
+
return null;
|
|
1970
|
+
if (queuedToRun(session, onlyTasks).length === 0)
|
|
1971
|
+
return null;
|
|
1972
|
+
const [group] = await runVcsPreflight([session], () => this.store.listSessions());
|
|
1973
|
+
if (!group || group.ready)
|
|
1974
|
+
return null;
|
|
1975
|
+
return `Version control is not ready for "${session.name}", so it did not start and its tasks stay queued: ${group.problem ?? 'it needs your approval'} Fix it under "Prepare version control for this run" on the Sessions page.`;
|
|
1976
|
+
}
|
|
1977
|
+
/** Recorded in the run's log just before the browser is opened for it: see `runPreflight`. */
|
|
1978
|
+
async browserRequested(runId, sessionIds) {
|
|
1979
|
+
const cfg = await this.settings.load();
|
|
1980
|
+
const entry = { at: new Date().toISOString(), type: 'browser-launch-requested', message: 'the version control preflight passed; opening the browser', data: { sessions: sessionIds } };
|
|
1981
|
+
for (const id of sessionIds)
|
|
1982
|
+
this.bus.publish({ sessionId: id, type: entry.type, level: 'info', message: entry.message, data: { runId, ...entry.data } });
|
|
1983
|
+
await appendRunLog(cfg.resolved.runsDir, runId, [entry]).catch(() => undefined);
|
|
1984
|
+
}
|
|
1985
|
+
/** One action of "Prepare version control for this run". Refused while anything runs in that repository. */
|
|
1986
|
+
async runVcsPrepare(sessionIds, repoDir, action, choices) {
|
|
1987
|
+
await this.init();
|
|
1988
|
+
if (this.batch?.running)
|
|
1989
|
+
return { ok: false, problem: 'A run is going. Stop it before preparing version control.' };
|
|
1990
|
+
const other = await this.runningIn(repoDir);
|
|
1991
|
+
if (other)
|
|
1992
|
+
return { ok: false, problem: `Session "${other.name}" is running in this repository. Stop it first.` };
|
|
1993
|
+
const sessions = [];
|
|
1994
|
+
for (const id of sessionIds) {
|
|
1995
|
+
const s = await this.store.getSession(id);
|
|
1996
|
+
if (s && queuedToRun(s).length > 0)
|
|
1997
|
+
sessions.push(s);
|
|
1998
|
+
}
|
|
1999
|
+
const done = await runVcsPrepare(sessions, repoDir, action, choices && typeof choices === 'object' ? choices : {}, this.bus, async (id, mutate) => void (await this.store.updateSession(id, mutate)), () => this.store.listSessions());
|
|
2000
|
+
return done.ok ? { ok: true, result: done.result } : { ok: false, problem: done.problem };
|
|
2001
|
+
}
|
|
1858
2002
|
/** Whether version control can do its job in this session, asked before a run. */
|
|
1859
2003
|
async vcsStatus(sessionId) {
|
|
1860
2004
|
await this.init();
|
|
@@ -2328,15 +2472,45 @@ let OperatorService = class OperatorService {
|
|
|
2328
2472
|
*/
|
|
2329
2473
|
async syncPlan(dir) {
|
|
2330
2474
|
await this.init();
|
|
2475
|
+
await this.knownProjectFolder(dir, 'no command is offered for it');
|
|
2476
|
+
return await planSync(resolve(dir.trim()));
|
|
2477
|
+
}
|
|
2478
|
+
/** Throws unless the folder is a project in Settings or the repository of a session. */
|
|
2479
|
+
async knownProjectFolder(dir, otherwise) {
|
|
2331
2480
|
const wanted = normaliseDir(dir);
|
|
2332
2481
|
const cfg = await this.settings.load();
|
|
2333
2482
|
const known = new Set([cfg.project?.rootDir ?? '', ...(cfg.project?.others ?? []).map((o) => o.rootDir), ...(await this.store.listSessions()).flatMap((s) => [s.vcs?.repoDir ?? '', s.projectDir ?? ''])]
|
|
2334
2483
|
.filter((d) => d.trim())
|
|
2335
2484
|
.map(normaliseDir));
|
|
2336
2485
|
if (!wanted || !known.has(wanted)) {
|
|
2337
|
-
throw new Error(
|
|
2486
|
+
throw new Error(`That folder is not one of the projects in Settings or the repository of a session, so ${otherwise}.`);
|
|
2338
2487
|
}
|
|
2339
|
-
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* "Prepare the folder from the remote main branch", first half: fetches and says what would be
|
|
2491
|
+
* kept where and what the folder would be. See `vcs/prepareFromRemote.ts`.
|
|
2492
|
+
*/
|
|
2493
|
+
async preparePreview(dir, fetch = true) {
|
|
2494
|
+
await this.init();
|
|
2495
|
+
await this.knownProjectFolder(dir, 'it is not prepared from here');
|
|
2496
|
+
// The operator pressed the button and is looking: the remote may ask for a password through Git's own window.
|
|
2497
|
+
return await planPrepare(resolve(dir.trim()), { fetch, askpass: fetch ? await askpassProgram() : null });
|
|
2498
|
+
}
|
|
2499
|
+
/**
|
|
2500
|
+
* The second half: does it, if the folder is still what the preview showed. Refused while anything
|
|
2501
|
+
* runs, or is being started, in that folder or one around it: it moves the checkout.
|
|
2502
|
+
*/
|
|
2503
|
+
async prepareProject(dir, fingerprint) {
|
|
2504
|
+
await this.init();
|
|
2505
|
+
await this.knownProjectFolder(dir, 'it is not prepared from here');
|
|
2506
|
+
if (this.batch?.running)
|
|
2507
|
+
return { ok: false, problem: 'A run is going. Stop it before preparing the folder; nothing was changed.' };
|
|
2508
|
+
if (this.browser?.kind === 'run' || this.browser?.kind === 'batch')
|
|
2509
|
+
return { ok: false, problem: 'A run is being started. Wait for it, or stop it, before preparing the folder; nothing was changed.' };
|
|
2510
|
+
const other = await this.runningIn(dir);
|
|
2511
|
+
if (other)
|
|
2512
|
+
return { ok: false, problem: `Session "${other.name}" is running in this folder. Stop it before preparing the folder; nothing was changed.` };
|
|
2513
|
+
return await prepareFromRemote(resolve(dir.trim()), String(fingerprint ?? ''));
|
|
2340
2514
|
}
|
|
2341
2515
|
// --- the project being worked on ---------------------------------------------------------
|
|
2342
2516
|
/**
|
|
@@ -630,11 +630,16 @@ export async function runTask(transport, session, task, deps) {
|
|
|
630
630
|
*/
|
|
631
631
|
const patterns = artifactPatterns(session.vcs);
|
|
632
632
|
const projectRoot = repoDirOf(session) || session.projectDir?.trim() || '';
|
|
633
|
-
|
|
633
|
+
// Only after the task started: one refused before it ran made nothing, and what is there is not its own.
|
|
634
|
+
if ((patterns.length > 0 || (task.outputs?.length ?? 0) > 0) && projectRoot && artifactsBefore) {
|
|
634
635
|
// Only what this task made or changed, inside its scope, or declared as its outputs.
|
|
635
636
|
const kept = await keepArtifacts(projectRoot, patterns, join(artifactsDir, 'project'), { before: artifactsBefore, scope: task.scope, outputs: task.outputs })
|
|
636
637
|
.catch((e) => ({ kept: [], skipped: [`(all: ${e.message})`], unchanged: 0, outsideScope: [] }));
|
|
637
|
-
|
|
638
|
+
/*
|
|
639
|
+
* `artifacts-kept` only when something was: files that were there before and untouched are not this
|
|
640
|
+
* task's evidence, and an event of that name with nothing in it read as if they had been collected.
|
|
641
|
+
*/
|
|
642
|
+
sink.event(kept.kept.length > 0 || kept.skipped.length > 0 ? 'artifacts-kept' : 'artifacts-none', { kept: kept.kept.length, skipped: kept.skipped, unchanged: kept.unchanged, outsideScope: kept.outsideScope.slice(0, 20) }, `artifacts kept with the run: ${kept.kept.length} file(s) this task made or changed` +
|
|
638
643
|
`${kept.unchanged > 0 ? `; ${kept.unchanged} already there and untouched, not kept` : ''}` +
|
|
639
644
|
`${kept.outsideScope.length > 0 ? `; ${kept.outsideScope.length} outside the task's scope, not kept` : ''}` +
|
|
640
645
|
`${kept.skipped.length > 0 ? `; not kept: ${kept.skipped.slice(0, 5).join('; ')}` : ''}`, kept.skipped.length > 0 ? 'warn' : 'info');
|
|
@@ -2226,6 +2231,12 @@ export async function runSession(sessionId, deps) {
|
|
|
2226
2231
|
bus.publish({ sessionId, type: 'session-idle', level: 'info', message: 'no queued tasks' });
|
|
2227
2232
|
return { ran: 0, paused: false };
|
|
2228
2233
|
}
|
|
2234
|
+
// Version control before anything else: a session that may not start is refused with its tasks queued.
|
|
2235
|
+
const refused = deps.vcsGate ? await deps.vcsGate(session) : null;
|
|
2236
|
+
if (refused) {
|
|
2237
|
+
bus.publish({ sessionId, type: 'run-preflight-refused', level: 'warn', message: refused, data: { queued: queued.length } });
|
|
2238
|
+
return { ran: 0, paused: false, refused };
|
|
2239
|
+
}
|
|
2229
2240
|
await store.updateSession(sessionId, (s) => {
|
|
2230
2241
|
s.status = 'running';
|
|
2231
2242
|
});
|
|
@@ -2245,6 +2256,7 @@ export async function runSession(sessionId, deps) {
|
|
|
2245
2256
|
await enterSessionConversation(borrowed, session, { closeOnFailure: false });
|
|
2246
2257
|
}
|
|
2247
2258
|
else {
|
|
2259
|
+
await deps.beforeBrowser?.();
|
|
2248
2260
|
transport = await openSessionTransport(cfg, session, bus, sessionRunsDir);
|
|
2249
2261
|
}
|
|
2250
2262
|
const chat = transport;
|
|
@@ -685,6 +685,110 @@ export const SYSTEM_GUIDE = [
|
|
|
685
685
|
doesEn: 'The model that reviews each task afterwards, chosen separately from the one doing the work.',
|
|
686
686
|
doesBg: 'Моделът, който проверява всяка задача после, избран отделно от този, който върши работата.',
|
|
687
687
|
},
|
|
688
|
+
{
|
|
689
|
+
key: 'runvcs.title',
|
|
690
|
+
kind: 'note',
|
|
691
|
+
en: 'Prepare version control for this run',
|
|
692
|
+
bg: 'Подготви контрола на версиите за това пускане',
|
|
693
|
+
doesEn: 'Shown under the run settings when a repository of the ticked sessions is not ready: one box per repository, with the fixes. The run buttons stay off until every box is ready, and a start is refused before the browser opens.',
|
|
694
|
+
doesBg: 'Показва се под настройките на пускането, когато хранилище на отметнатите сесии не е готово: по една кутия на хранилище, с решенията. Бутоните за пускане остават изключени, докато всяка кутия не е готова, а старт се отказва преди да се отвори браузърът.',
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
key: 'runvcs.repo',
|
|
698
|
+
kind: 'note',
|
|
699
|
+
en: 'Repository',
|
|
700
|
+
bg: 'Хранилище',
|
|
701
|
+
doesEn: 'The repository the box is about.',
|
|
702
|
+
doesBg: 'Хранилището, за което е кутията.',
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
key: 'runvcs.current',
|
|
706
|
+
kind: 'note',
|
|
707
|
+
en: 'Current branch and HEAD',
|
|
708
|
+
bg: 'Текущ клон и HEAD',
|
|
709
|
+
doesEn: 'Where the repository is now.',
|
|
710
|
+
doesBg: 'Къде е хранилището сега.',
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
key: 'runvcs.base',
|
|
714
|
+
kind: 'note',
|
|
715
|
+
en: 'Requested base branch and HEAD',
|
|
716
|
+
bg: 'Поискан базов клон и HEAD',
|
|
717
|
+
doesEn: 'The branch the plan asked the sessions to start from, and its tip.',
|
|
718
|
+
doesBg: 'Клонът, от който планът иска сесиите да тръгнат, и върхът му.',
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
key: 'runvcs.sessions',
|
|
722
|
+
kind: 'note',
|
|
723
|
+
en: 'Sessions',
|
|
724
|
+
bg: 'Сесии',
|
|
725
|
+
doesEn: 'The ticked sessions in this repository and how each starts: first, from the same base, or carrying on the one before.',
|
|
726
|
+
doesBg: 'Отметнатите сесии в това хранилище и как тръгва всяка: първа, от същата база или продължава предната.',
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
key: 'runvcs.patterns',
|
|
730
|
+
kind: 'note',
|
|
731
|
+
en: 'Input file patterns',
|
|
732
|
+
bg: 'Шаблони за входни файлове',
|
|
733
|
+
doesEn: 'The "Input files" of the first session.',
|
|
734
|
+
doesBg: '„Входни файлове“ на първата сесия.',
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
key: 'runvcs.review',
|
|
738
|
+
kind: 'button',
|
|
739
|
+
en: 'Review input files',
|
|
740
|
+
bg: 'Прегледай входните файлове',
|
|
741
|
+
doesEn: 'Lists the matched input files with git status and size. Changes nothing.',
|
|
742
|
+
doesBg: 'Показва съвпадналите входни файлове с git статус и размер. Нищо не променя.',
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
key: 'runvcs.snapshotOnBase',
|
|
746
|
+
kind: 'button',
|
|
747
|
+
en: 'Create starting snapshot on {branch}',
|
|
748
|
+
bg: 'Създай начална снимка върху {branch}',
|
|
749
|
+
doesEn: 'Commits the approved input files on top of the base branch, on a new cop/baseline/ branch, without switching branches or touching the checkout; every session of the run starting from that branch starts from it, chained ones carry it on. Recommended when the plan names that branch. Not offered while other files are uncommitted.',
|
|
750
|
+
doesBg: 'Комитва одобрените входни файлове върху базовия клон, на нов клон cop/baseline/, без да сменя клона и без да пипа работното копие; всяка сесия от пускането, тръгваща от този клон, тръгва от снимката, а свързаните във верига я продължават. Препоръчано, когато планът посочва този клон. Не се предлага, докато има други некомитнати файлове.',
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
key: 'runvcs.snapshotHere',
|
|
754
|
+
kind: 'button',
|
|
755
|
+
en: 'Create starting snapshot on {current}',
|
|
756
|
+
bg: 'Създай начална снимка върху {current}',
|
|
757
|
+
doesEn: 'Takes the listed files as the starting snapshot where the repository is, as the session page does: one commit on a new branch, the current branch not moved.',
|
|
758
|
+
doesBg: 'Взема изброените файлове като начална снимка там, където е хранилището, както страницата на сесията: един комит на нов клон, текущият клон не се мести.',
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
key: 'runvcs.useCurrent',
|
|
762
|
+
kind: 'button',
|
|
763
|
+
en: 'Use the current branch instead of {branch}',
|
|
764
|
+
bg: 'Използвай текущия branch вместо {branch}',
|
|
765
|
+
doesEn: 'The sessions start from the branch the repository is on instead of the base branch: their "Start from" becomes "From wherever the repository is". Nothing is committed.',
|
|
766
|
+
doesBg: 'Сесиите тръгват от клона, на който е хранилището, вместо от базовия: „Откъде тръгва“ им става „Откъдето е хранилището“. Нищо не се комитва.',
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
key: 'runvcs.allowSnapshot',
|
|
770
|
+
kind: 'button',
|
|
771
|
+
en: 'Take uncommitted changes as a starting snapshot',
|
|
772
|
+
bg: 'Вземи некомитнатите промени като начална снимка',
|
|
773
|
+
doesEn: 'Sets "Uncommitted changes" of the first session to "Take them as a starting snapshot", so the files are listed to approve. Nothing is committed.',
|
|
774
|
+
doesBg: 'Задава „Некомитнати промени“ на първата сесия на „Запази ги като начална снимка“, за да се изброят файловете за одобрение. Нищо не се комитва.',
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
key: 'runvcs.cancel',
|
|
778
|
+
kind: 'button',
|
|
779
|
+
en: 'Cancel',
|
|
780
|
+
bg: 'Отказ',
|
|
781
|
+
doesEn: 'Hides the fixes; nothing is changed and the run stays off.',
|
|
782
|
+
doesBg: 'Скрива решенията; нищо не се променя и пускането остава изключено.',
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
key: 'runvcs.showAgain',
|
|
786
|
+
kind: 'button',
|
|
787
|
+
en: 'Show the fixes again',
|
|
788
|
+
bg: 'Покажи решенията отново',
|
|
789
|
+
doesEn: 'Brings the fixes back after Cancel.',
|
|
790
|
+
doesBg: 'Връща решенията след „Отказ“.',
|
|
791
|
+
},
|
|
688
792
|
{
|
|
689
793
|
key: 'batch.run',
|
|
690
794
|
kind: 'button',
|
|
@@ -2837,28 +2941,52 @@ export const SYSTEM_GUIDE = [
|
|
|
2837
2941
|
doesBg: 'Секунди чакане на отговор от чата (по подразбиране 900); незавършен дотогава отговор проваля задачата с „Copilot did not finish a reply within …“.',
|
|
2838
2942
|
},
|
|
2839
2943
|
{
|
|
2840
|
-
key: '
|
|
2944
|
+
key: 'prep.show',
|
|
2841
2945
|
kind: 'button',
|
|
2842
|
-
en: '
|
|
2843
|
-
bg: '
|
|
2844
|
-
doesEn: 'Under each project in Settings and on a session
|
|
2845
|
-
doesBg: 'Под всеки проект в Настройки и в контрола на версиите на сесия:
|
|
2946
|
+
en: 'Prepare the folder from the remote main branch',
|
|
2947
|
+
bg: 'Подготви папката от remote main',
|
|
2948
|
+
doesEn: 'Under each project in Settings and on a session’s version control: fetches, then shows what would be kept where — uncommitted changes and new files on a saved branch of their own, commits of the local main the remote lacks on another — and where the folder ends up (the remote main, nothing uncommitted). Changes nothing until "Confirm".',
|
|
2949
|
+
doesBg: 'Под всеки проект в Настройки и в контрола на версиите на сесия: изтегля и показва какво къде ще се запази — некомитнатите промени и новите файлове в отделен запазен клон, комитите на локалния main, които ги няма в remote, в друг — и къде ще е папката (основният клон на remote, без некомитнати промени). Нищо не се променя до „Потвърди“.',
|
|
2846
2950
|
},
|
|
2847
2951
|
{
|
|
2848
|
-
key: '
|
|
2952
|
+
key: 'prep.confirm',
|
|
2849
2953
|
kind: 'button',
|
|
2850
|
-
en: '
|
|
2851
|
-
bg: '
|
|
2852
|
-
doesEn: '
|
|
2853
|
-
doesBg: '
|
|
2954
|
+
en: 'Confirm',
|
|
2955
|
+
bg: 'Потвърди',
|
|
2956
|
+
doesEn: 'Keeps everything as shown, then checks out the remote main branch as the local one and removes the new files from the folder (they are on the saved branch); ignored files stay. Refused if the folder changed since the preview, or while a run works in it. Afterwards the operator adds the files a task needs and the run screen offers a starting snapshot of them.',
|
|
2957
|
+
doesBg: 'Запазва всичко, както е показано, после избира основния клон на remote като локален и маха новите файлове от папката (те са в запазения клон); игнорираните остават. Отказва се, ако папката се е променила след прегледа или докато пускане работи в нея. След това операторът добавя файловете за задачата и екранът за пускане предлага начална снимка от тях.',
|
|
2854
2958
|
},
|
|
2855
2959
|
{
|
|
2856
|
-
key: '
|
|
2960
|
+
key: 'prep.retry',
|
|
2857
2961
|
kind: 'button',
|
|
2858
|
-
en: '
|
|
2859
|
-
bg: '
|
|
2860
|
-
doesEn: 'fetch
|
|
2861
|
-
doesBg: '
|
|
2962
|
+
en: 'Try again',
|
|
2963
|
+
bg: 'Опитай пак',
|
|
2964
|
+
doesEn: 'Shown when the fetch did not get through: fetches again; a remote that wants a password or an SSH key passphrase asks through Git\u2019s own window, which the bot never reads.',
|
|
2965
|
+
doesBg: 'Показва се, когато изтеглянето не е минало: изтегля отново; remote, който иска парола или passphrase за SSH ключ, пита през собствения прозорец на Git, който ботът не чете.',
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
key: 'prep.copyFetch',
|
|
2969
|
+
kind: 'button',
|
|
2970
|
+
en: 'Copy the fetch command',
|
|
2971
|
+
bg: 'Копирай командата за изтегляне',
|
|
2972
|
+
doesEn: 'Copies git fetch for this repository, to run in the operator\u2019s own PowerShell, where ssh asks for the passphrase directly.',
|
|
2973
|
+
doesBg: 'Копира git fetch за това хранилище, за собствения PowerShell на оператора, където ssh пита за passphrase директно.',
|
|
2974
|
+
},
|
|
2975
|
+
{
|
|
2976
|
+
key: 'prep.useFetched',
|
|
2977
|
+
kind: 'button',
|
|
2978
|
+
en: 'Continue with what was last fetched',
|
|
2979
|
+
bg: 'Продължи с последно изтегленото',
|
|
2980
|
+
doesEn: 'The preview again without fetching, against the remote as it was last fetched (say, by the command above); it says when that was.',
|
|
2981
|
+
doesBg: 'Прегледът отново без изтегляне, спрямо remote, както е изтеглен последно (например с командата по-горе); казва кога е било.',
|
|
2982
|
+
},
|
|
2983
|
+
{
|
|
2984
|
+
key: 'prep.cancel',
|
|
2985
|
+
kind: 'button',
|
|
2986
|
+
en: 'Cancel',
|
|
2987
|
+
bg: 'Отказ',
|
|
2988
|
+
doesEn: 'Closes the preview; nothing is changed.',
|
|
2989
|
+
doesBg: 'Затваря прегледа; нищо не се променя.',
|
|
2862
2990
|
},
|
|
2863
2991
|
{
|
|
2864
2992
|
key: 'exec.minApproaches',
|