atris 3.74.0 → 3.74.2
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 +9 -10
- package/commands/avail.js +36 -7
- package/commands/clean.js +2 -1
- package/commands/close.js +7 -4
- package/commands/computer.js +3 -1
- package/commands/console.js +1 -1
- package/commands/context-sync.js +1 -60
- package/commands/experiments.js +4 -1
- package/commands/fleet.js +0 -1
- package/commands/improve.js +3 -1
- package/commands/interview.js +13 -5
- package/commands/land.js +5 -1
- package/commands/lifecycle.js +17 -6
- package/commands/log-sync.js +1 -1
- package/commands/loops.js +5 -2
- package/commands/member.js +17 -11
- package/commands/mission.js +1 -4
- package/commands/now.js +0 -7
- package/commands/play.js +3 -1
- package/commands/plugin.js +1 -1
- package/commands/pull.js +5 -3
- 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/security-review.js +4 -1
- package/commands/setup.js +6 -2
- package/commands/soul.js +2 -1
- package/commands/sync.js +8 -2
- package/commands/task.js +0 -522
- package/commands/terminal.js +1 -1
- package/commands/x-search.js +5 -3
- package/commands/youtube.js +9 -7
- package/commands/zen.js +4 -4
- package/lib/agent-today.js +412 -0
- package/lib/apply-gate.js +5 -3
- 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/experiments/daily.js +2 -1
- package/lib/file-ops.js +6 -1
- package/lib/first-minute.js +2 -0
- package/lib/gm-bet.js +73 -9
- package/lib/gm-phone-page.js +42 -5
- package/lib/gm-phone.js +3 -0
- package/lib/local-day.js +10 -0
- package/lib/manifest.js +0 -10
- package/lib/member-switches.js +0 -11
- package/lib/review-integrity.js +0 -4
- package/lib/scorecard.js +7 -6
- package/lib/wiki.js +4 -1
- 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
|
}
|
|
@@ -314,6 +316,7 @@ function buildQuestionPrompt(bundle, context, { answers = [], avoid = [] } = {})
|
|
|
314
316
|
`- fact: the plain reason this question matters now, one sentence he would say himself, drawn from the files below, with a number or at least four words in a row copied from them, under ${QUESTION_FACT_MAX} characters. it must lead straight to the question. if nothing in the files gives a reason for this question, set fact to "".`,
|
|
315
317
|
`- choices: 2 or 3 short concrete answers he can tap, each under ${QUESTION_CHOICE_MAX} characters.`,
|
|
316
318
|
`- never use these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
|
|
319
|
+
'- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
|
|
317
320
|
'- a section marked stale is old: prefer the fresh ones, and never present a stale line as what is happening now.',
|
|
318
321
|
'- ask about the product and the people who use it or pay for it, for example "who do you most want using this?". never mention ideas, the team, or what happens to his answer.',
|
|
319
322
|
'- plain lowercase-friendly words, no hype, no em dashes.',
|
|
@@ -344,10 +347,31 @@ function buildQuestionRewrite(previous, reasons) {
|
|
|
344
347
|
].join('\n');
|
|
345
348
|
}
|
|
346
349
|
|
|
350
|
+
// The owner's first name, read at run time from deps or the workspace's git
|
|
351
|
+
// user, so a label a writer wrote about him by name reads to him as "you".
|
|
352
|
+
// Null when there is none to read, and then nothing is swapped.
|
|
353
|
+
function ownerFirstName(workspaceRoot, deps = {}) {
|
|
354
|
+
if (deps.ownerName !== undefined) return deps.ownerName || null;
|
|
355
|
+
try {
|
|
356
|
+
const name = require('child_process').execFileSync('git', ['config', 'user.name'], { cwd: workspaceRoot, timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim().split(/\s+/)[0];
|
|
357
|
+
return /^[A-Za-z][A-Za-z'-]{1,}$/.test(name) ? name : null;
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function toYou(label, name) {
|
|
364
|
+
if (!name || typeof label !== 'string') return label;
|
|
365
|
+
const esc = name.replace(/[-']/g, m => `\\${m}`);
|
|
366
|
+
return label.replace(new RegExp(`\\b${esc}'s\\b`, 'gi'), 'your').replace(new RegExp(`\\b${esc}\\b`, 'gi'), 'you');
|
|
367
|
+
}
|
|
368
|
+
|
|
347
369
|
// ---- reading free text: is it a question back, an answer, an instruction? ----
|
|
348
370
|
|
|
349
371
|
const SAY_READ_MS = 15000;
|
|
350
372
|
const SAY_KINDS = new Set(['question', 'answer', 'instruction', 'unclear']);
|
|
373
|
+
// on the weekly number card, what an instruction wants done with the number
|
|
374
|
+
const NUMBER_INTENTS = new Set(['stop', 'change', 'later', 'other']);
|
|
351
375
|
const SAY_FALLBACK = 'say that another way, or tap a button.';
|
|
352
376
|
// the last turns of a card's conversation the reader sees
|
|
353
377
|
const THREAD_TURNS = 6;
|
|
@@ -379,8 +403,10 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
379
403
|
'- instruction: he wants something done, or has a new idea. reply: one plain sentence saying what will happen next.',
|
|
380
404
|
'- unclear: you cannot tell. reply: one short question back.',
|
|
381
405
|
'- a message that ends with "?" or asks why, what, or how is a question back, never an answer.',
|
|
382
|
-
'- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters.',
|
|
406
|
+
'- you may add "suggestions": 0 to 3 short replies he could tap next, each under 40 characters, never one of the buttons already on the card.',
|
|
407
|
+
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
408
|
`- plain lowercase-friendly words, no em dashes, and never these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
|
|
409
|
+
'- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
|
|
384
410
|
'',
|
|
385
411
|
`## the card on screen (${card.kind})`,
|
|
386
412
|
card.title || '',
|
|
@@ -404,7 +430,8 @@ async function readSaid({ text, card, thread = [], hint = null, workspaceRoot, b
|
|
|
404
430
|
const v = parsed.bet;
|
|
405
431
|
if (!v || !SAY_KINDS.has(v.kind) || typeof v.reply !== 'string') return { ok: false, reason: 'the read was not the shape asked for' };
|
|
406
432
|
const suggestions = Array.isArray(v.suggestions) ? v.suggestions.filter(x => typeof x === 'string' && x.trim()).map(x => x.trim()) : [];
|
|
407
|
-
|
|
433
|
+
const intent = card.kind === 'number' && NUMBER_INTENTS.has(v.intent) ? v.intent : null;
|
|
434
|
+
return { ok: true, kind: v.kind, reply: clip(v.reply, 280), ...(intent ? { intent } : {}), ...(suggestions.length ? { suggestions } : {}) };
|
|
408
435
|
}
|
|
409
436
|
|
|
410
437
|
// Two or three numbers that would tell the owner this project is going well,
|
|
@@ -418,6 +445,7 @@ async function generateScoreboardChoices({ workspaceRoot, bundle = null, deps =
|
|
|
418
445
|
'Answer with strict JSON only, no prose and no code fence: {"choices": [{"name": "...", "up": true}]}',
|
|
419
446
|
'- name: a short plain phrase under 40 characters. up: true when higher is better, false when lower is better.',
|
|
420
447
|
`- never use these words: ${OWNER_BANNED_WORDS.join(', ')}.`,
|
|
448
|
+
'- talk to him as "you", for example "changes you redid". never use his name, and never say "the owner".',
|
|
421
449
|
'',
|
|
422
450
|
"## this project's files",
|
|
423
451
|
context || '(no readme or goals found)',
|
|
@@ -2702,17 +2730,18 @@ function createBetDesk(deps) {
|
|
|
2702
2730
|
const board = readScoreboard(scoreboardFile, deps.workspaceRoot);
|
|
2703
2731
|
if (!board) {
|
|
2704
2732
|
if (state.passedQuestions.has('scoreboard')) return null;
|
|
2705
|
-
const list = (state.suggestions || fallbackNumbers()).slice(0, 3);
|
|
2733
|
+
const list = (state.suggestions || fallbackNumbers()).slice(0, 3).map(c => ({ ...c, name: you(c.name) }));
|
|
2706
2734
|
return {
|
|
2707
2735
|
kind: 'question',
|
|
2708
2736
|
id: 'question:scoreboard',
|
|
2709
2737
|
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 },
|
|
2710
2738
|
};
|
|
2711
2739
|
}
|
|
2712
|
-
|
|
2740
|
+
const today = dayKey(now());
|
|
2741
|
+
// passed over today (not now, or a tap past it): asked again tomorrow
|
|
2742
|
+
if (board.source !== 'reported' || state.passedQuestions.has(`report:${today}`)) return null;
|
|
2713
2743
|
const last = board.reports[board.reports.length - 1] || null;
|
|
2714
2744
|
const weekly = !last || now() - (Date.parse(last.at) || 0) >= WEEK_MS;
|
|
2715
|
-
const today = dayKey(now());
|
|
2716
2745
|
const waiting = readLedger(deps.ledgerFile, deps.workspaceRoot).some(r => r.status === 'doing' && r.bet && r.bet.target && r.bet.target.metric === 'scoreboard'
|
|
2717
2746
|
&& String(r.bet.check_date || '') < today && !board.reports.some(x => String(x.at).slice(0, 10) >= r.bet.check_date));
|
|
2718
2747
|
if (!weekly && !waiting) return null;
|
|
@@ -2805,7 +2834,7 @@ function createBetDesk(deps) {
|
|
|
2805
2834
|
const row = readyQuestions().find(r => !state.passedQuestions.has(r.id));
|
|
2806
2835
|
if (row) {
|
|
2807
2836
|
const q = row.question;
|
|
2808
|
-
return { kind: 'question', id: `question:${row.id}`, question: { key: row.id, ask: q.ask, line: q.fact, choices: q.choices.map((label, i) => ({ id: `a${i + 1}`, label })) } };
|
|
2837
|
+
return { kind: 'question', id: `question:${row.id}`, question: { key: row.id, ask: q.ask, line: q.fact, choices: q.choices.map((label, i) => ({ id: `a${i + 1}`, label: you(label) })) } };
|
|
2809
2838
|
}
|
|
2810
2839
|
const F = FALLBACK_QUESTION;
|
|
2811
2840
|
const said = readFocus(focusFile, deps.workspaceRoot)[F.key];
|
|
@@ -2860,7 +2889,7 @@ function createBetDesk(deps) {
|
|
|
2860
2889
|
// a re-tap on a job card moves on: a yes waits for a later session, a done is seen
|
|
2861
2890
|
function passJob(item) {
|
|
2862
2891
|
if (item.kind === 'wait') return;
|
|
2863
|
-
if (item.kind === 'report') { state.passedQuestions.add(
|
|
2892
|
+
if (item.kind === 'report') { state.passedQuestions.add(item.id); return; }
|
|
2864
2893
|
if (item.kind === 'choice') {
|
|
2865
2894
|
for (const i of item.items) record(i.id, { status: 'passed', note: 'not picked' });
|
|
2866
2895
|
log({ card: 'bet', choice: 'pass', command: null, ran: 'in process', exit_code: 0, kind: 'choice' });
|
|
@@ -3259,6 +3288,9 @@ function createBetDesk(deps) {
|
|
|
3259
3288
|
t.turns = [...t.turns, ...turns].slice(-THREAD_TURNS);
|
|
3260
3289
|
}
|
|
3261
3290
|
const plainReply = text => (text && !ownerWord(text) ? text : null);
|
|
3291
|
+
let ownerName;
|
|
3292
|
+
const you = label => toYou(label, ownerName === undefined ? (ownerName = ownerFirstName(deps.workspaceRoot, deps)) : ownerName);
|
|
3293
|
+
const sameLabel = (a, b) => String(a).trim().toLowerCase() === String(b).trim().toLowerCase();
|
|
3262
3294
|
const plainSuggestions = list => (Array.isArray(list) ? list : [])
|
|
3263
3295
|
.filter(x => typeof x === 'string' && x.trim() && x.length <= 40 && !ownerWord(x) && !PROCESS_TALK.test(x))
|
|
3264
3296
|
.slice(0, 3);
|
|
@@ -3279,7 +3311,7 @@ function createBetDesk(deps) {
|
|
|
3279
3311
|
if (v.kind === 'question') return { kind: 'question', title: v.question.ask, detail: v.question.line, buttons: v.question.choices.map(c => c.label) };
|
|
3280
3312
|
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'] };
|
|
3281
3313
|
if (v.kind === 'choice') return { kind: 'choice', title: 'which one next?', detail: v.choice.options.map(o => o.title).join(' / '), buttons: [] };
|
|
3282
|
-
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: [] };
|
|
3314
|
+
if (v.kind === 'report') return { kind: 'number', title: v.report.what, detail: v.report.why, buttons: ['save', 'not now'] };
|
|
3283
3315
|
return { kind: v.kind, title: v.job ? v.job.what : '', detail: v.job ? v.job.why : '', buttons: [] };
|
|
3284
3316
|
}
|
|
3285
3317
|
|
|
@@ -3299,7 +3331,9 @@ function createBetDesk(deps) {
|
|
|
3299
3331
|
if (read && read.ok && (read.kind === 'answer' || read.kind === 'instruction')) return { act: read };
|
|
3300
3332
|
const kind = read && read.ok ? read.kind : 'unclear';
|
|
3301
3333
|
const reply = (read && read.ok && plainReply(read.reply)) || SAY_FALLBACK;
|
|
3302
|
-
const suggestions = read && read.ok && reply !== SAY_FALLBACK
|
|
3334
|
+
const suggestions = read && read.ok && reply !== SAY_FALLBACK
|
|
3335
|
+
? plainSuggestions((Array.isArray(read.suggestions) ? read.suggestions : []).map(you).filter(sug => !(card.buttons || []).some(b => sameLabel(b, sug))))
|
|
3336
|
+
: [];
|
|
3303
3337
|
addTurns(t, { who: 'owner', text }, { who: 'orb', text: reply });
|
|
3304
3338
|
return { stay: { ok: true, stay: true, kind: reply === SAY_FALLBACK ? 'unclear' : kind, reply, ...(suggestions.length ? { suggestions } : {}), thread: t.turns.slice() } };
|
|
3305
3339
|
}
|
|
@@ -3349,6 +3383,35 @@ function createBetDesk(deps) {
|
|
|
3349
3383
|
pump();
|
|
3350
3384
|
return { ok: true, ack: plainReply(read.reply) || 'got it. the next idea will fit that.' };
|
|
3351
3385
|
}
|
|
3386
|
+
// Words on the weekly number card are about that number, and the
|
|
3387
|
+
// reader says what they want. A plain number is this week's value. Stop
|
|
3388
|
+
// tracking this, or pick a different number, stops tracking it, and the
|
|
3389
|
+
// next card asks which number, if any. Ask me tomorrow is not now. Any
|
|
3390
|
+
// other answer or instruction gets a reply on the card and changes nothing.
|
|
3391
|
+
if (kind === 'report') {
|
|
3392
|
+
const stay = (reply) => {
|
|
3393
|
+
addTurns(t, { who: 'owner', text: said }, { who: 'orb', text: reply });
|
|
3394
|
+
return { ok: true, stay: true, kind: 'unclear', reply, thread: t.turns.slice() };
|
|
3395
|
+
};
|
|
3396
|
+
if (read.kind === 'answer') {
|
|
3397
|
+
const saved = report({ value: said });
|
|
3398
|
+
if (saved.ok) { endThread(); return saved; }
|
|
3399
|
+
return stay('type this week\'s number in the box, or say stop tracking this.');
|
|
3400
|
+
}
|
|
3401
|
+
if (read.intent === 'later') {
|
|
3402
|
+
passJob(cur);
|
|
3403
|
+
state.status = 'cleared';
|
|
3404
|
+
endThread();
|
|
3405
|
+
return { ok: true, ack: 'got it. i will ask tomorrow.' };
|
|
3406
|
+
}
|
|
3407
|
+
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.');
|
|
3408
|
+
const name = cur.board.name;
|
|
3409
|
+
appendLedger(scoreboardFile, { workspace: deps.workspaceRoot, kind: 'clear', name, at: iso() });
|
|
3410
|
+
log({ card: 'bet', choice: 'scoreboard_clear', command: null, ran: 'in process', exit_code: 0 });
|
|
3411
|
+
state.status = 'cleared';
|
|
3412
|
+
endThread();
|
|
3413
|
+
return { ok: true, ack: `got it. i stopped tracking ${name}.` };
|
|
3414
|
+
}
|
|
3352
3415
|
if (kind === 'yes' || kind === 'done') {
|
|
3353
3416
|
const out = await steer({ member: cur.job.owner, text: said, confirm, note });
|
|
3354
3417
|
if (out.ok) endThread();
|
|
@@ -3513,6 +3576,7 @@ module.exports = {
|
|
|
3513
3576
|
FALLBACK_QUESTION,
|
|
3514
3577
|
readScoreboard,
|
|
3515
3578
|
readSaid,
|
|
3579
|
+
generateScoreboardChoices,
|
|
3516
3580
|
scoreboardPath,
|
|
3517
3581
|
choiceWhy,
|
|
3518
3582
|
JOB_POLL_MS,
|
package/lib/gm-phone-page.js
CHANGED
|
@@ -174,6 +174,8 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
174
174
|
/* a chosen answer holds its spot with an amber edge, and the card takes no taps */
|
|
175
175
|
.choice.chosen, .option.chosen, .field.chosen { border-color: var(--amber); background: rgba(255, 140, 66, 0.1); }
|
|
176
176
|
.chosen .send { visibility: hidden; }
|
|
177
|
+
.chosen .send.reading { visibility: visible; }
|
|
178
|
+
.bet h1.said { font-size: 20px; line-height: 1.4; font-weight: 400; letter-spacing: 0; }
|
|
177
179
|
.bet.holding { pointer-events: none; }
|
|
178
180
|
.muted { opacity: 0.35; transition: opacity 0.2s ease; }
|
|
179
181
|
|
|
@@ -752,15 +754,18 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
752
754
|
b.addEventListener('click', function () {
|
|
753
755
|
if (bet.busy) return;
|
|
754
756
|
buzz();
|
|
755
|
-
|
|
757
|
+
// typed words are read first, so a question back is explained and nothing is saved
|
|
758
|
+
if (c.other) { typeHere(b, c.label, 60, function (text, field) { say(field, { text: text }); }); return; }
|
|
756
759
|
answer(b, { choice: c.id });
|
|
757
760
|
});
|
|
761
|
+
if (c.other) b.classList.add('other');
|
|
758
762
|
act.appendChild(b);
|
|
759
763
|
});
|
|
760
764
|
// a quiet line under the answers, empty until an answer does not go through
|
|
761
765
|
act.appendChild(el('div', 'links'));
|
|
762
766
|
box.appendChild(act);
|
|
763
|
-
box
|
|
767
|
+
// its own something else box is the one place to type
|
|
768
|
+
if (!q.choices.some(function (c) { return c.other; })) box.appendChild(sayBox());
|
|
764
769
|
}
|
|
765
770
|
|
|
766
771
|
// ---- answering in place: the eyes stay in one spot ----------------------------
|
|
@@ -887,10 +892,25 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
887
892
|
return spot.sendId;
|
|
888
893
|
}
|
|
889
894
|
|
|
895
|
+
// while the words are read, one quiet ellipsis in the held spot: where
|
|
896
|
+
// send was in a box, or after a tapped reply. Nowhere else.
|
|
897
|
+
function reading(spot) {
|
|
898
|
+
var send = spot.querySelector('.send');
|
|
899
|
+
if (send) { send.textContent = '\u2026'; send.classList.add('reading'); return; }
|
|
900
|
+
spot.appendChild(el('span', 'reading', ' \u2026'));
|
|
901
|
+
}
|
|
902
|
+
function doneReading(spot) {
|
|
903
|
+
var send = spot.querySelector('.send.reading');
|
|
904
|
+
if (send) { send.textContent = 'send'; send.classList.remove('reading'); }
|
|
905
|
+
var dots = spot.querySelector('span.reading');
|
|
906
|
+
if (dots) dots.remove();
|
|
907
|
+
}
|
|
908
|
+
|
|
890
909
|
function say(spot, body) {
|
|
891
910
|
body.card = shownCard();
|
|
892
911
|
body.send = sendId(spot, body.text);
|
|
893
912
|
var heldAt = hold(spot);
|
|
913
|
+
reading(spot);
|
|
894
914
|
api('/bet/say', body).catch(offline).then(function (r) {
|
|
895
915
|
if (r.ok && !r.stay) { think(false, heldAt); return; }
|
|
896
916
|
if (!r.ok && r.reason !== 'needs_confirm') { unsent(spot, r.message || 'that did not send. tap send to try again'); return; }
|
|
@@ -903,6 +923,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
903
923
|
// the same tap) tries again. Left empty, the box turns back into the
|
|
904
924
|
// button and the line goes with it.
|
|
905
925
|
function unsent(spot, why) {
|
|
926
|
+
doneReading(spot);
|
|
906
927
|
unhold();
|
|
907
928
|
var note = spot.nextElementSibling;
|
|
908
929
|
var input = spot.querySelector('input');
|
|
@@ -939,16 +960,24 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
939
960
|
if (bet.last) { drawBetCard(bet.last); own = box.querySelector(':scope > .act, :scope > .options'); }
|
|
940
961
|
box.textContent = '';
|
|
941
962
|
bet.drawn = null;
|
|
942
|
-
|
|
963
|
+
var title = el('h1', null, r.stay ? r.reply : (r.message || 'that did not go through'));
|
|
964
|
+
box.appendChild(title);
|
|
965
|
+
// more than one line reads at the body size, in the same spot
|
|
966
|
+
var lines = document.createRange();
|
|
967
|
+
lines.selectNodeContents(title);
|
|
968
|
+
if (lines.getClientRects().length > 1) title.classList.add('said');
|
|
943
969
|
if (own) box.appendChild(own);
|
|
944
970
|
var act = el('div', 'act answers');
|
|
945
|
-
|
|
971
|
+
var have = own ? Array.prototype.map.call(own.querySelectorAll('button'), function (b) { return b.textContent.trim().toLowerCase(); }) : [];
|
|
972
|
+
if (r.stay) (r.suggestions || []).forEach(function (sug) {
|
|
973
|
+
if (have.indexOf(String(sug).trim().toLowerCase()) < 0) act.appendChild(sayChoice(sug, { text: sug }));
|
|
974
|
+
});
|
|
946
975
|
if (r.reason === 'needs_confirm') {
|
|
947
976
|
act.appendChild(sayChoice('start it', { text: said, confirm: true }));
|
|
948
977
|
act.appendChild(sayChoice('keep it as a note instead', { text: said, note: true }));
|
|
949
978
|
}
|
|
950
979
|
box.appendChild(act);
|
|
951
|
-
box.appendChild(sayBox());
|
|
980
|
+
if (!own || !own.querySelector('.other')) box.appendChild(sayBox());
|
|
952
981
|
}
|
|
953
982
|
|
|
954
983
|
// the owner's own number, once a week: one box and one tap to save
|
|
@@ -978,7 +1007,15 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
978
1007
|
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
|
|
979
1008
|
act.appendChild(input);
|
|
980
1009
|
act.appendChild(save);
|
|
1010
|
+
// a quiet way out: moves on, saves nothing, and is not asked again today
|
|
1011
|
+
var links = el('div', 'links');
|
|
1012
|
+
var later = el('button', null, 'not now');
|
|
1013
|
+
later.type = 'button';
|
|
1014
|
+
later.addEventListener('click', function () { if (bet.busy) return; buzz(); think(false, 0); });
|
|
1015
|
+
links.appendChild(later);
|
|
1016
|
+
act.appendChild(links);
|
|
981
1017
|
box.appendChild(act);
|
|
1018
|
+
box.appendChild(sayBox());
|
|
982
1019
|
}
|
|
983
1020
|
|
|
984
1021
|
// A conversation in one box. While the words are read the reply spot shows
|
package/lib/gm-phone.js
CHANGED
|
@@ -509,6 +509,9 @@ function createPhoneHandler(deps) {
|
|
|
509
509
|
async function betAction(p, body) {
|
|
510
510
|
let out;
|
|
511
511
|
if (p === '/bet/think') out = bet.next({ sharper: body.sharper === true });
|
|
512
|
+
// typed words in something else are read first, like say something: a
|
|
513
|
+
// question back or confusion is explained and nothing is saved
|
|
514
|
+
else if (p === '/bet/answer' && body.choice === 'other') out = await bet.say({ text: String(body.text || ''), card: String(body.card || '') });
|
|
512
515
|
else if (p === '/bet/answer') out = bet.answer({ choice: String(body.choice || ''), text: String(body.text || ''), card: String(body.card || '') });
|
|
513
516
|
else if (p === '/bet/report') out = bet.report({ value: body.value });
|
|
514
517
|
else if (p === '/bet/pick') out = bet.pick({ id: String(body.id || '') });
|
package/lib/local-day.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The person's own calendar day as YYYY-MM-DD. toISOString() gives the UTC
|
|
4
|
+
// day, which is already tomorrow in the evening west of Greenwich.
|
|
5
|
+
function localDay(date = new Date()) {
|
|
6
|
+
const d = date instanceof Date ? date : new Date(date);
|
|
7
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { localDay };
|
package/lib/manifest.js
CHANGED
|
@@ -2,7 +2,6 @@ const crypto = require('crypto');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { stateHome } = require('./state-home');
|
|
5
|
-
const { computeContentHash } = require('./journal');
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Get the manifest file path for a business slug.
|
|
@@ -35,14 +34,6 @@ function saveManifest(slug, manifest) {
|
|
|
35
34
|
fs.writeFileSync(p, JSON.stringify(manifest, null, 2));
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
/**
|
|
39
|
-
* Compute SHA-256 hash of normalized content.
|
|
40
|
-
* Delegates to journal.js computeContentHash.
|
|
41
|
-
*/
|
|
42
|
-
function computeFileHash(content) {
|
|
43
|
-
return computeContentHash(content);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
/**
|
|
47
38
|
* Build a manifest object from a set of files.
|
|
48
39
|
* files: { [path]: { hash, size } }
|
|
@@ -247,7 +238,6 @@ function threeWayCompare(localFiles, remoteFiles, manifest) {
|
|
|
247
238
|
module.exports = {
|
|
248
239
|
loadManifest,
|
|
249
240
|
saveManifest,
|
|
250
|
-
computeFileHash,
|
|
251
241
|
buildManifest,
|
|
252
242
|
computeLocalHashes,
|
|
253
243
|
threeWayCompare,
|
package/lib/member-switches.js
CHANGED
|
@@ -70,17 +70,6 @@ function asleepMembers(root = process.cwd()) {
|
|
|
70
70
|
return Object.keys(members).filter((name) => normalizeMemberEntry(members[name]).awake === false).sort();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function isMemberLoopAwake(name, loopId, root = process.cwd()) {
|
|
74
|
-
const entry = getMemberSwitch(name, root);
|
|
75
|
-
if (entry.awake === false) return false;
|
|
76
|
-
const id = String(loopId || '').trim();
|
|
77
|
-
if (!id) return entry.awake !== false;
|
|
78
|
-
if (Object.prototype.hasOwnProperty.call(entry.loops, id)) {
|
|
79
|
-
return entry.loops[id] !== false;
|
|
80
|
-
}
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
function ensureMemberEntry(state, name) {
|
|
85
74
|
if (!state.members[name] || typeof state.members[name] !== 'object') {
|
|
86
75
|
state.members[name] = { awake: true, loops: {} };
|
package/lib/review-integrity.js
CHANGED
|
@@ -25,10 +25,6 @@ function normalizeActor(value) {
|
|
|
25
25
|
.replace(/^-+|-+$/g, '');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function envActor() {
|
|
29
|
-
return normalizeActor(process.env.ATRIS_AGENT_ID || process.env.USER || '');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
// Builder identity, oldest signal wins: explicit stamp, then who claimed it,
|
|
33
29
|
// then whoever sent the first proof.
|
|
34
30
|
function taskBuilder(task) {
|
package/lib/scorecard.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { parseTodo } = require('./todo');
|
|
4
|
+
const { localDay } = require('./local-day');
|
|
4
5
|
|
|
5
6
|
const PRIVATE_MEMORY_ROOT = '.atris/presidio';
|
|
6
7
|
|
|
@@ -72,8 +73,8 @@ function listLogFiles(atrisDir, startDate, endDate = new Date()) {
|
|
|
72
73
|
const logsDir = path.join(atrisDir, 'logs');
|
|
73
74
|
if (!fs.existsSync(logsDir)) return [];
|
|
74
75
|
|
|
75
|
-
const startKey = startDate
|
|
76
|
-
const endKey = endDate
|
|
76
|
+
const startKey = localDay(startDate);
|
|
77
|
+
const endKey = localDay(endDate);
|
|
77
78
|
const files = [];
|
|
78
79
|
|
|
79
80
|
for (const year of fs.readdirSync(logsDir)) {
|
|
@@ -163,7 +164,7 @@ function countLessonsGenerated(atrisDir, pickedAt) {
|
|
|
163
164
|
const lessonsPath = path.join(atrisDir, 'lessons.md');
|
|
164
165
|
if (!fs.existsSync(lessonsPath)) return 0;
|
|
165
166
|
|
|
166
|
-
const startDateKey = startAt
|
|
167
|
+
const startDateKey = localDay(startAt);
|
|
167
168
|
return fs.readFileSync(lessonsPath, 'utf8')
|
|
168
169
|
.split('\n')
|
|
169
170
|
.reduce((count, line) => {
|
|
@@ -191,8 +192,8 @@ function buildScorecardData(atrisDir, { slug, pickedAt } = {}) {
|
|
|
191
192
|
|
|
192
193
|
return {
|
|
193
194
|
slug,
|
|
194
|
-
startDate: startAt
|
|
195
|
-
endDate:
|
|
195
|
+
startDate: localDay(startAt),
|
|
196
|
+
endDate: localDay(),
|
|
196
197
|
tasksShipped: Math.max(shipped, 0),
|
|
197
198
|
tasksAttempted: Math.max(attempted, shipped),
|
|
198
199
|
wallClockHours: Math.max(0, (Date.now() - startAt.getTime()) / (1000 * 60 * 60)),
|
|
@@ -221,7 +222,7 @@ function writeScorecard(atrisDir, data) {
|
|
|
221
222
|
const {
|
|
222
223
|
slug,
|
|
223
224
|
startDate,
|
|
224
|
-
endDate =
|
|
225
|
+
endDate = localDay(),
|
|
225
226
|
tasksShipped = 0,
|
|
226
227
|
tasksAttempted = 0,
|
|
227
228
|
wallClockHours = 0,
|
package/lib/wiki.js
CHANGED
|
@@ -31,8 +31,11 @@ function getWikiLinkRoot(mode = 'public') {
|
|
|
31
31
|
return mode === 'private' ? PRIVATE_WIKI_ROOT : 'atris/wiki';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Local calendar day; toISOString is UTC and files an evening entry under
|
|
35
|
+
// tomorrow anywhere west of Greenwich, next to a local clock time.
|
|
34
36
|
function today() {
|
|
35
|
-
|
|
37
|
+
const now = new Date();
|
|
38
|
+
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
function nowTime() {
|
package/package.json
CHANGED