copilot-operator 0.1.17 → 0.1.18
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.service.js +23 -2
- package/dist/src/orchestrator/derivedChecks.js +19 -1
- package/dist/src/orchestrator/review.js +2 -2
- package/dist/src/orchestrator/taskRunner.js +81 -8
- package/dist/src/plan/brief.js +26 -0
- package/dist/src/plan/importPlan.js +28 -1
- package/dist/src/plan/schema.js +38 -0
- package/dist/src/plan/systemGuide.js +40 -0
- package/dist/src/protocol/replySchema.js +1 -1
- package/dist/src/protocol/reviewSchema.js +10 -5
- package/dist/src/session/exports.js +44 -13
- package/dist/src/session/model.js +20 -0
- package/dist/src/session/store.js +78 -0
- package/dist/src/vcs/artifacts.js +138 -0
- package/dist/src/vcs/inputs.js +332 -0
- package/dist/src/vcs/snapshot.js +59 -18
- package/dist/src/vcs/taskVcs.js +136 -25
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/549-5d7c845fb0a27dbe.js +1 -0
- package/dist/web/_next/static/chunks/867-c078b5b504bf4289.js +1 -0
- package/dist/web/_next/static/chunks/app/history/page-a0bd59986f7c8beb.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-27cb2d544b498d92.js +1 -0
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +11 -11
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +11 -11
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +11 -11
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +11 -11
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +11 -11
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +11 -11
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +11 -11
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +11 -11
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +11 -11
- package/package.json +7 -3
- package/prompts/level1.md +547 -546
- package/dist/web/_next/static/chunks/549-a13ae394c5e6a837.js +0 -1
- package/dist/web/_next/static/chunks/867-7f27ef819d251cc5.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-d28825430ca280de.js +0 -1
- /package/dist/web/_next/static/{kkxFcGzA9Uo1uRAgcoDti → JPmlfAEvbNkbghoyvY9uC}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{kkxFcGzA9Uo1uRAgcoDti → JPmlfAEvbNkbghoyvY9uC}/_ssgManifest.js +0 -0
|
@@ -19,7 +19,7 @@ import { join, resolve } from 'node:path';
|
|
|
19
19
|
import { installLayout } from '../config/layout.js';
|
|
20
20
|
import { SessionStore, DEFAULT_VCS, DEFAULT_REVIEW, applyDefaultProject, applyTaskPatch } from '../session/store.js';
|
|
21
21
|
import { EventBus } from '../session/events.js';
|
|
22
|
-
import { isContinuable, newId } from '../session/model.js';
|
|
22
|
+
import { freshRetriesOfLatestRun, isContinuable, newId } from '../session/model.js';
|
|
23
23
|
import { runSession, openBrowser, queuedToRun } from '../orchestrator/taskRunner.js';
|
|
24
24
|
import { buildExport } from '../session/exportRecord.js';
|
|
25
25
|
import { buildDebugExport } from '../session/debugExport.js';
|
|
@@ -46,6 +46,7 @@ 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 { dirtyPolicy, takeSnapshot } from '../vcs/snapshot.js';
|
|
49
|
+
import { normalisePatterns } from '../vcs/inputs.js';
|
|
49
50
|
import { computeMetrics } from '../session/metrics.js';
|
|
50
51
|
import { resolveDesktopDir, desktopIsSynced } from '../context/desktopDir.js';
|
|
51
52
|
import { saveAndReveal } from './saveToDesktop.js';
|
|
@@ -419,6 +420,26 @@ let OperatorService = class OperatorService {
|
|
|
419
420
|
delete merged.dirtyWorktree;
|
|
420
421
|
else
|
|
421
422
|
merged.dirtyWorktree = { policy: dirty.policy, ...(dirty.requireApproval ? {} : { requireApproval: false }) };
|
|
423
|
+
// Input files and artifacts: patterns as the matching reads them, absent when there are none.
|
|
424
|
+
const inputPaths = normalisePatterns(merged.userInputs?.paths ?? []);
|
|
425
|
+
if (inputPaths.length === 0)
|
|
426
|
+
delete merged.userInputs;
|
|
427
|
+
else {
|
|
428
|
+
merged.userInputs = {
|
|
429
|
+
paths: inputPaths,
|
|
430
|
+
...(merged.userInputs?.readOnly === false ? { readOnly: false } : {}),
|
|
431
|
+
...(merged.userInputs?.requireApproval === false ? { requireApproval: false } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const artifactPaths = normalisePatterns(merged.artifacts?.paths ?? []);
|
|
435
|
+
if (artifactPaths.length === 0)
|
|
436
|
+
delete merged.artifacts;
|
|
437
|
+
else
|
|
438
|
+
merged.artifacts = { paths: artifactPaths };
|
|
439
|
+
// New inputs are settled again at the next task: the recorded ones are no longer the whole list.
|
|
440
|
+
if (JSON.stringify(merged.userInputs ?? null) !== JSON.stringify(s.vcs?.userInputs ?? null) && s.vcsStart?.inputs) {
|
|
441
|
+
s.vcsStart = { ...s.vcsStart, inputs: undefined };
|
|
442
|
+
}
|
|
422
443
|
// Only the program marks a name as one of its own branches, and a name changed is not that one.
|
|
423
444
|
const stillOwn = !!s.vcs?.branchNameExact && (merged.branchName ?? '').trim() === (s.vcs.branchName ?? '').trim();
|
|
424
445
|
if (stillOwn)
|
|
@@ -2601,7 +2622,7 @@ let OperatorService = class OperatorService {
|
|
|
2601
2622
|
commit: t.vcs?.commit,
|
|
2602
2623
|
vcsProblem: t.vcs?.problem,
|
|
2603
2624
|
sessionInactive: s.active === false || undefined,
|
|
2604
|
-
autoRetries: t
|
|
2625
|
+
autoRetries: freshRetriesOfLatestRun(t) || undefined,
|
|
2605
2626
|
attempt: t.attempt,
|
|
2606
2627
|
changedFiles: t.vcs?.commit && t.vcs.baseCommit ? (t.vcs.files?.length ?? 0) : undefined,
|
|
2607
2628
|
interrupted: t.status === 'aborted' && !!t.interruption ? true : undefined,
|
|
@@ -23,7 +23,25 @@ export function derivedCheckName(findingId, name) {
|
|
|
23
23
|
return `review ${findingId}: ${name}`;
|
|
24
24
|
}
|
|
25
25
|
export function isDerivedCheck(check) {
|
|
26
|
-
return /^review r\d+f\d+: /.test(check.name);
|
|
26
|
+
return /^review (?:a\d+)?r\d+f\d+: /.test(check.name);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Kept checks written before a finding's id carried its attempt, given the id it carries now.
|
|
30
|
+
*
|
|
31
|
+
* Those records hold `r1f1` for attempt 1's first finding and again for attempt 2's, under one check
|
|
32
|
+
* name, and a dispute, a deferral or a drop aimed at one acted on both. Renamed from the attempt each
|
|
33
|
+
* was made on (see `findingId`), check name included, so the gate and the implementer see the same
|
|
34
|
+
* name a dispute can then use. A check of the first attempt, and one already renamed, is left as it is.
|
|
35
|
+
*/
|
|
36
|
+
export function withAttemptIds(reviewChecks) {
|
|
37
|
+
return reviewChecks.map((rc) => {
|
|
38
|
+
if ((rc.attempt ?? 1) <= 1 || !/^r\d+f\d+$/.test(rc.findingId))
|
|
39
|
+
return rc;
|
|
40
|
+
const id = `a${rc.attempt}${rc.findingId}`;
|
|
41
|
+
const before = derivedCheckName(rc.findingId, '');
|
|
42
|
+
const name = rc.check.name.startsWith(before) ? derivedCheckName(id, rc.check.name.slice(before.length)) : rc.check.name;
|
|
43
|
+
return { ...rc, findingId: id, check: { ...rc.check, name } };
|
|
44
|
+
});
|
|
27
45
|
}
|
|
28
46
|
/**
|
|
29
47
|
* Runs each finding's check on the current state. Kept if it fails now, refused if it passes.
|
|
@@ -329,7 +329,7 @@ ${machineNote}` : contract);
|
|
|
329
329
|
problem: `every finding of round ${round} rested on something the task never asked for, twice; the review is inconclusive`,
|
|
330
330
|
};
|
|
331
331
|
}
|
|
332
|
-
const named = grounded.map((f, i) => ({ ...f, id: findingId(round, i) }));
|
|
332
|
+
const named = grounded.map((f, i) => ({ ...f, id: findingId(round, i, task.attempt ?? 1) }));
|
|
333
333
|
/*
|
|
334
334
|
* A read-only task's findings never become gate conditions.
|
|
335
335
|
*
|
|
@@ -605,7 +605,7 @@ export function findingsMessage(outcome, round, maxRounds, repeated = [], leftov
|
|
|
605
605
|
`Fix these, then verify as usual and close the task again. This is review round ${round} of ${maxRounds};`,
|
|
606
606
|
'after that the task is closed as blocked with whatever is still outstanding.',
|
|
607
607
|
'',
|
|
608
|
-
'If you believe a finding is wrong, say so in `disputed`: its id (the `[r1f2]` in front of it), why it',
|
|
608
|
+
'If you believe a finding is wrong, say so in `disputed`: its id as written (the `[r1f2]` in front of it), why it',
|
|
609
609
|
'is wrong, and the evidence that shows it — the command you ran and what came back. A dispute goes to',
|
|
610
610
|
'the next reviewer as a claim to test; a sentence in your summary goes nowhere. Do not change the',
|
|
611
611
|
'thing the reviewer looked at in order to make the objection go away.',
|
|
@@ -21,7 +21,7 @@ import { buildCoveringMessage, assertSendable } from '../protocol/reporter.js';
|
|
|
21
21
|
import { runStep } from '../exec/runner.js';
|
|
22
22
|
import { availableShells, detectShells, effectiveShell, preferredShell, refusalForChat, resolveShell, shellNote } from '../exec/shells.js';
|
|
23
23
|
import { runChecks, failureMessage, failureRedactions, failureReport, environmentProblemIn, COMMIT_CLEAN_CHECK, CONTENT_CLEAN_CHECK, RUNNER_CHECK_KINDS } from '../exec/checks.js';
|
|
24
|
-
import { activeChecks, suspendDisputed, settleAfterReview, onlyDerivedFailing, isDerivedCheck } from './derivedChecks.js';
|
|
24
|
+
import { activeChecks, suspendDisputed, settleAfterReview, onlyDerivedFailing, isDerivedCheck, withAttemptIds } from './derivedChecks.js';
|
|
25
25
|
import { workingDirFor, isWorkingDirProblem, workingDirNote } from '../exec/workDir.js';
|
|
26
26
|
import { redactSecrets } from '../exec/redaction.js';
|
|
27
27
|
import { snapshotProcesses, reapLeftovers, describeLeftovers, ProcessTracker } from '../exec/processes.js';
|
|
@@ -46,6 +46,8 @@ import { ProgressWatch } from './progress.js';
|
|
|
46
46
|
import { contractConflicts, readsTreeClean } from './contract.js';
|
|
47
47
|
import { treeFingerprint } from '../vcs/git.js';
|
|
48
48
|
import { prepareForTask, commitTaskResult, repoDirOf, trackedRepoOf } from '../vcs/taskVcs.js';
|
|
49
|
+
import { protectInputs, inputsMessage } from '../vcs/inputs.js';
|
|
50
|
+
import { artifactPatterns, keepArtifacts } from '../vcs/artifacts.js';
|
|
49
51
|
import { exportMachine, writeAttemptRecord } from '../session/exports.js';
|
|
50
52
|
/** Why a task that was reported as done is being closed as failed. */
|
|
51
53
|
function checksFailedReason(outcomes) {
|
|
@@ -538,9 +540,10 @@ export async function runTask(transport, session, task, deps) {
|
|
|
538
540
|
let runAfterCommit = null;
|
|
539
541
|
/**
|
|
540
542
|
* Checks earlier reviews gave with their findings — carried over from every attempt before
|
|
541
|
-
* this one, and grown by this one. See `derivedChecks.ts` for the three rules.
|
|
543
|
+
* this one, and grown by this one. See `derivedChecks.ts` for the three rules. Ones written before
|
|
544
|
+
* a finding's id carried its attempt are renamed here, before anything finds one by id or name.
|
|
542
545
|
*/
|
|
543
|
-
let reviewChecks = task.reviewChecks ?? [];
|
|
546
|
+
let reviewChecks = withAttemptIds(task.reviewChecks ?? []);
|
|
544
547
|
/** Derived checks that still failed when the rounds ran out; the reviewer is told. */
|
|
545
548
|
let derivedStillFailing = [];
|
|
546
549
|
/**
|
|
@@ -577,10 +580,57 @@ export async function runTask(transport, session, task, deps) {
|
|
|
577
580
|
await record(`LEFT RUNNING BY ${by.toUpperCase()}`, describeLeftovers([...result.killed, ...result.failed]));
|
|
578
581
|
return all.map((l) => ({ name: l.name, ports: l.ports }));
|
|
579
582
|
};
|
|
583
|
+
/** The session's read-only input files and the commit they are put back from, once the task is on its own branch. */
|
|
584
|
+
let inputsGuard = null;
|
|
585
|
+
/** Puts back whatever a round did to the input files, and tells the chat and the record. */
|
|
586
|
+
const guardInputs = async (iteration) => {
|
|
587
|
+
if (!inputsGuard)
|
|
588
|
+
return null;
|
|
589
|
+
const guard = inputsGuard;
|
|
590
|
+
const check = await protectInputs(repoDirOf(session), guard.base, guard.inputs).catch((e) => ({
|
|
591
|
+
changed: ['(the input files)'],
|
|
592
|
+
restored: [],
|
|
593
|
+
failed: [{ path: '(the input files)', why: e.message }],
|
|
594
|
+
}));
|
|
595
|
+
if (check.changed.length > 0) {
|
|
596
|
+
sink.event('inputs-restored', { iteration, restored: check.restored, failed: check.failed }, `the operator's input files are read-only; put back: ${check.restored.join(', ') || '(none)'}` +
|
|
597
|
+
(check.failed.length > 0 ? `; could not be put back: ${check.failed.map((f) => f.path).join(', ')}` : ''), 'warn');
|
|
598
|
+
runnerNotes.push(inputsMessage(check));
|
|
599
|
+
await setTask((t) => {
|
|
600
|
+
t.inputsRestored = [...new Set([...(t.inputsRestored ?? []), ...check.restored])];
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
return check;
|
|
604
|
+
};
|
|
580
605
|
const finish = async (status, reason, summary, finalReply) => {
|
|
581
606
|
await record(`TASK ${status.toUpperCase()}`, [summary ?? '', reason ? `Reason: ${reason}` : ''].filter(Boolean).join('\n\n') || '(no details)');
|
|
582
607
|
// The net under the plan's own checks: whatever the task left running is stopped and named.
|
|
583
608
|
await reap(processesBefore, 'the task');
|
|
609
|
+
/*
|
|
610
|
+
* The operator's input files, once more before the commit: put back what the last round changed,
|
|
611
|
+
* so it is not committed as the task's work. One that cannot be put back fails a done task. See
|
|
612
|
+
* `vcs/inputs.ts`.
|
|
613
|
+
*/
|
|
614
|
+
if (inputsGuard) {
|
|
615
|
+
const check = await guardInputs();
|
|
616
|
+
if (check && check.failed.length > 0 && status === 'done') {
|
|
617
|
+
status = 'failed';
|
|
618
|
+
reason = `the operator's input file(s) ${check.failed.map((f) => f.path).join(', ')} were changed and could not be put back as they were: ${check.failed.map((f) => f.why).join('; ')}.`;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/*
|
|
622
|
+
* The session's artifacts, whatever the outcome: copied into this attempt's record, so the
|
|
623
|
+
* evidence of each attempt is kept even after a later one overwrites it. See `vcs/artifacts.ts`.
|
|
624
|
+
*/
|
|
625
|
+
const patterns = artifactPatterns(session.vcs);
|
|
626
|
+
const projectRoot = repoDirOf(session) || session.projectDir?.trim() || '';
|
|
627
|
+
if (patterns.length > 0 && projectRoot) {
|
|
628
|
+
const kept = await keepArtifacts(projectRoot, patterns, join(artifactsDir, 'project')).catch((e) => ({ kept: [], skipped: [`(all: ${e.message})`] }));
|
|
629
|
+
sink.event('artifacts-kept', { kept: kept.kept.length, skipped: kept.skipped }, `artifacts kept with the run: ${kept.kept.length} file(s)${kept.skipped.length > 0 ? `; not kept: ${kept.skipped.slice(0, 5).join('; ')}` : ''}`, kept.skipped.length > 0 ? 'warn' : 'info');
|
|
630
|
+
await setTask((t) => {
|
|
631
|
+
t.artifactsKept = kept.kept.length > 0 ? kept.kept : undefined;
|
|
632
|
+
});
|
|
633
|
+
}
|
|
584
634
|
/*
|
|
585
635
|
* A read-only task that changed files has failed, whatever it reported.
|
|
586
636
|
*
|
|
@@ -737,6 +787,9 @@ export async function runTask(transport, session, task, deps) {
|
|
|
737
787
|
t.runId = runId;
|
|
738
788
|
t.runGroup = deps.runGroup;
|
|
739
789
|
t.startedAt = new Date().toISOString();
|
|
790
|
+
// The kept checks under the ids this run uses (`withAttemptIds`), so the record names them as the gate does.
|
|
791
|
+
if (reviewChecks.length > 0)
|
|
792
|
+
t.reviewChecks = reviewChecks;
|
|
740
793
|
});
|
|
741
794
|
sink.event('task-started', { runId, title: task.title }, `task "${task.title}" starting (run ${runId})`);
|
|
742
795
|
await writeFile(taskLogPath, `TASK: ${task.title}\nSESSION: ${session.name} (${session.id})\nRUN: ${runId}\nSTARTED: ${new Date().toISOString()}\n`, 'utf8');
|
|
@@ -856,6 +909,11 @@ export async function runTask(transport, session, task, deps) {
|
|
|
856
909
|
* findings described a tree that was not the one it had been asked to audit.
|
|
857
910
|
*/
|
|
858
911
|
const scopeEnforced = (scope.length > 0 || !!task.readOnly) && onOwnBranch;
|
|
912
|
+
// The operator's input files, read-only on a branch of the task's own; see `vcs/inputs.ts`.
|
|
913
|
+
const startNow = (await store.getSession(session.id))?.vcsStart;
|
|
914
|
+
if (onOwnBranch && startNow?.inputs?.readOnly && startNow.inputs.files.length > 0) {
|
|
915
|
+
inputsGuard = { base: prepared.vcs.baseCommit, inputs: startNow.inputs };
|
|
916
|
+
}
|
|
859
917
|
/** The working tree's fingerprint, when the task is on a branch of its own; null otherwise. */
|
|
860
918
|
const treeNow = async () => prepared.vcs.branch && !prepared.vcs.problem && prepared.vcs.baseCommit ? await treeFingerprint(repoDirOf(session)).catch(() => null) : null;
|
|
861
919
|
/*
|
|
@@ -1409,7 +1467,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1409
1467
|
// `repeated` still identifies them.
|
|
1410
1468
|
const named = outcome.findings.map((f, i) => ({
|
|
1411
1469
|
...f,
|
|
1412
|
-
id: f.id ?? findingId(reviewRounds, i),
|
|
1470
|
+
id: f.id ?? findingId(reviewRounds, i, attempt),
|
|
1413
1471
|
...(repeated.includes(f) ? { repeated: true } : {}),
|
|
1414
1472
|
}));
|
|
1415
1473
|
outcome = { ...outcome, findings: named };
|
|
@@ -1936,6 +1994,7 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1936
1994
|
});
|
|
1937
1995
|
}
|
|
1938
1996
|
}
|
|
1997
|
+
await guardInputs(iterations);
|
|
1939
1998
|
// --- report back ---------------------------------------------------------------
|
|
1940
1999
|
const report = await writeReport(results, {
|
|
1941
2000
|
runId,
|
|
@@ -2160,9 +2219,23 @@ export async function runSession(sessionId, deps) {
|
|
|
2160
2219
|
break;
|
|
2161
2220
|
}
|
|
2162
2221
|
session = (await store.getSession(sessionId));
|
|
2163
|
-
|
|
2222
|
+
let task = session.tasks.find((t) => t.id === queuedTask.id);
|
|
2164
2223
|
if (!task || task.status !== 'queued')
|
|
2165
2224
|
continue;
|
|
2225
|
+
/*
|
|
2226
|
+
* A task taken from the queue was started by the operator, never by the retry loop below, which
|
|
2227
|
+
* runs its own. A fresh-retry mark on it is left from a retry that was queued and never ran —
|
|
2228
|
+
* the new chat failed to open, the program was closed — and counted as this run's, it added the
|
|
2229
|
+
* old run's retry to this one's (`freshRetriesOfLatestRun`).
|
|
2230
|
+
*/
|
|
2231
|
+
if (task.freshRetry) {
|
|
2232
|
+
session = await store.updateSession(sessionId, (s) => {
|
|
2233
|
+
const t = s.tasks.find((x) => x.id === queuedTask.id);
|
|
2234
|
+
if (t)
|
|
2235
|
+
t.freshRetry = undefined;
|
|
2236
|
+
});
|
|
2237
|
+
task = session.tasks.find((t) => t.id === queuedTask.id);
|
|
2238
|
+
}
|
|
2166
2239
|
let outcome = await runTask(chat, session, task, deps);
|
|
2167
2240
|
ran += 1;
|
|
2168
2241
|
/*
|
|
@@ -2194,11 +2267,11 @@ export async function runSession(sessionId, deps) {
|
|
|
2194
2267
|
session = await store.updateSession(sessionId, (s) => {
|
|
2195
2268
|
s.chat = undefined;
|
|
2196
2269
|
s.contractSent = false;
|
|
2270
|
+
// Marked, not counted: how many retries this run made is read back from the marks
|
|
2271
|
+
// (`freshRetriesOfLatestRun`), so a later run cannot add to an earlier one's count.
|
|
2197
2272
|
const again = s.tasks.find((x) => x.id === task.id);
|
|
2198
|
-
if (again)
|
|
2199
|
-
again.autoRetries = (again.autoRetries ?? 0) + 1;
|
|
2273
|
+
if (again)
|
|
2200
2274
|
again.freshRetry = true;
|
|
2201
|
-
}
|
|
2202
2275
|
});
|
|
2203
2276
|
await chat.newChat();
|
|
2204
2277
|
/*
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -464,6 +464,19 @@ taken where the changes are, so use it with \`startFrom\` \`"branch"\` only when
|
|
|
464
464
|
that branch, and never with \`"existing-branch"\`. Leaving it out keeps \`"reject"\`: version control stays out
|
|
465
465
|
of a tree with uncommitted changes.
|
|
466
466
|
|
|
467
|
+
**userInputs** — files the user provides for the work (schemas, test data):
|
|
468
|
+
\`{ "paths": ["rules-engine/test-data/schemas/*.yaml"] }\`. They go into the commit every session starts from —
|
|
469
|
+
new or ignored ones in the starting snapshot, approved by the user; a later session whose start lacks them
|
|
470
|
+
gets them committed on top of it — and they are read-only while the tasks run (\`"readOnly": false\` only if
|
|
471
|
+
the user wants the work to change them). Ask the user which files are inputs; never name a whole folder of
|
|
472
|
+
work, and never \`.env\`. **artifacts** — evidence the work writes (reports, ZIP archives, test results):
|
|
473
|
+
\`{ "paths": ["rules-engine/test-results/**"] }\`. Never committed, kept with the run; tell tasks to write
|
|
474
|
+
evidence there instead of changing \`.gitignore\`.
|
|
475
|
+
|
|
476
|
+
A session with \`"startFrom": "previous-session"\` continues the whole work of the one before it only when
|
|
477
|
+
that one is \`"branchMode": "per-session"\`: per task, its work is on several branches and the next session
|
|
478
|
+
refuses to start from part of it.
|
|
479
|
+
|
|
467
480
|
Put the same \`startFrom\` on every session that works in one repository unless the user wants it
|
|
468
481
|
mixed. Leaving it out keeps the old behaviour — whatever branch the repository is on — which is
|
|
469
482
|
the one to avoid.
|
|
@@ -532,6 +545,19 @@ run“, на отделен клон, и сесията тръгва от нег
|
|
|
532
545
|
този клон, и никога с \`"existing-branch"\`. Ако го пропуснеш, остава \`"reject"\`: контролът на версиите
|
|
533
546
|
не влиза в дърво с некомитнати промени.
|
|
534
547
|
|
|
548
|
+
**userInputs** — файлове, които потребителят дава за работата (схеми, тестови данни):
|
|
549
|
+
\`{ "paths": ["rules-engine/test-data/schemas/*.yaml"] }\`. Влизат в комита, от който тръгва всяка сесия —
|
|
550
|
+
новите или ignored в началната снимка, с одобрение от потребителя; следваща сесия, чието начало ги няма, ги
|
|
551
|
+
получава комитнати върху него — и са само за четене, докато задачите работят (\`"readOnly": false\` само ако
|
|
552
|
+
потребителят иска работата да ги променя). Питай кои файлове са вход; никога цяла папка с работа и никога
|
|
553
|
+
\`.env\`. **artifacts** — доказателства, които работата пише (отчети, ZIP архиви, резултати от тестове):
|
|
554
|
+
\`{ "paths": ["rules-engine/test-results/**"] }\`. Никога не се комитват, пазят се към пускането; кажи на
|
|
555
|
+
задачите да пишат доказателствата там, вместо да променят \`.gitignore\`.
|
|
556
|
+
|
|
557
|
+
Сесия с \`"startFrom": "previous-session"\` продължава цялата работа на предната само ако тя е
|
|
558
|
+
\`"branchMode": "per-session"\`: при per-task работата ѝ е на няколко клона и следващата сесия отказва да
|
|
559
|
+
тръгне от част от нея.
|
|
560
|
+
|
|
535
561
|
Сложи един и същ \`startFrom\` на всички сесии в едно хранилище, освен ако потребителят не иска
|
|
536
562
|
различни. Ако го пропуснеш, остава старото поведение — от който клон е хранилището в момента —
|
|
537
563
|
което е за избягване.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_VCS, applyDefaultProject } from '../session/store.js';
|
|
2
|
+
import { normalisePatterns } from '../vcs/inputs.js';
|
|
2
3
|
import { availableShells, detectShells } from '../exec/shells.js';
|
|
3
4
|
/** The heading under which a task's acceptance bar reaches Copilot. English, like level 1. */
|
|
4
5
|
const EXPECTED_HEADER = '### Expected result';
|
|
@@ -165,6 +166,23 @@ defaultProjectDir = '') {
|
|
|
165
166
|
const sharedGroup = plan.conversation === 'shared'
|
|
166
167
|
? (plan.plan.trim() || `plan-${new Date().toISOString().slice(0, 16).replace(/[:T-]/g, '')}`).slice(0, 60)
|
|
167
168
|
: '';
|
|
169
|
+
/*
|
|
170
|
+
* A session that continues the one before it in the same repository, when that one ran per task
|
|
171
|
+
* with several tasks: its work is on several branches, and the continuation is refused at its start
|
|
172
|
+
* (see `sessionStart`). Said now, when the plan can still be changed.
|
|
173
|
+
*/
|
|
174
|
+
const lastIn = new Map();
|
|
175
|
+
for (const planned of plan.sessions) {
|
|
176
|
+
const repo = (planned.vcs?.repoDir || planned.projectDir || '').trim().replace(/[\\/]+$/, '').toLowerCase();
|
|
177
|
+
if (!planned.vcs?.enabled || !repo)
|
|
178
|
+
continue;
|
|
179
|
+
const before = lastIn.get(repo);
|
|
180
|
+
if (planned.vcs.startFrom === 'previous-session' && before?.perTask && before.tasks > 1) {
|
|
181
|
+
warnings.push(`Session "${planned.name}" continues "${before.name}", which runs a branch per task: its ${before.tasks} tasks' work will be on ${before.tasks} branches, ` +
|
|
182
|
+
`and "${planned.name}" will refuse to start from part of it. Give "${before.name}" "branchMode": "per-session" if the next session builds on all of it.`);
|
|
183
|
+
}
|
|
184
|
+
lastIn.set(repo, { name: planned.name, perTask: (planned.vcs.branchMode ?? 'per-task') === 'per-task', tasks: planned.tasks.length });
|
|
185
|
+
}
|
|
168
186
|
for (const planned of plan.sessions) {
|
|
169
187
|
// Attaching project files to the chat was removed; an old plan's root is still its project folder.
|
|
170
188
|
if (planned.mirror?.enabled) {
|
|
@@ -172,8 +190,17 @@ defaultProjectDir = '') {
|
|
|
172
190
|
'the chat reads the project by running commands. "mirror" was ignored.');
|
|
173
191
|
}
|
|
174
192
|
const created = await store.createSession(planned.name, planned.projectDir || planned.mirror?.rootDir || '');
|
|
175
|
-
const { dirtyWorktree: plannedDirty, ...plannedVcs } = planned.vcs ?? {};
|
|
193
|
+
const { dirtyWorktree: plannedDirty, userInputs: plannedInputs, artifacts: plannedArtifacts, ...plannedVcs } = planned.vcs ?? {};
|
|
176
194
|
const vcs = { ...DEFAULT_VCS, ...plannedVcs };
|
|
195
|
+
// Input files and artifacts in the one spelling the session has: see `UserInputs`.
|
|
196
|
+
const inputPaths = plannedInputs?.enabled === false ? [] : normalisePatterns(plannedInputs?.paths ?? []);
|
|
197
|
+
if (inputPaths.length > 0) {
|
|
198
|
+
const readOnly = plannedInputs?.readOnly ?? plannedInputs?.readOnlyAfterCapture;
|
|
199
|
+
vcs.userInputs = { paths: inputPaths, ...(readOnly === false ? { readOnly: false } : {}), ...(plannedInputs?.requireApproval === false ? { requireApproval: false } : {}) };
|
|
200
|
+
}
|
|
201
|
+
const artifactPaths = normalisePatterns(plannedArtifacts?.paths ?? []);
|
|
202
|
+
if (artifactPaths.length > 0)
|
|
203
|
+
vcs.artifacts = { paths: artifactPaths };
|
|
177
204
|
// Stored in the one spelling the session has: see `DirtyWorktree`.
|
|
178
205
|
const policy = plannedDirty?.policy === 'snapshot' && plannedDirty.includeUntracked === false ? 'tracked-only-snapshot' : plannedDirty?.policy;
|
|
179
206
|
if (policy && policy !== 'reject')
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* sending the user back to the chat; it is reported as a warning instead.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
import { namesEverything } from '../vcs/inputs.js';
|
|
16
17
|
export const PLAN_VERSION = 1;
|
|
17
18
|
/**
|
|
18
19
|
* Version control, exactly as a session stores it, and required on every session.
|
|
@@ -77,6 +78,35 @@ const VcsInput = z
|
|
|
77
78
|
.optional(),
|
|
78
79
|
})
|
|
79
80
|
.optional(),
|
|
81
|
+
/**
|
|
82
|
+
* The operator's input files. See `UserInputs`. Accepts the spelling a chat model may write:
|
|
83
|
+
* `capture` can only be "baseline-commit", `allowUntracked`/`allowIgnored` only true (an input
|
|
84
|
+
* may be either), `readOnlyAfterCapture` is `readOnly`.
|
|
85
|
+
*/
|
|
86
|
+
userInputs: z
|
|
87
|
+
.object({
|
|
88
|
+
enabled: z.boolean().optional(),
|
|
89
|
+
paths: z.array(z.string().trim()).default([]),
|
|
90
|
+
capture: z.literal('baseline-commit', { error: 'userInputs.capture can only be "baseline-commit": input files go into the commit the session starts from. Leave the field out.' }).optional(),
|
|
91
|
+
allowUntracked: z.literal(true, { error: 'userInputs.allowUntracked can only be true: an input file may be untracked. Leave the field out.' }).optional(),
|
|
92
|
+
allowIgnored: z.literal(true, { error: 'userInputs.allowIgnored can only be true: an input file may be ignored by git. Leave the field out.' }).optional(),
|
|
93
|
+
readOnly: z.boolean().optional(),
|
|
94
|
+
readOnlyAfterCapture: z.boolean().optional(),
|
|
95
|
+
requireApproval: z.boolean().optional(),
|
|
96
|
+
})
|
|
97
|
+
.optional(),
|
|
98
|
+
/**
|
|
99
|
+
* Evidence kept with the run and never committed. See `VersionControl.artifacts`. `commit` can
|
|
100
|
+
* only be false and `attachToRun`/`allowIgnored` only true: that is what an artifact is.
|
|
101
|
+
*/
|
|
102
|
+
artifacts: z
|
|
103
|
+
.object({
|
|
104
|
+
paths: z.array(z.string().trim()).default([]),
|
|
105
|
+
allowIgnored: z.literal(true, { error: 'artifacts.allowIgnored can only be true. Leave the field out.' }).optional(),
|
|
106
|
+
attachToRun: z.literal(true, { error: 'artifacts.attachToRun can only be true: artifacts are always kept with the run. Leave the field out.' }).optional(),
|
|
107
|
+
commit: z.literal(false, { error: 'artifacts.commit can only be false: an artifact is never committed. Files that should be committed are the work, not artifacts.' }).optional(),
|
|
108
|
+
})
|
|
109
|
+
.optional(),
|
|
80
110
|
}, {
|
|
81
111
|
error: 'Every session needs a "vcs" object saying whether the runner does version control for it and, when it does, which git repository to work in. Ask the user both questions and write the answers here.',
|
|
82
112
|
})
|
|
@@ -102,6 +132,14 @@ const VcsInput = z
|
|
|
102
132
|
message: '"tracked-only-snapshot" takes tracked changes only, and includeUntracked true asks for new files too. Keep one: policy "snapshot", or leave includeUntracked out.',
|
|
103
133
|
});
|
|
104
134
|
}
|
|
135
|
+
for (const [i, p] of (vcs.userInputs?.paths ?? []).entries()) {
|
|
136
|
+
if (namesEverything(p)) {
|
|
137
|
+
ctx.addIssue({ code: 'custom', path: ['userInputs', 'paths', i], message: `"${p}" names the whole repository; an input pattern names the operator's files, such as "rules-engine/test-data/schemas/*.yaml".` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (vcs.userInputs && vcs.userInputs.enabled !== false && (vcs.userInputs.paths ?? []).length > 0 && !vcs.enabled) {
|
|
141
|
+
ctx.addIssue({ code: 'custom', path: ['userInputs'], message: 'userInputs needs version control on: the input files go into the commit the session starts from. Turn vcs.enabled on, or leave userInputs out.' });
|
|
142
|
+
}
|
|
105
143
|
if (vcs.enabled && !vcs.repoDir.trim()) {
|
|
106
144
|
ctx.addIssue({
|
|
107
145
|
code: 'custom',
|
|
@@ -1224,6 +1224,46 @@ export const SYSTEM_GUIDE = [
|
|
|
1224
1224
|
doesEn: 'Under the list of uncommitted files, each set to "Include in the starting snapshot" or "Leave out of the run": commits the included ones on the snapshot branch; left-out files go to the repository\'s .git/info/exclude. Until it is pressed a run of the session is refused and nothing changes.',
|
|
1225
1225
|
doesBg: 'Под списъка с некомитнати файлове, всеки с „Включи в началната снимка“ или „Остави извън изпълнението“: комитва включените на клона на снимката; оставените извън отиват в .git/info/exclude на хранилището. Докато не е натиснат, пускане на сесията се отказва и нищо не се променя.',
|
|
1226
1226
|
},
|
|
1227
|
+
{
|
|
1228
|
+
key: 'vcs.inputs',
|
|
1229
|
+
kind: 'note',
|
|
1230
|
+
en: 'Input files',
|
|
1231
|
+
bg: 'Входни файлове',
|
|
1232
|
+
doesEn: 'The heading over the operator\'s input files and whether they are read-only.',
|
|
1233
|
+
doesBg: 'Заглавието над входните файлове на оператора и дали са само за четене.',
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
key: 'vcs.inputsField',
|
|
1237
|
+
kind: 'field',
|
|
1238
|
+
en: 'Your input files, one pattern per line (repository-relative)',
|
|
1239
|
+
bg: 'Твоите входни файлове, по един шаблон на ред (спрямо хранилището)',
|
|
1240
|
+
doesEn: 'Files the operator provides — schemas, test data. They go into the commit every session of the run starts from (new or ignored ones in the starting snapshot, listed for approval); a later session whose start lacks them has them committed on top of it; their SHA-256 sums are recorded. In a plan: "userInputs": { "paths": [...] }.',
|
|
1241
|
+
doesBg: 'Файлове, които операторът дава — схеми, тестови данни. Влизат в комита, от който тръгва всяка сесия на пускането (новите или ignored — в началната снимка, със списък за одобрение); следваща сесия, чието начало ги няма, ги получава комитнати върху него; SHA-256 сумите им се записват. В план: "userInputs": { "paths": [...] }.',
|
|
1242
|
+
},
|
|
1243
|
+
{
|
|
1244
|
+
key: 'vcs.inputsReadOnly',
|
|
1245
|
+
kind: 'checkbox',
|
|
1246
|
+
en: 'Read-only while the tasks run',
|
|
1247
|
+
bg: 'Само за четене, докато задачите работят',
|
|
1248
|
+
doesEn: 'On by default. A changed, deleted or added input file is put back after every round and the chat told; one that cannot be put back fails the task.',
|
|
1249
|
+
doesBg: 'Включено по подразбиране. Променен, изтрит или добавен входен файл се връща след всеки рунд и чатът научава; такъв, който не може да се върне, проваля задачата.',
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
key: 'vcs.artifacts',
|
|
1253
|
+
kind: 'note',
|
|
1254
|
+
en: 'Artifacts',
|
|
1255
|
+
bg: 'Artifacts',
|
|
1256
|
+
doesEn: 'The heading over the evidence files the work writes.',
|
|
1257
|
+
doesBg: 'Заглавието над файловете с доказателства, които работата пише.',
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
key: 'vcs.artifactsField',
|
|
1261
|
+
kind: 'field',
|
|
1262
|
+
en: 'Evidence the work writes, one pattern per line (repository-relative)',
|
|
1263
|
+
bg: 'Доказателства, които работата пише, по един шаблон на ред (спрямо хранилището)',
|
|
1264
|
+
doesEn: 'Reports, ZIP archives, test results: never committed (kept out through .git/info/exclude, .gitignore untouched) and copied into each attempt\'s record with their sums. In a plan: "artifacts": { "paths": [...] }.',
|
|
1265
|
+
doesBg: 'Отчети, ZIP архиви, резултати от тестове: никога не се комитват (стоят извън git чрез .git/info/exclude, .gitignore не се пипа) и се копират към записа на всеки опит със сумите си. В план: "artifacts": { "paths": [...] }.',
|
|
1266
|
+
},
|
|
1227
1267
|
{
|
|
1228
1268
|
key: 'vcs.ready',
|
|
1229
1269
|
kind: 'note',
|
|
@@ -83,7 +83,7 @@ export const DeviationSchema = z.object({
|
|
|
83
83
|
* claim to test, and to the record. The finding is named by the id the runner gave it.
|
|
84
84
|
*/
|
|
85
85
|
export const DisputeSchema = z.object({
|
|
86
|
-
/** The finding's id as it was given: `r1f2
|
|
86
|
+
/** The finding's id as it was given: `r1f2`, or `a2r1f2` on a later attempt. */
|
|
87
87
|
finding: z.string().trim().min(1),
|
|
88
88
|
/** Why it is wrong. */
|
|
89
89
|
why: z.string().trim().min(1),
|
|
@@ -166,13 +166,18 @@ export function isGrounded(basis, sources) {
|
|
|
166
166
|
return sources.some((s) => groundingKey(s).includes(key));
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
|
-
* The name a finding is referred to by, once the runner has it: round and position
|
|
169
|
+
* The name a finding is referred to by, once the runner has it: round and position, and from the
|
|
170
|
+
* task's second attempt on the attempt as well — `r1f2`, then `a2r1f2`.
|
|
170
171
|
*
|
|
171
|
-
* Given by the runner rather than the reviewer so that it is unique
|
|
172
|
-
*
|
|
172
|
+
* Given by the runner rather than the reviewer so that it is unique and cannot be forgotten. It is
|
|
173
|
+
* what the implementer names in `disputed`, what a check given with the finding is named after, and
|
|
174
|
+
* what the record shows. Unique across the task's attempts, not only its rounds, because those checks
|
|
175
|
+
* are kept for every later attempt (`Task.reviewChecks`) and the rounds count from 1 on each: with
|
|
176
|
+
* round and position alone, attempt 2's first finding was `r1f1` beside attempt 1's kept `r1f1`, and
|
|
177
|
+
* a dispute of the one suspended the check of the other. The first attempt keeps the short form.
|
|
173
178
|
*/
|
|
174
|
-
export function findingId(round, index) {
|
|
175
|
-
return `r${round}f${index + 1}`;
|
|
179
|
+
export function findingId(round, index, attempt = 1) {
|
|
180
|
+
return `${attempt > 1 ? `a${attempt}` : ''}r${round}f${index + 1}`;
|
|
176
181
|
}
|
|
177
182
|
/** True when nothing the reviewer found is something the implementer could fix. */
|
|
178
183
|
export function allAboutTheTask(findings) {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { existsSync } from 'node:fs';
|
|
22
22
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
|
+
import { TASK_FIELD_ON_RERUN } from './store.js';
|
|
24
25
|
import { withoutPersona } from '../plan/importPlan.js';
|
|
25
26
|
const EXPECTED_HEADER = '### Expected result';
|
|
26
27
|
const GOAL_HEADER = '## Goal of this session';
|
|
@@ -149,6 +150,8 @@ export function buildPlanExport(scope) {
|
|
|
149
150
|
...(s.vcs?.updateFromRemote === false ? { updateFromRemote: false } : {}),
|
|
150
151
|
// Only when chosen: absent is reject, as it always was.
|
|
151
152
|
...(s.vcs?.dirtyWorktree && s.vcs.dirtyWorktree.policy !== 'reject' ? { dirtyWorktree: { ...s.vcs.dirtyWorktree } } : {}),
|
|
153
|
+
...(s.vcs?.userInputs?.paths.length ? { userInputs: { ...s.vcs.userInputs } } : {}),
|
|
154
|
+
...(s.vcs?.artifacts?.paths.length ? { artifacts: { ...s.vcs.artifacts } } : {}),
|
|
152
155
|
},
|
|
153
156
|
review: { enabled: s.review?.enabled !== false, model: s.review?.model ?? '' },
|
|
154
157
|
...(s.projectDir ? { projectDir: s.projectDir } : {}),
|
|
@@ -231,27 +234,32 @@ export function taskAtAttempt(task, attempt) {
|
|
|
231
234
|
const a = task.attempts?.[attempt - 1];
|
|
232
235
|
if (!a)
|
|
233
236
|
return null;
|
|
237
|
+
/*
|
|
238
|
+
* By the same table `rerunTask` follows: what was archived is read from the attempt, what was
|
|
239
|
+
* cleared is absent. Field by field, by hand, an earlier attempt read back with the latest one's
|
|
240
|
+
* handoff, counts, limit and fresh-chat mark.
|
|
241
|
+
*/
|
|
242
|
+
const asItWas = {};
|
|
243
|
+
for (const [key, what] of Object.entries(TASK_FIELD_ON_RERUN)) {
|
|
244
|
+
if (what === 'archived')
|
|
245
|
+
asItWas[key] = a[key];
|
|
246
|
+
if (what === 'cleared')
|
|
247
|
+
asItWas[key] = undefined;
|
|
248
|
+
}
|
|
234
249
|
return {
|
|
235
250
|
...task,
|
|
251
|
+
...asItWas,
|
|
236
252
|
attempt,
|
|
237
253
|
status: a.status,
|
|
238
|
-
runId: a.runId,
|
|
239
|
-
runGroup: a.runGroup,
|
|
240
|
-
startedAt: a.startedAt,
|
|
241
|
-
finishedAt: a.finishedAt,
|
|
242
254
|
iterations: a.iterations,
|
|
243
|
-
|
|
244
|
-
reason: a.reason,
|
|
245
|
-
deviations: a.deviations,
|
|
246
|
-
disputes: a.disputes,
|
|
255
|
+
// The task's own text and definition as they were when that attempt ran, where it was kept.
|
|
247
256
|
title: a.title,
|
|
248
257
|
prompt: a.prompt,
|
|
249
258
|
level2: a.level2,
|
|
250
259
|
checks: a.checks ?? task.checks,
|
|
251
260
|
vcsPlan: a.vcsPlan ?? task.vcsPlan,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
vcs: a.vcs,
|
|
261
|
+
scope: a.scope ?? task.scope,
|
|
262
|
+
readOnly: a.readOnly ?? task.readOnly,
|
|
255
263
|
attempts: task.attempts?.slice(0, attempt - 1),
|
|
256
264
|
continuing: undefined,
|
|
257
265
|
buildsOn: undefined,
|
|
@@ -436,7 +444,25 @@ async function domainTask(session, task, runsDir) {
|
|
|
436
444
|
const started = task.startedAt ? Date.parse(task.startedAt) : NaN;
|
|
437
445
|
const finished = task.finishedAt ? Date.parse(task.finishedAt) : NaN;
|
|
438
446
|
const out = {
|
|
439
|
-
|
|
447
|
+
/*
|
|
448
|
+
* Where the session started — the baseline commit, a snapshot of the operator's changes, the
|
|
449
|
+
* input files with their SHA-256 sums — so a reader can check the work against its inputs.
|
|
450
|
+
*/
|
|
451
|
+
session: {
|
|
452
|
+
id: session.id,
|
|
453
|
+
name: session.name,
|
|
454
|
+
...(session.vcsStart
|
|
455
|
+
? {
|
|
456
|
+
start: {
|
|
457
|
+
kind: session.vcsStart.kind,
|
|
458
|
+
commit: session.vcsStart.commit,
|
|
459
|
+
branch: session.vcsStart.branch,
|
|
460
|
+
...(session.vcsStart.snapshot ? { snapshot: session.vcsStart.snapshot } : {}),
|
|
461
|
+
...(session.vcsStart.inputs ? { inputs: session.vcsStart.inputs } : {}),
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
: {}),
|
|
465
|
+
},
|
|
440
466
|
task: {
|
|
441
467
|
id: task.id,
|
|
442
468
|
title: task.title,
|
|
@@ -444,6 +470,8 @@ async function domainTask(session, task, runsDir) {
|
|
|
444
470
|
readOnly: task.readOnly ?? false,
|
|
445
471
|
scope: task.scope ?? [],
|
|
446
472
|
scopeReverted: task.scopeReverted ?? [],
|
|
473
|
+
inputsRestored: task.inputsRestored ?? [],
|
|
474
|
+
artifactsKept: task.artifactsKept ?? [],
|
|
447
475
|
handoff: task.handoff ?? null,
|
|
448
476
|
stopCode: task.stopCode ?? null,
|
|
449
477
|
prompt: p.prompt,
|
|
@@ -526,7 +554,7 @@ async function domainTask(session, task, runsDir) {
|
|
|
526
554
|
whatTheChatTried: aRounds,
|
|
527
555
|
whatWasActuallyDone: { repository: a.vcs, deviations: a.deviations, disputes: a.disputes },
|
|
528
556
|
whyItFailed: aFailed
|
|
529
|
-
? { status: a.status, reason: a.reason, failingChecks: (a.checkResults ?? []).filter((c) => !c.passed), lastRound: aRounds.at(-1) }
|
|
557
|
+
? { status: a.status, reason: a.reason, failingChecks: (a.checkResults ?? []).filter((c) => !c.passed), lastRound: aRounds.at(-1), leftovers: a.leftovers }
|
|
530
558
|
: undefined,
|
|
531
559
|
});
|
|
532
560
|
}
|
|
@@ -607,6 +635,9 @@ async function botTask(session, task, runsDir) {
|
|
|
607
635
|
steps: stepsOf(aEvents).map((s) => ({ iteration: s.iteration, id: s.id, command: s.description, outcome: s.outcome, exitCode: s.exitCode, durationMs: s.durationMs, refused: s.refused })),
|
|
608
636
|
problems: aEvents.filter((e) => e.level === 'error' || e.level === 'warn').map(trimmed),
|
|
609
637
|
events: aEvents.map(trimmed),
|
|
638
|
+
// That attempt's own: what it left running and the machine it ran on.
|
|
639
|
+
processes: { leftovers: a.leftovers },
|
|
640
|
+
environment: a.environment,
|
|
610
641
|
};
|
|
611
642
|
})),
|
|
612
643
|
};
|