atris 3.71.3 → 3.72.1

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.
Files changed (46) hide show
  1. package/atris/skills/campaigns/SKILL.md +1 -1
  2. package/bin/atris.js +7 -1
  3. package/commands/auth.js +29 -2
  4. package/commands/autoland.js +102 -2
  5. package/commands/campaign.js +86 -13
  6. package/commands/clean.js +60 -4
  7. package/commands/gm.js +1273 -7
  8. package/commands/member.js +28 -3
  9. package/commands/mission.js +72 -23
  10. package/commands/sync.js +2 -2
  11. package/commands/task.js +49 -13
  12. package/commands/zen.js +183 -0
  13. package/lib/auto-accept-certified.js +9 -4
  14. package/lib/campaign-cadence.js +15 -5
  15. package/lib/campaign-core.js +44 -17
  16. package/lib/campaign-loop.js +45 -14
  17. package/lib/campaign-reading.js +14 -1
  18. package/lib/campaign-rsi.js +28 -11
  19. package/lib/campaign-schedule.js +124 -0
  20. package/lib/campaign-store.js +34 -2
  21. package/lib/campaign-trace.js +40 -14
  22. package/lib/engine-resource.js +5 -1
  23. package/lib/gm-bet.js +2219 -0
  24. package/lib/gm-phone-page.js +1102 -0
  25. package/lib/gm-phone.js +682 -0
  26. package/lib/gm-season.js +1162 -0
  27. package/lib/gm-target.js +292 -0
  28. package/lib/job-code-verify.js +169 -0
  29. package/lib/known-commands.js +1 -1
  30. package/lib/member-autoland.js +105 -14
  31. package/lib/member-progress.js +2 -2
  32. package/lib/member-report.js +11 -1
  33. package/lib/member-self-next-job.js +6 -1
  34. package/lib/member-up.js +17 -8
  35. package/lib/merged-review-close.js +206 -0
  36. package/lib/mission-protected-lane.js +52 -4
  37. package/lib/qr.js +338 -0
  38. package/lib/site-publish.js +16 -2
  39. package/lib/site.js +17 -7
  40. package/lib/yes-lock.js +530 -0
  41. package/lib/zen-apply.js +619 -0
  42. package/lib/zen-workspace.js +121 -0
  43. package/lib/zen.js +409 -0
  44. package/package.json +1 -1
  45. package/utils/api.js +15 -2
  46. package/utils/auth.js +8 -7
