atris 3.73.1 → 3.74.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.
- package/bin/atris.js +85 -82
- package/commands/aeo.js +2 -2
- package/commands/align.js +10 -19
- package/commands/auth.js +1 -1
- package/commands/autopilot.js +4 -8
- package/commands/avail.js +36 -7
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/fleet.js +0 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/member.js +158 -18
- package/commands/mission.js +157 -69
- package/commands/now.js +0 -7
- package/commands/plugin.js +1 -1
- package/commands/pull.js +1 -1
- package/commands/push.js +0 -1
- package/commands/radar.js +0 -4
- package/commands/recap.js +25 -1
- package/commands/report.js +0 -4
- package/commands/review.js +1 -1
- package/commands/setup.js +6 -2
- package/commands/slop.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/youtube.js +4 -6
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/autoland.js +0 -8
- package/lib/bench/runner.js +0 -4
- package/lib/chat-log-scan.js +0 -28
- package/lib/cli-scope.js +4 -2
- package/lib/engine-registry.js +0 -4
- package/lib/falsifier-probe.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +125 -30
- package/lib/gm-phone-page.js +398 -99
- package/lib/gm-phone.js +30 -3
- package/lib/handoff-wake.js +180 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/member-up.js +18 -3
- package/lib/mission-protected-lane.js +38 -6
- package/lib/review-integrity.js +0 -4
- 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
|
@@ -152,6 +152,8 @@ function readScoreboard(file, workspaceRoot) {
|
|
|
152
152
|
if (!row || row.workspace !== workspaceRoot) continue;
|
|
153
153
|
if (row.kind === 'pick' && row.name) board = { name: row.name, up: row.up !== false, source: row.source === 'installs' ? 'installs' : 'reported', at: row.at, reports: [] };
|
|
154
154
|
else if (row.kind === 'report' && board && Number.isFinite(row.value)) board.reports.push({ value: row.value, at: row.at });
|
|
155
|
+
// the owner stopped tracking it: no number until one is picked again
|
|
156
|
+
else if (row.kind === 'clear') board = null;
|
|
155
157
|
}
|
|
156
158
|
return board;
|
|
157
159
|
}
|
|
@@ -348,6 +350,8 @@ function buildQuestionRewrite(previous, reasons) {
|
|
|
348
350
|
|
|
349
351
|
const SAY_READ_MS = 15000;
|
|
350
352
|
const SAY_KINDS = new Set(['question', 'answer', 'instruction', 'unclear']);
|
|
353
|
+
// on the weekly number card, what an instruction wants done with the number
|
|
354
|
+
const NUMBER_INTENTS = new Set(['stop', 'change', 'later', 'other']);
|
|
351
355
|
const SAY_FALLBACK = 'say that another way, or tap a button.';
|
|
352
356
|
// the last turns of a card's conversation the reader sees
|
|
353
357
|
const THREAD_TURNS = 6;
|
|
@@ -380,6 +384,7 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
380
384
|
'- unclear: you cannot tell. reply: one short question back.',
|
|
381
385
|
'- a message that ends with "?" or asks why, what, or how is a question back, never an answer.',
|
|
382
386
|
'- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters.',
|
|
387
|
+
card.kind === 'number' ? '- on this number card an instruction also has "intent": "stop" when he wants to stop tracking this number, "change" when he wants a different number, "later" when he wants to be asked another time, else "other". for "other", reply says what this card can do.' : '',
|
|
383
388
|
`- plain lowercase-friendly words, no em dashes, and never these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
|
|
384
389
|
'',
|
|
385
390
|
`## the card on screen (${card.kind})`,
|
|
@@ -404,7 +409,8 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
404
409
|
const v = parsed.bet;
|
|
405
410
|
if (!v || !SAY_KINDS.has(v.kind) || typeof v.reply !== 'string') return { ok: false, reason: 'the read was not the shape asked for' };
|
|
406
411
|
const suggestions = Array.isArray(v.suggestions) ? v.suggestions.filter(x => typeof x === 'string' && x.trim()).map(x => x.trim()) : [];
|
|
407
|
-
|
|
412
|
+
const intent = card.kind === 'number' && NUMBER_INTENTS.has(v.intent) ? v.intent : null;
|
|
413
|
+
return { ok: true, kind: v.kind, reply: clip(v.reply, 280), ...(intent ? { intent } : {}), ...(suggestions.length ? { suggestions } : {}) };
|
|
408
414
|
}
|
|
409
415
|
|
|
410
416
|
// Two or three numbers that would tell the owner this project is going well,
|
|
@@ -1991,11 +1997,21 @@ const MISSION_STOP = 'every team step is done with proof, and every your-yes ste
|
|
|
1991
1997
|
|
|
1992
1998
|
// The bet's own verify is the check; a bet made before verify existed keeps
|
|
1993
1999
|
// the old formatting check.
|
|
1994
|
-
function missionStartArgs(bet, { owner, lane }, betId = null) {
|
|
2000
|
+
function missionStartArgs(bet, { owner, lane }, betId = null, workspaceRoot = process.cwd()) {
|
|
1995
2001
|
// the owner steps are sealed into the mission record at start
|
|
1996
2002
|
const seal = require('./yes-lock').sealFor(bet.steps);
|
|
1997
2003
|
const time = jobTime(bet);
|
|
1998
|
-
|
|
2004
|
+
// --worktree: the job runs, and its check runs, in its own copy of the
|
|
2005
|
+
// code. Mission #266 ran in the shared main folder, which sat on an old
|
|
2006
|
+
// branch, so its check could not see the work it had merged. A folder with
|
|
2007
|
+
// no git has no master to protect and no copy to make, so the job runs in
|
|
2008
|
+
// place there, as it always did.
|
|
2009
|
+
const worktree = inGitCheckout(workspaceRoot) ? ['--worktree'] : [];
|
|
2010
|
+
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'];
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function inGitCheckout(workspaceRoot) {
|
|
2014
|
+
return String(gitOut(workspaceRoot, ['rev-parse', '--is-inside-work-tree']) || '').trim() === 'true';
|
|
1999
2015
|
}
|
|
2000
2016
|
|
|
2001
2017
|
// the run gets the same time as the start, so the detached driver never
|
|
@@ -2041,10 +2057,22 @@ const STALL_MS = 2 * 60 * 60 * 1000;
|
|
|
2041
2057
|
|
|
2042
2058
|
// Every mission in this workspace, read-only, through the same reader the
|
|
2043
2059
|
// mission command uses (it only reads .atris/state/missions.jsonl).
|
|
2044
|
-
|
|
2045
|
-
|
|
2060
|
+
// Every mission the desk follows: the main folder's, plus those in worktrees
|
|
2061
|
+
// ("do it" missions run in their own), newest record winning.
|
|
2062
|
+
function readDeskMissions(workspaceRoot) {
|
|
2063
|
+
const missionCmd = require(path.join(__dirname, '..', 'commands', 'mission.js'));
|
|
2064
|
+
const map = new Map(missionCmd.loadMissionMap(workspaceRoot));
|
|
2065
|
+
const at = m => Date.parse((m && (m.updated_at || m.last_tick_at || m.created_at)) || '') || 0;
|
|
2066
|
+
for (const rolled of missionCmd.listWorktreeRollupMissions(workspaceRoot)) {
|
|
2067
|
+
if (!rolled || !rolled.id) continue;
|
|
2068
|
+
const known = map.get(rolled.id);
|
|
2069
|
+
if (!known || at(rolled) >= at(known)) map.set(rolled.id, rolled);
|
|
2070
|
+
}
|
|
2071
|
+
return map;
|
|
2046
2072
|
}
|
|
2047
2073
|
|
|
2074
|
+
const defaultReadMissions = readDeskMissions;
|
|
2075
|
+
|
|
2048
2076
|
// A mission's last receipt, read-only. Null when there is none.
|
|
2049
2077
|
function defaultReadReceipt(mission, workspaceRoot) {
|
|
2050
2078
|
if (!mission || !mission.receipt_path) return null;
|
|
@@ -2223,9 +2251,23 @@ function defaultReadHandoffs(workspaceRoot) {
|
|
|
2223
2251
|
}
|
|
2224
2252
|
|
|
2225
2253
|
function currentMember(betId, owner, handoffs) {
|
|
2254
|
+
const hit = latestHandoff(betId, handoffs);
|
|
2255
|
+
return hit ? hit.to : owner;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function latestHandoff(betId, handoffs) {
|
|
2226
2259
|
const tag = handoffTag(betId);
|
|
2227
2260
|
const hits = handoffs.filter(h => h && typeof h.summary === 'string' && h.summary.includes(tag) && h.to);
|
|
2228
|
-
return hits.length ? hits[hits.length - 1]
|
|
2261
|
+
return hits.length ? hits[hits.length - 1] : null;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
// When the newest handoff for a job could not start its member (the team was
|
|
2265
|
+
// at its limit, no engine, asleep), say that instead of "<member> has it".
|
|
2266
|
+
function handoffWaiting(betId, handoffs) {
|
|
2267
|
+
const hit = latestHandoff(betId, handoffs);
|
|
2268
|
+
const wake = hit && hit.wake;
|
|
2269
|
+
if (!wake || wake.status !== 'not_started') return null;
|
|
2270
|
+
return clip(String(wake.text || `${hit.to} did not start`).split('. to start it:')[0], 160);
|
|
2229
2271
|
}
|
|
2230
2272
|
|
|
2231
2273
|
// a line that ends like a sentence
|
|
@@ -2286,8 +2328,8 @@ function queuedProblem(bet, team) {
|
|
|
2286
2328
|
|
|
2287
2329
|
// ---- the desk: a queue of ready bets, one shown at a time --------------------------------
|
|
2288
2330
|
|
|
2331
|
+
// ideas kept ready ahead; one is written again the moment one is shown
|
|
2289
2332
|
const QUEUE_TARGET = 3;
|
|
2290
|
-
const REFILL_BELOW = 2;
|
|
2291
2333
|
// one generation at a time; the cap of 3 calls in flight per session is never reached
|
|
2292
2334
|
const MAX_IN_FLIGHT = 1;
|
|
2293
2335
|
const SESSION_IN_FLIGHT_CAP = 3;
|
|
@@ -2348,7 +2390,6 @@ function createBetDesk(deps) {
|
|
|
2348
2390
|
gen: null,
|
|
2349
2391
|
// a sharper idea asked for, made in the background like any other
|
|
2350
2392
|
pendingSharper: null,
|
|
2351
|
-
topping: false,
|
|
2352
2393
|
failures: 0,
|
|
2353
2394
|
backoffUntil: 0,
|
|
2354
2395
|
retry: null,
|
|
@@ -2490,7 +2531,7 @@ function createBetDesk(deps) {
|
|
|
2490
2531
|
// still because the guard holds a step for the owner's yes: that is a yes card, not a halt
|
|
2491
2532
|
const held = job.stalled ? require('./yes-lock').askedYesLocks(lockDir, mission.id) : [];
|
|
2492
2533
|
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}` });
|
|
2534
|
+
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
2535
|
continue;
|
|
2495
2536
|
}
|
|
2496
2537
|
// finished with nothing failed: the roster's reviewer looks before it counts as done
|
|
@@ -2515,15 +2556,24 @@ function createBetDesk(deps) {
|
|
|
2515
2556
|
});
|
|
2516
2557
|
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
2558
|
} else if (job.kind === 'working') {
|
|
2518
|
-
|
|
2559
|
+
const waiting = handoffWaiting(row.id, handoffs());
|
|
2560
|
+
working.push({ ...job, owner: currentMember(row.id, job.owner, handoffs()), ...(waiting ? { waiting } : {}) });
|
|
2519
2561
|
// a step the job tried and the guard held is a yes card
|
|
2520
2562
|
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}` });
|
|
2563
|
+
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
2564
|
}
|
|
2523
2565
|
} else if (job.kind === 'yes') {
|
|
2524
2566
|
for (const ask of job.asks) yes.push({ ...job, ask_index: ask.index, ask: ask.text, key: `${job.mission_id}:${ask.index}` });
|
|
2525
2567
|
}
|
|
2526
2568
|
}
|
|
2569
|
+
// a run woken by a handoff, held from a command: the card shows on the
|
|
2570
|
+
// job it was handed off from when that job is on the desk, else on its
|
|
2571
|
+
// own; the yes always goes to the run that was held
|
|
2572
|
+
for (const card of require('./yes-lock').handoffYesCards(lockDir)) {
|
|
2573
|
+
const parent = [...working, ...yes].find(j => j.mission_id && j.mission_id === card.parent_mission_id) || null;
|
|
2574
|
+
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' };
|
|
2575
|
+
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 || ''}` });
|
|
2576
|
+
}
|
|
2527
2577
|
const expectOf = new Map(readLedger(deps.ledgerFile, deps.workspaceRoot).map(r => [r.id, r.bet && r.bet.expect]));
|
|
2528
2578
|
state.jobs = { working, yes: yes.map(y => ({ ...y, expect: expectOf.get(y.bet_id) || null })), at: now() };
|
|
2529
2579
|
retryFailed();
|
|
@@ -2665,10 +2715,11 @@ function createBetDesk(deps) {
|
|
|
2665
2715
|
question: { key: 'scoreboard', ask: SCOREBOARD_ASK, line: null, choices: [...list.map((c, i) => ({ id: `s${i + 1}`, label: c.name })), { id: 'other', label: 'something else', other: true }], numbers: list },
|
|
2666
2716
|
};
|
|
2667
2717
|
}
|
|
2668
|
-
|
|
2718
|
+
const today = dayKey(now());
|
|
2719
|
+
// passed over today (not now, or a tap past it): asked again tomorrow
|
|
2720
|
+
if (board.source !== 'reported' || state.passedQuestions.has(`report:${today}`)) return null;
|
|
2669
2721
|
const last = board.reports[board.reports.length - 1] || null;
|
|
2670
2722
|
const weekly = !last || now() - (Date.parse(last.at) || 0) >= WEEK_MS;
|
|
2671
|
-
const today = dayKey(now());
|
|
2672
2723
|
const waiting = readLedger(deps.ledgerFile, deps.workspaceRoot).some(r => r.status === 'doing' && r.bet && r.bet.target && r.bet.target.metric === 'scoreboard'
|
|
2673
2724
|
&& String(r.bet.check_date || '') < today && !board.reports.some(x => String(x.at).slice(0, 10) >= r.bet.check_date));
|
|
2674
2725
|
if (!weekly && !waiting) return null;
|
|
@@ -2793,8 +2844,9 @@ function createBetDesk(deps) {
|
|
|
2793
2844
|
return { ok: true, ...view() };
|
|
2794
2845
|
}
|
|
2795
2846
|
|
|
2796
|
-
// nothing ready and nothing left to ask: one
|
|
2797
|
-
|
|
2847
|
+
// nothing ready and nothing left to ask: one calm line, shown at once; the
|
|
2848
|
+
// page brings the idea in once it is written
|
|
2849
|
+
const WAIT_CARD = { what: 'writing your next idea.' };
|
|
2798
2850
|
function showWait() {
|
|
2799
2851
|
state.current = { kind: 'wait', id: `wait:${now()}` };
|
|
2800
2852
|
state.status = 'ready';
|
|
@@ -2815,7 +2867,7 @@ function createBetDesk(deps) {
|
|
|
2815
2867
|
// a re-tap on a job card moves on: a yes waits for a later session, a done is seen
|
|
2816
2868
|
function passJob(item) {
|
|
2817
2869
|
if (item.kind === 'wait') return;
|
|
2818
|
-
if (item.kind === 'report') { state.passedQuestions.add(
|
|
2870
|
+
if (item.kind === 'report') { state.passedQuestions.add(item.id); return; }
|
|
2819
2871
|
if (item.kind === 'choice') {
|
|
2820
2872
|
for (const i of item.items) record(i.id, { status: 'passed', note: 'not picked' });
|
|
2821
2873
|
log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: 'choice' });
|
|
@@ -2884,13 +2936,16 @@ function createBetDesk(deps) {
|
|
|
2884
2936
|
// the sharper or reworked idea the owner asked for is the next shown
|
|
2885
2937
|
if (sharperOf || ownerNote) state.queue.unshift(item);
|
|
2886
2938
|
else state.queue.push(item);
|
|
2939
|
+
// the calm line stays the card on screen; the page sees the idea in
|
|
2940
|
+
// the queue and asks for it when the owner is not typing there
|
|
2887
2941
|
return pump();
|
|
2888
2942
|
});
|
|
2889
2943
|
return gen;
|
|
2890
2944
|
}
|
|
2891
2945
|
|
|
2892
|
-
// Keeps the queue
|
|
2893
|
-
// time. A sharper idea the
|
|
2946
|
+
// Keeps the queue full: below 3 the next one is written, so showing one
|
|
2947
|
+
// starts the next at once, one generation at a time. A sharper idea the
|
|
2948
|
+
// owner asked for goes first.
|
|
2894
2949
|
function pump() {
|
|
2895
2950
|
pumpQuestions();
|
|
2896
2951
|
pumpSuggestions();
|
|
@@ -2902,9 +2957,7 @@ function createBetDesk(deps) {
|
|
|
2902
2957
|
const gen = startGeneration(asked);
|
|
2903
2958
|
return gen && gen.done;
|
|
2904
2959
|
}
|
|
2905
|
-
if (state.queue.length
|
|
2906
|
-
if (state.queue.length >= QUEUE_TARGET) state.topping = false;
|
|
2907
|
-
if (!state.topping) return null;
|
|
2960
|
+
if (state.queue.length >= QUEUE_TARGET) return null;
|
|
2908
2961
|
if (state.failures >= MAX_BACKGROUND_FAILURES) {
|
|
2909
2962
|
if (now() < state.backoffUntil) return null;
|
|
2910
2963
|
state.failures = 0;
|
|
@@ -2970,7 +3023,7 @@ function createBetDesk(deps) {
|
|
|
2970
3023
|
if (!isBet()) return { ok: false, reason: 'gone', message: 'there is no idea on screen to start' };
|
|
2971
3024
|
const { id, bet } = state.current;
|
|
2972
3025
|
const pick = pickOwner(bet, deps.workspaceRoot);
|
|
2973
|
-
const startArgs = missionStartArgs(bet, pick, id);
|
|
3026
|
+
const startArgs = missionStartArgs(bet, pick, id, deps.workspaceRoot);
|
|
2974
3027
|
state.status = 'starting';
|
|
2975
3028
|
const display = args => require('./gm-phone').displayCommand(args);
|
|
2976
3029
|
let res = await deps.runCli(startArgs);
|
|
@@ -3015,10 +3068,13 @@ function createBetDesk(deps) {
|
|
|
3015
3068
|
}
|
|
3016
3069
|
|
|
3017
3070
|
// 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
|
-
|
|
3071
|
+
// the writer reads it for every bet after this one. card: the id of the
|
|
3072
|
+
// card the page shows; a page that missed the next card names the old one,
|
|
3073
|
+
// and its answer is saved against nothing (answer ids repeat across questions)
|
|
3074
|
+
function answer({ choice, text, card } = {}) {
|
|
3020
3075
|
const item = onJob('question');
|
|
3021
3076
|
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen' };
|
|
3077
|
+
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
3078
|
const picked = item.question.choices.find(c => c.id === choice);
|
|
3023
3079
|
if (!picked) return { ok: false, reason: 'bad_choice', message: 'that is not one of the answers' };
|
|
3024
3080
|
const q = item.question;
|
|
@@ -3062,7 +3118,7 @@ function createBetDesk(deps) {
|
|
|
3062
3118
|
const again = { ...row.bet, why_now: `the last run ${why}. try again. ${row.bet.why_now}` };
|
|
3063
3119
|
const pick = pickOwner(again, deps.workspaceRoot);
|
|
3064
3120
|
const display = args => require('./gm-phone').displayCommand(args);
|
|
3065
|
-
const startArgs = missionStartArgs(again, pick, row.id);
|
|
3121
|
+
const startArgs = missionStartArgs(again, pick, row.id, deps.workspaceRoot);
|
|
3066
3122
|
let res = await deps.runCli(startArgs);
|
|
3067
3123
|
log({ card: 'bet', choice: 'send_back', command: display(startArgs), exit_code: res.code, bet_id: row.id });
|
|
3068
3124
|
const started = parseJsonOut(res.stdout);
|
|
@@ -3084,12 +3140,19 @@ function createBetDesk(deps) {
|
|
|
3084
3140
|
if (!item) return { ok: false, reason: 'gone', message: 'there is no question on screen' };
|
|
3085
3141
|
const { job } = item;
|
|
3086
3142
|
if (job.lock_step) {
|
|
3087
|
-
|
|
3143
|
+
// the yes releases only the command on this card (its hash), once
|
|
3144
|
+
const approved = require('./yes-lock').approveYesLock(lockDir, job.mission_id, job.lock_step, null, iso(), { hash: job.lock_hash || null });
|
|
3145
|
+
if (job.lock_hash && !approved) {
|
|
3146
|
+
state.seenYes.add(job.key);
|
|
3147
|
+
state.status = 'cleared';
|
|
3148
|
+
pollJobs();
|
|
3149
|
+
return { ok: true, ack: 'that request was already answered.' };
|
|
3150
|
+
}
|
|
3088
3151
|
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
3152
|
const res = await deps.runCli(args);
|
|
3090
3153
|
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
3154
|
state.seenYes.add(job.key);
|
|
3092
|
-
record(job.bet_id, { yes_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3155
|
+
if (job.bet_id) record(job.bet_id, { yes_answered: { mission_id: job.mission_id, step: job.lock_step, at: iso() } });
|
|
3093
3156
|
state.status = 'cleared';
|
|
3094
3157
|
pollJobs();
|
|
3095
3158
|
return { ok: true, ack: `${job.owner}: got your yes` };
|
|
@@ -3180,7 +3243,7 @@ function createBetDesk(deps) {
|
|
|
3180
3243
|
} : null,
|
|
3181
3244
|
// each option: its title, and one plain line of why, the result it expects
|
|
3182
3245
|
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 } : {}) })),
|
|
3246
|
+
working: state.jobs.working.map(j => ({ title: j.title, owner: j.owner, ...(j.left ? { left: j.left } : {}), ...(j.waiting ? { waiting: j.waiting } : {}) })),
|
|
3184
3247
|
// the conversation on this card so far
|
|
3185
3248
|
thread: cur && state.thread.id === cur.id ? state.thread.turns.slice() : [],
|
|
3186
3249
|
// the footer count: only members really working
|
|
@@ -3223,7 +3286,7 @@ function createBetDesk(deps) {
|
|
|
3223
3286
|
if (v.kind === 'question') return { kind: 'question', title: v.question.ask, detail: v.question.line, buttons: v.question.choices.map(c => c.label) };
|
|
3224
3287
|
if (v.kind === 'bet') return { kind: 'idea', title: v.bet.title, detail: [v.bet.why_now, v.bet.expect].join(' '), buttons: ['do it', 'sharper idea', 'not now'] };
|
|
3225
3288
|
if (v.kind === 'choice') return { kind: 'choice', title: 'which one next?', detail: v.choice.options.map(o => o.title).join(' / '), buttons: [] };
|
|
3226
|
-
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: [] };
|
|
3289
|
+
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: ['save', 'not now'] };
|
|
3227
3290
|
return { kind: v.kind, title: v.job ? v.job.what : '', detail: v.job ? v.job.why : '', buttons: [] };
|
|
3228
3291
|
}
|
|
3229
3292
|
|
|
@@ -3251,12 +3314,14 @@ function createBetDesk(deps) {
|
|
|
3251
3314
|
// On a question it is the answer; on an idea, a note the writer reworks it
|
|
3252
3315
|
// with (the rework is the next idea); on a choice or the plain card, the
|
|
3253
3316
|
// 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 } = {}) {
|
|
3317
|
+
async function say({ text, confirm = false, note = false, card } = {}) {
|
|
3255
3318
|
const said = String(text || '').trim();
|
|
3256
3319
|
if (!said) return { ok: false, reason: 'empty', message: 'say something first' };
|
|
3257
3320
|
if (said.length > SAY_MAX) return { ok: false, reason: 'too_long', message: `keep it under ${SAY_MAX} characters` };
|
|
3258
3321
|
const cur = state.status === 'ready' ? state.current : null;
|
|
3259
3322
|
if (!cur) return { ok: false, reason: 'gone', message: 'there is no card on screen' };
|
|
3323
|
+
// words said on a card the page no longer shows act on nothing
|
|
3324
|
+
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
3325
|
const kind = cur.kind || 'bet';
|
|
3261
3326
|
log({ card: 'bet', choice: 'say', command: null, ran: 'in process', exit_code: 0, kind, chars: said.length });
|
|
3262
3327
|
const t = threadFor(cur.id);
|
|
@@ -3291,6 +3356,35 @@ function createBetDesk(deps) {
|
|
|
3291
3356
|
pump();
|
|
3292
3357
|
return { ok: true, ack: plainReply(read.reply) || 'got it. the next idea will fit that.' };
|
|
3293
3358
|
}
|
|
3359
|
+
// Words on the weekly number card are about that number, and the
|
|
3360
|
+
// reader says what they want. A plain number is this week's value. Stop
|
|
3361
|
+
// tracking this, or pick a different number, stops tracking it, and the
|
|
3362
|
+
// next card asks which number, if any. Ask me tomorrow is not now. Any
|
|
3363
|
+
// other answer or instruction gets a reply on the card and changes nothing.
|
|
3364
|
+
if (kind === 'report') {
|
|
3365
|
+
const stay = (reply) => {
|
|
3366
|
+
addTurns(t, { who: 'owner', text: said }, { who: 'orb', text: reply });
|
|
3367
|
+
return { ok: true, stay: true, kind: 'unclear', reply, thread: t.turns.slice() };
|
|
3368
|
+
};
|
|
3369
|
+
if (read.kind === 'answer') {
|
|
3370
|
+
const saved = report({ value: said });
|
|
3371
|
+
if (saved.ok) { endThread(); return saved; }
|
|
3372
|
+
return stay('type this week\'s number in the box, or say stop tracking this.');
|
|
3373
|
+
}
|
|
3374
|
+
if (read.intent === 'later') {
|
|
3375
|
+
passJob(cur);
|
|
3376
|
+
state.status = 'cleared';
|
|
3377
|
+
endThread();
|
|
3378
|
+
return { ok: true, ack: 'got it. i will ask tomorrow.' };
|
|
3379
|
+
}
|
|
3380
|
+
if (read.intent !== 'stop' && read.intent !== 'change') return stay(plainReply(read.reply) || 'this card keeps one number a week. say stop tracking this, or ask me tomorrow.');
|
|
3381
|
+
const name = cur.board.name;
|
|
3382
|
+
appendLedger(scoreboardFile, { workspace: deps.workspaceRoot, kind: 'clear', name, at: iso() });
|
|
3383
|
+
log({ card: 'bet', choice: 'scoreboard_clear', command: null, ran: 'in process', exit_code: 0 });
|
|
3384
|
+
state.status = 'cleared';
|
|
3385
|
+
endThread();
|
|
3386
|
+
return { ok: true, ack: `got it. i stopped tracking ${name}.` };
|
|
3387
|
+
}
|
|
3294
3388
|
if (kind === 'yes' || kind === 'done') {
|
|
3295
3389
|
const out = await steer({ member: cur.job.owner, text: said, confirm, note });
|
|
3296
3390
|
if (out.ok) endThread();
|
|
@@ -3458,6 +3552,7 @@ module.exports = {
|
|
|
3458
3552
|
scoreboardPath,
|
|
3459
3553
|
choiceWhy,
|
|
3460
3554
|
JOB_POLL_MS,
|
|
3555
|
+
readDeskMissions,
|
|
3461
3556
|
OWNER_BANNED,
|
|
3462
3557
|
jobTime,
|
|
3463
3558
|
generateQuestion,
|