copilot-operator 0.1.10 → 0.1.12
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 +12 -1
- package/dist/src/api/operator.service.js +92 -7
- package/dist/src/config/schema.js +20 -3
- package/dist/src/context/desktopMirror.js +34 -12
- package/dist/src/context/projectMirror.js +69 -1
- package/dist/src/orchestrator/contract.js +56 -1
- package/dist/src/orchestrator/taskRunner.js +160 -13
- package/dist/src/plan/brief.js +20 -8
- package/dist/src/plan/systemGuide.js +74 -10
- package/dist/src/session/compose.js +17 -2
- package/dist/src/session/exportRecord.js +8 -1
- package/dist/src/session/handoff.js +41 -3
- package/dist/src/session/model.js +9 -0
- package/dist/src/session/store.js +11 -7
- package/dist/src/session/story.js +74 -18
- package/dist/src/transport/chatTransport.js +1 -0
- package/dist/src/transport/copilotTransport.js +89 -8
- package/dist/src/vcs/git.js +15 -0
- package/dist/src/vcs/syncCommand.js +105 -0
- package/dist/src/vcs/taskVcs.js +29 -6
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/295-04151ded0741d18d.js +1 -0
- package/dist/web/_next/static/chunks/36-c3dd06c6a459c114.js +1 -0
- package/dist/web/_next/static/chunks/549-a5a59a94f92bb3eb.js +1 -0
- package/dist/web/_next/static/chunks/867-b64c436f5b25f39e.js +1 -0
- package/dist/web/_next/static/chunks/app/defaults/page-5f180a88398be4c9.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-854e3dde672e38a2.js +1 -0
- package/dist/web/_next/static/chunks/app/page-13572a726de0b7db.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-cc99401da5173b36.js +1 -0
- package/dist/web/_next/static/css/{20c6b36996df5e4c.css → a0642fdb8adf9edb.css} +1 -1
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +12 -12
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +12 -12
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +12 -12
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +12 -12
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +12 -12
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +12 -12
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +12 -12
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +12 -12
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +12 -12
- package/package.json +7 -3
- package/dist/web/_next/static/chunks/36-17a3bcb0c2553c7c.js +0 -1
- package/dist/web/_next/static/chunks/549-43d1a5681752fbd4.js +0 -1
- package/dist/web/_next/static/chunks/867-5ae3adf431bd10a2.js +0 -1
- package/dist/web/_next/static/chunks/app/defaults/page-0f88c46dcd9dd52c.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-a9f2060f86a4a10e.js +0 -1
- package/dist/web/_next/static/chunks/app/page-2ac6b67f4ddcb808.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-fb634b39ce9ba4a3.js +0 -1
- /package/dist/web/_next/static/{d6YL07r-SeksvVLrCSZ3o → bcBE4tY_aNh6kzLzN1luP}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{d6YL07r-SeksvVLrCSZ3o → bcBE4tY_aNh6kzLzN1luP}/_ssgManifest.js +0 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createHash } from 'node:crypto';
|
|
14
14
|
import { readFile, mkdir, writeFile, appendFile } from 'node:fs/promises';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
-
import { createTransport } from '../transport/chatTransport.js';
|
|
16
|
+
import { createTransport, isReplyTimeout } from '../transport/chatTransport.js';
|
|
17
17
|
import { buildChatName, chatCode, savePointer } from '../transport/chatSession.js';
|
|
18
18
|
import { parseReply, formatErrorMessage, findLikelyDamage, damageGuidance } from '../protocol/parser.js';
|
|
19
19
|
import { resolveDeviations, describeDeviations, mergeDisputes, describeDisputes } from '../protocol/replySchema.js';
|
|
@@ -38,12 +38,13 @@ import { writeReport } from '../exec/reportFile.js';
|
|
|
38
38
|
import { Pacer } from '../util/pacing.js';
|
|
39
39
|
import { RunLog } from '../log/runLog.js';
|
|
40
40
|
import { composeOpening, READ_ONLY_NOTE } from '../session/compose.js';
|
|
41
|
+
import { MIN_TRIED_APPROACHES } from '../protocol/replySchema.js';
|
|
41
42
|
import { enforceScope, scopeMessage, scopeNote } from '../vcs/scope.js';
|
|
42
43
|
import { composeHandoff } from '../session/handoff.js';
|
|
43
44
|
import { ProgressWatch } from './progress.js';
|
|
44
|
-
import { contractConflicts } from './contract.js';
|
|
45
|
+
import { contractConflicts, readsTreeClean } from './contract.js';
|
|
45
46
|
import { treeFingerprint } from '../vcs/git.js';
|
|
46
|
-
import { mirrorProject, describeMirror } from '../context/projectMirror.js';
|
|
47
|
+
import { mirrorProject, describeMirror, MirrorSelectionError } from '../context/projectMirror.js';
|
|
47
48
|
import { projectNameFor, projectTargetDir, removeLegacyFlatMirror } from '../context/desktopMirror.js';
|
|
48
49
|
import { prepareForTask, commitTaskResult, repoDirOf } from '../vcs/taskVcs.js';
|
|
49
50
|
import { exportMachine, writeAttemptRecord } from '../session/exports.js';
|
|
@@ -185,11 +186,12 @@ export async function openBrowser(cfg, bus, transportDir, sessionId) {
|
|
|
185
186
|
'verification-cleared': 'Verification cleared, continuing.',
|
|
186
187
|
'error-banner': 'The chat reported a transient error; reloading the page.',
|
|
187
188
|
'reply-files-ignored': 'The reply came with a file attached. It was not downloaded and will not be run: the runner takes only the text of a reply.',
|
|
189
|
+
'model-menu-stuck': 'The model menu would not close — Escape, the menu button and a click outside all left it open. Close it in the Edge window; the run goes on.',
|
|
188
190
|
};
|
|
189
191
|
bus.publish({
|
|
190
192
|
sessionId,
|
|
191
193
|
type: `browser:${event}`,
|
|
192
|
-
level: event === 'verification-required' || event === 'reply-files-ignored' ? 'warn' : 'info',
|
|
194
|
+
level: event === 'verification-required' || event === 'reply-files-ignored' || event === 'model-menu-stuck' ? 'warn' : 'info',
|
|
193
195
|
message: spoken[event],
|
|
194
196
|
data: detail,
|
|
195
197
|
});
|
|
@@ -361,6 +363,15 @@ export async function runTask(transport, session, task, deps) {
|
|
|
361
363
|
let runnerNotes = [];
|
|
362
364
|
/** Steps the chat sent that were not run, for the handoff at the end. */
|
|
363
365
|
const notRun = [];
|
|
366
|
+
/**
|
|
367
|
+
* How many different approaches "blocked" needs, from Settings, never below the format's own two.
|
|
368
|
+
* A "blocked" with fewer is sent back; after `MAX_EARLY_BLOCKS` of them in a row with nothing tried
|
|
369
|
+
* in between the verdict is accepted, so a chat that cannot think of anything else does not spend
|
|
370
|
+
* the rest of the iterations saying so.
|
|
371
|
+
*/
|
|
372
|
+
const minApproaches = Math.max(MIN_TRIED_APPROACHES, cfg.limits.minApproachesBeforeBlocked ?? MIN_TRIED_APPROACHES);
|
|
373
|
+
const MAX_EARLY_BLOCKS = 3;
|
|
374
|
+
let earlyBlocks = 0;
|
|
364
375
|
/** What went wrong on the way, counted for the register's figures. See `TaskStats`. */
|
|
365
376
|
const stats = { formatErrors: 0, doneRejected: 0, repeatsRefused: 0, stepsRefused: 0, operatorStops: 0, reviewRejections: 0, scopeReverts: 0 };
|
|
366
377
|
/** Loops the older guards do not see; see `progress.ts`. */
|
|
@@ -373,6 +384,15 @@ export async function runTask(transport, session, task, deps) {
|
|
|
373
384
|
* lacks a shell. Written by `finish`. See `Task.stopCode`.
|
|
374
385
|
*/
|
|
375
386
|
let stopCode;
|
|
387
|
+
/** The setting whose limit ended the attempt, when one did. Written by `finish`. See `TaskLimit`. */
|
|
388
|
+
let limitHit;
|
|
389
|
+
/**
|
|
390
|
+
* Checks that the working tree is clean, decided after the runner's commit rather than at the gate.
|
|
391
|
+
* The chat may not commit, so before that commit such a check could only fail (2026-09-30: failed,
|
|
392
|
+
* and then the commit left the tree clean). Set by the gate, run by `finish`. See `readsTreeClean`.
|
|
393
|
+
*/
|
|
394
|
+
let afterCommitChecks = [];
|
|
395
|
+
let runAfterCommit = null;
|
|
376
396
|
/**
|
|
377
397
|
* Checks earlier reviews gave with their findings — carried over from every attempt before
|
|
378
398
|
* this one, and grown by this one. See `derivedChecks.ts` for the three rules.
|
|
@@ -445,6 +465,40 @@ export async function runTask(transport, session, task, deps) {
|
|
|
445
465
|
sink.event('vcs-error', { error: String(e) }, `version control failed after the task: ${e.message}`, 'warn');
|
|
446
466
|
return undefined;
|
|
447
467
|
});
|
|
468
|
+
/*
|
|
469
|
+
* The checks about a clean tree, now that the runner has committed. A done task whose tree is
|
|
470
|
+
* still not clean after the commit has failed; any other ending keeps its own reason, and the
|
|
471
|
+
* results are recorded all the same.
|
|
472
|
+
*/
|
|
473
|
+
let afterCommitResults = [];
|
|
474
|
+
if (afterCommitChecks.length > 0 && runAfterCommit) {
|
|
475
|
+
const outcomes = await runAfterCommit(afterCommitChecks).catch((e) => {
|
|
476
|
+
sink.event('checks-after-commit-error', { error: String(e) }, `the checks after the commit could not run: ${e.message}`, 'warn');
|
|
477
|
+
return [];
|
|
478
|
+
});
|
|
479
|
+
afterCommitResults = outcomes.map((o) => ({ name: `${o.check.name} (after the commit)`, passed: o.passed, detail: o.detail }));
|
|
480
|
+
for (const o of outcomes) {
|
|
481
|
+
sink.event(o.passed ? 'check-passed' : 'check-failed', { name: o.check.name, detail: o.detail, afterCommit: true }, `${o.passed ? 'passed' : 'FAILED'} after the commit: ${o.check.name} — ${o.detail}`, o.passed ? 'info' : 'warn');
|
|
482
|
+
}
|
|
483
|
+
const failedAfter = outcomes.filter((o) => !o.passed);
|
|
484
|
+
if (status === 'done' && failedAfter.length > 0) {
|
|
485
|
+
status = 'failed';
|
|
486
|
+
reason =
|
|
487
|
+
`after the runner's commit${vcsAfter?.commit ? ` (${vcsAfter.commit.slice(0, 8)})` : ''}${vcsAfter?.problem ? `, which did not happen: ${vcsAfter.problem}` : ''}, ` +
|
|
488
|
+
`${failedAfter.length} check(s) about the working tree still failed: ${failedAfter.map((o) => `${o.check.name} (${o.detail})`).join('; ')}`;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/*
|
|
492
|
+
* What the task said about the repository was written before the commit. When it did not end
|
|
493
|
+
* done and the runner then committed its changes, the reason says so, so "not committed, the
|
|
494
|
+
* tree is dirty" is not left standing beside a commit on the record.
|
|
495
|
+
*/
|
|
496
|
+
if (status !== 'done' && vcsAfter?.commit && vcsAfter.commit !== task.vcs?.commit) {
|
|
497
|
+
reason =
|
|
498
|
+
`${reason ?? ''} After that, the runner committed the task's changes as ${vcsAfter.commit.slice(0, 8)} on ${vcsAfter.branch}` +
|
|
499
|
+
`${vcsAfter.afterCommit ? (vcsAfter.afterCommit.clean ? '; the working tree is clean' : `; still uncommitted: ${vcsAfter.afterCommit.changed.slice(0, 8).join(', ')}`) : ''}.`;
|
|
500
|
+
reason = reason.trim();
|
|
501
|
+
}
|
|
448
502
|
if (vcsAfter?.branch) {
|
|
449
503
|
await record('VERSION CONTROL', [
|
|
450
504
|
`branch : ${vcsAfter.branch}`,
|
|
@@ -476,11 +530,26 @@ export async function runTask(transport, session, task, deps) {
|
|
|
476
530
|
t.stats = stats;
|
|
477
531
|
if (stopCode)
|
|
478
532
|
t.stopCode = stopCode;
|
|
533
|
+
if (limitHit)
|
|
534
|
+
t.limit = limitHit;
|
|
535
|
+
if (afterCommitResults.length > 0)
|
|
536
|
+
t.checkResults = [...(t.checkResults ?? []), ...afterCommitResults];
|
|
537
|
+
// A review that was asked for and never ran says why, instead of reading as zero rounds.
|
|
538
|
+
if (!t.review && (task.reviewEnabled ?? session.review?.enabled ?? true) && status !== 'done') {
|
|
539
|
+
t.review = {
|
|
540
|
+
verdict: 'skipped',
|
|
541
|
+
rounds: 0,
|
|
542
|
+
stepsRun: 0,
|
|
543
|
+
skippedBecause: `the task ended ${status} before its work reached the review; the review runs only on work reported done whose checks pass`,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
479
546
|
});
|
|
480
547
|
// The end in one fixed shape, read off the record just written. See `session/handoff.ts`.
|
|
481
548
|
await setTask((t) => {
|
|
482
549
|
t.handoff = composeHandoff(t, notRun);
|
|
483
550
|
});
|
|
551
|
+
// Its last changes, checks and commit included, go to the Desktop copies too.
|
|
552
|
+
deps.onWorkChanged?.();
|
|
484
553
|
sink.event('task-finished', { status, reason, iterations }, `task "${task.title}" ${status}${reason ? `: ${reason}` : ''}`);
|
|
485
554
|
await log.close();
|
|
486
555
|
/*
|
|
@@ -634,7 +703,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
634
703
|
* finding the chat could never have fixed. The contradiction is named instead, now, and nothing
|
|
635
704
|
* is run. See `orchestrator/contract.ts`.
|
|
636
705
|
*/
|
|
637
|
-
const conflicts = await contractConflicts(task, work.cwd, repoDirOf(session) || work.cwd
|
|
706
|
+
const conflicts = await contractConflicts(task, work.cwd, repoDirOf(session) || work.cwd, {
|
|
707
|
+
branch: prepared.vcs.problem ? undefined : prepared.vcs.branch,
|
|
708
|
+
});
|
|
638
709
|
if (conflicts.length > 0) {
|
|
639
710
|
stopCode = 'contract-conflict';
|
|
640
711
|
sink.event('contract-conflict', { conflicts }, `the task contradicts itself: ${conflicts.join(' | ')}`, 'error');
|
|
@@ -648,8 +719,14 @@ export async function runTask(transport, session, task, deps) {
|
|
|
648
719
|
const projectName = projectNameFor(session.mirror.rootDir, cfg);
|
|
649
720
|
const targetDir = projectTargetDir(cfg, projectName);
|
|
650
721
|
await removeLegacyFlatMirror(cfg).catch(() => undefined);
|
|
722
|
+
/*
|
|
723
|
+
* A selection that cannot be copied stops the task before anything is sent: the chat would
|
|
724
|
+
* otherwise work without the files it was promised. The copies already there are kept.
|
|
725
|
+
*/
|
|
651
726
|
const result = await mirrorProject({
|
|
652
727
|
rootDir: session.mirror.rootDir,
|
|
728
|
+
// `rules-engine` in a plan usually means the repository's folder, wherever the root is.
|
|
729
|
+
alsoUnder: [repoDirOf(session)],
|
|
653
730
|
includeDirs: session.mirror.includeDirs.length ? session.mirror.includeDirs : ['.'],
|
|
654
731
|
excludeDirs: session.mirror.excludeDirs,
|
|
655
732
|
targetDir,
|
|
@@ -662,7 +739,11 @@ export async function runTask(transport, session, task, deps) {
|
|
|
662
739
|
ignoreDirs: cfg.projectMirror.ignoreDirs,
|
|
663
740
|
includeEnvFiles: session.mirror.includeEnvFiles ?? cfg.projectMirror.includeEnvFiles,
|
|
664
741
|
maxFileBytes: cfg.projectMirror.maxFileBytes,
|
|
665
|
-
});
|
|
742
|
+
}).catch((e) => (e instanceof MirrorSelectionError ? e : Promise.reject(e)));
|
|
743
|
+
if (result instanceof MirrorSelectionError) {
|
|
744
|
+
sink.event('mirror-refused', { error: result.message }, result.message, 'error');
|
|
745
|
+
return await finish('failed', `${result.message} Nothing was sent to the chat. Fix the files to attach and queue the task again.`);
|
|
746
|
+
}
|
|
666
747
|
sink.event('mirror', { targetDir, ...result }, `project mirror: ${describeMirror(result)}`);
|
|
667
748
|
if (session.mirror.includeEnvFiles) {
|
|
668
749
|
const envCount = Object.keys(result.mapping).filter((p) => /(^|\/)\.env(\.|$)/i.test(p)).length;
|
|
@@ -695,6 +776,10 @@ export async function runTask(transport, session, task, deps) {
|
|
|
695
776
|
vcsNote: prepared.note,
|
|
696
777
|
readOnlyNote: task.readOnly ? READ_ONLY_NOTE : undefined,
|
|
697
778
|
scopeNote: scopeNote(scope, scopeEnforced),
|
|
779
|
+
approachesNote: minApproaches > MIN_TRIED_APPROACHES
|
|
780
|
+
? `## Giving up\n\nThis task may end with status "blocked" only after at least ${minApproaches} genuinely different approaches, ` +
|
|
781
|
+
`each listed in "tried". With fewer, the runner asks you for another approach instead of accepting it.`
|
|
782
|
+
: undefined,
|
|
698
783
|
shellNote: shellNote(shells, defaultShell),
|
|
699
784
|
});
|
|
700
785
|
await setTask((t) => {
|
|
@@ -771,14 +856,21 @@ export async function runTask(transport, session, task, deps) {
|
|
|
771
856
|
* happened, and a task cannot be trusted to answer that about itself.
|
|
772
857
|
*/
|
|
773
858
|
const gateOnChecks = async () => {
|
|
774
|
-
const
|
|
859
|
+
const every = [...(task.checks ?? []), ...activeChecks(reviewChecks)];
|
|
860
|
+
// Whether the tree is clean is decided after the runner's commit; see `afterCommitChecks`.
|
|
861
|
+
const deferred = willCommit ? every.filter(readsTreeClean) : [];
|
|
862
|
+
if (deferred.length > 0 && afterCommitChecks.length === 0) {
|
|
863
|
+
sink.event('checks-after-commit', { checks: deferred.map((c) => c.name) }, `${deferred.length} check(s) about a clean working tree are decided after the runner commits the work: ${deferred.map((c) => c.name).join(', ')}`);
|
|
864
|
+
}
|
|
865
|
+
afterCommitChecks = deferred;
|
|
866
|
+
const checks = [...every.filter((c) => !deferred.includes(c)), ...(willCommit ? [COMMIT_CLEAN_CHECK, CONTENT_CLEAN_CHECK] : [])];
|
|
775
867
|
if (checks.length === 0)
|
|
776
868
|
return 'accept';
|
|
777
869
|
// Counted only once the round turns out to have been a real attempt. A round that died of
|
|
778
870
|
// a missing interpreter asked nothing of the work and must not cost the task one.
|
|
779
871
|
const round = checkRounds + 1;
|
|
780
872
|
sink.event('checks-started', { round, count: checks.length }, `checking the task against ${checks.length} condition(s)`);
|
|
781
|
-
const
|
|
873
|
+
const checkOptions = {
|
|
782
874
|
cwd: work.cwd,
|
|
783
875
|
tracker,
|
|
784
876
|
passEnv: cfg.execution.passEnv,
|
|
@@ -796,7 +888,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
796
888
|
// The same shell a step that named none is given, so the gate and the work it judges
|
|
797
889
|
// cannot have been read by different interpreters.
|
|
798
890
|
defaultShell,
|
|
799
|
-
}
|
|
891
|
+
};
|
|
892
|
+
runAfterCommit = (cs) => runChecks(cs, checkOptions);
|
|
893
|
+
const outcomes = (lastOutcomes = await runChecks(checks, checkOptions));
|
|
800
894
|
/*
|
|
801
895
|
* The runner's own checks are advice given once, not a gate: a second "done" with the finding
|
|
802
896
|
* still there is committed and the finding stays on the task. Refusing instead would leave the
|
|
@@ -870,6 +964,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
870
964
|
sink.event('checks-derived-deferred', { rounds: checkRounds - 1, checks: failed.map((o) => o.check.name) }, `${failed.length} check(s) from earlier reviews still fail after ${maxCheckRounds} attempt(s); the work goes to the reviewer with them named`, 'warn');
|
|
871
965
|
return 'accept';
|
|
872
966
|
}
|
|
967
|
+
limitHit = { setting: 'maxCheckRounds', value: maxCheckRounds };
|
|
873
968
|
sink.event('checks-exhausted', { rounds: checkRounds - 1 }, `${failed.length} check(s) still failing after ${maxCheckRounds} attempt(s); the task is closed as failed`, 'warn');
|
|
874
969
|
return 'give-up';
|
|
875
970
|
}
|
|
@@ -929,7 +1024,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
929
1024
|
*/
|
|
930
1025
|
const gateOnReview = async (closing) => {
|
|
931
1026
|
if (!reviewWanted) {
|
|
932
|
-
await saveReview({ verdict: 'skipped', rounds: 0, stepsRun: 0 });
|
|
1027
|
+
await saveReview({ verdict: 'skipped', rounds: 0, stepsRun: 0, skippedBecause: `the review is switched off for this ${task.reviewEnabled === false ? 'task' : 'session'}` });
|
|
933
1028
|
return 'accept';
|
|
934
1029
|
}
|
|
935
1030
|
reviewRounds += 1;
|
|
@@ -1114,6 +1209,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1114
1209
|
* at the fix. It had been fixed.
|
|
1115
1210
|
*/
|
|
1116
1211
|
if (reviewRounds > maxReviewRounds) {
|
|
1212
|
+
limitHit = { setting: 'maxReviewRounds', value: maxReviewRounds };
|
|
1117
1213
|
sink.event('review-exhausted', { rounds: maxReviewRounds, findings: outcome.findings.length }, `the review still has findings after ${maxReviewRounds} round(s) of fixing; the task is closed as blocked`, 'warn');
|
|
1118
1214
|
return 'give-up';
|
|
1119
1215
|
}
|
|
@@ -1175,10 +1271,14 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1175
1271
|
for (;;) {
|
|
1176
1272
|
if (signal?.aborted)
|
|
1177
1273
|
return await finish('aborted', 'stopped by the operator');
|
|
1178
|
-
if (Date.now() > deadline)
|
|
1274
|
+
if (Date.now() > deadline) {
|
|
1275
|
+
limitHit = { setting: 'maxRunMinutes', value: cfg.limits.maxRunMinutes };
|
|
1179
1276
|
return await finish('limit-reached', `maxRunMinutes (${cfg.limits.maxRunMinutes}) reached`);
|
|
1180
|
-
|
|
1277
|
+
}
|
|
1278
|
+
if (iterations >= cfg.limits.maxIterations) {
|
|
1279
|
+
limitHit = { setting: 'maxIterations', value: cfg.limits.maxIterations };
|
|
1181
1280
|
return await finish('limit-reached', `maxIterations (${cfg.limits.maxIterations}) reached`);
|
|
1281
|
+
}
|
|
1182
1282
|
const parsed = parseReply(lastMarkdown, {
|
|
1183
1283
|
stopMarker: cfg.copilot.stopMarker,
|
|
1184
1284
|
defaultShell,
|
|
@@ -1199,6 +1299,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1199
1299
|
if (formatRetries > cfg.limits.maxFormatRetries) {
|
|
1200
1300
|
await transport.dumpFailure(log.path('failures'), 'format-error');
|
|
1201
1301
|
stopCode = 'format-repair-exhausted';
|
|
1302
|
+
limitHit = { setting: 'maxFormatRetries', value: cfg.limits.maxFormatRetries };
|
|
1202
1303
|
return await finish('limit-reached', `format repair exhausted: ${formatRetries} replies in a row did not match the format (maxFormatRetries ${cfg.limits.maxFormatRetries}), ` +
|
|
1203
1304
|
`the last because ${parsed.detail}. Nothing from those replies was run; the work so far is kept.`, undefined, lastMarkdown);
|
|
1204
1305
|
}
|
|
@@ -1277,9 +1378,36 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1277
1378
|
* what the reply said, at greater length and one message later. The reason carries the
|
|
1278
1379
|
* approaches that were tried, and that is what the register shows.
|
|
1279
1380
|
*/
|
|
1381
|
+
if (!blocked && reply.steps.length > 0)
|
|
1382
|
+
earlyBlocks = 0;
|
|
1383
|
+
if (blocked && reply.tried.length < minApproaches && earlyBlocks < MAX_EARLY_BLOCKS) {
|
|
1384
|
+
/*
|
|
1385
|
+
* Too early to give up, by the operator's own measure (Settings → Execution). Sent back as a
|
|
1386
|
+
* request for another approach rather than accepted: the format only knows that two is the
|
|
1387
|
+
* floor, the operator decided this work deserves more tries than that.
|
|
1388
|
+
*/
|
|
1389
|
+
earlyBlocks += 1;
|
|
1390
|
+
stats.blockedTooEarly = (stats.blockedTooEarly ?? 0) + 1;
|
|
1391
|
+
sink.event('blocked-too-early', { tried: reply.tried.length, needed: minApproaches, time: earlyBlocks }, `"blocked" after ${reply.tried.length} approach(es); Settings ask for ${minApproaches}, so the chat is asked for another`, 'warn');
|
|
1392
|
+
const message = `Not yet. This task is given up only after at least ${minApproaches} genuinely different approaches, and your reply lists ` +
|
|
1393
|
+
`${reply.tried.length}: ${reply.tried.map((x, i) => `(${i + 1}) ${x}`).join(' ')}. Try a different approach now — not one of ` +
|
|
1394
|
+
'these again, and not the same command reworded — and send its steps with status "continue". If it fails too, add it to ' +
|
|
1395
|
+
'"tried" and try another. Nothing you listed is lost: the runner keeps what has been tried.';
|
|
1396
|
+
await record('BLOCKED TOO EARLY — MESSAGE SENT', message);
|
|
1397
|
+
await pacer.throttleSend();
|
|
1398
|
+
const before = await transport.sendAndConfirm(message);
|
|
1399
|
+
const next = await transport.waitForReply(before);
|
|
1400
|
+
await saveReply(`blocked-too-early-${earlyBlocks}`, next);
|
|
1401
|
+
lastMarkdown = next.markdown;
|
|
1402
|
+
await pacer.settle();
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1280
1405
|
if (blocked) {
|
|
1281
1406
|
sink.event('task-blocked', { tried: reply.tried, needed: reply.needed }, `the task was given up as blocked after ${reply.tried.length} approach(es)`, 'warn');
|
|
1282
|
-
|
|
1407
|
+
const early = reply.tried.length < minApproaches
|
|
1408
|
+
? ` It gave up after ${reply.tried.length} of the ${minApproaches} approaches Settings ask for, having been asked ${earlyBlocks} time(s) for another.`
|
|
1409
|
+
: '';
|
|
1410
|
+
return await finish('blocked', blockedReason(reply.tried, reply.needed) + early, reply.summary, lastMarkdown);
|
|
1283
1411
|
}
|
|
1284
1412
|
if (done && reply.steps.length === 0) {
|
|
1285
1413
|
const verdict = await gateOnChecks();
|
|
@@ -1475,6 +1603,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1475
1603
|
}
|
|
1476
1604
|
if (aborted)
|
|
1477
1605
|
return await finish('aborted', 'the operator aborted the task', undefined, lastMarkdown);
|
|
1606
|
+
// The files may have changed: the Desktop copies catch up, in the background (see RunDeps).
|
|
1607
|
+
if (results.some((r) => r.outcome !== 'refused' && r.outcome !== 'aborted'))
|
|
1608
|
+
deps.onWorkChanged?.();
|
|
1478
1609
|
// A loop in which every round looks new: see `progress.ts`. After the report is written, so
|
|
1479
1610
|
// the evidence the diagnosis points at is on disk.
|
|
1480
1611
|
const stuck = progress.afterRound(await treeNow(), results);
|
|
@@ -1562,6 +1693,13 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1562
1693
|
sent = true;
|
|
1563
1694
|
}
|
|
1564
1695
|
catch (e) {
|
|
1696
|
+
/*
|
|
1697
|
+
* The report went out and the chat is still answering it. Sending it again would stack a
|
|
1698
|
+
* second copy under a reply in progress; this is the reply wait from the settings running
|
|
1699
|
+
* out, which ends the task at that limit (see the catch at the end), not an upload to retry.
|
|
1700
|
+
*/
|
|
1701
|
+
if (isReplyTimeout(e))
|
|
1702
|
+
throw e;
|
|
1565
1703
|
sink.event('report-send-failed', { attempt, error: String(e) }, `sending the report failed (attempt ${attempt + 1}): ${e.message}`, 'warn');
|
|
1566
1704
|
if (attempt === cfg.report.uploadRetries) {
|
|
1567
1705
|
const body = await readFile(report.paths[0], 'utf8');
|
|
@@ -1606,6 +1744,15 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1606
1744
|
}
|
|
1607
1745
|
catch (e) {
|
|
1608
1746
|
const message = e.message;
|
|
1747
|
+
/*
|
|
1748
|
+
* The chat took longer than Settings allow for a reply. A limit, not a fault: the conversation
|
|
1749
|
+
* and the work are where they were, and "Continue" carries the task on in that chat.
|
|
1750
|
+
*/
|
|
1751
|
+
if (isReplyTimeout(e)) {
|
|
1752
|
+
sink.event('reply-timeout', { seconds: e.seconds }, message, 'warn');
|
|
1753
|
+
limitHit = { setting: 'replyTimeoutSec', value: e.seconds };
|
|
1754
|
+
return await finish('limit-reached', `replyTimeoutSec (${e.seconds}) reached: ${message}`);
|
|
1755
|
+
}
|
|
1609
1756
|
sink.event('task-error', { error: message, stack: e.stack }, message, 'error');
|
|
1610
1757
|
await transport.dumpFailure(log.path('failures'), 'crash').catch(() => undefined);
|
|
1611
1758
|
/*
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -402,12 +402,12 @@ const FIELD_ROWS_BG = [
|
|
|
402
402
|
];
|
|
403
403
|
const VCS_ROWS_EN = [
|
|
404
404
|
['vcs', 'session', 'yes, always', 'Whether the runner does version control for this session, and where. See below. A session without it is refused.'],
|
|
405
|
-
['vcs.branch', 'task', 'yes in per-task mode', 'The branch this task works on, without the prefix.'],
|
|
405
|
+
['vcs.branch', 'task', 'yes in per-task mode', 'The branch this task works on. A name with a "/" (feature/x, recovery/y) is used as written; one without gets the prefix.'],
|
|
406
406
|
['vcs.commitMessage', 'task', 'yes', 'The commit this task ends with. Imperative, first line under 72 characters.'],
|
|
407
407
|
];
|
|
408
408
|
const VCS_ROWS_BG = [
|
|
409
409
|
['vcs', 'сесия', 'да, винаги', 'Дали runner-ът прави контрол на версиите за тази сесия и къде. Виж по-долу. Сесия без него се отказва.'],
|
|
410
|
-
['vcs.branch', 'задача', 'да, в режим per-task', 'Клонът, по който работи
|
|
410
|
+
['vcs.branch', 'задача', 'да, в режим per-task', 'Клонът, по който работи задачата. Име с „/“ (feature/x, recovery/y) се ползва както е; без „/“ получава представката.'],
|
|
411
411
|
['vcs.commitMessage', 'задача', 'да', 'Комитът, с който задачата завършва. В повелително наклонение, първият ред под 72 знака.'],
|
|
412
412
|
];
|
|
413
413
|
function table(header, rows) {
|
|
@@ -417,7 +417,12 @@ const VCS_DETAIL_EN = `
|
|
|
417
417
|
**vcs on a session**: \`enabled\` (true here), \`repoDir\` (absolute path to the git repository),
|
|
418
418
|
\`branchMode\`, \`commitOnFinish\` (true unless the user says otherwise), \`branchPrefix\` (leave it
|
|
419
419
|
"cop/"), \`branchName\` — used **only** in per-session mode, for the one branch the whole
|
|
420
|
-
session works on
|
|
420
|
+
session works on — and \`startFrom\` with \`baseBranch\`. A branch name with a "/" follows the
|
|
421
|
+
team's own convention and is used exactly as written (in per-session mode an existing branch of that
|
|
422
|
+
name is carried on, not copied); a name without one gets the prefix. With version control on, a check never tests which
|
|
423
|
+
branch is checked out — the runner chooses it; to check that earlier work is included, check the
|
|
424
|
+
commit: \`git merge-base --is-ancestor <commit> HEAD\`. Nor does a check test that the working tree
|
|
425
|
+
is clean: the runner commits after the checks, and its own commit is what leaves it clean.
|
|
421
426
|
|
|
422
427
|
**startFrom** decides where each session's work begins, and it is the user's choice — ask:
|
|
423
428
|
|
|
@@ -460,7 +465,12 @@ const VCS_DETAIL_BG = `
|
|
|
460
465
|
**vcs на сесия**: \`enabled\` (тук true), \`repoDir\` (абсолютен път до git хранилището),
|
|
461
466
|
\`branchMode\`, \`commitOnFinish\` (true, освен ако потребителят не каже друго), \`branchPrefix\`
|
|
462
467
|
(остави "cop/"), \`branchName\` — използва се **само** в режим per-session, за единствения клон,
|
|
463
|
-
по който работи цялата
|
|
468
|
+
по който работи цялата сесия — и \`startFrom\` с \`baseBranch\`. Име на клон с „/“ следва
|
|
469
|
+
конвенцията на екипа и се ползва точно както е написано (в режим per-session съществуващ клон с това
|
|
470
|
+
име се продължава, не се копира); име без „/“ получава представката. При включен контрол на версиите проверка никога не
|
|
471
|
+
проверява кой клон е активен — него го избира runner-ът; за да провериш, че по-ранна работа е
|
|
472
|
+
включена, провери commit-а: \`git merge-base --is-ancestor <commit> HEAD\`. Проверка не проверява и
|
|
473
|
+
че работното дърво е чисто: runner-ът комитва след проверките и неговият commit го оставя чисто.
|
|
464
474
|
|
|
465
475
|
**startFrom** решава откъде започва работата на всяка сесия, и изборът е на потребителя — питай:
|
|
466
476
|
|
|
@@ -738,8 +748,9 @@ in those words, every time:
|
|
|
738
748
|
A check that is wrong rather than a prompt that is wrong is edited instead: **"Edit"** on the
|
|
739
749
|
task card, then **"What it checks"**, **"Must be"** and **"Command"** under **"Checks"**, then
|
|
740
750
|
**"Save and queue it again"** — which, like every "queue" button, starts nothing by itself.
|
|
741
|
-
A task
|
|
742
|
-
|
|
751
|
+
A task stopped by a limit from the settings, or **aborted** because the bot stopped under it, is carried on in its own chat instead:
|
|
752
|
+
**"Continue where it stopped"** on its row in the register, which starts it, or **"Continue in the same chat"** on its card on the session page,
|
|
753
|
+
then **"Run {n} task(s)"** at the top of that page.
|
|
743
754
|
When a failure shows up while the run is still going, **"Pause after this task"** holds the rest
|
|
744
755
|
while you work it out.
|
|
745
756
|
Putting the repository back to before the task is **"Restore"**; putting it back and re-running
|
|
@@ -899,8 +910,9 @@ const PHASE3_BG = `
|
|
|
899
910
|
Когато е сгрешена проверката, а не prompt-ът, се редактира друго: **„Редактирай“** на картата
|
|
900
911
|
на задачата, после **„Какво проверява“**, **„Трябва“** и **„Команда“** под **„Проверки“**, после
|
|
901
912
|
**„Запази и върни в опашката“** — който, като всеки бутон за връщане в опашката, сам не пуска нищо.
|
|
902
|
-
Задача,
|
|
903
|
-
на
|
|
913
|
+
Задача, спряна от лимит в настройките, или **aborted**, защото ботът е спрял под нея, продължава в собствения си чат:
|
|
914
|
+
**„Продължи откъдето спря“** на реда ѝ в регистъра, който я пуска, или **„Продължи в същия чат“** на картата ѝ на страницата на сесията,
|
|
915
|
+
после **„Пусни {n} задача(и)“** горе на същата страница.
|
|
904
916
|
Когато провал се покаже, докато пускането още върви, **„Пауза след тази задача“** задържа
|
|
905
917
|
останалото, докато го разбереш.
|
|
906
918
|
Връщането на хранилището отпреди задачата е **„Върни“**; връщането му плюс ново изпълнение на
|
|
@@ -798,18 +798,34 @@ export const SYSTEM_GUIDE = [
|
|
|
798
798
|
{
|
|
799
799
|
key: 'home.tickForBoth',
|
|
800
800
|
kind: 'note',
|
|
801
|
-
en: 'The ticks
|
|
802
|
-
bg: '
|
|
803
|
-
doesEn: '
|
|
804
|
-
doesBg: '
|
|
801
|
+
en: 'The first column of ticks manages the list — status and delete — and works while a run is going, except on the session running now. The second chooses what to run; only active sessions with something queued can be run.',
|
|
802
|
+
bg: 'Първата колона отметки управлява списъка — статус и изтриване — и работи и докато тече пускане, освен за сесията, която работи в момента. Втората избира какво да се пусне; пускат се само активни сесии с нещо в опашката.',
|
|
803
|
+
doesEn: 'Two sets of tick boxes: the first for status and delete, the second for the run panel above.',
|
|
804
|
+
doesBg: 'Два набора отметки: първият за статус и изтриване, вторият за панела за пускане горе.',
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
key: 'home.makeActive',
|
|
808
|
+
kind: 'button',
|
|
809
|
+
en: 'Make active',
|
|
810
|
+
bg: 'Направи активни',
|
|
811
|
+
doesEn: 'Every session ticked in the first column can be run again. New and imported sessions are active.',
|
|
812
|
+
doesBg: 'Всички отметнати в първата колона сесии отново могат да се пускат. Новите и импортираните са активни.',
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
key: 'home.makeInactive',
|
|
816
|
+
kind: 'button',
|
|
817
|
+
en: 'Make inactive',
|
|
818
|
+
bg: 'Направи неактивни',
|
|
819
|
+
doesEn: 'Sets them aside: nothing starts them — not a run of several, not "Continue" — until made active. Not for a running session.',
|
|
820
|
+
doesBg: 'Оставя ги настрана: нищо не ги пуска — нито пускане на няколко, нито „Продължи“ — докато не станат активни. Не и за работеща сесия.',
|
|
805
821
|
},
|
|
806
822
|
{
|
|
807
823
|
key: 'home.deleteSelected',
|
|
808
824
|
kind: 'button',
|
|
809
825
|
en: 'Delete the {n} selected',
|
|
810
826
|
bg: 'Изтрий избраните {n}',
|
|
811
|
-
doesEn: 'Deletes every ticked
|
|
812
|
-
doesBg: 'Изтрива всички отметнати сесии след един
|
|
827
|
+
doesEn: 'Deletes every session ticked in the first column after one question, also during a run; never the one running. Run folders and chats survive.',
|
|
828
|
+
doesBg: 'Изтрива всички отметнати в първата колона сесии след един въпрос, и по време на пускане; никога работещата. Папките и разговорите остават.',
|
|
813
829
|
},
|
|
814
830
|
{
|
|
815
831
|
key: 'home.delete',
|
|
@@ -1474,8 +1490,8 @@ export const SYSTEM_GUIDE = [
|
|
|
1474
1490
|
kind: 'button',
|
|
1475
1491
|
en: 'Continue in the same chat',
|
|
1476
1492
|
bg: 'Продължи в същия чат',
|
|
1477
|
-
doesEn: 'On a task that stopped before it finished (
|
|
1478
|
-
doesBg: 'На задача, спряла преди да приключи (
|
|
1493
|
+
doesEn: 'On a task that stopped before it finished (a limit from the settings, the bot stopping, the operator): queues it to carry on in the same chat and branch. Then "Run {n} task(s)".',
|
|
1494
|
+
doesBg: 'На задача, спряла преди да приключи (лимит от настройките, спрял бот, оператор): нарежда я да продължи в същия чат и клон. После „Пусни {n} задача(и)“.',
|
|
1479
1495
|
},
|
|
1480
1496
|
{
|
|
1481
1497
|
key: 'task.rerun',
|
|
@@ -2174,6 +2190,14 @@ export const SYSTEM_GUIDE = [
|
|
|
2174
2190
|
doesEn: 'The same button once the story is open: folds it away again.',
|
|
2175
2191
|
doesBg: 'Същият бутон, когато историята е отворена: сгъва я обратно.',
|
|
2176
2192
|
},
|
|
2193
|
+
{
|
|
2194
|
+
key: 'reg.continueHere',
|
|
2195
|
+
kind: 'button',
|
|
2196
|
+
en: 'Continue where it stopped',
|
|
2197
|
+
bg: 'Продължи откъдето спря',
|
|
2198
|
+
doesEn: 'On a row stopped by a limit from the settings (minutes, messages, reply wait, rounds of fixing), by the bot stopping or by the operator: says what happens to the chat, branch and files, then starts that task alone in its chat.',
|
|
2199
|
+
doesBg: 'На ред, спрян от лимит в настройките (минути, съобщения, чакане на отговор, кръгове поправки), от спрял бот или от оператора: казва какво става с чата, клона и файловете, после пуска само тази задача в чата ѝ.',
|
|
2200
|
+
},
|
|
2177
2201
|
{
|
|
2178
2202
|
key: 'reg.fixPrompt',
|
|
2179
2203
|
kind: 'button',
|
|
@@ -2685,8 +2709,16 @@ export const SYSTEM_GUIDE = [
|
|
|
2685
2709
|
kind: 'checkbox',
|
|
2686
2710
|
en: 'Keep the projects on the Desktop',
|
|
2687
2711
|
bg: 'Дръж проектите на Desktop-а',
|
|
2688
|
-
doesEn: '
|
|
2689
|
-
doesBg: '
|
|
2712
|
+
doesEn: 'The master switch: copies the projects on this page to their own Desktop folders before a run and after every round of steps, which is how the files reach the chat.',
|
|
2713
|
+
doesBg: 'Главният ключ: копира проектите от тази страница в собствени папки на Desktop-а преди пускане и след всеки кръг стъпки — така файловете стигат до чата.',
|
|
2714
|
+
},
|
|
2715
|
+
{
|
|
2716
|
+
key: 'proj.desktopThis',
|
|
2717
|
+
kind: 'checkbox',
|
|
2718
|
+
en: 'Keep a copy of this project on the Desktop',
|
|
2719
|
+
bg: 'Дръж копие на този проект на Desktop-а',
|
|
2720
|
+
doesEn: 'Under each project, on by default: untick to keep that one off the Desktop while the switch is on. Greyed out while the switch is off.',
|
|
2721
|
+
doesBg: 'Под всеки проект, включено по подразбиране: махнете отметката, за да не се копира точно той, докато ключът е включен. Неактивно, докато ключът е изключен.',
|
|
2690
2722
|
},
|
|
2691
2723
|
{
|
|
2692
2724
|
key: 'proj.folders',
|
|
@@ -2844,6 +2876,38 @@ export const SYSTEM_GUIDE = [
|
|
|
2844
2876
|
doesEn: 'Seconds to wait for a chat reply (default 900); a reply not finished by then fails the task with "Copilot did not finish a reply within …".',
|
|
2845
2877
|
doesBg: 'Секунди чакане на отговор от чата (по подразбиране 900); незавършен дотогава отговор проваля задачата с „Copilot did not finish a reply within …“.',
|
|
2846
2878
|
},
|
|
2879
|
+
{
|
|
2880
|
+
key: 'sync.show',
|
|
2881
|
+
kind: 'button',
|
|
2882
|
+
en: 'Command to bring this project back to the remote main branch',
|
|
2883
|
+
bg: 'Команда за връщане на проекта към основния клон на отдалеченото хранилище',
|
|
2884
|
+
doesEn: 'Under each project in Settings and on a session\u2019s version control: lists what would be lost and gives a PowerShell command to paste; the bot never runs it.',
|
|
2885
|
+
doesBg: 'Под всеки проект в Настройки и в контрола на версиите на сесия: изброява какво ще се загуби и дава команда за PowerShell; ботът никога не я пуска.',
|
|
2886
|
+
},
|
|
2887
|
+
{
|
|
2888
|
+
key: 'sync.copyPreview',
|
|
2889
|
+
kind: 'button',
|
|
2890
|
+
en: 'Copy the preview (changes nothing)',
|
|
2891
|
+
bg: 'Копирай прегледа (не променя нищо)',
|
|
2892
|
+
doesEn: 'Fetches and lists the commits, changes and new files the command would remove.',
|
|
2893
|
+
doesBg: 'Изтегля и изброява commit-ите, промените и новите файлове, които командата би махнала.',
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
key: 'sync.copyCommand',
|
|
2897
|
+
kind: 'button',
|
|
2898
|
+
en: 'Copy the command',
|
|
2899
|
+
bg: 'Копирай командата',
|
|
2900
|
+
doesEn: 'fetch, reset --hard to the remote main, clean -fd — each only if the step before worked.',
|
|
2901
|
+
doesBg: 'fetch, reset --hard към основния клон, clean -fd — всяка само ако предишната е успяла.',
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
key: 'exec.minApproaches',
|
|
2905
|
+
kind: 'field',
|
|
2906
|
+
en: 'Approaches the chat must try before it may give up',
|
|
2907
|
+
bg: 'Подходи, които чатът трябва да опита, преди да се откаже',
|
|
2908
|
+
doesEn: 'How many different approaches a "blocked" must list (2–20); with fewer the chat is asked for another.',
|
|
2909
|
+
doesBg: 'Колко различни подхода трябва да изброи „blocked“ (2–20); с по-малко чатът е помолен за още един.',
|
|
2910
|
+
},
|
|
2847
2911
|
{
|
|
2848
2912
|
key: 'exec.retryBlocked',
|
|
2849
2913
|
kind: 'field',
|
|
@@ -11,7 +11,7 @@ const TASK_HEADER = '## Task';
|
|
|
11
11
|
* and a note that is empty simply leaves nothing behind.
|
|
12
12
|
*/
|
|
13
13
|
function runnerBlock(input) {
|
|
14
|
-
const notes = [input.workDirNote, input.readOnlyNote, input.scopeNote, input.vcsNote, input.shellNote].map((n) => (n ?? '').trim()).filter((n) => n.length > 0);
|
|
14
|
+
const notes = [input.workDirNote, input.readOnlyNote, input.scopeNote, input.approachesNote, input.vcsNote, input.shellNote].map((n) => (n ?? '').trim()).filter((n) => n.length > 0);
|
|
15
15
|
return notes.length > 0 ? `${notes.join('\n\n')}\n\n` : '';
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
@@ -54,8 +54,23 @@ function whyItStopped(c) {
|
|
|
54
54
|
return 'It was stopped by the operator before it finished — not because anything failed. The files are as you left them.';
|
|
55
55
|
}
|
|
56
56
|
const why = c.stoppedBecause ? ` (${c.stoppedBecause})` : '';
|
|
57
|
-
|
|
57
|
+
// Out of rounds of fixing: something did say no, and the chat needs to hear it was not waved through.
|
|
58
|
+
if (c.limit?.setting === 'maxCheckRounds' || c.limit?.setting === 'maxReviewRounds') {
|
|
59
|
+
return (`It stopped because the runner's limit of ${LIMIT_WORDS[c.limit.setting](c.limit.value)} was reached${why}. ` +
|
|
60
|
+
'What was still failing then is still open: deal with it first. The count starts again now. The files are as you left them.');
|
|
61
|
+
}
|
|
62
|
+
const which = c.limit ? ` of ${LIMIT_WORDS[c.limit.setting](c.limit.value)}` : '';
|
|
63
|
+
return `It stopped because the runner's limit${which} was reached${why} — not because anything failed. The files are as you left them.`;
|
|
58
64
|
}
|
|
65
|
+
/** Each limit from the settings, as the chat is told about it. See `TaskLimit`. */
|
|
66
|
+
const LIMIT_WORDS = {
|
|
67
|
+
maxRunMinutes: (n) => `${n} minutes per task`,
|
|
68
|
+
maxIterations: (n) => `${n} messages per task`,
|
|
69
|
+
replyTimeoutSec: (n) => `${n} seconds to wait for one reply`,
|
|
70
|
+
maxFormatRetries: (n) => `${n} replies in the wrong format in a row`,
|
|
71
|
+
maxCheckRounds: (n) => `${n} rounds of fixing failed checks`,
|
|
72
|
+
maxReviewRounds: (n) => `${n} rounds of fixing review findings`,
|
|
73
|
+
};
|
|
59
74
|
export function composeOpening(input) {
|
|
60
75
|
const taskBlock = `${TASK_HEADER} ${input.taskNumber}: ${input.taskTitle.trim() || 'untitled'}\n\n${input.prompt.trim()}`;
|
|
61
76
|
/*
|
|
@@ -87,10 +87,17 @@ function versionControlLines(task, session) {
|
|
|
87
87
|
if (!vcs.branch)
|
|
88
88
|
return [`version control : did not run — ${vcs.problem}`];
|
|
89
89
|
const commits = vcs.commits ?? [];
|
|
90
|
+
// In the order it happened: the tree before the runner's commit, the commit, the tree after it.
|
|
91
|
+
const before = vcs.beforeCommit;
|
|
92
|
+
const after = vcs.afterCommit;
|
|
90
93
|
const lines = [
|
|
91
94
|
`branch : ${vcs.branch}`,
|
|
92
95
|
`started at : ${vcs.baseCommit ? vcs.baseCommit.slice(0, 8) : '—'} (the commit this task branched from)`,
|
|
93
|
-
`commit
|
|
96
|
+
...(before ? [`before commit: ${before.changed.length} path(s) changed or new in the working tree${before.changed.length ? ` — ${before.changed.slice(0, 10).join(', ')}` : ''}`] : []),
|
|
97
|
+
`commit : ${vcs.commit ? vcs.commit.slice(0, 8) : vcs.problem ? 'none — see the note below' : 'none — the task changed no files'}`,
|
|
98
|
+
...(after
|
|
99
|
+
? [`after commit: on ${after.branch ?? '?'} at ${after.head ? after.head.slice(0, 8) : '?'}, ${after.clean ? 'working tree clean' : `still uncommitted: ${after.changed.slice(0, 10).join(', ')}`}`]
|
|
100
|
+
: []),
|
|
94
101
|
`commits : ${commits.length} on this branch since it was cut`,
|
|
95
102
|
];
|
|
96
103
|
for (const line of commits)
|