@@ -0,0 +1,292 @@
1
+ 'use strict';
2
+
3
+ // The owner's weekly target: the game's win condition. Each week the owner
4
+ // asks for N landed jobs, at most M abandoned, and at most K claims stuck for
5
+ // over a day. A target is saved against the week it was set in and carries
6
+ // forward to later weeks until changed or cleared.
7
+ //
8
+ // Storage: ~/.atris/state/gm-targets/<sha1 of workspace path>.json
9
+ // { workspace, weeks: { "<monday 00:00 ms>": { landed, max_abandoned, max_stuck } | { cleared: true } } }
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const crypto = require('crypto');
14
+
15
+ const HOUR_MS = 60 * 60 * 1000;
16
+ const DAY_MS = 24 * HOUR_MS;
17
+ const STUCK_OVER_MS = DAY_MS;
18
+ const DEFAULT_MAX_ABANDONED = 2;
19
+ const DEFAULT_MAX_STUCK = 0;
20
+ const HISTORY_WEEKS = 26;
21
+ const CLAIM_ENDS = new Set(['claim_released', 'completed', 'archived', 'reopened']);
22
+
23
+ // Monday 00:00 local time for the week that contains `at`.
24
+ function weekStart(at = Date.now()) {
25
+ const d = new Date(at);
26
+ const daysSinceMonday = (d.getDay() + 6) % 7;
27
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - daysSinceMonday).getTime();
28
+ }
29
+
30
+ // `days` calendar days before a moment's local midnight, by date so a daylight
31
+ // saving change cannot move it an hour.
32
+ function daysBefore(at, days) {
33
+ const d = new Date(at);
34
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() - days).getTime();
35
+ }
36
+
37
+ // Weeks are keyed by the local calendar date of their monday, "2026-10-19",
38
+ // so a target saved in one time zone is the same week in another.
39
+ function weekKey(at) {
40
+ const d = new Date(weekStart(at));
41
+ const two = n => String(n).padStart(2, '0');
42
+ return `${d.getFullYear()}-${two(d.getMonth() + 1)}-${two(d.getDate())}`;
43
+ }
44
+
45
+ function keyStart(key) {
46
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(key));
47
+ return m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])).getTime() : null;
48
+ }
49
+
50
+ // A stored week key: only a "YYYY-MM-DD" date is valid; anything else is ignored.
51
+ function normalizeWeekKey(key) {
52
+ return /^\d{4}-\d{2}-\d{2}$/.test(String(key)) ? String(key) : null;
53
+ }
54
+
55
+ // The monday after, found by date so a daylight saving change cannot shift it.
56
+ function nextWeekStart(start) {
57
+ return weekStart(Number(start) + 8 * DAY_MS);
58
+ }
59
+
60
+ // ---- storage -----------------------------------------------------------------
61
+
62
+ function targetsDir() {
63
+ const { atrisHome } = require('./state-home');
64
+ return path.join(atrisHome(), 'state', 'gm-targets');
65
+ }
66
+
67
+ function targetsPath(workspaceRoot, dir) {
68
+ const key = path.resolve(String(workspaceRoot || '.'));
69
+ return path.join(dir, `${crypto.createHash('sha1').update(key).digest('hex')}.json`);
70
+ }
71
+
72
+ function cleanTarget(raw) {
73
+ if (!raw || typeof raw !== 'object') return null;
74
+ if (raw.cleared === true) return { cleared: true };
75
+ const landed = Number(raw.landed);
76
+ const maxAbandoned = Number(raw.max_abandoned);
77
+ const maxStuck = Number(raw.max_stuck);
78
+ const whole = n => Number.isInteger(n) && n >= 0;
79
+ if (!whole(landed) || landed < 1 || !whole(maxAbandoned) || !whole(maxStuck)) return null;
80
+ return { landed, max_abandoned: maxAbandoned, max_stuck: maxStuck };
81
+ }
82
+
83
+ // A missing or corrupt file reads as no targets; it never throws.
84
+ function readTargets(workspaceRoot, { dir = targetsDir() } = {}) {
85
+ try {
86
+ const raw = JSON.parse(fs.readFileSync(targetsPath(workspaceRoot, dir), 'utf8'));
87
+ const weeks = {};
88
+ for (const [key, value] of Object.entries(raw && raw.weeks && typeof raw.weeks === 'object' ? raw.weeks : {})) {
89
+ const week = normalizeWeekKey(key);
90
+ const target = cleanTarget(value);
91
+ if (week && target) weeks[week] = target;
92
+ }
93
+ return { weeks };
94
+ } catch {
95
+ return { weeks: {} };
96
+ }
97
+ }
98
+
99
+ // Saves a target (or { cleared: true }) for the week containing `now`.
100
+ // Written to a temp file then renamed. Returns false instead of throwing.
101
+ function writeTarget(workspaceRoot, target, { dir = targetsDir(), now = Date.now() } = {}) {
102
+ const clean = cleanTarget(target);
103
+ if (!clean) return false;
104
+ try {
105
+ const store = readTargets(workspaceRoot, { dir });
106
+ store.weeks[weekKey(now)] = clean;
107
+ const file = targetsPath(workspaceRoot, dir);
108
+ fs.mkdirSync(dir, { recursive: true });
109
+ const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
110
+ const body = { workspace: path.resolve(String(workspaceRoot || '.')), weeks: store.weeks };
111
+ fs.writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`);
112
+ fs.renameSync(tmp, file);
113
+ return true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ // The target in force for a week: the newest entry at or before that week.
120
+ // A cleared entry means no target from then on.
121
+ function targetForWeek(store, start) {
122
+ const want = weekKey(start);
123
+ const weeks = (store && store.weeks) || {};
124
+ const keys = Object.keys(weeks).map(k => [k, normalizeWeekKey(k)]).filter(([, k]) => k && k <= want).sort((a, b) => (a[1] < b[1] ? 1 : -1));
125
+ if (!keys.length) return null;
126
+ const entry = weeks[keys[0][0]];
127
+ return entry && !entry.cleared ? entry : null;
128
+ }
129
+
130
+ function firstTargetWeek(store) {
131
+ const keys = Object.keys((store && store.weeks) || {}).map(normalizeWeekKey).filter(Boolean).sort();
132
+ return keys.length ? keyStart(keys[0]) : null;
133
+ }
134
+
135
+ // ---- scoring -------------------------------------------------------------------
136
+
137
+ function countKind(outcomes, kind, from, to) {
138
+ return outcomes.filter(o => o.kind === kind && Number(o.at) >= from && Number(o.at) < to).length;
139
+ }
140
+
141
+ // The suggested target: the last 4 full weeks' average landed, rounded up,
142
+ // at least 1; at most 2 abandoned; no claim stuck for over a day.
143
+ function defaultTarget(outcomes = [], now = Date.now()) {
144
+ const thisWeek = weekStart(now);
145
+ let from = thisWeek;
146
+ for (let i = 0; i < 4; i += 1) from = weekStart(from - DAY_MS);
147
+ const landed = countKind(outcomes, 'landed', from, thisWeek);
148
+ return { landed: Math.max(1, Math.ceil(landed / 4)), max_abandoned: DEFAULT_MAX_ABANDONED, max_stuck: DEFAULT_MAX_STUCK };
149
+ }
150
+
151
+ // won only once the week is over and all three held. During the week a
152
+ // broken abandon or stuck limit is already missed; a landed shortfall is
153
+ // only behind.
154
+ function weekVerdict({ target, landed, abandoned, stuck, over }) {
155
+ const limitsBroken = abandoned > target.max_abandoned || stuck > target.max_stuck;
156
+ if (over) return landed >= target.landed && !limitsBroken ? 'won' : 'missed';
157
+ if (limitsBroken) return 'missed';
158
+ return landed >= target.landed ? 'on track' : 'behind';
159
+ }
160
+
161
+ // Claims held at time `at` whose task had no event for over a day, replaying
162
+ // each task's status: proof_ready moves a claim into review (waiting on the
163
+ // reviewer, not stuck with the player); revision_requested puts it back with
164
+ // the holder; reopened, release, completion, or archive ends it, so nobody
165
+ // holds the task until a new claim.
166
+ // eventsByTask: Map task_id -> [{ event_type, actor, created_at }] by version.
167
+ function stuckOverDayAt(eventsByTask, at) {
168
+ let count = 0;
169
+ for (const events of eventsByTask.values()) {
170
+ let holder = null;
171
+ let inReview = false;
172
+ let last = null;
173
+ for (const e of events) {
174
+ if (Number(e.created_at) >= at) break;
175
+ last = Number(e.created_at);
176
+ if (e.event_type === 'claimed') {
177
+ holder = e.actor || 'someone';
178
+ inReview = false;
179
+ } else if (e.event_type === 'proof_ready') {
180
+ inReview = true;
181
+ } else if (e.event_type === 'revision_requested') {
182
+ inReview = false;
183
+ } else if (CLAIM_ENDS.has(e.event_type)) {
184
+ holder = null;
185
+ inReview = false;
186
+ }
187
+ }
188
+ if (holder && !inReview && last != null && at - last >= STUCK_OVER_MS) count += 1;
189
+ }
190
+ return count;
191
+ }
192
+
193
+ function liveStuckOverDay(claims = [], now = Date.now()) {
194
+ return claims.filter(c => c && c.member && Number(now) - Number(c.last_event_at || 0) >= STUCK_OVER_MS).length;
195
+ }
196
+
197
+ // This week's standing against the target in force, or null with no target.
198
+ function currentStatus({ store, outcomes = [], claims = [], now = Date.now() }) {
199
+ const start = weekStart(now);
200
+ const target = targetForWeek(store, start);
201
+ if (!target) return null;
202
+ const landed = countKind(outcomes, 'landed', start, Number(now) + 1);
203
+ const abandoned = countKind(outcomes, 'abandoned', start, Number(now) + 1);
204
+ const stuck = liveStuckOverDay(claims, now);
205
+ return { week_of: start, target, landed, abandoned, stuck, verdict: weekVerdict({ target, landed, abandoned, stuck, over: false }) };
206
+ }
207
+
208
+ // Completed weeks that had a target, each scored with the target in force
209
+ // that week and the claims stuck over a day at the moment the week ended.
210
+ function seasonRecord({ store, outcomes = [], eventsByTask = new Map(), now = Date.now() }) {
211
+ const first = firstTargetWeek(store);
212
+ const thisWeek = weekStart(now);
213
+ const weeks = [];
214
+ if (first != null) {
215
+ let start = Math.max(first, daysBefore(thisWeek, HISTORY_WEEKS * 7));
216
+ start = weekStart(start + DAY_MS);
217
+ while (start < thisWeek) {
218
+ const end = nextWeekStart(start);
219
+ const target = targetForWeek(store, start);
220
+ if (target) {
221
+ const landed = countKind(outcomes, 'landed', start, end);
222
+ const abandoned = countKind(outcomes, 'abandoned', start, end);
223
+ const stuck = stuckOverDayAt(eventsByTask, end);
224
+ weeks.push({ week_of: start, target, landed, abandoned, stuck, verdict: weekVerdict({ target, landed, abandoned, stuck, over: true }) });
225
+ }
226
+ start = end;
227
+ }
228
+ }
229
+ const won = weeks.filter(w => w.verdict === 'won').length;
230
+ return { won, lost: weeks.length - won, weeks };
231
+ }
232
+
233
+ function targetLine(status) {
234
+ if (!status) return null;
235
+ const t = status.target;
236
+ return `target: ${status.landed} of ${t.landed} landed | abandoned ${status.abandoned} of max ${t.max_abandoned} | stuck over a day ${status.stuck}, max ${t.max_stuck} (${status.verdict})`;
237
+ }
238
+
239
+ function recordLine(record) {
240
+ return `season: ${record.won} won, ${record.lost} lost`;
241
+ }
242
+
243
+ // ---- loading -------------------------------------------------------------------
244
+
245
+ // The oldest moment the target views need plays from: four weeks before this
246
+ // week for the default, or the first target week for the record.
247
+ function historyStart(store, now = Date.now()) {
248
+ let from = weekStart(now);
249
+ for (let i = 0; i < 4; i += 1) from = weekStart(from - DAY_MS);
250
+ const first = firstTargetWeek(store);
251
+ const floor = daysBefore(weekStart(now), HISTORY_WEEKS * 7);
252
+ return Math.max(floor, Math.min(from, first == null ? from : first));
253
+ }
254
+
255
+ // Task events needed to know who held what at each week's end: every task in
256
+ // the workspace that moved since `since` or is claimed now, with its whole
257
+ // history. Uses an already open read-only db handle.
258
+ function loadClaimHistory(db, workspaceRoot, since) {
259
+ const rows = db.prepare(`
260
+ SELECT e.task_id, e.event_type, e.actor, e.created_at, e.version
261
+ FROM task_events e
262
+ WHERE e.task_id IN (
263
+ SELECT DISTINCT task_id FROM task_events WHERE workspace_root = ? AND created_at >= ?
264
+ UNION
265
+ SELECT id FROM tasks WHERE workspace_root = ? AND status = 'claimed')
266
+ ORDER BY e.task_id, e.version
267
+ `).all(workspaceRoot, since, workspaceRoot);
268
+ const byTask = new Map();
269
+ for (const row of rows) {
270
+ if (!byTask.has(row.task_id)) byTask.set(row.task_id, []);
271
+ byTask.get(row.task_id).push(row);
272
+ }
273
+ return byTask;
274
+ }
275
+
276
+ module.exports = {
277
+ currentStatus,
278
+ defaultTarget,
279
+ historyStart,
280
+ loadClaimHistory,
281
+ readTargets,
282
+ recordLine,
283
+ seasonRecord,
284
+ stuckOverDayAt,
285
+ targetForWeek,
286
+ targetLine,
287
+ targetsDir,
288
+ targetsPath,
289
+ weekKey,
290
+ weekVerdict,
291
+ writeTarget,
292
+ };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ // The second check and the landing re-check run against the job's own code.
4
+ //
5
+ // They used to run in the main folder, so a job whose test exists only on its
6
+ // own branch failed every hour (the test file was simply not there), and a
7
+ // job that broke nothing on its branch was judged by whatever the main folder
8
+ // held. Same pattern as lib/land-green.js: a scratch copy at the job's commit,
9
+ // the check runs there, the copy is removed after. A job with no commit of its
10
+ // own (or one that cannot be found) keeps the old behavior and runs where the
11
+ // job lives.
12
+
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { spawnSync } = require('child_process');
17
+
18
+ const SHA = /^[0-9a-f]{40,64}$/;
19
+
20
+ function git(args, cwd) {
21
+ const res = spawnSync('git', args, { cwd, encoding: 'utf8', timeout: 60000 });
22
+ return { ok: res.status === 0, out: String(res.stdout || '').trim(), err: String(res.stderr || '').trim() };
23
+ }
24
+
25
+ function isDir(dir) {
26
+ try {
27
+ return Boolean(dir) && fs.statSync(dir).isDirectory();
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function repoTop(task, root) {
34
+ for (const dir of [task && task.workspace_root, root, process.cwd()]) {
35
+ if (!isDir(dir)) continue;
36
+ const top = git(['rev-parse', '--show-toplevel'], dir);
37
+ if (top.ok && top.out) return top.out;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // The commit autoland queued for this job, when it is on the autoland list.
43
+ function queuedCommit(taskId, top) {
44
+ const common = git(['rev-parse', '--path-format=absolute', '--git-common-dir'], top);
45
+ if (!common.ok) return null;
46
+ try {
47
+ const file = path.join(path.dirname(common.out), '.atris', 'state', 'autoland-queue.json');
48
+ const entries = JSON.parse(fs.readFileSync(file, 'utf8')).entries || {};
49
+ for (const entry of Object.values(entries)) {
50
+ if (entry && Array.isArray(entry.task_ids) && entry.task_ids.includes(taskId) && SHA.test(String(entry.pushed || ''))) return entry.pushed;
51
+ }
52
+ } catch {}
53
+ return null;
54
+ }
55
+
56
+ // The job's own code: the commit autoland queued for it (what is actually on
57
+ // GitHub, catch-ups included), else the commit its builder recorded when it
58
+ // differs from where the job started. null when the job has no commit of its
59
+ // own. A recorded commit this repo does not hold comes back as missing, so
60
+ // the job is never judged by the main folder instead.
61
+ function jobCode(task, root) {
62
+ const top = repoTop(task, root);
63
+ if (!top || !task) return null;
64
+ const metadata = task.metadata || {};
65
+ const has = (sha) => SHA.test(sha) && git(['cat-file', '-e', `${sha}^{commit}`], top).ok;
66
+ const queued = queuedCommit(task.id, top);
67
+ if (queued && has(queued)) return { sha: queued, source: 'queued_branch', top };
68
+ const work = String(metadata.work_commit || '').trim();
69
+ const start = String(metadata.start_commit || '').trim();
70
+ if (SHA.test(work) && work !== start) return has(work) ? { sha: work, source: 'work_commit', top } : { sha: work, source: 'work_commit', top, missing: true };
71
+ if (queued) return { sha: queued, source: 'queued_branch', top, missing: true };
72
+ return null;
73
+ }
74
+
75
+ // Where in the scratch copy the job's check runs: the same place inside the
76
+ // repo as the folder the job lives in.
77
+ function scratchCwd(dir, task, top) {
78
+ let workspace = task && task.workspace_root;
79
+ try { workspace = fs.realpathSync(workspace); } catch { return dir; }
80
+ const rel = path.relative(top, workspace);
81
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return dir;
82
+ const inside = path.join(dir, rel);
83
+ return isDir(inside) ? inside : dir;
84
+ }
85
+
86
+ const SCRATCH_PREFIX = 'atris-job-check-';
87
+ const STALE_SCRATCH_MS = 2 * 60 * 60 * 1000;
88
+
89
+ // A pass killed mid-check (a timeout kill skips cleanup) leaves its scratch
90
+ // copy behind. Remove copies older than two hours, then drop their records.
91
+ function sweepStaleScratch(top) {
92
+ let removed = 0;
93
+ for (const name of fs.readdirSync(os.tmpdir()).filter((n) => n.startsWith(SCRATCH_PREFIX))) {
94
+ const dir = path.join(os.tmpdir(), name);
95
+ try {
96
+ if (Date.now() - fs.statSync(dir).mtimeMs < STALE_SCRATCH_MS) continue;
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ removed += 1;
99
+ } catch {}
100
+ }
101
+ if (removed) git(['worktree', 'prune'], top);
102
+ }
103
+
104
+ function checkoutHead(dir) {
105
+ const head = git(['rev-parse', 'HEAD'], dir || process.cwd());
106
+ return head.ok ? head.out : null;
107
+ }
108
+
109
+ // The code a check of this job runs against right now.
110
+ function checkedCodeSha(task, root) {
111
+ const code = jobCode(task, root);
112
+ return code ? code.sha : checkoutHead((task && task.workspace_root) || root);
113
+ }
114
+
115
+ function runInScratch(verify, code, task, runVerify) {
116
+ try { sweepStaleScratch(code.top); } catch {}
117
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), SCRATCH_PREFIX));
118
+ const added = git(['worktree', 'add', '--detach', dir, code.sha], code.top);
119
+ if (!added.ok) {
120
+ fs.rmSync(dir, { recursive: true, force: true });
121
+ return {
122
+ ok: false, reason: 'verify_unrunnable', unrunnable_cause: 'job_code_checkout_failed', alarm: true,
123
+ detail: (added.err || added.out).split('\n')[0],
124
+ };
125
+ }
126
+ try {
127
+ // Zero-dependency projects need nothing; others reuse the main folder's
128
+ // installed packages instead of installing per check.
129
+ const modules = path.join(code.top, 'node_modules');
130
+ if (isDir(modules) && !fs.existsSync(path.join(dir, 'node_modules'))) {
131
+ try { fs.symlinkSync(modules, path.join(dir, 'node_modules'), 'dir'); } catch {}
132
+ }
133
+ return runVerify(verify, scratchCwd(dir, task, code.top));
134
+ } finally {
135
+ git(['worktree', 'remove', '--force', dir], code.top);
136
+ fs.rmSync(dir, { recursive: true, force: true });
137
+ git(['worktree', 'prune'], code.top);
138
+ }
139
+ }
140
+
141
+ // Run `verify` for this job against its own code. The result carries the
142
+ // commit it ran on (code_sha) and where that commit came from (code_source:
143
+ // work_commit, queued_branch, or checkout for the old main-folder run).
144
+ function runVerifyAtJobCode(verify, task, { root, cache = null } = {}) {
145
+ const { runVerifyCommand, runVerifyCommandCached } = require('./auto-accept-certified');
146
+ const workspace = (task && task.workspace_root) || root || process.cwd();
147
+ const code = jobCode(task, root);
148
+ if (!code) {
149
+ const result = runVerifyCommandCached(verify, workspace, cache);
150
+ return { ...result, code_sha: checkoutHead(workspace), code_source: 'checkout' };
151
+ }
152
+ if (code.missing) {
153
+ return {
154
+ ok: false, reason: 'verify_unrunnable', unrunnable_cause: 'job_code_missing', alarm: true,
155
+ detail: `the job's commit ${code.sha.slice(0, 10)} is not in this repo; fetch it or record the job's commit again`,
156
+ code_sha: code.sha, code_source: code.source,
157
+ };
158
+ }
159
+ const key = `job:${code.sha}\u0000${(task && task.workspace_root) || ''}\u0000${String(verify || '').trim()}`;
160
+ if (cache instanceof Map && cache.has(key)) return { ...cache.get(key), reused: true };
161
+ const result = { ...runInScratch(verify, code, task, runVerifyCommand), code_sha: code.sha, code_source: code.source };
162
+ if (cache instanceof Map) cache.set(key, result);
163
+ return { ...result, reused: false };
164
+ }
165
+
166
+ module.exports = {
167
+ checkedCodeSha,
168
+ runVerifyAtJobCode,
169
+ };
@@ -3,7 +3,7 @@
3
3
  const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
4
4
  'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
5
5
  'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
6
- 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'sync-checkout',
6
+ 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'sync-checkout', 'zen',
7
7
  'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
8
8
  'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi'];
9
9