copilot-operator 0.1.18 → 0.1.19
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 +10 -0
- package/dist/src/cli.js +11 -29
- package/dist/src/config/version.js +30 -0
- package/dist/src/orchestrator/taskRunner.js +56 -9
- package/dist/src/plan/brief.js +16 -8
- package/dist/src/plan/importPlan.js +14 -0
- package/dist/src/plan/schema.js +15 -0
- package/dist/src/plan/systemGuide.js +10 -2
- package/dist/src/session/exports.js +9 -0
- package/dist/src/session/store.js +2 -0
- package/dist/src/vcs/artifacts.js +56 -9
- package/dist/src/vcs/inputs.js +145 -28
- package/dist/src/vcs/snapshot.js +117 -6
- package/dist/src/vcs/taskVcs.js +138 -14
- package/dist/web/404.html +2 -2
- package/dist/web/_next/static/chunks/867-d9c54f7a2b285b3f.js +1 -0
- package/dist/web/_next/static/chunks/app/appearance/page-a18019cdc395de3d.js +1 -0
- package/dist/web/_next/static/chunks/app/layout-3efbb282292ccc38.js +1 -0
- package/dist/web/_next/static/chunks/app/sessions/view/page-99f9153fd75078d0.js +1 -0
- package/dist/web/_next/static/chunks/app/system/page-5ae1f8054208e6eb.js +1 -0
- package/dist/web/appearance.html +2 -2
- package/dist/web/appearance.txt +25 -26
- package/dist/web/defaults.html +2 -2
- package/dist/web/defaults.txt +25 -26
- package/dist/web/history.html +2 -2
- package/dist/web/history.txt +25 -26
- package/dist/web/import.html +2 -2
- package/dist/web/import.txt +25 -26
- package/dist/web/index.html +2 -2
- package/dist/web/index.txt +25 -26
- package/dist/web/level1.html +2 -2
- package/dist/web/level1.txt +25 -26
- package/dist/web/presets.html +2 -2
- package/dist/web/presets.txt +25 -26
- package/dist/web/sessions/view.html +2 -2
- package/dist/web/sessions/view.txt +25 -26
- package/dist/web/system.html +2 -2
- package/dist/web/system.txt +25 -26
- package/package.json +4 -3
- package/dist/web/_next/static/chunks/867-c078b5b504bf4289.js +0 -1
- package/dist/web/_next/static/chunks/app/appearance/page-9b13583bfee63b32.js +0 -1
- package/dist/web/_next/static/chunks/app/layout-560bd6d173302e5a.js +0 -1
- package/dist/web/_next/static/chunks/app/sessions/view/page-27cb2d544b498d92.js +0 -1
- package/dist/web/_next/static/chunks/app/system/page-3aa2a0fef5454e69.js +0 -1
- /package/dist/web/_next/static/{JPmlfAEvbNkbghoyvY9uC → ofnRLEgWx9th5w_qtuFnB}/_buildManifest.js +0 -0
- /package/dist/web/_next/static/{JPmlfAEvbNkbghoyvY9uC → ofnRLEgWx9th5w_qtuFnB}/_ssgManifest.js +0 -0
|
@@ -47,6 +47,8 @@ import { composeHandoff } from '../session/handoff.js';
|
|
|
47
47
|
import { planSync } from '../vcs/syncCommand.js';
|
|
48
48
|
import { dirtyPolicy, takeSnapshot } from '../vcs/snapshot.js';
|
|
49
49
|
import { normalisePatterns } from '../vcs/inputs.js';
|
|
50
|
+
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
51
|
+
import { botVersion } from '../config/version.js';
|
|
50
52
|
import { computeMetrics } from '../session/metrics.js';
|
|
51
53
|
import { resolveDesktopDir, desktopIsSynced } from '../context/desktopDir.js';
|
|
52
54
|
import { saveAndReveal } from './saveToDesktop.js';
|
|
@@ -436,6 +438,12 @@ let OperatorService = class OperatorService {
|
|
|
436
438
|
delete merged.artifacts;
|
|
437
439
|
else
|
|
438
440
|
merged.artifacts = { paths: artifactPaths };
|
|
441
|
+
// Artifacts are kept out of git; one that covers the input files would keep them out too.
|
|
442
|
+
const hidden = coveredByArtifacts(merged.userInputs?.paths ?? [], artifactPaths);
|
|
443
|
+
if (hidden.length > 0) {
|
|
444
|
+
throw new Error(`An artifacts pattern covers the input files ${hidden.map((h) => `"${h}"`).join(', ')}: artifacts are kept out of git, so the inputs would be too. ` +
|
|
445
|
+
'Name only the folders the evidence is written to, not a whole project folder.');
|
|
446
|
+
}
|
|
439
447
|
// New inputs are settled again at the next task: the recorded ones are no longer the whole list.
|
|
440
448
|
if (JSON.stringify(merged.userInputs ?? null) !== JSON.stringify(s.vcs?.userInputs ?? null) && s.vcsStart?.inputs) {
|
|
441
449
|
s.vcsStart = { ...s.vcsStart, inputs: undefined };
|
|
@@ -2658,6 +2666,8 @@ let OperatorService = class OperatorService {
|
|
|
2658
2666
|
const edge = ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe'].find((p) => existsSync(p));
|
|
2659
2667
|
const holders = findEdgeUsingProfile(cfg.resolved.profileDir);
|
|
2660
2668
|
return {
|
|
2669
|
+
// Which copilot-operator this is: `cop --version` without a terminal.
|
|
2670
|
+
version: botVersion(),
|
|
2661
2671
|
node: process.versions.node,
|
|
2662
2672
|
edge: edge ?? null,
|
|
2663
2673
|
profileDir: cfg.resolved.profileDir,
|
package/dist/src/cli.js
CHANGED
|
@@ -24,32 +24,14 @@ import { PolicyLockUnusableError } from './config/lockedPolicy.js';
|
|
|
24
24
|
import { EventBus } from './session/events.js';
|
|
25
25
|
import { terminalAuthorizer, unattendedAuthorizer } from './exec/authorizer.js';
|
|
26
26
|
import { unattendedPrecondition } from './exec/policy.js';
|
|
27
|
-
import {
|
|
27
|
+
import { isOwnCheckout } from './exec/workDir.js';
|
|
28
28
|
import { desktopIsSynced, resolveDesktopDir } from './context/desktopDir.js';
|
|
29
29
|
import { Url } from './transport/locators.js';
|
|
30
|
+
import { botVersion } from './config/version.js';
|
|
30
31
|
import { findEdgeUsingProfile } from './transport/profileLock.js';
|
|
31
32
|
import { assessIsolation, readIsolationSignals } from './exec/isolation.js';
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* It was a literal, '0.1.0', and stayed that through thirteen releases, so a bug report's
|
|
36
|
-
* `cop --version` named a version that never had the bug. Read where the code is — `src/` under
|
|
37
|
-
* tsx, `dist/src/` in a clone, `node_modules/copilot-operator/dist/src/` once installed — so it
|
|
38
|
-
* cannot drift from what npm installed. botRootDir() falls back to the working directory when it
|
|
39
|
-
* finds no package.json of ours, and that one is the user's project's: its version is not ours to
|
|
40
|
-
* print, so the name is checked and anything else says "unknown".
|
|
41
|
-
*/
|
|
42
|
-
function ownVersion() {
|
|
43
|
-
try {
|
|
44
|
-
const pkg = JSON.parse(readFileSync(join(botRootDir(), 'package.json'), 'utf8'));
|
|
45
|
-
if (pkg.name === 'copilot-operator' && typeof pkg.version === 'string' && pkg.version)
|
|
46
|
-
return pkg.version;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
// No readable package.json: say so rather than guess.
|
|
50
|
-
}
|
|
51
|
-
return 'unknown';
|
|
52
|
-
}
|
|
33
|
+
/** The running version; see `config/version.ts`. */
|
|
34
|
+
const ownVersion = botVersion;
|
|
53
35
|
const program = new Command();
|
|
54
36
|
program
|
|
55
37
|
.name('cop')
|
|
@@ -213,7 +195,7 @@ program
|
|
|
213
195
|
replyTimeoutMs: 60_000,
|
|
214
196
|
signInTimeoutMs: 15 * 60_000,
|
|
215
197
|
humanWaitMs: 15 * 60_000,
|
|
216
|
-
onEvent: (e, d) => process.stdout.write(` ${e}${d && Object.keys(d).length ? ' ' + JSON.stringify(d) : ''}
|
|
198
|
+
onEvent: (e, d) => process.stdout.write(` ${e}${d && Object.keys(d).length ? ' ' + JSON.stringify(d) : ''}
|
|
217
199
|
`),
|
|
218
200
|
});
|
|
219
201
|
await transport.open();
|
|
@@ -506,14 +488,14 @@ program
|
|
|
506
488
|
console.log('');
|
|
507
489
|
console.log(`task "${done?.title}": ${done?.status} after ${done?.iterations ?? 0} iteration(s)`);
|
|
508
490
|
if (done?.summary)
|
|
509
|
-
console.log(`
|
|
510
|
-
Summary:
|
|
491
|
+
console.log(`
|
|
492
|
+
Summary:
|
|
511
493
|
${done.summary}`);
|
|
512
494
|
if (done?.reason)
|
|
513
|
-
console.log(`
|
|
495
|
+
console.log(`
|
|
514
496
|
Reason: ${done.reason}`);
|
|
515
497
|
if (final?.chat)
|
|
516
|
-
console.log(`
|
|
498
|
+
console.log(`
|
|
517
499
|
chat: ${final.chat.name} — ${final.chat.url}`);
|
|
518
500
|
if (done?.runId)
|
|
519
501
|
console.log(`log: ${join(cfg.resolved.runsDir, done.runId, 'task-log.txt')}`);
|
|
@@ -533,8 +515,8 @@ program
|
|
|
533
515
|
return;
|
|
534
516
|
}
|
|
535
517
|
const c = withChat.chat;
|
|
536
|
-
console.log(`${c.name}
|
|
537
|
-
${c.url}
|
|
518
|
+
console.log(`${c.name}
|
|
519
|
+
${c.url}
|
|
538
520
|
session ${withChat.id}, ${withChat.tasks.length} task(s), created ${c.createdAt}`);
|
|
539
521
|
});
|
|
540
522
|
program.parseAsync(process.argv).catch((e) => {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The version of the copilot-operator that is running, from its own package.json.
|
|
3
|
+
*
|
|
4
|
+
* It was a literal, '0.1.0', and stayed that through thirteen releases, so a bug report's
|
|
5
|
+
* `cop --version` named a version that never had the bug. Read where the code is — `src/` under
|
|
6
|
+
* tsx, `dist/src/` in a clone, `node_modules/copilot-operator/dist/src/` once installed — so it
|
|
7
|
+
* cannot drift from what npm installed. botRootDir() falls back to the working directory when it
|
|
8
|
+
* finds no package.json of ours, and that one is the user's project's: its version is not ours to
|
|
9
|
+
* print, so the name is checked and anything else says "unknown".
|
|
10
|
+
*
|
|
11
|
+
* Written into every export and attempt record too: a report sent as a screenshot from another
|
|
12
|
+
* machine could not say which version made it, and the operator had to be asked.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { botRootDir } from '../exec/workDir.js';
|
|
17
|
+
let cached;
|
|
18
|
+
export function botVersion() {
|
|
19
|
+
if (cached)
|
|
20
|
+
return cached;
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(join(botRootDir(), 'package.json'), 'utf8'));
|
|
23
|
+
if (pkg.name === 'copilot-operator' && typeof pkg.version === 'string' && pkg.version)
|
|
24
|
+
return (cached = pkg.version);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// No readable package.json: say so rather than guess.
|
|
28
|
+
}
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
@@ -45,9 +45,9 @@ import { composeHandoff } from '../session/handoff.js';
|
|
|
45
45
|
import { ProgressWatch } from './progress.js';
|
|
46
46
|
import { contractConflicts, readsTreeClean } from './contract.js';
|
|
47
47
|
import { treeFingerprint } from '../vcs/git.js';
|
|
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';
|
|
48
|
+
import { prepareForTask, commitTaskResult, commitShortfall, repoDirOf, trackedRepoOf } from '../vcs/taskVcs.js';
|
|
49
|
+
import { protectInputs, inputsMessage, inputsAtCommit, untrackedInputs } from '../vcs/inputs.js';
|
|
50
|
+
import { artifactPatterns, artifactState, keepArtifacts } from '../vcs/artifacts.js';
|
|
51
51
|
import { exportMachine, writeAttemptRecord } from '../session/exports.js';
|
|
52
52
|
/** Why a task that was reported as done is being closed as failed. */
|
|
53
53
|
function checksFailedReason(outcomes) {
|
|
@@ -582,12 +582,14 @@ export async function runTask(transport, session, task, deps) {
|
|
|
582
582
|
};
|
|
583
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
584
|
let inputsGuard = null;
|
|
585
|
+
/** The artifact files as they were when the task started; what is kept afterwards is what changed. */
|
|
586
|
+
let artifactsBefore;
|
|
585
587
|
/** Puts back whatever a round did to the input files, and tells the chat and the record. */
|
|
586
588
|
const guardInputs = async (iteration) => {
|
|
587
589
|
if (!inputsGuard)
|
|
588
590
|
return null;
|
|
589
591
|
const guard = inputsGuard;
|
|
590
|
-
const check = await protectInputs(repoDirOf(session), guard.base, guard.inputs).catch((e) => ({
|
|
592
|
+
const check = await protectInputs(repoDirOf(session), guard.base, guard.inputs, guard.keep).catch((e) => ({
|
|
591
593
|
changed: ['(the input files)'],
|
|
592
594
|
restored: [],
|
|
593
595
|
failed: [{ path: '(the input files)', why: e.message }],
|
|
@@ -617,6 +619,10 @@ export async function runTask(transport, session, task, deps) {
|
|
|
617
619
|
status = 'failed';
|
|
618
620
|
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
621
|
}
|
|
622
|
+
// Said on the record either way: the inputs as the task leaves them, against their recorded sums.
|
|
623
|
+
if (check && check.failed.length === 0) {
|
|
624
|
+
sink.event('inputs-verified', { files: inputsGuard.inputs.files.length }, `${inputsGuard.inputs.files.length} input file(s) are as the session's start recorded them: same sums`);
|
|
625
|
+
}
|
|
620
626
|
}
|
|
621
627
|
/*
|
|
622
628
|
* The session's artifacts, whatever the outcome: copied into this attempt's record, so the
|
|
@@ -624,9 +630,14 @@ export async function runTask(transport, session, task, deps) {
|
|
|
624
630
|
*/
|
|
625
631
|
const patterns = artifactPatterns(session.vcs);
|
|
626
632
|
const projectRoot = repoDirOf(session) || session.projectDir?.trim() || '';
|
|
627
|
-
if (patterns.length > 0 && projectRoot) {
|
|
628
|
-
|
|
629
|
-
|
|
633
|
+
if ((patterns.length > 0 || (task.outputs?.length ?? 0) > 0) && projectRoot) {
|
|
634
|
+
// Only what this task made or changed, inside its scope, or declared as its outputs.
|
|
635
|
+
const kept = await keepArtifacts(projectRoot, patterns, join(artifactsDir, 'project'), { before: artifactsBefore, scope: task.scope, outputs: task.outputs })
|
|
636
|
+
.catch((e) => ({ kept: [], skipped: [`(all: ${e.message})`], unchanged: 0, outsideScope: [] }));
|
|
637
|
+
sink.event('artifacts-kept', { kept: kept.kept.length, skipped: kept.skipped, unchanged: kept.unchanged, outsideScope: kept.outsideScope.slice(0, 20) }, `artifacts kept with the run: ${kept.kept.length} file(s) this task made or changed` +
|
|
638
|
+
`${kept.unchanged > 0 ? `; ${kept.unchanged} already there and untouched, not kept` : ''}` +
|
|
639
|
+
`${kept.outsideScope.length > 0 ? `; ${kept.outsideScope.length} outside the task's scope, not kept` : ''}` +
|
|
640
|
+
`${kept.skipped.length > 0 ? `; not kept: ${kept.skipped.slice(0, 5).join('; ')}` : ''}`, kept.skipped.length > 0 ? 'warn' : 'info');
|
|
630
641
|
await setTask((t) => {
|
|
631
642
|
t.artifactsKept = kept.kept.length > 0 ? kept.kept : undefined;
|
|
632
643
|
});
|
|
@@ -654,10 +665,26 @@ export async function runTask(transport, session, task, deps) {
|
|
|
654
665
|
// The task is re-read first, because the branch was recorded on it after this closure
|
|
655
666
|
// was created.
|
|
656
667
|
const fresh = (await store.getSession(session.id))?.tasks.find((x) => x.id === task.id);
|
|
668
|
+
let vcsError;
|
|
657
669
|
const vcsAfter = await commitTaskResult(session, { ...task, vcs: fresh?.vcs }, { status, summary, reason, deviations }, bus).catch((e) => {
|
|
670
|
+
vcsError = e.message;
|
|
658
671
|
sink.event('vcs-error', { error: String(e) }, `version control failed after the task: ${e.message}`, 'warn');
|
|
659
672
|
return undefined;
|
|
660
673
|
});
|
|
674
|
+
/*
|
|
675
|
+
* With version control on, a task is done only when its work is committed. The commit used to
|
|
676
|
+
* fail — on the branch it was moved off, on a git error, on uncommitted files left behind — and
|
|
677
|
+
* the task still ended done, its work loose in the tree and the next task refusing to start over
|
|
678
|
+
* it. Now it ends failed with why; the work stays in the working tree, nothing is lost.
|
|
679
|
+
*/
|
|
680
|
+
if (status === 'done') {
|
|
681
|
+
const uncommitted = await commitShortfall(session, fresh?.vcs ?? task.vcs, vcsAfter, vcsError);
|
|
682
|
+
if (uncommitted) {
|
|
683
|
+
status = 'failed';
|
|
684
|
+
reason = uncommitted;
|
|
685
|
+
sink.event('commit-failed', { problem: vcsAfter?.problem ?? vcsError, uncommitted: vcsAfter?.afterCommit?.changed?.slice(0, 20) }, uncommitted, 'error');
|
|
686
|
+
}
|
|
687
|
+
}
|
|
661
688
|
/*
|
|
662
689
|
* The checks about a clean tree, now that the runner has committed. A done task whose tree is
|
|
663
690
|
* still not clean after the commit has failed; any other ending keeps its own reason, and the
|
|
@@ -912,8 +939,23 @@ export async function runTask(transport, session, task, deps) {
|
|
|
912
939
|
// The operator's input files, read-only on a branch of the task's own; see `vcs/inputs.ts`.
|
|
913
940
|
const startNow = (await store.getSession(session.id))?.vcsStart;
|
|
914
941
|
if (onOwnBranch && startNow?.inputs?.readOnly && startNow.inputs.files.length > 0) {
|
|
915
|
-
|
|
942
|
+
/*
|
|
943
|
+
* Held to the inputs as this task's own starting commit has them, not the session's record: a
|
|
944
|
+
* re-run is cut from where the task first started, before any inputs changed since. And what is
|
|
945
|
+
* under the patterns untracked now is the operator's, never removed by the guard.
|
|
946
|
+
*/
|
|
947
|
+
const baseCommit = prepared.vcs.baseCommit;
|
|
948
|
+
inputsGuard = {
|
|
949
|
+
base: baseCommit,
|
|
950
|
+
inputs: await inputsAtCommit(repoDirOf(session), baseCommit, startNow.inputs),
|
|
951
|
+
keep: await untrackedInputs(repoDirOf(session), startNow.inputs.patterns),
|
|
952
|
+
};
|
|
916
953
|
}
|
|
954
|
+
// Before the first step: what is under the artifact patterns now is not this task's evidence.
|
|
955
|
+
const artifactRoot = repoDirOf(session) || session.projectDir?.trim() || '';
|
|
956
|
+
const artifactPatternsNow = [...artifactPatterns(session.vcs), ...(task.outputs ?? [])];
|
|
957
|
+
if (artifactRoot && artifactPatternsNow.length > 0)
|
|
958
|
+
artifactsBefore = await artifactState(artifactRoot, artifactPatternsNow).catch(() => undefined);
|
|
917
959
|
/** The working tree's fingerprint, when the task is on a branch of its own; null otherwise. */
|
|
918
960
|
const treeNow = async () => prepared.vcs.branch && !prepared.vcs.problem && prepared.vcs.baseCommit ? await treeFingerprint(repoDirOf(session)).catch(() => null) : null;
|
|
919
961
|
/*
|
|
@@ -1004,7 +1046,12 @@ export async function runTask(transport, session, task, deps) {
|
|
|
1004
1046
|
workDirNote: workingDirNote(work),
|
|
1005
1047
|
vcsNote: prepared.note,
|
|
1006
1048
|
readOnlyNote: task.readOnly ? READ_ONLY_NOTE : undefined,
|
|
1007
|
-
scopeNote:
|
|
1049
|
+
scopeNote: [
|
|
1050
|
+
scopeNote(scope, scopeEnforced),
|
|
1051
|
+
(task.outputs?.length ?? 0) > 0
|
|
1052
|
+
? `## Outputs\n\nThis task is to produce: ${(task.outputs ?? []).map((o) => `\`${o}\``).join(', ')}. What it creates or changes there is kept with the run's record as its evidence.`
|
|
1053
|
+
: '',
|
|
1054
|
+
].filter(Boolean).join('\n\n'),
|
|
1008
1055
|
approachesNote: minApproaches > MIN_TRIED_APPROACHES
|
|
1009
1056
|
? `## Giving up\n\nThis task may end with status "blocked" only after at least ${minApproaches} genuinely different approaches, ` +
|
|
1010
1057
|
`each listed in "tried". With fewer, the runner asks you for another approach instead of accepting it.`
|
package/dist/src/plan/brief.js
CHANGED
|
@@ -324,6 +324,7 @@ const FIELD_ROWS_EN = [
|
|
|
324
324
|
['review', 'task', 'no', 'Set to `false` to skip the review for one task. Absent means the session decides.'],
|
|
325
325
|
['readOnly', 'task', 'no', '`true` for a task that must not change files — an audit, a smoke test, a report. The runner fails it if the tree changed, and still commits the change on its branch so nothing is lost.'],
|
|
326
326
|
['scope', 'task', 'no', 'The paths the task may change: files, folders (`tests/e2e/`) or `*`/`**` patterns. With version control on, the runner puts back any change outside them. Give it when the task is about one suite or module.'],
|
|
327
|
+
['outputs', 'task', 'no', 'Evidence the task is to produce (a report, an archive): patterns. What it creates or changes there is kept with the run\'s record.'],
|
|
327
328
|
['tasks', 'session', 'yes', 'At least one, in the order they must run.'],
|
|
328
329
|
['title', 'task', 'yes', '3 to 120 characters. Short, latin, hyphenated.'],
|
|
329
330
|
['prompt', 'task', 'yes', 'The task itself, at least 30 characters. This is what Copilot reads.'],
|
|
@@ -394,6 +395,7 @@ const FIELD_ROWS_BG = [
|
|
|
394
395
|
['review', 'задача', 'не', 'Сложи `false`, за да се пропусне рецензията само за тази задача. Липсата значи каквото казва сесията.'],
|
|
395
396
|
['readOnly', 'задача', 'не', '`true` за задача, която не бива да променя файлове — одит, smoke тест, доклад. Runner-ът я проваля, ако дървото е променено, и пак комитва промяната на клона ѝ, за да не се губи нищо.'],
|
|
396
397
|
['scope', 'задача', 'не', 'Пътищата, които задачата може да променя: файлове, папки (`tests/e2e/`) или шаблони с `*`/`**`. При включен контрол на версиите runner-ът връща всяка промяна извън тях. Давай го, когато задачата е за един suite или модул.'],
|
|
398
|
+
['outputs', 'задача', 'не', 'Доказателства, които задачата трябва да създаде (отчет, архив): шаблони. Каквото създаде или промени там, се пази към записа на пускането.'],
|
|
397
399
|
['tasks', 'сесия', 'да', 'Поне една, в реда, в който трябва да се изпълнят.'],
|
|
398
400
|
['title', 'задача', 'да', 'От 3 до 120 знака. Кратко, латиница, с тирета.'],
|
|
399
401
|
['prompt', 'задача', 'да', 'Самата задача, поне 30 знака. Това чете Copilot.'],
|
|
@@ -461,8 +463,8 @@ its own, and the session starts from it; the user approves the exact list on the
|
|
|
461
463
|
\`"tracked-only-snapshot"\` takes tracked changes only. Secrets (\`.env\`), tool output and files outside
|
|
462
464
|
the project are never taken, and an ignored file only when a task's \`scope\` names it. The snapshot is
|
|
463
465
|
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"\`:
|
|
465
|
-
|
|
466
|
+
that branch, and never with \`"existing-branch"\`. Leaving it out keeps \`"reject"\`: a tree with uncommitted
|
|
467
|
+
changes refuses the task, and nothing runs.
|
|
466
468
|
|
|
467
469
|
**userInputs** — files the user provides for the work (schemas, test data):
|
|
468
470
|
\`{ "paths": ["rules-engine/test-data/schemas/*.yaml"] }\`. They go into the commit every session starts from —
|
|
@@ -471,11 +473,14 @@ gets them committed on top of it — and they are read-only while the tasks run
|
|
|
471
473
|
the user wants the work to change them). Ask the user which files are inputs; never name a whole folder of
|
|
472
474
|
work, and never \`.env\`. **artifacts** — evidence the work writes (reports, ZIP archives, test results):
|
|
473
475
|
\`{ "paths": ["rules-engine/test-results/**"] }\`. Never committed, kept with the run; tell tasks to write
|
|
474
|
-
evidence there instead of changing \`.gitignore\`.
|
|
476
|
+
evidence there instead of changing \`.gitignore\`. Only what a task creates or changes there is kept, and an
|
|
477
|
+
artifacts pattern must never cover the input files or a task's work ("rules-engine/**" would hide both): name
|
|
478
|
+
the evidence folders only.
|
|
475
479
|
|
|
476
480
|
A session with \`"startFrom": "previous-session"\` continues the whole work of the one before it only when
|
|
477
481
|
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.
|
|
482
|
+
refuses to start from part of it. It also refuses when the session before has not finished (a task failed or
|
|
483
|
+
blocked): finish it first.
|
|
479
484
|
|
|
480
485
|
Put the same \`startFrom\` on every session that works in one repository unless the user wants it
|
|
481
486
|
mixed. Leaving it out keeps the old behaviour — whatever branch the repository is on — which is
|
|
@@ -542,8 +547,8 @@ run“, на отделен клон, и сесията тръгва от нег
|
|
|
542
547
|
сесията. \`"tracked-only-snapshot"\` взема само tracked промени. Тайни (\`.env\`), изход от инструменти и
|
|
543
548
|
файлове извън проекта никога не се вземат, а ignored файл — само когато \`scope\` на задача го посочва.
|
|
544
549
|
Снимката се прави там, където са промените, затова с \`startFrom\` \`"branch"\` само ако хранилището вече е на
|
|
545
|
-
този клон, и никога с \`"existing-branch"\`. Ако го пропуснеш, остава \`"reject"\`:
|
|
546
|
-
|
|
550
|
+
този клон, и никога с \`"existing-branch"\`. Ако го пропуснеш, остава \`"reject"\`: дърво с некомитнати
|
|
551
|
+
промени отказва задачата и нищо не се изпълнява.
|
|
547
552
|
|
|
548
553
|
**userInputs** — файлове, които потребителят дава за работата (схеми, тестови данни):
|
|
549
554
|
\`{ "paths": ["rules-engine/test-data/schemas/*.yaml"] }\`. Влизат в комита, от който тръгва всяка сесия —
|
|
@@ -552,11 +557,14 @@ run“, на отделен клон, и сесията тръгва от нег
|
|
|
552
557
|
потребителят иска работата да ги променя). Питай кои файлове са вход; никога цяла папка с работа и никога
|
|
553
558
|
\`.env\`. **artifacts** — доказателства, които работата пише (отчети, ZIP архиви, резултати от тестове):
|
|
554
559
|
\`{ "paths": ["rules-engine/test-results/**"] }\`. Никога не се комитват, пазят се към пускането; кажи на
|
|
555
|
-
задачите да пишат доказателствата там, вместо да променят \`.gitignore\`.
|
|
560
|
+
задачите да пишат доказателствата там, вместо да променят \`.gitignore\`. Пази се само каквото задачата създаде
|
|
561
|
+
или промени там, и шаблон за artifacts никога не покрива входните файлове или работата на задача
|
|
562
|
+
("rules-engine/**" би скрил и двете): посочвай само папките с доказателства.
|
|
556
563
|
|
|
557
564
|
Сесия с \`"startFrom": "previous-session"\` продължава цялата работа на предната само ако тя е
|
|
558
565
|
\`"branchMode": "per-session"\`: при per-task работата ѝ е на няколко клона и следващата сесия отказва да
|
|
559
|
-
тръгне от част от нея.
|
|
566
|
+
тръгне от част от нея. Отказва и когато предната сесия не е завършила (задача е провалена или блокирана):
|
|
567
|
+
първо я завърши.
|
|
560
568
|
|
|
561
569
|
Сложи един и същ \`startFrom\` на всички сесии в едно хранилище, освен ако потребителят не иска
|
|
562
570
|
различни. Ако го пропуснеш, остава старото поведение — от който клон е хранилището в момента —
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_VCS, applyDefaultProject } from '../session/store.js';
|
|
2
2
|
import { normalisePatterns } from '../vcs/inputs.js';
|
|
3
|
+
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
3
4
|
import { availableShells, detectShells } from '../exec/shells.js';
|
|
4
5
|
/** The heading under which a task's acceptance bar reaches Copilot. English, like level 1. */
|
|
5
6
|
const EXPECTED_HEADER = '### Expected result';
|
|
@@ -183,6 +184,18 @@ defaultProjectDir = '') {
|
|
|
183
184
|
}
|
|
184
185
|
lastIn.set(repo, { name: planned.name, perTask: (planned.vcs.branchMode ?? 'per-task') === 'per-task', tasks: planned.tasks.length });
|
|
185
186
|
}
|
|
187
|
+
// A task whose whole scope is under the artifact patterns would have its work kept out of git.
|
|
188
|
+
for (const planned of plan.sessions) {
|
|
189
|
+
const arts = planned.vcs?.artifacts?.paths ?? [];
|
|
190
|
+
if (!planned.vcs?.enabled || arts.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
for (const task of planned.tasks) {
|
|
193
|
+
if (task.scope.length > 0 && coveredByArtifacts(task.scope, arts).length === task.scope.length) {
|
|
194
|
+
warnings.push(`Task "${task.title}" in session "${planned.name}" may change only ${task.scope.join(', ')}, and the session's artifacts patterns cover all of it: ` +
|
|
195
|
+
'whatever it writes is kept with the run and never committed. If that work is meant to be committed, narrow "artifacts".');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
186
199
|
for (const planned of plan.sessions) {
|
|
187
200
|
// Attaching project files to the chat was removed; an old plan's root is still its project folder.
|
|
188
201
|
if (planned.mirror?.enabled) {
|
|
@@ -238,6 +251,7 @@ defaultProjectDir = '') {
|
|
|
238
251
|
reviewEnabled: task.review,
|
|
239
252
|
readOnly: task.readOnly || undefined,
|
|
240
253
|
scope: task.scope.length > 0 ? task.scope : undefined,
|
|
254
|
+
outputs: task.outputs.length > 0 ? task.outputs : undefined,
|
|
241
255
|
});
|
|
242
256
|
titles.push(added.title);
|
|
243
257
|
}
|
package/dist/src/plan/schema.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { namesEverything } from '../vcs/inputs.js';
|
|
17
|
+
import { coveredByArtifacts } from '../vcs/artifacts.js';
|
|
17
18
|
export const PLAN_VERSION = 1;
|
|
18
19
|
/**
|
|
19
20
|
* Version control, exactly as a session stores it, and required on every session.
|
|
@@ -140,6 +141,15 @@ const VcsInput = z
|
|
|
140
141
|
if (vcs.userInputs && vcs.userInputs.enabled !== false && (vcs.userInputs.paths ?? []).length > 0 && !vcs.enabled) {
|
|
141
142
|
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
143
|
}
|
|
144
|
+
const hidden = coveredByArtifacts(vcs.userInputs?.paths ?? [], vcs.artifacts?.paths ?? []);
|
|
145
|
+
if (hidden.length > 0) {
|
|
146
|
+
ctx.addIssue({
|
|
147
|
+
code: 'custom',
|
|
148
|
+
path: ['artifacts', 'paths'],
|
|
149
|
+
message: `An artifacts pattern covers the input files ${hidden.map((h) => `"${h}"`).join(', ')}: artifacts are kept out of git, so the inputs would be too. ` +
|
|
150
|
+
'Name only the folders the evidence is written to (for example "rules-engine/test-results/**"), not a whole project folder.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
143
153
|
if (vcs.enabled && !vcs.repoDir.trim()) {
|
|
144
154
|
ctx.addIssue({
|
|
145
155
|
code: 'custom',
|
|
@@ -297,6 +307,11 @@ const TaskInput = z.object({
|
|
|
297
307
|
* chat told — which a sentence in the prompt cannot be. See `vcs/scope.ts`.
|
|
298
308
|
*/
|
|
299
309
|
scope: z.array(z.string().trim().min(1, 'A scope entry needs a path or a pattern.')).default([]),
|
|
310
|
+
/**
|
|
311
|
+
* Files the task is to produce as evidence (reports, archives), repository-relative patterns.
|
|
312
|
+
* Kept with the run's record when the task creates or changes them. See `Task.outputs`.
|
|
313
|
+
*/
|
|
314
|
+
outputs: z.array(z.string().trim().min(1, 'An outputs entry needs a path or a pattern.')).default([]),
|
|
300
315
|
/**
|
|
301
316
|
* What earlier attempts of this task were asked, written by the plan export for a task that has
|
|
302
317
|
* been run more than once (`earlierPlans` in `session/exports.ts`). History, not instruction:
|
|
@@ -1213,8 +1213,8 @@ export const SYSTEM_GUIDE = [
|
|
|
1213
1213
|
kind: 'note',
|
|
1214
1214
|
en: 'Uncommitted changes',
|
|
1215
1215
|
bg: 'Некомитнати промени',
|
|
1216
|
-
doesEn: 'What the first task does when the repository has uncommitted changes:
|
|
1217
|
-
doesBg: 'Какво прави първата задача, когато хранилището има некомитнати промени:
|
|
1216
|
+
doesEn: 'What the first task does when the repository has uncommitted changes: refuse to start until they are committed, or take them — all, or tracked only — as a starting snapshot: one commit on a branch of its own that the session starts from. In a plan: "dirtyWorktree": { "policy": "snapshot" }. Not shown for "Carry on an existing branch".',
|
|
1217
|
+
doesBg: 'Какво прави първата задача, когато хранилището има некомитнати промени: отказва старта, докато не са комитнати, или ги взема — всички или само tracked — като начална снимка: един комит на отделен клон, от който сесията тръгва. В план: "dirtyWorktree": { "policy": "snapshot" }. Не се показва при „Продължи съществуващ клон“.',
|
|
1218
1218
|
},
|
|
1219
1219
|
{
|
|
1220
1220
|
key: 'vcs.snapshotTake',
|
|
@@ -3241,6 +3241,14 @@ export const SYSTEM_GUIDE = [
|
|
|
3241
3241
|
en: 'This machine',
|
|
3242
3242
|
bg: 'Тази машина',
|
|
3243
3243
|
controls: [
|
|
3244
|
+
{
|
|
3245
|
+
key: 'sys.version',
|
|
3246
|
+
kind: 'note',
|
|
3247
|
+
en: 'copilot-operator version',
|
|
3248
|
+
bg: 'Версия на copilot-operator',
|
|
3249
|
+
doesEn: 'The first row: which version of the bot is running, as `cop --version` says. Name it when reporting a problem.',
|
|
3250
|
+
doesBg: 'Първият ред: коя версия на бота работи, както я казва `cop --version`. Кажи я, когато съобщаваш за проблем.',
|
|
3251
|
+
},
|
|
3244
3252
|
{
|
|
3245
3253
|
key: 'sys.signin',
|
|
3246
3254
|
kind: 'note',
|
|
@@ -22,6 +22,7 @@ import { existsSync } from 'node:fs';
|
|
|
22
22
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import { TASK_FIELD_ON_RERUN } from './store.js';
|
|
25
|
+
import { botVersion } from '../config/version.js';
|
|
25
26
|
import { withoutPersona } from '../plan/importPlan.js';
|
|
26
27
|
const EXPECTED_HEADER = '### Expected result';
|
|
27
28
|
const GOAL_HEADER = '## Goal of this session';
|
|
@@ -171,6 +172,8 @@ export function buildPlanExport(scope) {
|
|
|
171
172
|
task.readOnly = true;
|
|
172
173
|
if (t.scope && t.scope.length > 0)
|
|
173
174
|
task.scope = [...t.scope];
|
|
175
|
+
if (t.outputs && t.outputs.length > 0)
|
|
176
|
+
task.outputs = [...t.outputs];
|
|
174
177
|
const earlier = earlierPlans(t, level2);
|
|
175
178
|
if (earlier.length > 0)
|
|
176
179
|
task.earlierAttempts = earlier;
|
|
@@ -578,6 +581,8 @@ export async function buildDomainExport(scope, runsDir) {
|
|
|
578
581
|
}
|
|
579
582
|
return {
|
|
580
583
|
exportedAt: new Date().toISOString(),
|
|
584
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
585
|
+
botVersion: botVersion(),
|
|
581
586
|
about: `copilot-operator, the work: ${scope.label}. What each task asked, what the chat tried, what was done to the repository, and why a task did not end done. Nothing about the runner itself; that is the bot export.`,
|
|
582
587
|
counts,
|
|
583
588
|
tasks,
|
|
@@ -649,6 +654,8 @@ export async function buildBotExport(scope, runsDir, machine) {
|
|
|
649
654
|
tasks.push(await botTask(session, task, runsDir));
|
|
650
655
|
return {
|
|
651
656
|
exportedAt: new Date().toISOString(),
|
|
657
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
658
|
+
botVersion: botVersion(),
|
|
652
659
|
about: `copilot-operator, the runner: ${scope.label}. The environment, every transcript event, every step with its exit code, the transport's retries, what was reaped, what the review machinery did. Read the domain export for what the task was about.`,
|
|
653
660
|
machine,
|
|
654
661
|
tasks,
|
|
@@ -680,6 +687,8 @@ export async function buildBundleExport(scope, runsDir, machine) {
|
|
|
680
687
|
const pairs = tasksOf(scope);
|
|
681
688
|
return {
|
|
682
689
|
exportedAt: new Date().toISOString(),
|
|
690
|
+
// Which copilot-operator made this, so a report from another machine says it without being asked.
|
|
691
|
+
botVersion: botVersion(),
|
|
683
692
|
about: `copilot-operator, all three views of ${pairs.length} chosen task(s): ${scope.label}. ` +
|
|
684
693
|
'`plan` is what was asked, in the format that imports again; `work` is what happened to it; ' +
|
|
685
694
|
'`runner` is what the machine did. Read `work` first, then `runner` when the machine is the suspect.',
|
|
@@ -76,6 +76,7 @@ export const TASK_FIELD_ON_RERUN = {
|
|
|
76
76
|
reviewEnabled: 'kept',
|
|
77
77
|
readOnly: 'kept',
|
|
78
78
|
scope: 'kept',
|
|
79
|
+
outputs: 'kept',
|
|
79
80
|
// What one review noticed is arithmetic for every attempt after it.
|
|
80
81
|
reviewChecks: 'kept',
|
|
81
82
|
attempts: 'kept',
|
|
@@ -468,6 +469,7 @@ export class SessionStore {
|
|
|
468
469
|
...(input.readOnly === true ? { readOnly: true } : {}),
|
|
469
470
|
// Stored only when it limits something: absent means anywhere, as before scopes existed.
|
|
470
471
|
...((input.scope ?? []).filter((p) => p.trim()).length > 0 ? { scope: (input.scope ?? []).map((p) => p.trim()).filter(Boolean) } : {}),
|
|
472
|
+
...((input.outputs ?? []).filter((p) => p.trim()).length > 0 ? { outputs: (input.outputs ?? []).map((p) => p.trim()).filter(Boolean) } : {}),
|
|
471
473
|
};
|
|
472
474
|
await this.updateSession(sessionId, (s) => {
|
|
473
475
|
s.tasks.push(task);
|
|
@@ -22,6 +22,21 @@ import { looksGenerated } from './commitHygiene.js';
|
|
|
22
22
|
import { git } from './git.js';
|
|
23
23
|
import { normalisePatterns } from './inputs.js';
|
|
24
24
|
import { inScope } from './scope.js';
|
|
25
|
+
/**
|
|
26
|
+
* The patterns among `patterns` whose files an artifact pattern would cover, and so keep out of git.
|
|
27
|
+
* Judged by a path the pattern stands for (`**` and `*` filled in). Found in a run on 0.1.18: an
|
|
28
|
+
* artifact pattern of a whole project folder covered the input files and the task's own work, every
|
|
29
|
+
* file the task wrote was ignored, and "nothing to commit" ended it done.
|
|
30
|
+
*/
|
|
31
|
+
export function coveredByArtifacts(patterns, artifacts) {
|
|
32
|
+
const arts = normalisePatterns(artifacts);
|
|
33
|
+
if (arts.length === 0)
|
|
34
|
+
return [];
|
|
35
|
+
return normalisePatterns(patterns).filter((p) => {
|
|
36
|
+
const sample = p.replace(/\/+$/, '/f').replace(/\*\*/g, 'a/b').replace(/[*?]/g, 'x');
|
|
37
|
+
return inScope(sample, arts);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
25
40
|
/** The session's artifact patterns, without ones that name the whole project. */
|
|
26
41
|
export function artifactPatterns(vcs) {
|
|
27
42
|
return normalisePatterns(vcs?.artifacts?.paths ?? []);
|
|
@@ -59,7 +74,23 @@ export async function excludeArtifacts(dir, session) {
|
|
|
59
74
|
const tracked = (await git(resolve(top), ['ls-files'])).stdout.split('\n').filter((p) => p && inScope(p, patterns));
|
|
60
75
|
return { tracked };
|
|
61
76
|
}
|
|
62
|
-
const LIMITS = { files: 1000, fileBytes: 100 * 1024 * 1024, totalBytes: 500 * 1024 * 1024 };
|
|
77
|
+
const LIMITS = { files: 1000, fileBytes: 100 * 1024 * 1024, totalBytes: 500 * 1024 * 1024, listed: 20_000 };
|
|
78
|
+
/**
|
|
79
|
+
* The artifact files as they are before a task, so that what is kept afterwards is what the task
|
|
80
|
+
* made or changed. Found in a real run: a broad pattern kept every file already under it, evidence
|
|
81
|
+
* of earlier sessions included, as if this task had produced it.
|
|
82
|
+
*/
|
|
83
|
+
export async function artifactState(root, patterns) {
|
|
84
|
+
const state = new Map();
|
|
85
|
+
if (patterns.length === 0 || !root)
|
|
86
|
+
return state;
|
|
87
|
+
for (const rel of await matching(root, patterns)) {
|
|
88
|
+
const info = await stat(join(root, rel)).catch(() => null);
|
|
89
|
+
if (info)
|
|
90
|
+
state.set(rel, `${info.size}:${info.mtimeMs}`);
|
|
91
|
+
}
|
|
92
|
+
return state;
|
|
93
|
+
}
|
|
63
94
|
/** Files under `root` matching the patterns, found on disk: git's view does not matter for evidence. */
|
|
64
95
|
async function matching(root, patterns) {
|
|
65
96
|
const out = [];
|
|
@@ -73,7 +104,7 @@ async function matching(root, patterns) {
|
|
|
73
104
|
return fixed.join('/');
|
|
74
105
|
}));
|
|
75
106
|
const walk = async (rel) => {
|
|
76
|
-
if (out.length > LIMITS.
|
|
107
|
+
if (out.length > LIMITS.listed)
|
|
77
108
|
return;
|
|
78
109
|
const abs = join(root, rel);
|
|
79
110
|
const info = await stat(abs).catch(() => null);
|
|
@@ -97,17 +128,25 @@ async function matching(root, patterns) {
|
|
|
97
128
|
return [...new Set(out)].sort();
|
|
98
129
|
}
|
|
99
130
|
/**
|
|
100
|
-
* Copies
|
|
101
|
-
*
|
|
131
|
+
* Copies into the attempt's record, `<dest>/<path>`, the artifact files this task made or changed:
|
|
132
|
+
* under the session's artifact patterns or the task's own `outputs`, new or changed since `before`
|
|
133
|
+
* (`artifactState` at the task's start), and inside the task's scope when it has one (its outputs
|
|
134
|
+
* always count). A file already there and untouched is not this task's evidence and is not kept.
|
|
135
|
+
* Secrets are never copied; past the limits the rest is skipped and said.
|
|
102
136
|
*/
|
|
103
|
-
export async function keepArtifacts(root, patterns, dest) {
|
|
137
|
+
export async function keepArtifacts(root, patterns, dest, opts = {}) {
|
|
104
138
|
const kept = [];
|
|
105
139
|
const skipped = [];
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
const outsideScope = [];
|
|
141
|
+
let unchanged = 0;
|
|
142
|
+
const outputs = normalisePatterns(opts.outputs ?? []);
|
|
143
|
+
const scope = opts.scope ?? [];
|
|
144
|
+
const all = [...new Set([...patterns, ...outputs])];
|
|
145
|
+
if (all.length === 0 || !root)
|
|
146
|
+
return { kept, skipped, unchanged, outsideScope };
|
|
108
147
|
let total = 0;
|
|
109
148
|
const base = resolve(root);
|
|
110
|
-
for (const rel of await matching(root,
|
|
149
|
+
for (const rel of await matching(root, all)) {
|
|
111
150
|
const from = resolve(join(root, rel));
|
|
112
151
|
if (!from.startsWith(base + sep))
|
|
113
152
|
continue;
|
|
@@ -118,6 +157,14 @@ export async function keepArtifacts(root, patterns, dest) {
|
|
|
118
157
|
const info = await stat(from).catch(() => null);
|
|
119
158
|
if (!info)
|
|
120
159
|
continue;
|
|
160
|
+
if (opts.before && opts.before.get(rel) === `${info.size}:${info.mtimeMs}`) {
|
|
161
|
+
unchanged += 1;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (scope.length > 0 && !inScope(rel, scope) && !(outputs.length > 0 && inScope(rel, outputs))) {
|
|
165
|
+
outsideScope.push(rel);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
121
168
|
if (kept.length >= LIMITS.files || info.size > LIMITS.fileBytes || total + info.size > LIMITS.totalBytes) {
|
|
122
169
|
skipped.push(`${rel} (past the limit of ${LIMITS.files} files, ${LIMITS.fileBytes / 1048576} MB a file, ${LIMITS.totalBytes / 1048576} MB in all)`);
|
|
123
170
|
continue;
|
|
@@ -134,5 +181,5 @@ export async function keepArtifacts(root, patterns, dest) {
|
|
|
134
181
|
skipped.push(`${rel} (${e.message})`);
|
|
135
182
|
}
|
|
136
183
|
}
|
|
137
|
-
return { kept, skipped };
|
|
184
|
+
return { kept, skipped, unchanged, outsideScope };
|
|
138
185
|
}
|