atris 3.71.3 → 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 +7 -1
- package/commands/autoland.js +102 -2
- package/commands/campaign.js +86 -13
- package/commands/clean.js +60 -4
- package/commands/gm.js +1273 -7
- package/commands/member.js +28 -3
- package/commands/mission.js +72 -23
- package/commands/sync.js +2 -2
- package/commands/task.js +49 -13
- package/commands/zen.js +183 -0
- package/lib/auto-accept-certified.js +9 -4
- package/lib/campaign-cadence.js +15 -5
- package/lib/campaign-core.js +44 -17
- package/lib/campaign-loop.js +45 -14
- package/lib/campaign-reading.js +14 -1
- package/lib/campaign-rsi.js +28 -11
- package/lib/campaign-schedule.js +124 -0
- package/lib/campaign-store.js +34 -2
- package/lib/campaign-trace.js +40 -14
- 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-protected-lane.js +52 -4
- package/lib/qr.js +338 -0
- 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/member.js
CHANGED
|
@@ -10636,11 +10636,13 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10636
10636
|
const required = require('../lib/member-task-pick').taskCheck(task).command;
|
|
10637
10637
|
const checked = mission.verifier_result && mission.verifier_result.passed === true
|
|
10638
10638
|
&& (!required || String(mission.verifier_result.command || '').trim() === required);
|
|
10639
|
-
|
|
10639
|
+
// The commit comes from the member's own work copy, not the main folder.
|
|
10640
|
+
const workRoot = (result && result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
|
|
10641
|
+
const passed = !thrown && result && result.started && ['complete', 'ready'].includes(mission.status) && checked;
|
|
10642
|
+
const changedNothing = passed && selfWrittenJob(task) && !runChangedSomething(taskDb, db, task, result, workRoot);
|
|
10643
|
+
if (passed && !changedNothing) {
|
|
10640
10644
|
const proof = [`[verified] ${owner} ran mission ${result.mission_id} and its check passed.`,
|
|
10641
10645
|
receipt ? `Receipt: ${receipt}` : ''].filter(Boolean).join('\n');
|
|
10642
|
-
// The commit comes from the member's own work copy, not the main folder.
|
|
10643
|
-
const workRoot = (result.worktree && result.worktree.path) || (mission.worktree && mission.worktree.path) || undefined;
|
|
10644
10646
|
const ready = taskDb.readyTask(db, { id: task.id, actor: owner, proof, workRoot });
|
|
10645
10647
|
if (ready.ready !== false) return { id: task.id, title: task.title, outcome: 'review', receipt: receipt || null };
|
|
10646
10648
|
// Its change already merged and the merge line closed the job.
|
|
@@ -10650,6 +10652,7 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10650
10652
|
const lastReason = String((ticks[ticks.length - 1] || {}).reason || (ticks[ticks.length - 1] || {}).error || '');
|
|
10651
10653
|
const outOfTime = /timeout|wall-exceeded/.test(lastReason);
|
|
10652
10654
|
const why = thrown ? thrown.message
|
|
10655
|
+
: changedNothing ? 'the run passed its check but changed nothing: no new commit, no pushed branch, no pull request'
|
|
10653
10656
|
: outOfTime ? 'ran out of time before the work finished'
|
|
10654
10657
|
: (result && result.error) || (result && result.started ? 'the check did not pass' : 'the run did not start');
|
|
10655
10658
|
const reason = String(why || 'the run did not finish').split(/\r?\n/)[0].trim().slice(0, 200);
|
|
@@ -10678,6 +10681,28 @@ function memberGoFinishTask(owner, task, result, thrown = null) {
|
|
|
10678
10681
|
return { id: task.id, title: task.title, outcome, reason, ...(held && forReview ? { held_for: 'review' } : {}) };
|
|
10679
10682
|
}
|
|
10680
10683
|
|
|
10684
|
+
// A job the member wrote for itself from its mission ("next step toward:").
|
|
10685
|
+
function selfWrittenJob(task) {
|
|
10686
|
+
return String((task && task.source_key) || '').startsWith('self-next:')
|
|
10687
|
+
|| (task && task.metadata && task.metadata.source === 'member mission');
|
|
10688
|
+
}
|
|
10689
|
+
|
|
10690
|
+
// Did the run leave something for review? A pushed branch, a pull request,
|
|
10691
|
+
// or a local landing counts, and so does a commit in the work copy that
|
|
10692
|
+
// changes files since the job started. With no start commit on record there
|
|
10693
|
+
// is nothing to compare, so the run is taken at its word.
|
|
10694
|
+
function runChangedSomething(taskDb, db, task, result, workRoot) {
|
|
10695
|
+
const landed = result && result.autoland;
|
|
10696
|
+
if (landed && (landed.pushed || landed.url || landed.merged)) return true;
|
|
10697
|
+
const start = String((taskDb.getTask(db, task.id) || task)?.metadata?.start_commit || '').trim();
|
|
10698
|
+
if (!start) return true;
|
|
10699
|
+
const cwd = workRoot || task.workspace_root || process.cwd();
|
|
10700
|
+
const diff = spawnSync('git', ['diff', '--quiet', start, 'HEAD'], { cwd, encoding: 'utf8' });
|
|
10701
|
+
// Exit 0 means no change. Exit 1 means the code differs; anything else
|
|
10702
|
+
// could not be read, so the run is taken at its word.
|
|
10703
|
+
return diff.status !== 0;
|
|
10704
|
+
}
|
|
10705
|
+
|
|
10681
10706
|
const MEMBER_GO_STRIKES = 3;
|
|
10682
10707
|
|
|
10683
10708
|
// Failed runs on a task since its last pass, review, reopen, or a person
|
package/commands/mission.js
CHANGED
|
@@ -1774,6 +1774,14 @@ function appendMemberLog(owner, title, fields = {}, root = process.cwd()) {
|
|
|
1774
1774
|
return logPath;
|
|
1775
1775
|
}
|
|
1776
1776
|
|
|
1777
|
+
// Shorten at a word boundary so a lesson never ends mid-word.
|
|
1778
|
+
function clipWords(text, max) {
|
|
1779
|
+
if (text.length <= max) return text;
|
|
1780
|
+
const cut = text.slice(0, max + 1);
|
|
1781
|
+
const space = cut.lastIndexOf(' ');
|
|
1782
|
+
return `${(space > max * 0.6 ? cut.slice(0, space) : text.slice(0, max)).replace(/[\s,;:.-]+$/, '')}...`;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1777
1785
|
function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
|
|
1778
1786
|
const dir = memberDir(owner, root);
|
|
1779
1787
|
if (!dir) return null;
|
|
@@ -1782,38 +1790,47 @@ function crystallizeMemberSkillLesson(owner, entry = {}, root = process.cwd()) {
|
|
|
1782
1790
|
const skillDir = path.join(dir, 'skills', 'auto-learned');
|
|
1783
1791
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
1784
1792
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
1785
|
-
|
|
1793
|
+
let lines = null;
|
|
1786
1794
|
if (fs.existsSync(skillPath)) {
|
|
1787
1795
|
try {
|
|
1788
|
-
|
|
1789
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
1790
|
-
if (line.trim().startsWith('- **')) existingBullets.push(line.trim());
|
|
1791
|
-
}
|
|
1796
|
+
lines = fs.readFileSync(skillPath, 'utf8').split(/\r?\n/);
|
|
1792
1797
|
} catch {}
|
|
1793
1798
|
}
|
|
1799
|
+
const bulletAt = (lines || []).map((line, i) => (line.trim().startsWith('- **') ? i : -1)).filter((i) => i >= 0);
|
|
1800
|
+
const existingBullets = bulletAt.map((i) => lines[i].trim());
|
|
1794
1801
|
const verifierText = String(entry.verifier || '').replace(/\s+/g, ' ').trim();
|
|
1795
1802
|
const nextPushText = String(entry.next_push || '').replace(/\s+/g, ' ').trim();
|
|
1796
1803
|
const layerLabel = String(entry.layer || 'capabilities').trim();
|
|
1797
|
-
const parts = [`- **${todayName()} (${layerLabel})**: ${summary
|
|
1798
|
-
|
|
1799
|
-
if (
|
|
1804
|
+
const parts = [`- **${todayName()} (${layerLabel})**: ${clipWords(summary, 240)}`];
|
|
1805
|
+
// A cut-off command cannot be run, so a long one is left out, never clipped.
|
|
1806
|
+
if (verifierText && verifierText.length <= 300) parts.push(`Verify: \`${verifierText}\``);
|
|
1807
|
+
if (nextPushText) parts.push(`Next push: ${clipWords(nextPushText, 200)}`);
|
|
1800
1808
|
const newBullet = parts.join(' -> ');
|
|
1801
1809
|
const deduped = [
|
|
1802
1810
|
newBullet,
|
|
1803
1811
|
...existingBullets.filter((b) => !b.includes(summary.slice(0, 80))),
|
|
1804
1812
|
].slice(0, 5);
|
|
1805
1813
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1814
|
+
let content;
|
|
1815
|
+
if (bulletAt.length) {
|
|
1816
|
+
// Keep whatever a person or reviewer wrote around the lessons; only the
|
|
1817
|
+
// lesson list itself is rewritten.
|
|
1818
|
+
const first = bulletAt[0];
|
|
1819
|
+
const last = bulletAt[bulletAt.length - 1];
|
|
1820
|
+
content = [...lines.slice(0, first), ...deduped, ...lines.slice(last + 1)].join('\n');
|
|
1821
|
+
} else {
|
|
1822
|
+
content = [
|
|
1823
|
+
'---',
|
|
1824
|
+
'name: auto-learned',
|
|
1825
|
+
`description: Verified patterns crystallized from passing runs for ${owner}. Bounded to 5 newest patterns so context never bloats.`,
|
|
1826
|
+
'---',
|
|
1827
|
+
'',
|
|
1828
|
+
'# Auto-Learned Verified Patterns',
|
|
1829
|
+
'',
|
|
1830
|
+
...deduped,
|
|
1831
|
+
'',
|
|
1832
|
+
].join('\n');
|
|
1833
|
+
}
|
|
1817
1834
|
fs.writeFileSync(skillPath, content, 'utf8');
|
|
1818
1835
|
return skillPath;
|
|
1819
1836
|
}
|
|
@@ -3199,6 +3216,7 @@ function missionFromArgs(args) {
|
|
|
3199
3216
|
'--base',
|
|
3200
3217
|
'--task',
|
|
3201
3218
|
'--ask',
|
|
3219
|
+
'--yes-lock',
|
|
3202
3220
|
'--campaign',
|
|
3203
3221
|
'--reserve',
|
|
3204
3222
|
'--mission-id',
|
|
@@ -3242,6 +3260,7 @@ function missionFromArgs(args) {
|
|
|
3242
3260
|
const taskIds = readRepeatedFlag(args, '--task');
|
|
3243
3261
|
// MEMBER.md approval-required becomes an open human ask the owner must clear.
|
|
3244
3262
|
const humanAsks = [...readRepeatedFlag(args, '--ask'), ...memberApprovalAsks(owner, process.cwd(), objective)];
|
|
3263
|
+
const yesLocks = readYesLockSeal(readFlag(args, '--yes-lock', ''), wantsJson(args));
|
|
3245
3264
|
const drive = missionDrive(readFlag(args, '--destination', ''));
|
|
3246
3265
|
const alwaysOn = hasFlag(args, '--always-on');
|
|
3247
3266
|
const xpTaskEnabled = hasFlag(args, '--xp-task') || hasFlag(args, '--agent-xp');
|
|
@@ -3278,6 +3297,8 @@ function missionFromArgs(args) {
|
|
|
3278
3297
|
stop_condition: stopCondition,
|
|
3279
3298
|
task_ids: taskIds,
|
|
3280
3299
|
human_asks: humanAsks,
|
|
3300
|
+
// the sealed owner steps: the guard holds what each locks until the owner says yes
|
|
3301
|
+
...(yesLocks.length ? { yes_locks: { steps: yesLocks, sealed_at: stampIso() } } : {}),
|
|
3281
3302
|
...(campaignId ? { campaign_id: campaignId } : {}),
|
|
3282
3303
|
...(drive ? { drive } : {}),
|
|
3283
3304
|
next_action: verifier ? 'run verifier with `atris mission tick <id> --verify`' : 'define verifier or run next task',
|
|
@@ -3296,6 +3317,22 @@ function missionFromArgs(args) {
|
|
|
3296
3317
|
return mission;
|
|
3297
3318
|
}
|
|
3298
3319
|
|
|
3320
|
+
// --yes-lock '[{"step":3,"text":"...","categories":["publish"]}]': the owner
|
|
3321
|
+
// steps a job may not do before the owner says yes. Bad input stops the start.
|
|
3322
|
+
const YES_LOCK_CATEGORIES = new Set(['publish', 'tag', 'post', 'send', 'merge', 'deploy']);
|
|
3323
|
+
function readYesLockSeal(raw, asJson) {
|
|
3324
|
+
if (!raw) return [];
|
|
3325
|
+
let list;
|
|
3326
|
+
try {
|
|
3327
|
+
list = JSON.parse(raw);
|
|
3328
|
+
} catch {
|
|
3329
|
+
exitMissionError('--yes-lock takes a JSON list of {step, text, categories}', 2, asJson);
|
|
3330
|
+
}
|
|
3331
|
+
const ok = Array.isArray(list) && list.every(l => l && Number.isInteger(l.step) && l.step > 0 && Array.isArray(l.categories) && l.categories.length && l.categories.every(c => YES_LOCK_CATEGORIES.has(c)));
|
|
3332
|
+
if (!ok) exitMissionError('--yes-lock takes a JSON list of {step, text, categories}, with categories from publish, tag, post, send, merge, deploy', 2, asJson);
|
|
3333
|
+
return list.map(l => ({ step: l.step, text: String(l.text || '').slice(0, 200), categories: [...new Set(l.categories)] }));
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3299
3336
|
function applyMissionStartPatch(mission, patch) {
|
|
3300
3337
|
if (!patch || typeof patch !== 'object') return mission;
|
|
3301
3338
|
const { metadata, ...rest } = patch;
|
|
@@ -9440,7 +9477,7 @@ function spawnGenericRunnerTick(mission, opts) {
|
|
|
9440
9477
|
}
|
|
9441
9478
|
let gitGuard;
|
|
9442
9479
|
try {
|
|
9443
|
-
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
9480
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
|
|
9444
9481
|
} catch (error) {
|
|
9445
9482
|
resolve({
|
|
9446
9483
|
ok: false,
|
|
@@ -9625,7 +9662,7 @@ function spawnClaudeTick(mission, opts) {
|
|
|
9625
9662
|
return new Promise((resolve) => {
|
|
9626
9663
|
let gitGuard;
|
|
9627
9664
|
try {
|
|
9628
|
-
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
9665
|
+
gitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
|
|
9629
9666
|
} catch (error) {
|
|
9630
9667
|
resolve({
|
|
9631
9668
|
ok: false,
|
|
@@ -10453,7 +10490,7 @@ async function executeMissionRunTicksPhase(context) {
|
|
|
10453
10490
|
const prompt = buildTickPrompt(tickRuntimeMission, tickIdx, effectiveMaxTicks, frozen, pendingMissionPings(mission), cwd);
|
|
10454
10491
|
let atris2GitGuard = null;
|
|
10455
10492
|
try {
|
|
10456
|
-
atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd) });
|
|
10493
|
+
atris2GitGuard = prepareMissionGitGuard({ root: cwd, tags: missionProtectedTags(mission, cwd), missionId: mission.id, yesLockSeal: (mission.yes_locks && mission.yes_locks.steps) || [], stateDir: path.join(require('../lib/state-home').atrisHome(), 'state') });
|
|
10457
10494
|
} catch (error) {
|
|
10458
10495
|
result = {
|
|
10459
10496
|
...result,
|
|
@@ -11281,12 +11318,23 @@ async function runMission(args) {
|
|
|
11281
11318
|
context.error = error;
|
|
11282
11319
|
throw error;
|
|
11283
11320
|
} finally {
|
|
11284
|
-
if (context.lock)
|
|
11321
|
+
if (context.lock) {
|
|
11322
|
+
completeMissionRunPhase(context);
|
|
11323
|
+
sweepMergedMemberWork(context);
|
|
11324
|
+
}
|
|
11285
11325
|
}
|
|
11286
11326
|
|
|
11287
11327
|
return context.returnValue;
|
|
11288
11328
|
}
|
|
11289
11329
|
|
|
11330
|
+
// Merged member work closes its jobs even when the team loop is down.
|
|
11331
|
+
function sweepMergedMemberWork(context) {
|
|
11332
|
+
try {
|
|
11333
|
+
const out = require('../lib/member-autoland').sweepAfterMissionRun({ cwd: context.cwd || process.cwd() });
|
|
11334
|
+
if (out && out.closed > 0 && !context.asJson) console.log(out.line);
|
|
11335
|
+
} catch {}
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11290
11338
|
function tickMission(args) {
|
|
11291
11339
|
const asJson = wantsJson(args);
|
|
11292
11340
|
if (hasFlag(args, '--help') || hasFlag(args, '-h') || String(args[0] || '').trim() === 'help') {
|
|
@@ -12724,6 +12772,7 @@ function missionCommand(args) {
|
|
|
12724
12772
|
}
|
|
12725
12773
|
|
|
12726
12774
|
module.exports = {
|
|
12775
|
+
crystallizeMemberSkillLesson,
|
|
12727
12776
|
listSiblingWorkspaceMissionHints,
|
|
12728
12777
|
missionProtectedLaneHold,
|
|
12729
12778
|
missionCommand,
|
package/commands/sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const { stateHome } = require('../lib/state-home');
|
|
4
4
|
const { ensureWikiScaffold } = require('../lib/wiki');
|
|
5
5
|
const { upsertAtrisClaudeBootBlock } = require('../lib/claude-boot-block');
|
|
6
6
|
const { ensureRunIgnore } = require('../lib/gitignore');
|
|
@@ -809,7 +809,7 @@ function syncSkills({ silent = false } = {}) {
|
|
|
809
809
|
}
|
|
810
810
|
|
|
811
811
|
let updated = 0;
|
|
812
|
-
const homeDir =
|
|
812
|
+
const homeDir = stateHome();
|
|
813
813
|
|
|
814
814
|
const skillFolders = fs.readdirSync(packageSkillsDir).filter(f =>
|
|
815
815
|
fs.statSync(path.join(packageSkillsDir, f)).isDirectory()
|
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/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
|
}
|