copilot-operator 0.1.16 → 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.controller.js +23 -0
- package/dist/src/api/operator.service.js +55 -3
- 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 +60 -0
- package/dist/src/plan/importPlan.js +35 -1
- package/dist/src/plan/schema.js +62 -0
- package/dist/src/plan/systemGuide.js +72 -0
- package/dist/src/protocol/replySchema.js +1 -1
- package/dist/src/protocol/reviewSchema.js +10 -5
- package/dist/src/session/exports.js +46 -13
- package/dist/src/session/model.js +20 -0
- package/dist/src/session/store.js +146 -6
- package/dist/src/vcs/artifacts.js +138 -0
- package/dist/src/vcs/inputs.js +332 -0
- package/dist/src/vcs/snapshot.js +395 -0
- package/dist/src/vcs/taskVcs.js +183 -27
- 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/page-409e37e208dc12e5.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-27cb2d544b498d92.js +1 -0
- package/dist/web/_next/static/css/{e2a2c9eb3c226313.css → c37978b44194c37b.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 +9 -3
- package/prompts/level1.md +547 -546
- package/dist/web/_next/static/chunks/549-7fd4fe96682299f5.js +0 -1
- package/dist/web/_next/static/chunks/867-631dc4314840fed6.js +0 -1
- package/dist/web/_next/static/chunks/app/history/page-d7314ac22d47a3ab.js +0 -1
- package/dist/web/_next/static/chunks/app/page-13572a726de0b7db.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-886f5fc99e146408.js +0 -1
- /package/dist/web/_next/static/{GhdY7nVcYi-WV3j7saVPK → JPmlfAEvbNkbghoyvY9uC}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{GhdY7nVcYi-WV3j7saVPK → JPmlfAEvbNkbghoyvY9uC}/_ssgManifest.js +0 -0
|
@@ -126,6 +126,10 @@ let OperatorController = class OperatorController {
|
|
|
126
126
|
sessions() {
|
|
127
127
|
return this.ops.listSessions();
|
|
128
128
|
}
|
|
129
|
+
/** The sessions list in the operator's order: every session id, top to bottom. */
|
|
130
|
+
reorderSessions(body) {
|
|
131
|
+
return this.ops.reorderSessions(body?.ids).catch(fail);
|
|
132
|
+
}
|
|
129
133
|
createSession(body) {
|
|
130
134
|
return this.ops.createSession(body?.name ?? '', body?.projectDir).catch(fail);
|
|
131
135
|
}
|
|
@@ -500,6 +504,10 @@ let OperatorController = class OperatorController {
|
|
|
500
504
|
vcsStatus(id) {
|
|
501
505
|
return this.ops.vcsStatus(id).catch(fail);
|
|
502
506
|
}
|
|
507
|
+
/** The operator's approval of the starting snapshot: one choice per file of the list they saw. */
|
|
508
|
+
vcsSnapshot(id, body) {
|
|
509
|
+
return this.ops.vcsSnapshot(id, body?.choices ?? {}).catch(fail);
|
|
510
|
+
}
|
|
503
511
|
// --- the model picker ---------------------------------------------------------------------
|
|
504
512
|
/** The cached list. Returns null when the picker has never been read on this machine. */
|
|
505
513
|
models() {
|
|
@@ -632,6 +640,13 @@ __decorate([
|
|
|
632
640
|
__metadata("design:paramtypes", []),
|
|
633
641
|
__metadata("design:returntype", Promise)
|
|
634
642
|
], OperatorController.prototype, "sessions", null);
|
|
643
|
+
__decorate([
|
|
644
|
+
Put('sessions-order'),
|
|
645
|
+
__param(0, Body()),
|
|
646
|
+
__metadata("design:type", Function),
|
|
647
|
+
__metadata("design:paramtypes", [Object]),
|
|
648
|
+
__metadata("design:returntype", Promise)
|
|
649
|
+
], OperatorController.prototype, "reorderSessions", null);
|
|
635
650
|
__decorate([
|
|
636
651
|
Post('sessions'),
|
|
637
652
|
__param(0, Body()),
|
|
@@ -1027,6 +1042,14 @@ __decorate([
|
|
|
1027
1042
|
__metadata("design:paramtypes", [String]),
|
|
1028
1043
|
__metadata("design:returntype", Promise)
|
|
1029
1044
|
], OperatorController.prototype, "vcsStatus", null);
|
|
1045
|
+
__decorate([
|
|
1046
|
+
Post('sessions/:id/vcs/snapshot'),
|
|
1047
|
+
__param(0, Param('id')),
|
|
1048
|
+
__param(1, Body()),
|
|
1049
|
+
__metadata("design:type", Function),
|
|
1050
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
1051
|
+
__metadata("design:returntype", Promise)
|
|
1052
|
+
], OperatorController.prototype, "vcsSnapshot", null);
|
|
1030
1053
|
__decorate([
|
|
1031
1054
|
Get('models'),
|
|
1032
1055
|
__metadata("design:type", Function),
|
|
@@ -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';
|
|
@@ -45,6 +45,8 @@ import { unattendedPrecondition } from '../exec/policy.js';
|
|
|
45
45
|
import { createTransport } from '../transport/chatTransport.js';
|
|
46
46
|
import { composeHandoff } from '../session/handoff.js';
|
|
47
47
|
import { planSync } from '../vcs/syncCommand.js';
|
|
48
|
+
import { dirtyPolicy, takeSnapshot } from '../vcs/snapshot.js';
|
|
49
|
+
import { normalisePatterns } from '../vcs/inputs.js';
|
|
48
50
|
import { computeMetrics } from '../session/metrics.js';
|
|
49
51
|
import { resolveDesktopDir, desktopIsSynced } from '../context/desktopDir.js';
|
|
50
52
|
import { saveAndReveal } from './saveToDesktop.js';
|
|
@@ -320,6 +322,14 @@ let OperatorService = class OperatorService {
|
|
|
320
322
|
await this.store.deletePersona(name);
|
|
321
323
|
}
|
|
322
324
|
// --- sessions and tasks ---------------------------------------------------------------
|
|
325
|
+
/** The operator's order for the sessions list, top to bottom. See `SessionStore.reorderSessions`. */
|
|
326
|
+
async reorderSessions(ids) {
|
|
327
|
+
await this.init();
|
|
328
|
+
if (!Array.isArray(ids) || ids.some((x) => typeof x !== 'string'))
|
|
329
|
+
throw new Error('ids must be a list of session ids.');
|
|
330
|
+
await this.store.reorderSessions(ids);
|
|
331
|
+
return await this.listSessions();
|
|
332
|
+
}
|
|
323
333
|
async listSessions() {
|
|
324
334
|
await this.init();
|
|
325
335
|
const all = await this.store.listSessions();
|
|
@@ -404,6 +414,32 @@ let OperatorService = class OperatorService {
|
|
|
404
414
|
merged.baseBranch = merged.baseBranch.trim();
|
|
405
415
|
if (merged.existingBranch !== undefined)
|
|
406
416
|
merged.existingBranch = merged.existingBranch.trim();
|
|
417
|
+
// Only the two known fields, and absent when it says what absent says: reject, asking first.
|
|
418
|
+
const dirty = dirtyPolicy(merged);
|
|
419
|
+
if (dirty.policy === 'reject')
|
|
420
|
+
delete merged.dirtyWorktree;
|
|
421
|
+
else
|
|
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
|
+
}
|
|
407
443
|
// Only the program marks a name as one of its own branches, and a name changed is not that one.
|
|
408
444
|
const stillOwn = !!s.vcs?.branchNameExact && (merged.branchName ?? '').trim() === (s.vcs.branchName ?? '').trim();
|
|
409
445
|
if (stillOwn)
|
|
@@ -1822,11 +1858,27 @@ let OperatorService = class OperatorService {
|
|
|
1822
1858
|
return { ok: false, repoDir: '', problem: "git is not installed, or not on this machine's PATH.", git: null };
|
|
1823
1859
|
// `branch` is where HEAD is; `work` is where the session's work is. After a per-task run
|
|
1824
1860
|
// those differ, and the second is the one the operator is asking about.
|
|
1825
|
-
const preflight = await vcsPreflight(session);
|
|
1861
|
+
const preflight = await vcsPreflight(session, () => this.store.listSessions());
|
|
1826
1862
|
// The local branches, so "carry on an existing branch" can offer them rather than be typed blind.
|
|
1827
1863
|
const branches = preflight.repoDir ? await localBranches(preflight.repoDir) : [];
|
|
1828
1864
|
return { ...preflight, git, work: sessionBranches(session), branches };
|
|
1829
1865
|
}
|
|
1866
|
+
/**
|
|
1867
|
+
* The operator's approval of the starting snapshot: their uncommitted changes become the commit the
|
|
1868
|
+
* session starts from, each file taken or left out as they chose on the list. See `vcs/snapshot.ts`.
|
|
1869
|
+
* Refused while anything could be working in the repository, as a restore is: it changes branches.
|
|
1870
|
+
*/
|
|
1871
|
+
async vcsSnapshot(sessionId, choices) {
|
|
1872
|
+
await this.init();
|
|
1873
|
+
const session = await this.store.getSession(sessionId);
|
|
1874
|
+
if (!session)
|
|
1875
|
+
throw new Error('No such session.');
|
|
1876
|
+
const refused = await this.restoreRefusal(session);
|
|
1877
|
+
if (refused)
|
|
1878
|
+
return { ok: false, problem: refused };
|
|
1879
|
+
const taken = await takeSnapshot(session, { approved: true, choices: choices && typeof choices === 'object' ? choices : {} }, this.bus, async (mutate) => void (await this.store.updateSession(sessionId, mutate)), () => this.store.listSessions());
|
|
1880
|
+
return taken.ok ? { ok: true, branch: taken.start.branch, commit: taken.start.commit } : { ok: false, problem: taken.problem };
|
|
1881
|
+
}
|
|
1830
1882
|
/**
|
|
1831
1883
|
* What going back to before a task would do, and then doing it.
|
|
1832
1884
|
*
|
|
@@ -2570,7 +2622,7 @@ let OperatorService = class OperatorService {
|
|
|
2570
2622
|
commit: t.vcs?.commit,
|
|
2571
2623
|
vcsProblem: t.vcs?.problem,
|
|
2572
2624
|
sessionInactive: s.active === false || undefined,
|
|
2573
|
-
autoRetries: t
|
|
2625
|
+
autoRetries: freshRetriesOfLatestRun(t) || undefined,
|
|
2574
2626
|
attempt: t.attempt,
|
|
2575
2627
|
changedFiles: t.vcs?.commit && t.vcs.baseCommit ? (t.vcs.files?.length ?? 0) : undefined,
|
|
2576
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
|
@@ -454,6 +454,29 @@ Before a session's first branch the runner fetches and fast-forwards the branch
|
|
|
454
454
|
merges. \`"updateFromRemote": false\` turns that off — only when the user asks to work offline or from
|
|
455
455
|
local commits on purpose.
|
|
456
456
|
|
|
457
|
+
**dirtyWorktree** — what happens when the repository has uncommitted changes (the user's own, such as
|
|
458
|
+
input files a task needs) before a session's first task. Ask only when the user mentions such files:
|
|
459
|
+
\`{ "policy": "snapshot" }\` makes them one commit, "Capture operator baseline before run", on a branch of
|
|
460
|
+
its own, and the session starts from it; the user approves the exact list on the session's page first.
|
|
461
|
+
\`"tracked-only-snapshot"\` takes tracked changes only. Secrets (\`.env\`), tool output and files outside
|
|
462
|
+
the project are never taken, and an ignored file only when a task's \`scope\` names it. The snapshot is
|
|
463
|
+
taken where the changes are, so use it with \`startFrom\` \`"branch"\` only when the repository is already on
|
|
464
|
+
that branch, and never with \`"existing-branch"\`. Leaving it out keeps \`"reject"\`: version control stays out
|
|
465
|
+
of a tree with uncommitted changes.
|
|
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
|
+
|
|
457
480
|
Put the same \`startFrom\` on every session that works in one repository unless the user wants it
|
|
458
481
|
mixed. Leaving it out keeps the old behaviour — whatever branch the repository is on — which is
|
|
459
482
|
the one to avoid.
|
|
@@ -512,6 +535,29 @@ const VCS_DETAIL_BG = `
|
|
|
512
535
|
reset или merge. \`"updateFromRemote": false\` го изключва — само ако потребителят поиска да работи
|
|
513
536
|
офлайн или нарочно от локални commit-и.
|
|
514
537
|
|
|
538
|
+
**dirtyWorktree** — какво става, когато хранилището има некомитнати промени (на потребителя, например
|
|
539
|
+
входни файлове, които задача използва) преди първата задача на сесията. Питай само ако потребителят
|
|
540
|
+
спомене такива файлове: \`{ "policy": "snapshot" }\` ги прави един commit, „Capture operator baseline before
|
|
541
|
+
run“, на отделен клон, и сесията тръгва от него; потребителят първо одобрява точния списък на страницата на
|
|
542
|
+
сесията. \`"tracked-only-snapshot"\` взема само tracked промени. Тайни (\`.env\`), изход от инструменти и
|
|
543
|
+
файлове извън проекта никога не се вземат, а ignored файл — само когато \`scope\` на задача го посочва.
|
|
544
|
+
Снимката се прави там, където са промените, затова с \`startFrom\` \`"branch"\` само ако хранилището вече е на
|
|
545
|
+
този клон, и никога с \`"existing-branch"\`. Ако го пропуснеш, остава \`"reject"\`: контролът на версиите
|
|
546
|
+
не влиза в дърво с некомитнати промени.
|
|
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
|
+
|
|
515
561
|
Сложи един и същ \`startFrom\` на всички сесии в едно хранилище, освен ако потребителят не иска
|
|
516
562
|
различни. Ако го пропуснеш, остава старото поведение — от който клон е хранилището в момента —
|
|
517
563
|
което е за избягване.
|
|
@@ -1438,6 +1484,13 @@ bot actually does, because it changes what a good task looks like:
|
|
|
1438
1484
|
reads it and decides the next step. This repeats until Copilot writes a final summary.
|
|
1439
1485
|
- Nothing is interactive. A command that waits for a keypress, opens an editor or needs a
|
|
1440
1486
|
browser login will hang the task.
|
|
1487
|
+
- **How the working chat learns the project: by running commands.** No file of the project is
|
|
1488
|
+
copied, attached or uploaded to it. In each task it reads what it needs itself, with terminal
|
|
1489
|
+
commands in the project folder — it lists the folders, opens the files, looks at the git log —
|
|
1490
|
+
and the output comes back to it. So never ask the user how the chat will get the project's files,
|
|
1491
|
+
how it will see the code or whether to attach anything; that is settled. If the organisation
|
|
1492
|
+
document still says how the chat reaches a project (attached files, a Desktop copy), ignore it:
|
|
1493
|
+
that way no longer exists. Write prompts that name the paths to read, not files to attach.
|
|
1441
1494
|
- **Only the project folders.** The runner refuses any command, check or file path that reaches
|
|
1442
1495
|
outside the session's folder and the projects listed below — reading as well as writing — and
|
|
1443
1496
|
anything that manages the machine: the registry, services, the network, users, installs that
|
|
@@ -1655,6 +1708,13 @@ function buildBg(projects, ctx = {}) {
|
|
|
1655
1708
|
чете и решава следващата стъпка. Това се повтаря, докато Copilot не напише финално резюме.
|
|
1656
1709
|
- Нищо не е интерактивно. Команда, която чака клавиш, отваря редактор или иска вход през
|
|
1657
1710
|
браузър, ще увисне.
|
|
1711
|
+
- **Как работният чат опознава проекта: с команди.** Никой файл от проекта не се копира, прикачва
|
|
1712
|
+
или качва в него. Във всяка задача той сам чете каквото му трябва, с терминални команди в папката
|
|
1713
|
+
на проекта — изброява папките, отваря файловете, гледа git историята — и изходът се връща при него.
|
|
1714
|
+
Затова никога не питай потребителя как чатът ще получи файловете на проекта, как ще види кода или
|
|
1715
|
+
дали да прикачи нещо; това е решено. Ако документът за организацията още казва как чатът стига до
|
|
1716
|
+
проект (прикачени файлове, копие на Desktop-а), не го слушай: такъв начин вече няма. Пиши prompt-ове,
|
|
1717
|
+
които назовават пътищата за четене, а не файлове за прикачване.
|
|
1658
1718
|
- **Само папките на проектите.** Runner-ът отказва всяка команда, проверка или път към файл, който
|
|
1659
1719
|
излиза извън папката на сесията и проектите, изброени по-долу — и за четене, и за запис — и всичко,
|
|
1660
1720
|
което управлява машината: регистъра, услугите, мрежата, потребителите, инсталации извън проекта.
|
|
@@ -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,7 +190,21 @@ 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
|
|
193
|
+
const { dirtyWorktree: plannedDirty, userInputs: plannedInputs, artifacts: plannedArtifacts, ...plannedVcs } = planned.vcs ?? {};
|
|
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 };
|
|
204
|
+
// Stored in the one spelling the session has: see `DirtyWorktree`.
|
|
205
|
+
const policy = plannedDirty?.policy === 'snapshot' && plannedDirty.includeUntracked === false ? 'tracked-only-snapshot' : plannedDirty?.policy;
|
|
206
|
+
if (policy && policy !== 'reject')
|
|
207
|
+
vcs.dirtyWorktree = { policy, ...(plannedDirty?.requireApproval === false ? { requireApproval: false } : {}) };
|
|
176
208
|
vcs.branchPrefix = vcs.branchPrefix.trim() || DEFAULT_VCS.branchPrefix;
|
|
177
209
|
vcs.repoDir = vcs.repoDir.trim();
|
|
178
210
|
// A branch to carry on, given alone, is that choice: see `VersionControl.startFrom`.
|
|
@@ -211,5 +243,7 @@ defaultProjectDir = '') {
|
|
|
211
243
|
}
|
|
212
244
|
sessions.push({ id: created.id, name: created.name, tasks: titles.length, titles });
|
|
213
245
|
}
|
|
246
|
+
// The plan's sessions on top of the list, in the order the plan gives them, not newest first.
|
|
247
|
+
await store.placeOnTop(sessions.map((s) => s.id));
|
|
214
248
|
return { sessions, taskCount: sessions.reduce((n, s) => n + s.tasks, 0), warnings };
|
|
215
249
|
}
|
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.
|
|
@@ -60,6 +61,52 @@ const VcsInput = z
|
|
|
60
61
|
* Absent means true. See `VersionControl.updateFromRemote`.
|
|
61
62
|
*/
|
|
62
63
|
updateFromRemote: z.boolean().optional(),
|
|
64
|
+
/**
|
|
65
|
+
* What the session's first run does with uncommitted changes. See `DirtyWorktree`. Accepts the
|
|
66
|
+
* longer spelling a chat model may write — `includeUntracked: false` is `tracked-only-snapshot`,
|
|
67
|
+
* and `includeIgnoredOnlyWhenScoped` can only be true, since that is the rule, not a choice.
|
|
68
|
+
*/
|
|
69
|
+
dirtyWorktree: z
|
|
70
|
+
.object({
|
|
71
|
+
policy: z.enum(['reject', 'snapshot', 'tracked-only-snapshot'], {
|
|
72
|
+
error: 'dirtyWorktree.policy must be "reject" (uncommitted changes keep version control out, as until now), "snapshot" (they become the commit the session starts from) or "tracked-only-snapshot" (the same, tracked changes only). "discard" is not offered: the runner never throws work away.',
|
|
73
|
+
}),
|
|
74
|
+
requireApproval: z.boolean().optional(),
|
|
75
|
+
includeUntracked: z.boolean().optional(),
|
|
76
|
+
includeIgnoredOnlyWhenScoped: z
|
|
77
|
+
.literal(true, { error: 'dirtyWorktree.includeIgnoredOnlyWhenScoped can only be true: an ignored file goes into a snapshot only when a task\'s scope names it. Leave the field out.' })
|
|
78
|
+
.optional(),
|
|
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(),
|
|
63
110
|
}, {
|
|
64
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.',
|
|
65
112
|
})
|
|
@@ -78,6 +125,21 @@ const VcsInput = z
|
|
|
78
125
|
message: `"existingBranch" carries on a branch that exists, and startFrom "${vcs.startFrom}" cuts a new one. Keep one: startFrom "existing-branch" with existingBranch, or leave existingBranch out.`,
|
|
79
126
|
});
|
|
80
127
|
}
|
|
128
|
+
if (vcs.dirtyWorktree?.policy === 'tracked-only-snapshot' && vcs.dirtyWorktree.includeUntracked === true) {
|
|
129
|
+
ctx.addIssue({
|
|
130
|
+
code: 'custom',
|
|
131
|
+
path: ['dirtyWorktree', 'includeUntracked'],
|
|
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.',
|
|
133
|
+
});
|
|
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
|
+
}
|
|
81
143
|
if (vcs.enabled && !vcs.repoDir.trim()) {
|
|
82
144
|
ctx.addIssue({
|
|
83
145
|
code: 'custom',
|