atris 3.73.1 → 3.74.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/commands/member.js +141 -7
- package/commands/mission.js +156 -65
- package/commands/slop.js +8 -2
- package/lib/falsifier-probe.js +2 -1
- package/lib/gm-bet.js +84 -25
- package/lib/gm-phone-page.js +386 -98
- package/lib/gm-phone.js +27 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/spawn-env.js +92 -3
- package/lib/task-proof.js +2 -1
- package/lib/task-receipt.js +3 -2
- package/lib/yes-lock.js +633 -81
- package/package.json +1 -1
package/lib/gm-bet.js
CHANGED
|
@@ -1991,11 +1991,21 @@ const MISSION_STOP = 'every team step is done with proof, and every your-yes ste
|
|
|
1991
1991
|
|
|
1992
1992
|
// The bet's own verify is the check; a bet made before verify existed keeps
|
|
1993
1993
|
// the old formatting check.
|
|
1994
|
-
function missionStartArgs(bet, { owner, lane }, betId = null) {
|
|
1994
|
+
function missionStartArgs(bet, { owner, lane }, betId = null, workspaceRoot = process.cwd()) {
|
|
1995
1995
|
// the owner steps are sealed into the mission record at start
|
|
1996
1996
|
const seal = require('./yes-lock').sealFor(bet.steps);
|
|
1997
1997
|
const time = jobTime(bet);
|
|
1998
|
-
|
|
1998
|
+
// --worktree: the job runs, and its check runs, in its own copy of the
|
|
1999
|
+
// code. Mission #266 ran in the shared main folder, which sat on an old
|
|
2000
|
+
// branch, so its check could not see the work it had merged. A folder with
|
|
2001
|
+
// no git has no master to protect and no copy to make, so the job runs in
|
|
2002
|
+
// place there, as it always did.
|
|
2003
|
+
const worktree = inGitCheckout(workspaceRoot) ? ['--worktree'] : [];
|
|
2004
|
+
return ['mission', 'start', missionObjective(bet, { owner, betId }), '--owner', owner, '--runner', 'claude', '--lane', lane, '--verify', bet.verify || MISSION_VERIFY, '--stop', MISSION_STOP, '--max-wall', String(time.seconds), '--max-ticks', String(time.ticks), '--stop-when-done', ...worktree, ...(seal.length ? ['--yes-lock', JSON.stringify(seal)] : []), '--json'];
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function inGitCheckout(workspaceRoot) {
|
|
2008
|
+
return String(gitOut(workspaceRoot, ['rev-parse', '--is-inside-work-tree']) || '').trim() === 'true';
|
|
1999
2009
|
}
|
|
2000
2010
|
|
|
2001
2011
|
// the run gets the same time as the start, so the detached driver never
|
|
@@ -2041,10 +2051,22 @@ const STALL_MS = 2 * 60 * 60 * 1000;
|
|
|
2041
2051
|
|
|
2042
2052
|
// Every mission in this workspace, read-only, through the same reader the
|
|
2043
2053
|
// mission command uses (it only reads .atris/state/missions.jsonl).
|
|
2044
|
-
|
|
2045
|
-
|
|
2054
|
+
// Every mission the desk follows: the main folder's, plus those in worktrees
|
|
2055
|
+
// ("do it" missions run in their own), newest record winning.
|
|
2056
|
+
function readDeskMissions(workspaceRoot) {
|
|
2057
|
+
const missionCmd = require(path.join(__dirname, '..', 'commands', 'mission.js'));
|
|
2058
|
+
const map = new Map(missionCmd.loadMissionMap(workspaceRoot));
|
|
2059
|
+
const at = m => Date.parse((m && (m.updated_at || m.last_tick_at || m.created_at)) || '') || 0;
|
|
2060
|
+
for (const rolled of missionCmd.listWorktreeRollupMissions(workspaceRoot)) {
|
|
2061
|
+
if (!rolled || !rolled.id) continue;
|
|
2062
|
+
const known = map.get(rolled.id);
|
|
2063
|
+
if (!known || at(rolled) >= at(known)) map.set(rolled.id, rolled);
|
|
2064
|
+
}
|
|
2065
|
+
return map;
|
|
2046
2066
|
}
|
|
2047
2067
|
|
|
2068
|
+
const defaultReadMissions = readDeskMissions;
|
|
2069
|
+
|
|
2048
2070
|
// A mission's last receipt, read-only. Null when there is none.
|
|
2049
2071
|
function defaultReadReceipt(mission, workspaceRoot) {
|
|
2050
2072
|
if (!mission || !mission.receipt_path) return null;
|
|
@@ -2223,9 +2245,23 @@ function defaultReadHandoffs(workspaceRoot) {
|
|
|
2223
2245
|
}
|
|
2224
2246
|
|
|
2225
2247
|
function currentMember(betId, owner, handoffs) {
|
|
2248
|
+
const hit = latestHandoff(betId, handoffs);
|
|
2249
|
+
return hit ? hit.to : owner;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
function latestHandoff(betId, handoffs) {
|
|
2226
2253
|
const tag = handoffTag(betId);
|
|
2227
2254
|
const hits = handoffs.filter(h => h && typeof h.summary === 'string' && h.summary.includes(tag) && h.to);
|
|
2228
|
-
return hits.length ? hits[hits.length - 1]
|
|
2255
|
+
return hits.length ? hits[hits.length - 1] : null;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// When the newest handoff for a job could not start its member (the team was
|
|
2259
|
+
// at its limit, no engine, asleep), say that instead of "<member> has it".
|
|
2260
|
+
function handoffWaiting(betId, handoffs) {
|
|
2261
|
+
const hit = latestHandoff(betId, handoffs);
|
|
2262
|
+
const wake = hit && hit.wake;
|
|
2263
|
+
if (!wake || wake.status !== 'not_started') return null;
|
|
2264
|
+
return clip(String(wake.text || `${hit.to} did not start`).split('. to start it:')[0], 160);
|
|
2229
2265
|
}
|
|
2230
2266
|
|
|
2231
2267
|
// a line that ends like a sentence
|
|
@@ -2286,8 +2322,8 @@ function queuedProblem(bet, team) {
|
|
|
2286
2322
|
|
|
2287
2323
|
// ---- the desk: a queue of ready bets, one shown at a time --------------------------------
|
|
2288
2324
|
|
|
2325
|
+
// ideas kept ready ahead; one is written again the moment one is shown
|
|
2289
2326
|
const QUEUE_TARGET = 3;
|
|
2290
|
-
const REFILL_BELOW = 2;
|
|
2291
2327
|
// one generation at a time; the cap of 3 calls in flight per session is never reached
|
|
2292
2328
|
const MAX_IN_FLIGHT = 1;
|
|
2293
2329
|
const SESSION_IN_FLIGHT_CAP = 3;
|
|
@@ -2348,7 +2384,6 @@ function createBetDesk(deps) {
|
|
|
2348
2384
|
gen: null,
|
|
2349
2385
|
// a sharper idea asked for, made in the background like any other
|
|
2350
2386
|
pendingSharper: null,
|
|
2351
|
-
topping: false,
|
|
2352
2387
|
failures: 0,
|
|
2353
2388
|
backoffUntil: 0,
|
|
2354
2389
|
retry: null,
|
|
@@ -2490,7 +2525,7 @@ function createBetDesk(deps) {
|
|
|
2490
2525
|
// still because the guard holds a step for the owner's yes: that is a yes card, not a halt
|
|
2491
2526
|
const held = job.stalled ? require('./yes-lock').askedYesLocks(lockDir, mission.id) : [];
|
|
2492
2527
|
if (held.length) {
|
|
2493
|
-
for (const lock of held) yes.push({ ...job, kind: 'yes', ask: lock.text, command: lock.held_command || null, lock_step: lock.step, ask_index: null, key: `${mission.id}:step-${lock.step}` });
|
|
2528
|
+
for (const lock of held) yes.push({ ...job, kind: 'yes', ask: lock.text, command: lock.held_command || null, lock_step: lock.step, lock_hash: lock.hash || null, ask_index: null, key: `${mission.id}:step-${lock.step}:${lock.hash || ''}` });
|
|
2494
2529
|
continue;
|
|
2495
2530
|
}
|
|
2496
2531
|
// finished with nothing failed: the roster's reviewer looks before it counts as done
|
|
@@ -2515,15 +2550,24 @@ function createBetDesk(deps) {
|
|
|
2515
2550
|
});
|
|
2516
2551
|
log({ card: 'bet', choice: 'job_finished', command: null, ran: 'in process', exit_code: job.failed ? 1 : 0, bet_id: row.id, mission_id: mission.id });
|
|
2517
2552
|
} else if (job.kind === 'working') {
|
|
2518
|
-
|
|
2553
|
+
const waiting = handoffWaiting(row.id, handoffs());
|
|
2554
|
+
working.push({ ...job, owner: currentMember(row.id, job.owner, handoffs()), ...(waiting ? { waiting } : {}) });
|
|
2519
2555
|
// a step the job tried and the guard held is a yes card
|
|
2520
2556
|
for (const lock of require('./yes-lock').askedYesLocks(lockDir, mission.id)) {
|
|
2521
|
-
yes.push({ ...job, kind: 'yes', ask: lock.text, command: lock.held_command || null, lock_step: lock.step, ask_index: null, key: `${mission.id}:step-${lock.step}` });
|
|
2557
|
+
yes.push({ ...job, kind: 'yes', ask: lock.text, command: lock.held_command || null, lock_step: lock.step, lock_hash: lock.hash || null, ask_index: null, key: `${mission.id}:step-${lock.step}:${lock.hash || ''}` });
|
|
2522
2558
|
}
|
|
2523
2559
|
} else if (job.kind === 'yes') {
|
|
2524
2560
|
for (const ask of job.asks) yes.push({ ...job, ask_index: ask.index, ask: ask.text, key: `${job.mission_id}:${ask.index}` });
|
|
2525
2561
|
}
|
|
2526
2562
|
}
|
|
2563
|
+
// a run woken by a handoff, held from a command: the card shows on the
|
|
2564
|
+
// job it was handed off from when that job is on the desk, else on its
|
|
2565
|
+
// own; the yes always goes to the run that was held
|
|
2566
|
+
for (const card of require('./yes-lock').handoffYesCards(lockDir)) {
|
|
2567
|
+
const parent = [...working, ...yes].find(j => j.mission_id && j.mission_id === card.parent_mission_id) || null;
|
|
2568
|
+
const where = parent ? { bet_id: parent.bet_id, title: parent.title, owner: parent.owner } : { bet_id: null, title: 'a handed-off job', owner: 'the handed-off job' };
|
|
2569
|
+
yes.push({ kind: 'yes', ...where, mission_id: card.mission_id, ask: card.ask, command: card.command, lock_step: card.step, lock_hash: card.hash || null, ask_index: null, key: `${card.mission_id}:step-${card.step}:${card.hash || ''}` });
|
|
2570
|
+
}
|
|
2527
2571
|
const expectOf = new Map(readLedger(deps.ledgerFile, deps.workspaceRoot).map(r => [r.id, r.bet && r.bet.expect]));
|
|
2528
2572
|
state.jobs = { working, yes: yes.map(y => ({ ...y, expect: expectOf.get(y.bet_id) || null })), at: now() };
|
|
2529
2573
|
retryFailed();
|
|
@@ -2793,8 +2837,9 @@ function createBetDesk(deps) {
|
|
|
2793
2837
|
return { ok: true, ...view() };
|
|
2794
2838
|
}
|
|
2795
2839
|
|
|
2796
|
-
// nothing ready and nothing left to ask: one
|
|
2797
|
-
|
|
2840
|
+
// nothing ready and nothing left to ask: one calm line, shown at once; the
|
|
2841
|
+
// page brings the idea in once it is written
|
|
2842
|
+
const WAIT_CARD = { what: 'writing your next idea.' };
|
|
2798
2843
|
function showWait() {
|
|
2799
2844
|
state.current = { kind: 'wait', id: `wait:${now()}` };
|
|
2800
2845
|
state.status = 'ready';
|
|
@@ -2884,13 +2929,16 @@ function createBetDesk(deps) {
|
|
|
2884
2929
|
// the sharper or reworked idea the owner asked for is the next shown
|
|
2885
2930
|
if (sharperOf || ownerNote) state.queue.unshift(item);
|
|
2886
2931
|
else state.queue.push(item);
|
|
2932
|
+
// the calm line stays the card on screen; the page sees the idea in
|
|
2933
|
+
// the queue and asks for it when the owner is not typing there
|
|
2887
2934
|
return pump();
|
|
2888
2935
|
});
|
|
2889
2936
|
return gen;
|
|
2890
2937
|
}
|
|
2891
2938
|
|
|
2892
|
-
// Keeps the queue
|
|
2893
|
-
// time. A sharper idea the
|
|
2939
|
+
// Keeps the queue full: below 3 the next one is written, so showing one
|
|
2940
|
+
// starts the next at once, one generation at a time. A sharper idea the
|
|
2941
|
+
// owner asked for goes first.
|
|
2894
2942
|
function pump() {
|
|
2895
2943
|
pumpQuestions();
|
|
2896
2944
|
pumpSuggestions();
|
|
@@ -2902,9 +2950,7 @@ function createBetDesk(deps) {
|
|
|
2902
2950
|
const gen = startGeneration(asked);
|
|
2903
2951
|
return gen && gen.done;
|
|
2904
2952
|
}
|
|
2905
|
-
if (state.queue.length
|
|
2906
|
-
if (state.queue.length >= QUEUE_TARGET) state.topping = false;
|
|
2907
|
-
if (!state.topping) return null;
|
|
2953
|
+
if (state.queue.length >= QUEUE_TARGET) return null;
|
|
2908
2954
|
if (state.failures >= MAX_BACKGROUND_FAILURES) {
|
|
2909
2955
|
if (now() < state.backoffUntil) return null;
|
|
2910
2956
|
state.failures = 0;
|
|
@@ -2970,7 +3016,7 @@ function createBetDesk(deps) {
|
|
|
2970
3016
|
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to start' };
|
|
2971
3017
|
const { id, bet } = state.current;
|
|
2972
3018
|
const pick = pickOwner(bet, deps.workspaceRoot);
|
|
2973
|
-
const startArgs = missionStartArgs(bet, pick, id);
|
|
3019
|
+
const startArgs = missionStartArgs(bet, pick, id, deps.workspaceRoot);
|
|
2974
3020
|
state.status = 'starting';
|
|
2975
3021
|
const display = args => require('./gm-phone').displayCommand(args);
|
|
2976
3022
|
let res = await deps.runCli(startArgs);
|
|
@@ -3015,10 +3061,13 @@ function createBetDesk(deps) {
|
|
|
3015
3061
|
}
|
|
3016
3062
|
|
|
3017
3063
|
// an answer to the question on screen is saved to the focus file, where
|
|
3018
|
-
// the writer reads it for every bet after this one
|
|
3019
|
-
|
|
3064
|
+
// the writer reads it for every bet after this one. card: the id of the
|
|
3065
|
+
// card the page shows; a page that missed the next card names the old one,
|
|
3066
|
+
// and its answer is saved against nothing (answer ids repeat across questions)
|
|
3067
|
+
function answer({ choice, text, card } = {}) {
|
|
3020
3068
|
const item = onJob('question');
|
|
3021
3069
|
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen' };
|
|
3070
|
+
if (card !== undefined && card !== item.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was saved. tap the orb for the new one' };
|
|
3022
3071
|
const picked = item.question.choices.find(c => c.id === choice);
|
|
3023
3072
|
if (!picked) return { ok: false, reason: 'bad_choice', message: 'that is not one of the answers' };
|
|
3024
3073
|
const q = item.question;
|
|
@@ -3062,7 +3111,7 @@ function createBetDesk(deps) {
|
|
|
3062
3111
|
const again = { ...row.bet, why_now: `the last run ${why}. try again. ${row.bet.why_now}` };
|
|
3063
3112
|
const pick = pickOwner(again, deps.workspaceRoot);
|
|
3064
3113
|
const display = args => require('./gm-phone').displayCommand(args);
|
|
3065
|
-
const startArgs = missionStartArgs(again, pick, row.id);
|
|
3114
|
+
const startArgs = missionStartArgs(again, pick, row.id, deps.workspaceRoot);
|
|
3066
3115
|
let res = await deps.runCli(startArgs);
|
|
3067
3116
|
log({ card: 'bet', choice: 'send_back', command: display(startArgs), exit_code: res.code, bet_id: row.id });
|
|
3068
3117
|
const started = parseJsonOut(res.stdout);
|
|
@@ -3084,12 +3133,19 @@ function createBetDesk(deps) {
|
|
|
3084
3133
|
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen' };
|
|
3085
3134
|
const { job } = item;
|
|
3086
3135
|
if (job.lock_step) {
|
|
3087
|
-
|
|
3136
|
+
// the yes releases only the command on this card (its hash), once
|
|
3137
|
+
const approved = require('./yes-lock').approveYesLock(lockDir, job.mission_id, job.lock_step, null, iso(), { hash: job.lock_hash || null });
|
|
3138
|
+
if (job.lock_hash && !approved) {
|
|
3139
|
+
state.seenYes.add(job.key);
|
|
3140
|
+
state.status = 'cleared';
|
|
3141
|
+
pollJobs();
|
|
3142
|
+
return { ok: true, ack: 'that request was already answered.' };
|
|
3143
|
+
}
|
|
3088
3144
|
const args = ['mission', 'ping', job.mission_id, `the owner said yes to step ${job.lock_step}: ${job.ask}. the guard lets it through now; run it again.`, '--from', 'gm-phone'];
|
|
3089
3145
|
const res = await deps.runCli(args);
|
|
3090
3146
|
log({ card: 'bet', choice: 'yes', command: require('./gm-phone').displayCommand(args), exit_code: res.code, bet_id: job.bet_id, mission_id: job.mission_id, step: job.lock_step });
|
|
3091
3147
|
state.seenYes.add(job.key);
|
|
3092
|
-
record(job.bet_id, { yes_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3148
|
+
if (job.bet_id) record(job.bet_id, { yes_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3093
3149
|
state.status = 'cleared';
|
|
3094
3150
|
pollJobs();
|
|
3095
3151
|
return { ok: true, ack: `${job.owner}: got your yes` };
|
|
@@ -3180,7 +3236,7 @@ function createBetDesk(deps) {
|
|
|
3180
3236
|
} : null,
|
|
3181
3237
|
// each option: its title, and one plain line of why, the result it expects
|
|
3182
3238
|
choice: kind === 'choice' ? { options: cur.items.map(i => ({ id: i.id, title: i.bet.title, why: choiceWhy(i.bet.expect) })) } : null,
|
|
3183
|
-
working: state.jobs.working.map(j => ({ title: j.title, owner: j.owner, ...(j.left ? { left: j.left } : {}) })),
|
|
3239
|
+
working: state.jobs.working.map(j => ({ title: j.title, owner: j.owner, ...(j.left ? { left: j.left } : {}), ...(j.waiting ? { waiting: j.waiting } : {}) })),
|
|
3184
3240
|
// the conversation on this card so far
|
|
3185
3241
|
thread: cur && state.thread.id === cur.id ? state.thread.turns.slice() : [],
|
|
3186
3242
|
// the footer count: only members really working
|
|
@@ -3251,12 +3307,14 @@ function createBetDesk(deps) {
|
|
|
3251
3307
|
// On a question it is the answer; on an idea, a note the writer reworks it
|
|
3252
3308
|
// with (the rework is the next idea); on a choice or the plain card, the
|
|
3253
3309
|
// next idea fits it; on a yes or a finished job, a word to that member.
|
|
3254
|
-
async function say({ text, confirm = false, note = false } = {}) {
|
|
3310
|
+
async function say({ text, confirm = false, note = false, card } = {}) {
|
|
3255
3311
|
const said = String(text || '').trim();
|
|
3256
3312
|
if (!said) return { ok: false, reason: 'empty', message: 'say something first' };
|
|
3257
3313
|
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters` };
|
|
3258
3314
|
const cur = state.status === 'ready' ? state.current : null;
|
|
3259
3315
|
if (!cur) return { ok: false, reason: 'gone', message: 'there is no card on screen' };
|
|
3316
|
+
// words said on a card the page no longer shows act on nothing
|
|
3317
|
+
if (card !== undefined && card !== cur.id) return { ok: false, reason: 'gone', message: 'that card changed, so nothing was done. tap the orb for the new one' };
|
|
3260
3318
|
const kind = cur.kind || 'bet';
|
|
3261
3319
|
log({ card: 'bet', choice: 'say', command: null, ran: 'in process', exit_code: 0, kind, chars: said.length });
|
|
3262
3320
|
const t = threadFor(cur.id);
|
|
@@ -3458,6 +3516,7 @@ module.exports = {
|
|
|
3458
3516
|
scoreboardPath,
|
|
3459
3517
|
choiceWhy,
|
|
3460
3518
|
JOB_POLL_MS,
|
|
3519
|
+
readDeskMissions,
|
|
3461
3520
|
OWNER_BANNED,
|
|
3462
3521
|
jobTime,
|
|
3463
3522
|
generateQuestion,
|