copilot-operator 0.1.22 → 0.1.25
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 +2 -2
- package/dist/src/api/operator.service.js +330 -35
- package/dist/src/exec/confinement.js +85 -7
- package/dist/src/exec/programs.js +12 -3
- package/dist/src/exec/reportFile.js +5 -3
- package/dist/src/exec/shells.js +13 -2
- package/dist/src/log/runLog.js +6 -1
- package/dist/src/orchestrator/contract.js +29 -7
- package/dist/src/orchestrator/review.js +16 -3
- package/dist/src/orchestrator/taskRunner.js +391 -54
- package/dist/src/plan/schema.js +11 -1
- package/dist/src/plan/systemGuide.js +36 -4
- package/dist/src/protocol/parser.js +33 -5
- package/dist/src/protocol/reporter.js +2 -0
- package/dist/src/session/compose.js +18 -9
- package/dist/src/session/exportRecord.js +12 -3
- package/dist/src/session/exports.js +46 -9
- package/dist/src/session/handoff.js +5 -2
- package/dist/src/session/interruption.js +3 -1
- package/dist/src/session/runLog.js +42 -1
- package/dist/src/session/runName.js +3 -1
- package/dist/src/session/store.js +7 -2
- package/dist/src/transport/copilotTransport.js +89 -62
- package/dist/src/transport/modelMatch.js +37 -0
- package/dist/src/vcs/artifacts.js +20 -7
- package/dist/src/vcs/commitHygiene.js +7 -2
- package/dist/src/vcs/contentIntegrity.js +65 -6
- package/dist/src/vcs/git.js +44 -7
- package/dist/src/vcs/prepareFromRemote.js +46 -9
- package/dist/src/vcs/remoteAuth.js +92 -0
- package/dist/src/vcs/runPreflight.js +100 -31
- package/dist/src/vcs/scope.js +18 -2
- package/dist/src/vcs/snapshot.js +61 -16
- package/dist/src/vcs/syncCommand.js +3 -11
- package/dist/src/vcs/taskVcs.js +173 -24
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-7e35e4fdefadb05d.js +1 -0
- package/dist/web/_next/static/chunks/867-52e371cb1ec49089.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/{page-cdc730adea753446.js → page-62c8fbe84baa436a.js} +1 -1
- package/dist/web/_next/static/chunks/app/page-4defcfd1eb482644.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/{page-185755ada66e2930.js → page-2d3bbc68b27fed2c.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 +3 -2
- package/prompts/level1.md +10 -3
- package/dist/web/_next/static/chunks/549-5f90780046c1c24f.js +0 -1
- package/dist/web/_next/static/chunks/867-79f07bebe3084811.js +0 -1
- package/dist/web/_next/static/chunks/app/page-0a861b38da0a1c51.js +0 -1
- /package/dist/web/_next/static/{9Ze8aagkvT-WLfD4KCKKS → beP1HgHJZvBVRTsoU1sZv}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{9Ze8aagkvT-WLfD4KCKKS → beP1HgHJZvBVRTsoU1sZv}/_ssgManifest.js +0 -0
|
@@ -498,11 +498,11 @@ let OperatorController = class OperatorController {
|
|
|
498
498
|
}
|
|
499
499
|
/** "Prepare the folder from the remote main branch": what it would do (fetches, changes nothing else). */
|
|
500
500
|
repoPreparePreview(body) {
|
|
501
|
-
return this.ops.preparePreview(body?.dir ?? '').catch(fail);
|
|
501
|
+
return this.ops.preparePreview(body?.dir ?? '', body?.fetch !== false).catch(fail);
|
|
502
502
|
}
|
|
503
503
|
/** …and doing it, if the folder is still what the preview showed. */
|
|
504
504
|
repoPrepare(body) {
|
|
505
|
-
return this.ops.prepareProject(body?.dir ?? '', body?.fingerprint ?? '').catch(fail);
|
|
505
|
+
return this.ops.prepareProject(body?.dir ?? '', body?.fingerprint ?? '', { savedBranch: body?.savedBranch, savedMainBranch: body?.savedMainBranch }).catch(fail);
|
|
506
506
|
}
|
|
507
507
|
repo(dir) {
|
|
508
508
|
const problem = this.ops.repoProblem(dir ?? '');
|
|
@@ -35,8 +35,8 @@ function sameFolder(a, b) {
|
|
|
35
35
|
}
|
|
36
36
|
import { importPlan, plannedSessionSignature, taskSignature } from '../plan/importPlan.js';
|
|
37
37
|
import { readInterruption } from '../session/interruption.js';
|
|
38
|
-
import { branchAt, commitInterrupted, repoDirOf, vcsPreflight, restorePoint, restorePreview, restoreToBase, sessionBranches, sessionBranchName } from '../vcs/taskVcs.js';
|
|
39
|
-
import { branchExists, changedFilesBetween, fileAt, freeBranchName, gitAvailable, localBranches, plannedBranchName, repoUnusableReason } from '../vcs/git.js';
|
|
38
|
+
import { branchAt, commitInterrupted, repoDirOf, trackedRepoOf, vcsPreflight, restorePoint, restorePreview, restoreToBase, sessionBranches, sessionBranchName } from '../vcs/taskVcs.js';
|
|
39
|
+
import { branchExists, branchNameFrom, changedFilesBetween, fileAt, freeBranchName, git, gitAvailable, localBranches, plannedBranchName, repoUnusableReason } from '../vcs/git.js';
|
|
40
40
|
import { makeAuthorizer } from '../exec/authorizer.js';
|
|
41
41
|
import { pickFolder } from './folderPicker.js';
|
|
42
42
|
import { findEdgeUsingProfile } from '../transport/profileLock.js';
|
|
@@ -46,10 +46,16 @@ import { createTransport } from '../transport/chatTransport.js';
|
|
|
46
46
|
import { composeHandoff } from '../session/handoff.js';
|
|
47
47
|
import { planSync } from '../vcs/syncCommand.js';
|
|
48
48
|
import { planPrepare, prepareFromRemote } from '../vcs/prepareFromRemote.js';
|
|
49
|
-
import {
|
|
49
|
+
import { askpassProgram } from '../vcs/remoteAuth.js';
|
|
50
|
+
import { appendRunLog, appendSessionLog } from '../session/runLog.js';
|
|
51
|
+
import { workingDirFor, isWorkingDirProblem } from '../exec/workDir.js';
|
|
52
|
+
import { contractConflicts } from '../orchestrator/contract.js';
|
|
53
|
+
import { checkRefusalForOperator, lineRefusal } from '../orchestrator/taskRunner.js';
|
|
54
|
+
import { preferredShell } from '../exec/shells.js';
|
|
55
|
+
import { sessionRoots } from '../exec/confinement.js';
|
|
50
56
|
import { dirtyPolicy, takeSnapshot } from '../vcs/snapshot.js';
|
|
51
57
|
import { runVcsPreflight, runVcsPrepare } from '../vcs/runPreflight.js';
|
|
52
|
-
import { normalisePatterns } from '../vcs/inputs.js';
|
|
58
|
+
import { inputSettings, normalisePatterns } from '../vcs/inputs.js';
|
|
53
59
|
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
54
60
|
import { botVersion } from '../config/version.js';
|
|
55
61
|
import { computeMetrics } from '../session/metrics.js';
|
|
@@ -496,11 +502,27 @@ let OperatorService = class OperatorService {
|
|
|
496
502
|
*/
|
|
497
503
|
async updateTask(sessionId, taskId, patch) {
|
|
498
504
|
await this.init();
|
|
499
|
-
|
|
505
|
+
let before;
|
|
506
|
+
const task = await this.store.updateTask(sessionId, taskId, (t) => {
|
|
500
507
|
if (t.status !== 'queued')
|
|
501
508
|
throw new Error('Only a queued task can be edited.');
|
|
509
|
+
before = structuredClone(t);
|
|
502
510
|
applyTaskPatch(t, patch);
|
|
503
511
|
});
|
|
512
|
+
/*
|
|
513
|
+
* On record with the operator's other actions: a check or the prompt changed after the import is a
|
|
514
|
+
* different contract from the plan's, and the export showed only the new one (live run 2026-10-04).
|
|
515
|
+
*/
|
|
516
|
+
const changed = Object.keys(patch).filter((k) => JSON.stringify(before?.[k]) !== JSON.stringify(task[k]));
|
|
517
|
+
if (changed.length > 0) {
|
|
518
|
+
const cfg = await this.settings.load();
|
|
519
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, {
|
|
520
|
+
type: 'task-edited',
|
|
521
|
+
message: `"${task.title}" edited while queued: ${changed.join(', ')} changed`,
|
|
522
|
+
data: { taskId, changed, before: Object.fromEntries(changed.map((k) => [k, before?.[k] ?? null])) },
|
|
523
|
+
}).catch(() => undefined);
|
|
524
|
+
}
|
|
525
|
+
return task;
|
|
504
526
|
}
|
|
505
527
|
async deleteTask(sessionId, taskId) {
|
|
506
528
|
await this.init();
|
|
@@ -516,7 +538,16 @@ let OperatorService = class OperatorService {
|
|
|
516
538
|
/** "Continue" on a task that stopped before it finished — a limit from the settings, the bot stopping, the operator: same chat, same branch, fresh count. */
|
|
517
539
|
async continueTask(sessionId, taskId) {
|
|
518
540
|
await this.init();
|
|
519
|
-
|
|
541
|
+
/*
|
|
542
|
+
* An attempt the operator stopped: which of its last steps ran, and what they printed, read from its
|
|
543
|
+
* run folder, so "Continue" can tell the chat. Without it the chat was told only that it had been
|
|
544
|
+
* stopped, and ran its inspection steps again (live run 2026-10-03).
|
|
545
|
+
*/
|
|
546
|
+
const before = (await this.store.getSession(sessionId))?.tasks.find((t) => t.id === taskId);
|
|
547
|
+
const cfg = await this.settings.load();
|
|
548
|
+
const stoppedAt = before?.status === 'aborted' && !before.interruption && before.runId ? await readInterruption(join(cfg.resolved.runsDir, before.runId)).catch(() => null) : null;
|
|
549
|
+
const task = await this.store.continueTask(sessionId, taskId, stoppedAt);
|
|
550
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, { type: 'continue', message: `"${task.title}" queued to continue as attempt ${task.attempt ?? 1}`, data: { taskId, attempt: task.attempt } }).catch(() => undefined);
|
|
520
551
|
this.bus.publish({
|
|
521
552
|
sessionId,
|
|
522
553
|
taskId,
|
|
@@ -537,7 +568,21 @@ let OperatorService = class OperatorService {
|
|
|
537
568
|
await this.init();
|
|
538
569
|
const before = (await this.store.getSession(sessionId))?.tasks.find((t) => t.id === taskId);
|
|
539
570
|
const buildsOn = opts.buildOnFinished && before?.status === 'done' ? { fromAttempt: before.attempt ?? 1 } : undefined;
|
|
571
|
+
/*
|
|
572
|
+
* A new prompt is a new change: the plan's commit subject was written for the old one, and two
|
|
573
|
+
* different commits went out under it (live run 2026-10-03). Dropped unless the edit names one.
|
|
574
|
+
*/
|
|
575
|
+
// A correction added to the old prompt is not a new change, and keeps the subject (live run 2026-10-04).
|
|
576
|
+
const appended = !!before && !!patch.prompt && patch.prompt.trim().startsWith(before.prompt.trim().split('\n### Expected result')[0].trim());
|
|
577
|
+
if (patch.prompt?.trim() && before && patch.prompt.trim() !== before.prompt.trim() && !appended && before.vcsPlan?.commitMessage && !patch.vcsPlan?.commitMessage) {
|
|
578
|
+
patch = { ...patch, vcsPlan: { ...before.vcsPlan, ...(patch.vcsPlan ?? {}), commitMessage: undefined } };
|
|
579
|
+
}
|
|
540
580
|
const task = await this.store.rerunTask(sessionId, taskId, patch, undefined, buildsOn);
|
|
581
|
+
{
|
|
582
|
+
const cfg = await this.settings.load();
|
|
583
|
+
const changed = Object.keys(patch).filter((k) => patch[k] !== undefined);
|
|
584
|
+
await appendSessionLog(cfg.resolved.runsDir, sessionId, { type: 'rerun', message: `"${task.title}" queued again as attempt ${task.attempt ?? 1}${buildsOn ? `, building on attempt ${buildsOn.fromAttempt}` : ''}${changed.length > 0 ? `, with ${changed.join(', ')} changed` : ''}`, data: { taskId, attempt: task.attempt, changed, ...(buildsOn ? { buildsOn } : {}) } }).catch(() => undefined);
|
|
585
|
+
}
|
|
541
586
|
this.bus.publish({
|
|
542
587
|
sessionId,
|
|
543
588
|
taskId,
|
|
@@ -1013,9 +1058,11 @@ let OperatorService = class OperatorService {
|
|
|
1013
1058
|
sessionId,
|
|
1014
1059
|
type: 'upload-notice',
|
|
1015
1060
|
level: 'info',
|
|
1016
|
-
message:
|
|
1017
|
-
|
|
1018
|
-
|
|
1061
|
+
message:
|
|
1062
|
+
// Project files are no longer mirrored to the chat (removed in 0.1.14): only the reports go (live run 2026-10-03).
|
|
1063
|
+
"every report of this run — each step's terminal output, the check results, the reviewer's transcripts — is " +
|
|
1064
|
+
"uploaded into the Copilot chat and stored in the signed-in account's OneDrive and Copilot history, inside the " +
|
|
1065
|
+
'tenant; secret-shaped strings are redacted first',
|
|
1019
1066
|
});
|
|
1020
1067
|
const done = runSession(sessionId, {
|
|
1021
1068
|
cfg: { ...cfg, execution: { ...cfg.execution, mode } },
|
|
@@ -1035,10 +1082,32 @@ let OperatorService = class OperatorService {
|
|
|
1035
1082
|
transport,
|
|
1036
1083
|
runGroup,
|
|
1037
1084
|
onlyTasks,
|
|
1038
|
-
vcsGate: (s) =>
|
|
1085
|
+
vcsGate: async (s) => {
|
|
1086
|
+
const refused = await this.sessionVcsGate(s, onlyTasks);
|
|
1087
|
+
// In the run's own log, where the export finds it: it was only in the live events (live run 2026-10-04).
|
|
1088
|
+
if (refused && runGroup) {
|
|
1089
|
+
await appendRunLog(cfg.resolved.runsDir, runGroup.id, [
|
|
1090
|
+
{ at: new Date().toISOString(), type: 'run-preflight-refused', message: `refused at its turn: ${refused}`, data: { sessions: [s.id], atTurn: true } },
|
|
1091
|
+
]).catch(() => undefined);
|
|
1092
|
+
}
|
|
1093
|
+
return refused;
|
|
1094
|
+
},
|
|
1039
1095
|
...(transport || !runGroup ? {} : { beforeBrowser: () => this.browserRequested(runGroup.id, [sessionId]) }),
|
|
1096
|
+
// The page is the source of the model names: Settings and the saved list follow it (see `ModelHooks`).
|
|
1097
|
+
models: {
|
|
1098
|
+
renamedDefault: async (which, from, to) => {
|
|
1099
|
+
const now = (await this.settings.load()).copilot;
|
|
1100
|
+
if (which === 'model' && (now.defaultModel ?? '').trim() === from)
|
|
1101
|
+
await this.setDefaultModel(to);
|
|
1102
|
+
if (which === 'review' && (now.defaultReviewModel ?? '').trim() === from)
|
|
1103
|
+
await this.setDefaultReviewModel(to);
|
|
1104
|
+
},
|
|
1105
|
+
seen: async (options, current) => {
|
|
1106
|
+
await this.store.saveModels({ options, current: current ?? undefined, readAt: new Date().toISOString() });
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1040
1109
|
})
|
|
1041
|
-
.then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused, ...(outcome.refused ? {
|
|
1110
|
+
.then(async (outcome) => ({ ...(await this.tally(sessionId, queuedIds)), paused: outcome.paused, ...(outcome.refused ? { refused: outcome.refused } : {}) }))
|
|
1042
1111
|
.catch(async (e) => {
|
|
1043
1112
|
this.bus.publish({ sessionId, type: 'run-failed', level: 'error', message: e.message });
|
|
1044
1113
|
return { ...(await this.tally(sessionId, queuedIds)), error: e.message };
|
|
@@ -1073,6 +1142,10 @@ let OperatorService = class OperatorService {
|
|
|
1073
1142
|
continue;
|
|
1074
1143
|
}
|
|
1075
1144
|
tally.ran += 1;
|
|
1145
|
+
if (task.status === 'aborted') {
|
|
1146
|
+
tally.stopped = (tally.stopped ?? 0) + 1;
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1076
1149
|
if (task.status !== 'done') {
|
|
1077
1150
|
tally.failed += 1;
|
|
1078
1151
|
tally.failedTitles.push(task.title);
|
|
@@ -1095,7 +1168,8 @@ let OperatorService = class OperatorService {
|
|
|
1095
1168
|
await this.store.updateSession(sessionId, (s) => {
|
|
1096
1169
|
s.status = 'stopping';
|
|
1097
1170
|
});
|
|
1098
|
-
|
|
1171
|
+
// The running step is cut off, not waited for: said as it is (live run 2026-10-03).
|
|
1172
|
+
this.bus.publish({ sessionId, type: 'stop-requested', level: 'warn', message: 'stopping now: nothing more is sent, and a step still running is cut off' });
|
|
1099
1173
|
return { stopping: true };
|
|
1100
1174
|
}
|
|
1101
1175
|
// --- running several sessions in turn -------------------------------------------------
|
|
@@ -1485,7 +1559,12 @@ let OperatorService = class OperatorService {
|
|
|
1485
1559
|
const tally = await begun.done;
|
|
1486
1560
|
entry.ran = tally.ran;
|
|
1487
1561
|
entry.failed = tally.failed;
|
|
1488
|
-
if (tally.
|
|
1562
|
+
if (tally.refused) {
|
|
1563
|
+
// Refused at its turn, before anything of it ran: not a failure of its tasks, which stay queued.
|
|
1564
|
+
entry.state = 'skipped';
|
|
1565
|
+
entry.reason = tally.refused;
|
|
1566
|
+
}
|
|
1567
|
+
else if (tally.error) {
|
|
1489
1568
|
entry.state = 'failed';
|
|
1490
1569
|
entry.reason = tally.error;
|
|
1491
1570
|
}
|
|
@@ -1493,6 +1572,12 @@ let OperatorService = class OperatorService {
|
|
|
1493
1572
|
entry.state = 'failed';
|
|
1494
1573
|
entry.reason = `${tally.failed} task(s) did not finish: ${tally.failedTitles.join(', ')}`;
|
|
1495
1574
|
}
|
|
1575
|
+
else if ((tally.stopped ?? 0) > 0) {
|
|
1576
|
+
entry.state = 'stopped';
|
|
1577
|
+
// The task cut short is counted as what it is: "after 0 task(s)" beside ran 1 read as if nothing ran (live run 2026-10-04).
|
|
1578
|
+
const finished = tally.ran - (tally.stopped ?? 0);
|
|
1579
|
+
entry.reason = `stopped by the operator: ${tally.stopped} task(s) cut short${finished > 0 ? `, after ${finished} that finished` : ''}; ${tally.leftQueued} still queued`;
|
|
1580
|
+
}
|
|
1496
1581
|
else if (tally.paused) {
|
|
1497
1582
|
entry.state = 'stopped';
|
|
1498
1583
|
entry.reason = `paused after ${tally.ran} task(s); ${tally.leftQueued} still queued`;
|
|
@@ -1507,11 +1592,12 @@ let OperatorService = class OperatorService {
|
|
|
1507
1592
|
this.bus.publish({
|
|
1508
1593
|
sessionId: entry.sessionId,
|
|
1509
1594
|
type: 'batch-session-finished',
|
|
1510
|
-
level: entry.state === 'failed' ? 'warn' : 'info',
|
|
1511
|
-
message: `this session ended ${entry.state} inside the batch`,
|
|
1595
|
+
level: entry.state === 'failed' || tally.refused ? 'warn' : 'info',
|
|
1596
|
+
message: tally.refused ? 'this session was refused at its turn inside the batch; its tasks stay queued' : `this session ended ${entry.state} inside the batch`,
|
|
1512
1597
|
data: { batchId: batch.id, ran: entry.ran, failed: entry.failed },
|
|
1513
1598
|
});
|
|
1514
|
-
|
|
1599
|
+
// A session refused at its turn breaks a chain as a failed one does.
|
|
1600
|
+
if ((entry.state === 'failed' || tally.refused) && batch.onFailure === 'stop') {
|
|
1515
1601
|
batch.stopping = true;
|
|
1516
1602
|
this.bus.publish({
|
|
1517
1603
|
sessionId: entry.sessionId,
|
|
@@ -1638,8 +1724,13 @@ let OperatorService = class OperatorService {
|
|
|
1638
1724
|
*/
|
|
1639
1725
|
const repoIssues = [];
|
|
1640
1726
|
check.plan.sessions.forEach((session, i) => {
|
|
1641
|
-
if (session.vcs?.enabled === false)
|
|
1727
|
+
if (session.vcs?.enabled === false) {
|
|
1728
|
+
// Without version control the project folder must still be there (live run 2026-10-03: not looked at).
|
|
1729
|
+
const folder = (session.projectDir ?? '').trim();
|
|
1730
|
+
if (folder && !existsSync(folder))
|
|
1731
|
+
repoIssues.push({ path: `sessions[${i}].projectDir`, message: `${folder} does not exist. Name the session's project folder, or create it.` });
|
|
1642
1732
|
return;
|
|
1733
|
+
}
|
|
1643
1734
|
const dir = plannedRepoDir(session);
|
|
1644
1735
|
const reason = repoUnusableReason(dir);
|
|
1645
1736
|
if (reason)
|
|
@@ -1685,7 +1776,30 @@ let OperatorService = class OperatorService {
|
|
|
1685
1776
|
}
|
|
1686
1777
|
if (repoIssues.length > 0)
|
|
1687
1778
|
return { ok: false, issues: repoIssues, warnings: check.warnings, duplicates: [] };
|
|
1688
|
-
|
|
1779
|
+
/*
|
|
1780
|
+
* A check the runner refuses for its own command line under these Settings: every start of its task
|
|
1781
|
+
* is refused for it, so it is said here, at the check, and not only at the start (live run 2026-10-04:
|
|
1782
|
+
* "check" said ok and the first start was refused). A warning, since the Settings may be changed.
|
|
1783
|
+
*/
|
|
1784
|
+
const cfg = await this.settings.load();
|
|
1785
|
+
const shell = preferredShell(cfg.execution.defaultShell);
|
|
1786
|
+
const others = [cfg.project.rootDir, ...cfg.project.others.map((o) => o.rootDir)];
|
|
1787
|
+
const lineWarnings = [];
|
|
1788
|
+
check.plan.sessions.forEach((session, i) => {
|
|
1789
|
+
const cwd = (session.vcs?.enabled === false ? '' : plannedRepoDir(session)) || (session.projectDir ?? '').trim() || cfg.resolved.cwd;
|
|
1790
|
+
const roots = sessionRoots(cwd, others);
|
|
1791
|
+
session.tasks.forEach((task, j) => {
|
|
1792
|
+
for (const c of task.checks ?? []) {
|
|
1793
|
+
const run = c.run?.trim();
|
|
1794
|
+
if (!run)
|
|
1795
|
+
continue;
|
|
1796
|
+
const why = lineRefusal(run, c.shell ?? shell, c.cwd ? resolve(cwd, c.cwd) : cwd, cfg.execution, roots);
|
|
1797
|
+
if (why)
|
|
1798
|
+
lineWarnings.push(`sessions[${i}].tasks[${j}] ("${task.title}"): the check "${c.name}" is refused by the runner for its own command line, so the task cannot start until it is changed: ${checkRefusalForOperator(why)}.`);
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
});
|
|
1802
|
+
return { ...check, warnings: [...check.warnings, ...lineWarnings], duplicates: await this.findDuplicates(check.plan) };
|
|
1689
1803
|
}
|
|
1690
1804
|
/**
|
|
1691
1805
|
* Sessions the store already holds that this plan would create again, task for task.
|
|
@@ -1918,7 +2032,8 @@ let OperatorService = class OperatorService {
|
|
|
1918
2032
|
const ids = g.sessions.map((x) => x.id);
|
|
1919
2033
|
const first = ids[0] ? await this.store.getSession(ids[0]) : null;
|
|
1920
2034
|
const start = first?.vcsStart;
|
|
1921
|
-
|
|
2035
|
+
// Said once, in the first run after the snapshot: a re-run clears `startedAt`, so earlier attempts count too.
|
|
2036
|
+
const untouched = !!first && !first.tasks.some((t) => t.startedAt || (t.attempts ?? []).some((a) => a.startedAt));
|
|
1922
2037
|
if (start?.kind === 'snapshot' && start.snapshot?.approved && untouched) {
|
|
1923
2038
|
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}"` : ''}`, {
|
|
1924
2039
|
repoDir: g.repoDir,
|
|
@@ -1935,7 +2050,9 @@ let OperatorService = class OperatorService {
|
|
|
1935
2050
|
branch: g.branch,
|
|
1936
2051
|
head: g.head,
|
|
1937
2052
|
...(g.baseBranch ? { baseBranch: g.baseBranch, baseHead: g.baseHead } : {}),
|
|
1938
|
-
|
|
2053
|
+
...(g.workBranch ? { workBranch: g.workBranch } : {}),
|
|
2054
|
+
...(g.carry ? { carry: g.carry } : {}),
|
|
2055
|
+
sessions: g.sessions.map((x) => ({ id: x.id, name: x.name, startFrom: x.startFrom, role: x.role, started: x.started })),
|
|
1939
2056
|
inputPatterns: g.inputPatterns,
|
|
1940
2057
|
inputs: g.inputs.map((e) => e.path),
|
|
1941
2058
|
unrelated: g.unrelated.map((u) => u.path),
|
|
@@ -1943,15 +2060,43 @@ let OperatorService = class OperatorService {
|
|
|
1943
2060
|
}, ids, g.ready ? 'info' : 'warn');
|
|
1944
2061
|
const fixes = g.actions.filter((a) => a.available && a.id !== 'review-inputs').map((a) => a.id);
|
|
1945
2062
|
if (!g.ready && fixes.length > 0) {
|
|
1946
|
-
|
|
2063
|
+
const label = {
|
|
2064
|
+
'snapshot-on-base': `Create starting snapshot on ${g.baseBranch ?? 'the base branch'}`,
|
|
2065
|
+
'snapshot-here': `Create starting snapshot on ${g.branch ?? 'HEAD'}`,
|
|
2066
|
+
'use-current-branch': `Use the current branch instead of ${g.baseBranch ?? 'the base branch'}`,
|
|
2067
|
+
'allow-snapshot': 'Take uncommitted changes as a starting snapshot',
|
|
2068
|
+
};
|
|
2069
|
+
note('snapshot-approval-required', `${g.repoDir}: nothing runs until one of these is pressed under "Prepare version control for this run" on the Sessions page: ${fixes.map((f) => `"${label[f] ?? f}"`).join(', ')}`, { repoDir: g.repoDir, actions: fixes }, ids, 'warn');
|
|
1947
2070
|
}
|
|
1948
2071
|
}
|
|
1949
2072
|
const open = groups.filter((g) => !g.ready);
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2073
|
+
// A task that contradicts itself, said here too: found at its start, it had already cut a branch and used an attempt.
|
|
2074
|
+
const contradictions = [];
|
|
2075
|
+
const reposBefore = new Set();
|
|
2076
|
+
for (const id of sessionIds) {
|
|
2077
|
+
const s = await this.store.getSession(id);
|
|
2078
|
+
const repo = s ? trackedRepoOf(s) : '';
|
|
2079
|
+
const afterOthers = !!repo && reposBefore.has(resolve(repo).toLowerCase());
|
|
2080
|
+
if (repo)
|
|
2081
|
+
reposBefore.add(resolve(repo).toLowerCase());
|
|
2082
|
+
const said = s ? await this.contractRefusal(s, onlyTasks, afterOthers) : null;
|
|
2083
|
+
if (said) {
|
|
2084
|
+
contradictions.push(said);
|
|
2085
|
+
// Said once here as what it is; the refusal below names it again with everything else in the way.
|
|
2086
|
+
note('task-contract-refused', said, { session: id, contract: true }, [id], 'warn');
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
const parts = [
|
|
2090
|
+
...(open.length > 0
|
|
2091
|
+
? [
|
|
2092
|
+
'Version control is not ready for this run, so nothing was opened or sent. ' +
|
|
2093
|
+
open.map((g) => `${g.repoDir}: ${g.problem ?? 'it needs your approval'}`).join(' ') +
|
|
2094
|
+
' Fix it under "Prepare version control for this run" on the Sessions page.',
|
|
2095
|
+
]
|
|
2096
|
+
: []),
|
|
2097
|
+
...(contradictions.length > 0 ? [`${open.length > 0 ? '' : 'Nothing was opened or sent. '}${contradictions.join(' ')}`] : []),
|
|
2098
|
+
];
|
|
2099
|
+
const reason = parts.length === 0 ? null : parts.join(' ');
|
|
1955
2100
|
if (reason)
|
|
1956
2101
|
note('run-preflight-refused', reason, { repositories: open.map((g) => g.repoDir) }, sessionIds, 'warn');
|
|
1957
2102
|
else
|
|
@@ -1959,11 +2104,98 @@ let OperatorService = class OperatorService {
|
|
|
1959
2104
|
await appendRunLog(cfg.resolved.runsDir, runId, log).catch(() => undefined);
|
|
1960
2105
|
return reason;
|
|
1961
2106
|
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Why the session's next task cannot start because it contradicts itself — read-only and scoped, or
|
|
2109
|
+
* a file check that needs a change the task may not make — or null. The part of the contract that
|
|
2110
|
+
* depends on the branch the runner will choose is checked at the task's start, once that is known.
|
|
2111
|
+
*/
|
|
2112
|
+
async contractRefusal(session, onlyTasks, afterOthers = false) {
|
|
2113
|
+
const next = queuedToRun(session, onlyTasks)[0];
|
|
2114
|
+
if (!next)
|
|
2115
|
+
return null;
|
|
2116
|
+
const cfg = await this.settings.load();
|
|
2117
|
+
const work = workingDirFor(session, cfg.resolved.cwd);
|
|
2118
|
+
if (isWorkingDirProblem(work))
|
|
2119
|
+
return null;
|
|
2120
|
+
/*
|
|
2121
|
+
* The branch the task will be on, where that is known before the run: so a check that expects another
|
|
2122
|
+
* branch is refused here too, not after the browser opened and a branch was cut (live run 2026-10-04).
|
|
2123
|
+
*/
|
|
2124
|
+
const prefix = session.vcs?.branchPrefix || 'cop/';
|
|
2125
|
+
const attempt = next.attempt ?? 1;
|
|
2126
|
+
const branch = !session.vcs?.enabled
|
|
2127
|
+
? undefined
|
|
2128
|
+
: session.vcs.startFrom === 'existing-branch'
|
|
2129
|
+
? session.vcs.existingBranch?.trim() || undefined
|
|
2130
|
+
: session.vcs.branchMode === 'per-session'
|
|
2131
|
+
? sessionBranchName(session)
|
|
2132
|
+
: next.vcsPlan?.branch?.trim()
|
|
2133
|
+
? plannedBranchName(next.vcsPlan.branch, prefix, attempt)
|
|
2134
|
+
: branchNameFrom([session.name, next.title, attempt > 1 ? `a${attempt}` : undefined], prefix);
|
|
2135
|
+
const files = await this.contractTree(session, afterOthers).catch(() => 'unknown');
|
|
2136
|
+
// Input files are carried onto a first start from their last capture: not judged before it (see `contract.ts`).
|
|
2137
|
+
const carried = !session.vcsBaseCommit ? inputSettings(session.vcs)?.patterns : undefined;
|
|
2138
|
+
const conflicts = await contractConflicts(next, work.cwd, repoDirOf(session) || work.cwd, { branch, files, carried }).catch(() => []);
|
|
2139
|
+
// A plan check the runner refuses for its own command line can never run: the task could never pass.
|
|
2140
|
+
const roots = sessionRoots(work.cwd, [cfg.project.rootDir, ...cfg.project.others.map((o) => o.rootDir)]);
|
|
2141
|
+
const shell = preferredShell(cfg.execution.defaultShell);
|
|
2142
|
+
for (const c of next.checks ?? []) {
|
|
2143
|
+
const run = c.run?.trim();
|
|
2144
|
+
if (!run)
|
|
2145
|
+
continue;
|
|
2146
|
+
const why = lineRefusal(run, c.shell ?? shell, c.cwd ? resolve(work.cwd, c.cwd) : work.cwd, cfg.execution, roots);
|
|
2147
|
+
if (why)
|
|
2148
|
+
conflicts.push(`The check "${c.name}" is refused by the runner for its own command line, so it can never run and the task could never pass: ${checkRefusalForOperator(why)}.`);
|
|
2149
|
+
}
|
|
2150
|
+
return conflicts.length === 0
|
|
2151
|
+
? null
|
|
2152
|
+
: `The task "${next.title}" of "${session.name}" contradicts itself, so it was not started and stays queued: ${conflicts.join(' ')} Change the prompt, the checks, the scope or read-only on the session's page, and start again.`;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Where the next task's file checks can be judged before the run: see `ContractTree`. The folder holds
|
|
2156
|
+
* the task's tree only when nothing is still to happen to it first — no update from a remote, no
|
|
2157
|
+
* earlier session of the batch in the same repository (`afterOthers`) — and otherwise the commit the
|
|
2158
|
+
* task starts from is read where it is already known, or the checks wait for the task's start.
|
|
2159
|
+
*/
|
|
2160
|
+
async contractTree(session, afterOthers) {
|
|
2161
|
+
const repo = trackedRepoOf(session);
|
|
2162
|
+
if (!repo)
|
|
2163
|
+
return 'tree';
|
|
2164
|
+
if (session.vcsBaseCommit) {
|
|
2165
|
+
if (session.vcs?.branchMode !== 'per-session')
|
|
2166
|
+
return { ref: session.vcsBaseCommit };
|
|
2167
|
+
// A started session goes on from where its last task left its branch: the folder, when still on it.
|
|
2168
|
+
const last = [...session.tasks].reverse().find((t) => t.vcs?.branch)?.vcs?.branch;
|
|
2169
|
+
const now = await git(repo, ['branch', '--show-current']);
|
|
2170
|
+
return last && now.ok && now.stdout === last ? 'tree' : 'unknown';
|
|
2171
|
+
}
|
|
2172
|
+
if (session.vcsStart?.commit)
|
|
2173
|
+
return { ref: session.vcsStart.commit };
|
|
2174
|
+
const how = session.vcs?.startFrom ?? 'head';
|
|
2175
|
+
if (how === 'head')
|
|
2176
|
+
return afterOthers ? 'unknown' : 'tree';
|
|
2177
|
+
if (how === 'previous-session')
|
|
2178
|
+
return 'unknown';
|
|
2179
|
+
const branch = how === 'existing-branch' ? session.vcs?.existingBranch?.trim() : session.vcs?.baseBranch?.trim() || 'main';
|
|
2180
|
+
if (!branch)
|
|
2181
|
+
return 'unknown';
|
|
2182
|
+
// Brought up to its remote first: what that brings is known only then.
|
|
2183
|
+
if (session.vcs?.updateFromRemote !== false) {
|
|
2184
|
+
const remotes = await git(repo, ['remote']);
|
|
2185
|
+
if (!remotes.ok || remotes.stdout.trim() !== '')
|
|
2186
|
+
return 'unknown';
|
|
2187
|
+
}
|
|
2188
|
+
const tip = await git(repo, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}^{commit}`]);
|
|
2189
|
+
return tip.ok && tip.stdout ? { ref: tip.stdout } : 'unknown';
|
|
2190
|
+
}
|
|
1962
2191
|
/**
|
|
1963
2192
|
* The same rule for one session that has not started, when its turn comes: see `RunDeps.vcsGate`.
|
|
1964
2193
|
* A session that has started is past its first preparation; its tasks are prepared as they come.
|
|
1965
2194
|
*/
|
|
1966
2195
|
async sessionVcsGate(session, onlyTasks) {
|
|
2196
|
+
const contradiction = await this.contractRefusal(session, onlyTasks);
|
|
2197
|
+
if (contradiction)
|
|
2198
|
+
return contradiction;
|
|
1967
2199
|
if (!session.vcs?.enabled || session.vcsBaseCommit)
|
|
1968
2200
|
return null;
|
|
1969
2201
|
if (queuedToRun(session, onlyTasks).length === 0)
|
|
@@ -1971,7 +2203,9 @@ let OperatorService = class OperatorService {
|
|
|
1971
2203
|
const [group] = await runVcsPreflight([session], () => this.store.listSessions());
|
|
1972
2204
|
if (!group || group.ready)
|
|
1973
2205
|
return null;
|
|
1974
|
-
|
|
2206
|
+
// The panel is named only where it has a fix to offer: for a chain waiting on its predecessor the remedy is that one's Continue (live run 2026-10-04).
|
|
2207
|
+
const panel = group.actions.some((a) => a.available && a.id !== 'review-inputs');
|
|
2208
|
+
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'}${panel ? ' Fix it under "Prepare version control for this run" on the Sessions page.' : ''}`;
|
|
1975
2209
|
}
|
|
1976
2210
|
/** Recorded in the run's log just before the browser is opened for it: see `runPreflight`. */
|
|
1977
2211
|
async browserRequested(runId, sessionIds) {
|
|
@@ -1996,6 +2230,15 @@ let OperatorService = class OperatorService {
|
|
|
1996
2230
|
sessions.push(s);
|
|
1997
2231
|
}
|
|
1998
2232
|
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());
|
|
2233
|
+
// Kept on every session the action was for, with what it did: see `appendSessionLog`.
|
|
2234
|
+
if (done.ok && action !== 'review-inputs') {
|
|
2235
|
+
const cfg = await this.settings.load();
|
|
2236
|
+
for (const s of sessions.filter((x) => normaliseDir(repoDirOf(x)) === normaliseDir(repoDir))) {
|
|
2237
|
+
const message = `run screen: "${action}" in ${repoDir}: ${done.result}`;
|
|
2238
|
+
await appendSessionLog(cfg.resolved.runsDir, s.id, { type: 'run-screen-action', message, data: { action, repoDir, before: { startFrom: s.vcs?.startFrom, baseBranch: s.vcs?.baseBranch } } }).catch(() => undefined);
|
|
2239
|
+
this.bus.publish({ sessionId: s.id, type: 'run-screen-action', level: 'info', message, data: { action, repoDir } });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
1999
2242
|
return done.ok ? { ok: true, result: done.result } : { ok: false, problem: done.problem };
|
|
2000
2243
|
}
|
|
2001
2244
|
/** Whether version control can do its job in this session, asked before a run. */
|
|
@@ -2061,7 +2304,30 @@ let OperatorService = class OperatorService {
|
|
|
2061
2304
|
const refused = await this.restoreRefusal(session);
|
|
2062
2305
|
if (refused)
|
|
2063
2306
|
return { ok: false, problem: refused };
|
|
2064
|
-
|
|
2307
|
+
const done = await restoreToBase(session, task, this.bus);
|
|
2308
|
+
// On record with the operator's other actions: it made a branch and moved the checkout (live run 2026-10-04).
|
|
2309
|
+
if (done.ok) {
|
|
2310
|
+
const cfg = await this.settings.load();
|
|
2311
|
+
await appendSessionLog(cfg.resolved.runsDir, session.id, { type: 'restore', message: `restored the code as it was before "${task.title}" on ${done.branch} at ${(done.commit ?? '').slice(0, 8)}`, data: { taskId, branch: done.branch, commit: done.commit, leftBehind: done.leftBehind, keptOn: done.keptOn } }).catch(() => undefined);
|
|
2312
|
+
}
|
|
2313
|
+
/*
|
|
2314
|
+
* In per-session mode the session's one branch is where its next task goes, so a Restore moves the
|
|
2315
|
+
* session onto the restore branch, as "Run again from here" does. Without that the next run checked
|
|
2316
|
+
* the old session branch out again and the restore did not hold (live run 2026-10-03).
|
|
2317
|
+
*/
|
|
2318
|
+
if (done.ok && done.branch && session.vcs?.branchMode === 'per-session' && session.vcs.startFrom !== 'existing-branch') {
|
|
2319
|
+
const left = sessionBranchName(session);
|
|
2320
|
+
await this.store.updateSession(session.id, (s) => {
|
|
2321
|
+
if (s.vcs) {
|
|
2322
|
+
s.vcs.branchName = done.branch;
|
|
2323
|
+
s.vcs.branchNameExact = true;
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
this.bus.publish({ sessionId: session.id, taskId: task.id, type: 'vcs-session-branch', level: 'info',
|
|
2327
|
+
message: `the session carries on from ${done.branch}, the code as it was before "${task.title}"; ${left} keeps the work that came after`,
|
|
2328
|
+
data: { branch: done.branch, left } });
|
|
2329
|
+
}
|
|
2330
|
+
return done;
|
|
2065
2331
|
}
|
|
2066
2332
|
/**
|
|
2067
2333
|
* Why the repository of this session cannot be moved to another branch now, or null when it can.
|
|
@@ -2219,6 +2485,16 @@ let OperatorService = class OperatorService {
|
|
|
2219
2485
|
keptOn: preview.keptOn,
|
|
2220
2486
|
});
|
|
2221
2487
|
}
|
|
2488
|
+
/*
|
|
2489
|
+
* The plan is "ok" only when the restart it describes can happen: a refused restore refuses the
|
|
2490
|
+
* restart, as `restartFrom` does and the dialog says, so a caller reading only `ok` is not told
|
|
2491
|
+
* otherwise (live run 2026-10-04: ok true beside its only restore refused).
|
|
2492
|
+
*/
|
|
2493
|
+
const refusedRestores = plan.restores.filter((r) => !r.ok);
|
|
2494
|
+
if (plan.ok && refusedRestores.length > 0) {
|
|
2495
|
+
plan.ok = false;
|
|
2496
|
+
plan.problem = refusedRestores.map((r) => `${r.repoDir}: ${r.problem ?? 'the restore cannot be made'}`).join(' ');
|
|
2497
|
+
}
|
|
2222
2498
|
return plan;
|
|
2223
2499
|
}
|
|
2224
2500
|
/**
|
|
@@ -2239,7 +2515,9 @@ let OperatorService = class OperatorService {
|
|
|
2239
2515
|
if (this.batch?.running)
|
|
2240
2516
|
return { ...idle, reason: 'A run is in progress. Stop it first.' };
|
|
2241
2517
|
const plan = await this.restartPlan(sessionId, taskId);
|
|
2242
|
-
|
|
2518
|
+
// A refused restore stops the restart only when the restore is asked for: `restore: false` queues without one.
|
|
2519
|
+
const onlyRestoreRefused = plan.tasks.length > 0 && plan.restores.some((r) => !r.ok);
|
|
2520
|
+
if (!plan.ok && !(opts.restore === false && onlyRestoreRefused))
|
|
2243
2521
|
return { ...idle, reason: plan.problem };
|
|
2244
2522
|
if (plan.sessions.some((s) => this.running.has(s.id))) {
|
|
2245
2523
|
return { ...idle, reason: 'One of these sessions is running on its own. Stop it first.' };
|
|
@@ -2354,8 +2632,9 @@ let OperatorService = class OperatorService {
|
|
|
2354
2632
|
taskId,
|
|
2355
2633
|
type: 'run-restarted',
|
|
2356
2634
|
level: 'info',
|
|
2357
|
-
message:
|
|
2358
|
-
(restored.length > 0 ? `, after taking ${restored.length} repository(ies) back` : '')
|
|
2635
|
+
message: `${opts.start === false ? 'prepared to start again' : 'starting again'} from "${plan.from.title}": ${plan.tasks.length} task(s) in ${plan.sessions.length} session(s)` +
|
|
2636
|
+
(restored.length > 0 ? `, after taking ${restored.length} repository(ies) back` : '') +
|
|
2637
|
+
(opts.start === false ? '; press a run button to start' : ''),
|
|
2359
2638
|
data: { requeued, restored },
|
|
2360
2639
|
});
|
|
2361
2640
|
if (opts.start === false)
|
|
@@ -2489,16 +2768,17 @@ let OperatorService = class OperatorService {
|
|
|
2489
2768
|
* "Prepare the folder from the remote main branch", first half: fetches and says what would be
|
|
2490
2769
|
* kept where and what the folder would be. See `vcs/prepareFromRemote.ts`.
|
|
2491
2770
|
*/
|
|
2492
|
-
async preparePreview(dir) {
|
|
2771
|
+
async preparePreview(dir, fetch = true) {
|
|
2493
2772
|
await this.init();
|
|
2494
2773
|
await this.knownProjectFolder(dir, 'it is not prepared from here');
|
|
2495
|
-
|
|
2774
|
+
// The operator pressed the button and is looking: the remote may ask for a password through Git's own window.
|
|
2775
|
+
return await planPrepare(resolve(dir.trim()), { fetch, askpass: fetch ? await askpassProgram() : null });
|
|
2496
2776
|
}
|
|
2497
2777
|
/**
|
|
2498
2778
|
* The second half: does it, if the folder is still what the preview showed. Refused while anything
|
|
2499
2779
|
* runs, or is being started, in that folder or one around it: it moves the checkout.
|
|
2500
2780
|
*/
|
|
2501
|
-
async prepareProject(dir, fingerprint) {
|
|
2781
|
+
async prepareProject(dir, fingerprint, names = {}) {
|
|
2502
2782
|
await this.init();
|
|
2503
2783
|
await this.knownProjectFolder(dir, 'it is not prepared from here');
|
|
2504
2784
|
if (this.batch?.running)
|
|
@@ -2508,7 +2788,18 @@ let OperatorService = class OperatorService {
|
|
|
2508
2788
|
const other = await this.runningIn(dir);
|
|
2509
2789
|
if (other)
|
|
2510
2790
|
return { ok: false, problem: `Session "${other.name}" is running in this folder. Stop it before preparing the folder; nothing was changed.` };
|
|
2511
|
-
|
|
2791
|
+
const done = await prepareFromRemote(resolve(dir.trim()), String(fingerprint ?? ''), { names });
|
|
2792
|
+
// Kept on every session working in that folder: it moved the checkout and made saved branches.
|
|
2793
|
+
if (done.ok) {
|
|
2794
|
+
const cfg = await this.settings.load();
|
|
2795
|
+
const here = normaliseDir(dir);
|
|
2796
|
+
for (const s of (await this.store.listSessions()).filter((x) => normaliseDir(repoDirOf(x)) === here)) {
|
|
2797
|
+
const message = `the folder was prepared from ${done.target}: ${done.result}`;
|
|
2798
|
+
await appendSessionLog(cfg.resolved.runsDir, s.id, { type: 'folder-prepared', message, data: { target: done.target, commit: done.commit, branch: done.branch, saved: done.saved, removedFromFolder: done.removedFromFolder } }).catch(() => undefined);
|
|
2799
|
+
this.bus.publish({ sessionId: s.id, type: 'folder-prepared', level: 'info', message, data: { target: done.target, commit: done.commit, saved: done.saved } });
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return done;
|
|
2512
2803
|
}
|
|
2513
2804
|
// --- the project being worked on ---------------------------------------------------------
|
|
2514
2805
|
/**
|
|
@@ -2820,6 +3111,10 @@ let OperatorService = class OperatorService {
|
|
|
2820
3111
|
reason: a.reason,
|
|
2821
3112
|
runId: a.runId,
|
|
2822
3113
|
branch: a.vcs?.branch,
|
|
3114
|
+
// Why the runner stopped this attempt, whether it was a fresh-chat retry, and its commit (live run 2026-10-03).
|
|
3115
|
+
stopCode: a.stopCode,
|
|
3116
|
+
freshRetry: a.freshRetry,
|
|
3117
|
+
commit: a.vcs?.commit,
|
|
2823
3118
|
changedFiles: a.vcs?.commit && a.vcs.baseCommit ? (a.vcs.files?.length ?? 0) : undefined,
|
|
2824
3119
|
};
|
|
2825
3120
|
}),
|