atris 3.71.2 → 3.72.0
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/atris/skills/campaigns/SKILL.md +1 -1
- package/bin/atris.js +8 -2
- package/commands/autoland.js +102 -2
- package/commands/campaign.js +263 -11
- package/commands/clean.js +60 -4
- package/commands/gm.js +1273 -7
- package/commands/improve.js +3 -2
- package/commands/member.js +28 -3
- package/commands/mission.js +82 -27
- package/commands/pulse.js +89 -5
- package/commands/sync.js +2 -2
- package/commands/task.js +49 -13
- package/commands/worktree.js +24 -2
- package/commands/zen.js +183 -0
- package/lib/auto-accept-certified.js +9 -4
- package/lib/campaign-cadence.js +53 -14
- package/lib/campaign-core.js +98 -17
- package/lib/campaign-keep.js +91 -0
- package/lib/campaign-lanes.js +11 -6
- package/lib/campaign-loop.js +173 -32
- package/lib/campaign-reading.js +14 -1
- package/lib/campaign-rsi.js +697 -0
- package/lib/campaign-run-lock.js +9 -1
- package/lib/campaign-schedule.js +124 -0
- package/lib/campaign-schema.js +3 -1
- package/lib/campaign-store.js +34 -2
- package/lib/campaign-trace.js +77 -29
- package/lib/engine-resource.js +5 -1
- package/lib/gm-bet.js +2219 -0
- package/lib/gm-phone-page.js +1102 -0
- package/lib/gm-phone.js +682 -0
- package/lib/gm-season.js +1162 -0
- package/lib/gm-target.js +292 -0
- package/lib/job-code-verify.js +169 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-autoland.js +105 -14
- package/lib/member-progress.js +2 -2
- package/lib/member-report.js +11 -1
- package/lib/member-self-next-job.js +6 -1
- package/lib/member-up.js +17 -8
- package/lib/merged-review-close.js +206 -0
- package/lib/mission-code-provenance.js +118 -0
- package/lib/mission-protected-lane.js +52 -4
- package/lib/pulse.js +2 -1
- package/lib/qr.js +338 -0
- package/lib/rsi-record.js +88 -0
- package/lib/task-receipt.js +24 -6
- package/lib/yes-lock.js +530 -0
- package/lib/zen-apply.js +511 -0
- package/lib/zen-workspace.js +121 -0
- package/lib/zen.js +395 -0
- package/package.json +1 -1
package/commands/task.js
CHANGED
|
@@ -29,10 +29,10 @@ const {
|
|
|
29
29
|
parseVerifyChain,
|
|
30
30
|
parseVerifyCommand,
|
|
31
31
|
runVerifyCommand,
|
|
32
|
-
runVerifyCommandCached,
|
|
33
32
|
DENIED_TAGS,
|
|
34
33
|
} = require('../lib/auto-accept-certified');
|
|
35
34
|
const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
|
|
35
|
+
const { checkedCodeSha, runVerifyAtJobCode } = require('../lib/job-code-verify');
|
|
36
36
|
const { proveTaskLanding } = require('../lib/landing-proof');
|
|
37
37
|
const {
|
|
38
38
|
taskExplanation,
|
|
@@ -10645,22 +10645,44 @@ function stampVerifyCacheMetadata(taskDb, db, taskId, { verify, result, actor, w
|
|
|
10645
10645
|
reason: (result && result.reason) || null,
|
|
10646
10646
|
ran_at: new Date().toISOString(),
|
|
10647
10647
|
ran_by: actor,
|
|
10648
|
-
|
|
10648
|
+
// The commit the check ran on: the job's own code when it has some,
|
|
10649
|
+
// else the folder the job lives in.
|
|
10650
|
+
code_sha: (result && result.code_sha) || currentHeadSha(workspaceRoot),
|
|
10651
|
+
code_source: (result && result.code_source) || 'checkout',
|
|
10649
10652
|
};
|
|
10653
|
+
// A check is not a touch. Bumping updated_at here put every failed job at
|
|
10654
|
+
// the front of the next pass, so the same six failing jobs won every hour.
|
|
10650
10655
|
db.prepare(`
|
|
10651
10656
|
UPDATE tasks
|
|
10652
|
-
SET metadata =
|
|
10653
|
-
updated_at = ?
|
|
10657
|
+
SET metadata = ?
|
|
10654
10658
|
WHERE id = ?
|
|
10655
|
-
`).run(JSON.stringify(metadata),
|
|
10659
|
+
`).run(JSON.stringify(metadata), taskId);
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
// When a review job was last given a second check; never checked reads 0.
|
|
10663
|
+
function lastCheckedMs(task) {
|
|
10664
|
+
const ranAt = Date.parse(String(task?.metadata?.verify_cache?.ran_at || ''));
|
|
10665
|
+
return Number.isFinite(ranAt) ? ranAt : 0;
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
// The check already failed on exactly this code: running it again proves
|
|
10669
|
+
// nothing until the code changes.
|
|
10670
|
+
function failedOnSameCode(task, verify, codeSha) {
|
|
10671
|
+
const cache = task?.metadata?.verify_cache;
|
|
10672
|
+
return Boolean(cache && typeof cache === 'object' && cache.ok === false && cache.reason === 'verify_failed'
|
|
10673
|
+
&& cache.command === verify && codeSha && cache.code_sha === codeSha);
|
|
10656
10674
|
}
|
|
10657
10675
|
|
|
10658
10676
|
function cachedVerifyVerdict(task, root) {
|
|
10659
10677
|
const metadata = (task && task.metadata) || {};
|
|
10660
10678
|
const cache = metadata.verify_cache;
|
|
10661
10679
|
if (!cache || typeof cache !== 'object') return { fresh: false, command: metadata.verify || null };
|
|
10680
|
+
// A check run on the job's own code stays fresh until that code changes.
|
|
10681
|
+
const currentCode = cache.code_source && cache.code_source !== 'checkout'
|
|
10682
|
+
? checkedCodeSha(task, root)
|
|
10683
|
+
: currentHeadSha(task.workspace_root || root);
|
|
10662
10684
|
const fresh = cache.command === metadata.verify
|
|
10663
|
-
&& (!cache.code_sha || cache.code_sha ===
|
|
10685
|
+
&& (!cache.code_sha || cache.code_sha === currentCode);
|
|
10664
10686
|
return { ...cache, fresh: Boolean(fresh) };
|
|
10665
10687
|
}
|
|
10666
10688
|
|
|
@@ -10711,7 +10733,7 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
|
|
|
10711
10733
|
revise_reason: revised.reason || null,
|
|
10712
10734
|
};
|
|
10713
10735
|
}
|
|
10714
|
-
const result =
|
|
10736
|
+
const result = runVerifyAtJobCode(verify, task, { root: process.cwd(), cache: verifyCache });
|
|
10715
10737
|
stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result, actor, workspaceRoot: task.workspace_root });
|
|
10716
10738
|
if (result.ok) return { ok: true, verify, result };
|
|
10717
10739
|
const note = landingVerifyFailureNote(verify, result);
|
|
@@ -10731,7 +10753,10 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
|
|
|
10731
10753
|
}
|
|
10732
10754
|
|
|
10733
10755
|
function cmdCertifyVerified(args, options = {}) {
|
|
10734
|
-
|
|
10756
|
+
// preview: the hourly pass's dry run. Reads the named workspace's jobs and
|
|
10757
|
+
// writes nothing, not even the task list file.
|
|
10758
|
+
const preview = options.preview === true;
|
|
10759
|
+
const dryRun = preview || hasFlag(args, '--dry-run');
|
|
10735
10760
|
const asJson = wantsJson(args);
|
|
10736
10761
|
const silent = options.silent === true;
|
|
10737
10762
|
const verifyCache = options.verifyCache || null;
|
|
@@ -10741,13 +10766,18 @@ function cmdCertifyVerified(args, options = {}) {
|
|
|
10741
10766
|
|
|
10742
10767
|
const taskDb = getTaskDb();
|
|
10743
10768
|
const db = taskDb.open();
|
|
10744
|
-
const
|
|
10769
|
+
const projection = preview
|
|
10770
|
+
? enrichTaskProjection(taskDb.taskProjection(db, { workspaceRoot: options.workspaceRoot || taskDb.workspaceRoot(), limit: 500 }))
|
|
10771
|
+
: writeDefaultProjection(taskDb, db).projection;
|
|
10772
|
+
// Least recently checked first, oldest first among never-checked jobs. The
|
|
10773
|
+
// limit counts checks that actually run, so skipped rows never use a turn.
|
|
10745
10774
|
const candidates = (projection.tasks || [])
|
|
10746
10775
|
.filter((t) => t && t.status === 'review')
|
|
10747
|
-
.sort((a, b) => Number(
|
|
10748
|
-
.slice(0, max);
|
|
10776
|
+
.sort((a, b) => lastCheckedMs(a) - lastCheckedMs(b) || Number(a.updated_at || 0) - Number(b.updated_at || 0));
|
|
10749
10777
|
const results = [];
|
|
10778
|
+
let checks = 0;
|
|
10750
10779
|
for (const item of candidates) {
|
|
10780
|
+
if (checks >= max) break;
|
|
10751
10781
|
const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
|
|
10752
10782
|
const task = fullProjection.tasks[0] || null;
|
|
10753
10783
|
if (!task) {
|
|
@@ -10809,11 +10839,16 @@ function cmdCertifyVerified(args, options = {}) {
|
|
|
10809
10839
|
results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
|
|
10810
10840
|
continue;
|
|
10811
10841
|
}
|
|
10842
|
+
if (failedOnSameCode(task, verify, checkedCodeSha(task, process.cwd()))) {
|
|
10843
|
+
results.push({ ref, action: 'skipped', reason: 'failed_on_this_code', verify });
|
|
10844
|
+
continue;
|
|
10845
|
+
}
|
|
10846
|
+
checks += 1;
|
|
10812
10847
|
if (dryRun) {
|
|
10813
10848
|
results.push({ ref, action: 'would_certify', verify });
|
|
10814
10849
|
continue;
|
|
10815
10850
|
}
|
|
10816
|
-
const run =
|
|
10851
|
+
const run = runVerifyAtJobCode(verify, task, { root: process.cwd(), cache: verifyCache });
|
|
10817
10852
|
stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result: run, actor, workspaceRoot: task.workspace_root });
|
|
10818
10853
|
if (!run.ok) {
|
|
10819
10854
|
results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
|
|
@@ -10840,7 +10875,7 @@ function cmdCertifyVerified(args, options = {}) {
|
|
|
10840
10875
|
const certifiedNow = readied.event?.payload?.agent_certified === true;
|
|
10841
10876
|
results.push({ ref, action: certifiedNow ? 'certified' : 'pass_recorded', verify });
|
|
10842
10877
|
}
|
|
10843
|
-
const
|
|
10878
|
+
const outPath = preview ? null : writeDefaultProjection(taskDb, db).outPath;
|
|
10844
10879
|
const certified = results.filter((r) => r.action === 'certified').length;
|
|
10845
10880
|
const payload = {
|
|
10846
10881
|
ok: true,
|
|
@@ -13698,6 +13733,7 @@ module.exports = {
|
|
|
13698
13733
|
delegateTask,
|
|
13699
13734
|
AGENT_ENV_MARKERS,
|
|
13700
13735
|
autoRenderTodoFromDb,
|
|
13736
|
+
cmdCertifyVerified,
|
|
13701
13737
|
refreshKeptTaskList,
|
|
13702
13738
|
keptCount,
|
|
13703
13739
|
projectionMissions,
|
package/commands/worktree.js
CHANGED
|
@@ -16,6 +16,9 @@ const close = require('./close');
|
|
|
16
16
|
// parallel landings that each carry a copy always clash.
|
|
17
17
|
const REGEN_ADAPTER_FILES = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.atris/state/tasks.projection.json'];
|
|
18
18
|
const COMMAND_MAX_BUFFER_BYTES = 64 * 1024 * 1024;
|
|
19
|
+
// Push also runs local hooks. Backend startup and regression checks alone can
|
|
20
|
+
// exceed the short network-only allowance used by fetch and discovery.
|
|
21
|
+
const GIT_PUSH_TIMEOUT_MS = 5 * 60 * 1000;
|
|
19
22
|
const GIT_OID_PATTERN = /^[0-9a-f]{40,64}$/;
|
|
20
23
|
const ONE_LAP_PROOF_REF_PATTERN = /^refs\/atris\/one-lap\/([0-9a-f]{40,64})$/;
|
|
21
24
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -37,13 +40,15 @@ function gitNetworkEnvironment(source = process.env, cwd = process.cwd()) {
|
|
|
37
40
|
// A killed network call leaves stderr empty; name the timeout instead.
|
|
38
41
|
function gitFailureDetail(result) {
|
|
39
42
|
if (result && result.error && result.error.code === 'ETIMEDOUT') return 'timed out';
|
|
40
|
-
return
|
|
43
|
+
return [result && result.stdout, result && result.stderr]
|
|
44
|
+
.map(output => String(output || '').trim()).filter(Boolean).join('\n');
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function runGit(args, { cwd = process.cwd(), check = true, timeout } = {}) {
|
|
44
48
|
const network = ['fetch', 'push', 'ls-remote'].includes(args[0]);
|
|
45
49
|
// Only network calls need the SSH probe; local calls skip its extra git spawn.
|
|
46
|
-
const
|
|
50
|
+
const defaultTimeout = args[0] === 'push' ? GIT_PUSH_TIMEOUT_MS : network ? 30000 : undefined;
|
|
51
|
+
const options = { cwd, encoding: 'utf8', timeout: timeout ?? defaultTimeout,
|
|
47
52
|
killSignal: 'SIGKILL', env: network ? gitNetworkEnvironment(process.env, cwd) : undefined };
|
|
48
53
|
let result = spawnSync('git', args, options);
|
|
49
54
|
// Linked checkouts share config. A concurrent start must not silently lose
|
|
@@ -1118,6 +1123,23 @@ function shipWorktree(args) {
|
|
|
1118
1123
|
if (/already merged/i.test(`${merged.stdout}${merged.stderr}`)) {
|
|
1119
1124
|
throw new Error(`gh pr merge reported the PR was already merged (stale PR reused): ${(merged.stdout || merged.stderr).trim()}`);
|
|
1120
1125
|
}
|
|
1126
|
+
// A merge queue accepts the request before merging. Deleting its head
|
|
1127
|
+
// here closes the PR and cancels that request, so confirm the outcome.
|
|
1128
|
+
const confirmation = spawnSync('gh', ['pr', 'view', mergeRef, '--json', 'state,mergedAt'], { cwd: root, encoding: 'utf8' });
|
|
1129
|
+
if (confirmation.status !== 0) {
|
|
1130
|
+
throw new Error('could not confirm the merge; the source branch was kept');
|
|
1131
|
+
}
|
|
1132
|
+
let state;
|
|
1133
|
+
try { state = JSON.parse(confirmation.stdout); } catch {
|
|
1134
|
+
throw new Error('could not read the merge result; the source branch was kept');
|
|
1135
|
+
}
|
|
1136
|
+
if (state.state === 'OPEN' && !state.mergedAt) {
|
|
1137
|
+
console.log('merge: pending; kept the source branch while required checks finish');
|
|
1138
|
+
return 0;
|
|
1139
|
+
}
|
|
1140
|
+
if (state.state !== 'MERGED' || !state.mergedAt) {
|
|
1141
|
+
throw new Error('the change was not merged; the source branch was kept');
|
|
1142
|
+
}
|
|
1121
1143
|
console.log('merge: merged');
|
|
1122
1144
|
const deleted = runGit(['push', 'origin', '--delete', branch], { cwd: root, check: false });
|
|
1123
1145
|
if (deleted.status === 0) {
|
package/commands/zen.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { abandonedDaysFromEnv, checkZen, sideCopyLines } = require('../lib/zen');
|
|
4
|
+
const { applyZen } = require('../lib/zen-apply');
|
|
5
|
+
const { workspaceZen } = require('../lib/zen-workspace');
|
|
6
|
+
|
|
7
|
+
function showZenHelp() {
|
|
8
|
+
console.log('usage: atris zen [--check] [--no-fetch] [--json]');
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log('Brings this repo to zen: code matches origin, nothing left lying around.');
|
|
11
|
+
console.log('Old stashes are archived, idle side copies put away, finished branches');
|
|
12
|
+
console.log('cleared, stray code edits saved to a zen-backup/ branch, and the main');
|
|
13
|
+
console.log('branch caught up. Only branches already on origin are cleared, so nothing');
|
|
14
|
+
console.log('is lost, and everything else can be brought back. Team notes');
|
|
15
|
+
console.log('in atris/ and agent config stay as they are. Anything touched in the last');
|
|
16
|
+
console.log('30 minutes is left alone. A side copy with unsaved work nobody has touched in');
|
|
17
|
+
console.log('3 days is saved to a zen-backup/ branch and put away. A side copy locked with git worktree lock');
|
|
18
|
+
console.log('always stays. Then the workspace is tidied with atris clean, and');
|
|
19
|
+
console.log('anything only you can do is listed with the one command that does it.');
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(' --check Only look. Report what is out of place and change nothing.');
|
|
22
|
+
console.log(' --no-fetch Use the last fetched origin instead of fetching.');
|
|
23
|
+
console.log(' --json Print machine-readable results.');
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log('Add more team-state paths in .atris/zen.json: {"team_state": ["logs/"]}');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pause(ms) {
|
|
29
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeOut(calm) {
|
|
33
|
+
const dim = (text) => (calm ? `\x1b[2m${text}\x1b[0m` : text);
|
|
34
|
+
const line = (text = '', ms = 110) => {
|
|
35
|
+
console.log(text);
|
|
36
|
+
if (calm && text) pause(ms);
|
|
37
|
+
};
|
|
38
|
+
// One raked line, drawn left to right.
|
|
39
|
+
const rake = (width = 34) => {
|
|
40
|
+
if (!calm) {
|
|
41
|
+
console.log(` ${'─'.repeat(width)}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(' ');
|
|
45
|
+
for (let i = 0; i < width; i += 1) {
|
|
46
|
+
process.stdout.write(dim('─'));
|
|
47
|
+
pause(14);
|
|
48
|
+
}
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
pause(160);
|
|
51
|
+
};
|
|
52
|
+
return { dim, line, rake };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function teamLine(n) {
|
|
56
|
+
if (!n) return 'no team notes changed.';
|
|
57
|
+
return n === 1 ? '1 team note kept as it is.' : `${n} team notes kept as they are.`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printYours(yours, line, dim) {
|
|
61
|
+
const width = Math.max(...yours.map((item) => item.text.length));
|
|
62
|
+
for (const item of yours) line(` ${item.text.padEnd(width)} ${dim(item.cmd)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printCheck(result, workspace) {
|
|
66
|
+
const kept = result.team_files
|
|
67
|
+
? (result.team_files === 1 ? '1 team file kept as it is' : `${result.team_files} team files kept as they are`)
|
|
68
|
+
: 'no team files changed';
|
|
69
|
+
const stale = result.fetched === false ? ' (could not reach origin, using last fetch)' : '';
|
|
70
|
+
const tidy = workspace?.todo.length || 0;
|
|
71
|
+
if (result.zen && tidy) {
|
|
72
|
+
console.log(`zen: almost, the code is calm and the workspace has ${tidy} ${tidy === 1 ? 'thing' : 'things'} to tidy`);
|
|
73
|
+
console.log(` code matches ${result.upstream}${stale}`);
|
|
74
|
+
console.log(` ${kept}`);
|
|
75
|
+
console.log(' run atris zen to make it zen');
|
|
76
|
+
} else if (result.zen) {
|
|
77
|
+
console.log('zen: yes');
|
|
78
|
+
console.log(` code matches ${result.upstream}${stale}`);
|
|
79
|
+
console.log(` ${kept}`);
|
|
80
|
+
} else {
|
|
81
|
+
const n = result.problems.length;
|
|
82
|
+
console.log(`zen: no, ${n} ${n === 1 ? 'thing is' : 'things are'} out of place${stale}`);
|
|
83
|
+
for (const problem of result.problems) console.log(` - ${problem.text}`);
|
|
84
|
+
console.log(` ${kept}`);
|
|
85
|
+
console.log(' run atris zen to make it zen');
|
|
86
|
+
}
|
|
87
|
+
if (result.worktrees_with_work.length) {
|
|
88
|
+
for (const text of sideCopyLines(result.worktrees_with_work, abandonedDaysFromEnv())) console.log(` ${text}`);
|
|
89
|
+
}
|
|
90
|
+
const held = result.worktrees_locked?.length || 0;
|
|
91
|
+
if (held) {
|
|
92
|
+
console.log(` ${held} side ${held === 1 ? 'copy is locked by its owner' : 'copies are locked by their owners'}, left alone`);
|
|
93
|
+
}
|
|
94
|
+
const live = result.worktrees_in_use?.length || 0;
|
|
95
|
+
if (live) {
|
|
96
|
+
const mins = result.fresh_minutes;
|
|
97
|
+
console.log(` ${live} side ${live === 1 ? 'copy was' : 'copies were'} used in the last ${mins} ${mins === 1 ? 'minute' : 'minutes'}, left alone`);
|
|
98
|
+
}
|
|
99
|
+
if (workspace?.todo.length) {
|
|
100
|
+
console.log('workspace:');
|
|
101
|
+
for (const text of workspace.todo) console.log(` - ${text}`);
|
|
102
|
+
}
|
|
103
|
+
if (workspace?.yours.length) {
|
|
104
|
+
console.log('for you:');
|
|
105
|
+
printYours(workspace.yours, (text) => console.log(text), (text) => text);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printApply(result, out) {
|
|
110
|
+
const { dim, line, rake } = out;
|
|
111
|
+
line('');
|
|
112
|
+
line(' atris zen', 200);
|
|
113
|
+
line('');
|
|
114
|
+
rake();
|
|
115
|
+
line('');
|
|
116
|
+
line(dim(' code'));
|
|
117
|
+
if (!result.done.length && result.zen) line(` matches ${result.upstream || 'origin'}`);
|
|
118
|
+
for (const text of result.done) line(` ${text}`);
|
|
119
|
+
for (const text of [...result.left, ...result.notes]) line(dim(` ${text}`));
|
|
120
|
+
line('');
|
|
121
|
+
const ws = result.workspace || { done: [], yours: [] };
|
|
122
|
+
if (ws.ran) {
|
|
123
|
+
line(dim(' workspace'));
|
|
124
|
+
if (!ws.done.length) line(' tidy');
|
|
125
|
+
for (const text of ws.done) line(` ${text}`);
|
|
126
|
+
line('');
|
|
127
|
+
}
|
|
128
|
+
if (ws.yours?.length) {
|
|
129
|
+
line(dim(' for you'));
|
|
130
|
+
printYours(ws.yours, line, dim);
|
|
131
|
+
line('');
|
|
132
|
+
}
|
|
133
|
+
if (result.zen) {
|
|
134
|
+
line(` ${result.already ? 'already zen.' : 'zen.'}`, 260);
|
|
135
|
+
} else {
|
|
136
|
+
line(' almost zen. still out of place:');
|
|
137
|
+
for (const text of result.remaining) line(dim(` - ${text}`));
|
|
138
|
+
}
|
|
139
|
+
line(dim(` ${teamLine(result.team_files)}`));
|
|
140
|
+
if (result.saved.length) {
|
|
141
|
+
line(dim(' everything is kept: git for-each-ref refs/archive refs/heads/zen-backup'));
|
|
142
|
+
}
|
|
143
|
+
line('');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function zenCommand(args = [], cwd = process.cwd()) {
|
|
147
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
148
|
+
showZenHelp();
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
const fetch = !args.includes('--no-fetch');
|
|
152
|
+
const json = args.includes('--json');
|
|
153
|
+
|
|
154
|
+
if (args.includes('--check')) {
|
|
155
|
+
const result = checkZen({ cwd, fetch });
|
|
156
|
+
if (json) {
|
|
157
|
+
console.log(JSON.stringify(result, null, 2));
|
|
158
|
+
return result.ok ? (result.zen ? 0 : 1) : 2;
|
|
159
|
+
}
|
|
160
|
+
if (!result.ok) {
|
|
161
|
+
console.log(`zen: can't check, ${result.error}.`);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
const workspace = workspaceZen({ root: result.root, dryRun: true });
|
|
165
|
+
printCheck(result, workspace);
|
|
166
|
+
return result.zen && !workspace.todo.length ? 0 : 1;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = applyZen({ cwd, fetch });
|
|
170
|
+
if (json) {
|
|
171
|
+
console.log(JSON.stringify(result, null, 2));
|
|
172
|
+
return result.ok ? (result.zen ? 0 : 1) : 2;
|
|
173
|
+
}
|
|
174
|
+
if (!result.ok) {
|
|
175
|
+
console.log(`zen: can't start, ${result.error}.`);
|
|
176
|
+
return 2;
|
|
177
|
+
}
|
|
178
|
+
const calm = Boolean(process.stdout.isTTY) && !process.env.CI;
|
|
179
|
+
printApply(result, makeOut(calm));
|
|
180
|
+
return result.zen ? 0 : 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { zenCommand, showZenHelp };
|
|
@@ -1157,8 +1157,13 @@ function agentBranchOnlyResult(ref, resolved) {
|
|
|
1157
1157
|
};
|
|
1158
1158
|
}
|
|
1159
1159
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1160
|
+
// A check recorded from main runs against the job's own code, the same place
|
|
1161
|
+
// the second check ran it (lib/job-code-verify.js), so the landing gate never
|
|
1162
|
+
// judges a branch-only test by the main folder.
|
|
1163
|
+
function runResolvedVerify(resolved, task, workspaceRoot, cache) {
|
|
1164
|
+
const result = resolved.rewritten
|
|
1165
|
+
? runVerifyCommandCached(resolved.verify, workspaceRoot, cache)
|
|
1166
|
+
: require('./job-code-verify').runVerifyAtJobCode(resolved.verify, task, { root: workspaceRoot, cache });
|
|
1162
1167
|
if (resolved.rewritten && result.reason === 'verify_failed') {
|
|
1163
1168
|
return { ...result, reason: 'verify_failed_on_main', recorded_verify: resolved.recorded };
|
|
1164
1169
|
}
|
|
@@ -1258,7 +1263,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1258
1263
|
if (tierRequiresStrictVerify && !verify) return strictVerifyMissingResult(ref);
|
|
1259
1264
|
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1260
1265
|
if (verify && executeVerify) {
|
|
1261
|
-
const verifyResult = runResolvedVerify(resolvedVerify, task.workspace_root || process.cwd(), verifyCache);
|
|
1266
|
+
const verifyResult = runResolvedVerify(resolvedVerify, task, task.workspace_root || process.cwd(), verifyCache);
|
|
1262
1267
|
// A check that runs and fails blocks. So does a check whose worktree
|
|
1263
1268
|
// is gone, otherwise the daily reap converts "has a failing check"
|
|
1264
1269
|
// into "lands unchecked" the morning after it clears the worktree.
|
|
@@ -1361,7 +1366,7 @@ function evaluateAutoAccept(task, options = {}) {
|
|
|
1361
1366
|
if (resolvedVerify.missing) return agentBranchOnlyResult(ref, resolvedVerify);
|
|
1362
1367
|
if (executeVerify) {
|
|
1363
1368
|
const workspaceRoot = task.workspace_root || process.cwd();
|
|
1364
|
-
const verifyResult = runResolvedVerify(resolvedVerify, workspaceRoot, verifyCache);
|
|
1369
|
+
const verifyResult = runResolvedVerify(resolvedVerify, task, workspaceRoot, verifyCache);
|
|
1365
1370
|
if (!verifyResult.ok) {
|
|
1366
1371
|
return { eligible: false, ref, reason: verifyResult.reason, verify, ...verifyResult };
|
|
1367
1372
|
}
|
package/lib/campaign-cadence.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { paysDollars } = require('./campaign-core');
|
|
13
13
|
const { getCampaign, openCampaignStore } = require('./campaign-store');
|
|
14
|
+
const { readingKey } = require('./campaign-loop');
|
|
14
15
|
|
|
15
16
|
const LESSON_LINES_FOR_PLANNING = 30;
|
|
16
17
|
|
|
@@ -26,18 +27,45 @@ function lessonsPath(root, campaignId) {
|
|
|
26
27
|
return path.join(root, 'atris', 'campaigns', campaignId, 'lessons.md');
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
function globalLessonsPath(root) {
|
|
31
|
+
return path.join(root, 'atris', 'campaigns', 'lessons.md');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseBulletLines(filePath) {
|
|
31
35
|
try {
|
|
32
|
-
const text = fs.readFileSync(
|
|
33
|
-
return text.split('\n').filter((line) =>
|
|
36
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
37
|
+
return text.split('\n').filter((line) => {
|
|
38
|
+
const bullet = line.trim();
|
|
39
|
+
return bullet.startsWith('- ') && bullet !== '- (no review written this round)';
|
|
40
|
+
});
|
|
34
41
|
} catch {
|
|
35
|
-
return
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The newest lessons for the planner: local campaign lessons plus deduplicated
|
|
47
|
+
// cross-campaign RSI lessons so every new campaign inherits earlier lessons.
|
|
48
|
+
function readLessons(root, campaignId, lines = LESSON_LINES_FOR_PLANNING) {
|
|
49
|
+
const local = parseBulletLines(lessonsPath(root, campaignId));
|
|
50
|
+
const global = parseBulletLines(globalLessonsPath(root));
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
const merged = [];
|
|
53
|
+
for (const line of [...local, ...global]) {
|
|
54
|
+
const key = line.trim().toLowerCase();
|
|
55
|
+
if (seen.has(key)) continue;
|
|
56
|
+
seen.add(key);
|
|
57
|
+
merged.push(line);
|
|
36
58
|
}
|
|
59
|
+
return merged.slice(-lines).join('\n');
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
function roundSummary(before, after) {
|
|
40
|
-
const
|
|
63
|
+
const previous = new Map();
|
|
64
|
+
for (const { id, status } of before.missions) {
|
|
65
|
+
if (!previous.has(id)) previous.set(id, new Set());
|
|
66
|
+
previous.get(id).add(status);
|
|
67
|
+
}
|
|
68
|
+
const fresh = after.missions.filter((m) => !previous.get(m.id)?.has(m.status));
|
|
41
69
|
const kept = fresh.filter((m) => m.status === 'ended' && m.outcome === 'kept').length;
|
|
42
70
|
const failed = fresh.filter((m) => m.status === 'ended' && m.outcome !== 'kept').length;
|
|
43
71
|
const vetoed = fresh.filter((m) => m.status === 'vetoed').length;
|
|
@@ -45,6 +73,12 @@ function roundSummary(before, after) {
|
|
|
45
73
|
return { kept, failed, vetoed, spent, fresh };
|
|
46
74
|
}
|
|
47
75
|
|
|
76
|
+
// Nothing finished and the check reads the same: there is nothing to learn.
|
|
77
|
+
function quietRound(before, after, summary) {
|
|
78
|
+
const key = (v) => (v.last_judge ? readingKey(v.last_judge.passed, v.last_judge.reading, v.answer.text) : null);
|
|
79
|
+
return !summary.fresh.some((m) => m.status === 'ended' || m.status === 'vetoed') && key(before) === key(after);
|
|
80
|
+
}
|
|
81
|
+
|
|
48
82
|
// On a subscription the round is told without dollars, since none were paid.
|
|
49
83
|
function roundTally(view, summary) {
|
|
50
84
|
return `${summary.kept} kept, ${summary.failed} failed${paysDollars(view) ? `, $${summary.spent} spent` : ''}`;
|
|
@@ -82,9 +116,10 @@ function makeCadence(root, loop, options = {}, deps = {}) {
|
|
|
82
116
|
async function review(id, before) {
|
|
83
117
|
const after = getCampaign(store, id);
|
|
84
118
|
const summary = roundSummary(before, after);
|
|
119
|
+
const quiet = quietRound(before, after, summary);
|
|
85
120
|
const lessons = readLessons(root, id, 200);
|
|
86
121
|
let written = [];
|
|
87
|
-
if (ask) {
|
|
122
|
+
if (ask && !quiet) {
|
|
88
123
|
const answer = await ask(root, after, reviewPrompt(after, summary, lessons));
|
|
89
124
|
// A lesson already on file is not written twice.
|
|
90
125
|
const known = new Set(lessons.split('\n').map((line) => line.trim().toLowerCase()));
|
|
@@ -96,8 +131,10 @@ function makeCadence(root, loop, options = {}, deps = {}) {
|
|
|
96
131
|
if (!fs.existsSync(file)) fs.writeFileSync(file, `# Lessons: ${after.title}\n\nWritten by the owner at each review; the next round plans from them.\n`);
|
|
97
132
|
const stamp = new Date(now()).toISOString().slice(0, 16).replace('T', ' ');
|
|
98
133
|
const header = `\n## ${stamp}: ${roundTally(after, summary)}\n`;
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
// A quiet round is noted as plain text, so the planner never reads it as a lesson.
|
|
135
|
+
const body = quiet ? ['(no review written this round: nothing ended and the reading held)'] : (written.length ? written : ['- (no review written this round)']);
|
|
136
|
+
fs.appendFileSync(file, `${header}${body.join('\n')}\n`);
|
|
137
|
+
return { summary, lessons: written, view: after, quiet };
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
async function run(id) {
|
|
@@ -109,27 +146,29 @@ function makeCadence(root, loop, options = {}, deps = {}) {
|
|
|
109
146
|
const until = now() + workMs;
|
|
110
147
|
log(`round ${round}: working until ${new Date(until).toISOString().slice(11, 16)}`);
|
|
111
148
|
let stopped = null;
|
|
149
|
+
let failed = false;
|
|
112
150
|
while (now() < until) {
|
|
113
151
|
const result = await loop.step(id);
|
|
114
152
|
if (result.stop) {
|
|
115
153
|
stopped = result.reason;
|
|
154
|
+
failed = result.ok === false;
|
|
116
155
|
break;
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
const breakStarted = now();
|
|
120
159
|
log(`round ${round}: review`);
|
|
121
|
-
const { summary, lessons, view: reviewed } = await review(id, before);
|
|
122
|
-
log(` ${roundTally(reviewed, summary)}; ${lessons.length} lesson${lessons.length === 1 ? '' : 's'} written`);
|
|
160
|
+
const { summary, lessons, view: reviewed, quiet } = await review(id, before);
|
|
161
|
+
log(quiet ? ' nothing new this round; no review' : ` ${roundTally(reviewed, summary)}; ${lessons.length} lesson${lessons.length === 1 ? '' : 's'} written`);
|
|
123
162
|
const view = getCampaign(store, id);
|
|
124
|
-
last = { reason: stopped || 'work window ended', round };
|
|
163
|
+
last = { reason: stopped || 'work window ended', round, failed };
|
|
125
164
|
// Finished, out of money, or waiting on a person: no point in another round.
|
|
126
165
|
if (['done', 'out_of_budget', 'needs_you', 'paused'].includes(view.state) || (stopped && !/missions running|nothing usable/.test(stopped))) {
|
|
127
|
-
return { rounds: round, reason: stopped || view.state, campaign: view };
|
|
166
|
+
return { ...(failed ? { ok: false } : {}), rounds: round, reason: stopped || view.state, campaign: view };
|
|
128
167
|
}
|
|
129
168
|
const rest = breakMs - (now() - breakStarted);
|
|
130
169
|
if (round < rounds && rest > 0) await sleep(rest);
|
|
131
170
|
}
|
|
132
|
-
return { rounds: round, reason: last.reason, campaign: getCampaign(store, id) };
|
|
171
|
+
return { ...(last.failed ? { ok: false } : {}), rounds: round, reason: last.reason, campaign: getCampaign(store, id) };
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
return { run, review };
|