atris 3.74.3 → 3.74.5
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/FOR_AGENTS.md +12 -12
- package/atris/AGENTS.md +7 -7
- package/atris/CLAUDE.md +10 -10
- package/atris/policies/atris-backend.md +1 -1
- package/atris/policies/atris-design.md +71 -71
- package/atris/policies/design-seed.md +28 -28
- package/atris/policies/writing.md +1 -1
- package/atris/skills/apps/SKILL.md +3 -3
- package/atris/skills/atris-feedback/SKILL.md +2 -2
- package/atris/skills/autopilot/SKILL.md +10 -10
- package/atris/skills/blocks/SKILL.md +16 -16
- package/atris/skills/calendar/SKILL.md +1 -1
- package/atris/skills/clawhub/atris/SKILL.md +4 -4
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
- package/atris/skills/create-app/SKILL.md +18 -18
- package/atris/skills/create-member/SKILL.md +11 -11
- package/atris/skills/design/SKILL.md +1 -1
- package/atris/skills/drive/SKILL.md +25 -25
- package/atris/skills/email-agent/SKILL.md +11 -11
- package/atris/skills/endgame/SKILL.md +33 -33
- package/atris/skills/magic-inbox/SKILL.md +15 -15
- package/atris/skills/notion/SKILL.md +1 -1
- package/atris/skills/ramp/SKILL.md +7 -7
- package/atris/skills/research-search/SKILL.md +7 -7
- package/atris/skills/slides/SKILL.md +7 -7
- package/atris/skills/tidy/SKILL.md +11 -11
- package/atris/skills/wake/SKILL.md +11 -11
- package/atris/skills/writing/SKILL.md +5 -5
- package/atris/team/_template/SOUL.md +3 -3
- package/atris/team/brainstormer/MEMBER.md +2 -2
- package/atris/team/launcher/MEMBER.md +2 -2
- package/atris/team/mission-lead/MISSION.md +7 -7
- package/atris/team/opus-overnight/MISSION.md +16 -16
- package/atris/team/opus-overnight/STEERING.md +5 -5
- package/atris.md +9 -0
- package/bin/atris.js +16 -3
- package/commands/campaign.js +17 -2
- package/commands/card.js +19 -12
- package/commands/engine.js +92 -17
- package/commands/gm.js +30 -0
- package/commands/idea.js +263 -0
- package/commands/init.js +11 -2
- package/commands/land.js +39 -18
- package/commands/mission.js +4 -3
- package/commands/ninety-nine.js +288 -0
- package/commands/one-lap.js +46 -27
- package/commands/reel.js +12 -7
- package/commands/sync.js +10 -2
- package/lib/campaign-keep.js +23 -8
- package/lib/campaign-loop.js +24 -5
- package/lib/claude-boot-block.js +7 -4
- package/lib/engine-registry.js +139 -1
- package/lib/fleet.js +11 -2
- package/lib/gm-bet.js +338 -34
- package/lib/gm-phone-page.js +325 -33
- package/lib/gm-phone.js +5 -1
- package/lib/gm-season.js +14 -0
- package/lib/headless-chrome.js +39 -0
- package/lib/idea-gate.js +33 -0
- package/lib/idea-inbox.js +220 -0
- package/lib/idea-ledger.js +196 -0
- package/lib/idea-tick.js +384 -0
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +1 -0
- package/lib/ninety-nine-sync.js +156 -0
- package/lib/ninety-nine.js +247 -0
- package/lib/opening-lines.js +94 -0
- package/lib/roster-markdown.js +39 -1
- package/lib/roster-models.js +1 -1
- package/lib/roster-swap.js +353 -0
- package/lib/spawn-env.js +29 -2
- package/lib/wish-audit.js +14 -0
- package/lib/yes-lock.js +358 -241
- package/lib/zen-apply.js +5 -3
- package/lib/zen.js +13 -1
- package/package.json +1 -1
- package/templates/business-starter/CLAUDE.md +7 -7
- package/templates/business-starter/MAP.md +1 -1
- package/templates/business-starter/TODO.md +1 -1
- package/templates/business-starter/atris.md +2 -2
- package/templates/business-starter/context/README.md +6 -6
- package/templates/business-starter/context/live-workspace.md +1 -1
- package/templates/business-starter/goals.md +1 -1
- package/templates/business-starter/instructions.md +1 -1
- package/templates/business-starter/memory.md +4 -4
- package/templates/business-starter/persona.md +3 -3
- package/templates/business-starter/policies/LESSONS.md +1 -1
- package/templates/business-starter/reports/README.md +1 -1
- package/templates/business-starter/skills/README.md +1 -1
- package/templates/business-starter/team/README.md +1 -1
- package/templates/business-starter/team/START_HERE.md +1 -1
- package/templates/business-starter/team/_template/SOUL.md +3 -3
- package/templates/business-starter/team/comms/SOUL.md +2 -2
- package/templates/business-starter/team/ops/SOUL.md +4 -4
- package/templates/business-starter/team/research/SOUL.md +3 -3
- package/templates/business-starter/wiki/index.md +1 -1
- package/templates/research-canonical/CLAUDE.md +7 -7
- package/templates/research-canonical/MAP.md +1 -1
- package/templates/research-canonical/TODO.md +1 -1
- package/templates/research-canonical/atris.md +2 -2
- package/templates/research-canonical/context/README.md +8 -8
- package/templates/research-canonical/context/live-workspace.md +1 -1
- package/templates/research-canonical/goals.md +2 -2
- package/templates/research-canonical/instructions.md +1 -1
- package/templates/research-canonical/memory.md +4 -4
- package/templates/research-canonical/persona.md +1 -1
- package/templates/research-canonical/policies/LESSONS.md +1 -1
- package/templates/research-canonical/reports/README.md +1 -1
- package/templates/research-canonical/skills/README.md +1 -1
- package/templates/research-canonical/team/README.md +1 -1
- package/templates/research-canonical/team/eval/SOUL.md +1 -1
- package/templates/research-canonical/team/experiment/SOUL.md +2 -2
- package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
- package/templates/research-canonical/team/literature/SOUL.md +2 -2
- package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
- package/templates/research-canonical/wiki/index.md +1 -1
- package/utils/auth.js +1 -0
package/lib/gm-phone-page.js
CHANGED
|
@@ -72,18 +72,54 @@ button { font: inherit; color: inherit; background: none; border: 0; padding: 0;
|
|
|
72
72
|
.line.bad { color: var(--text-2); }
|
|
73
73
|
.foot { position: absolute; left: 0; right: 0; bottom: max(20px, env(safe-area-inset-bottom)); text-align: center; }
|
|
74
74
|
.quiet { color: var(--text-2); font-size: 17px; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 44px; }
|
|
75
|
-
/* the team: one row per member
|
|
75
|
+
/* the team: one row per member: who, the role, the rating, the model, and the status */
|
|
76
76
|
.team { position: fixed; inset: 0; z-index: 20; overflow-y: auto; background: var(--bg); padding: max(24px, env(safe-area-inset-top)) 24px max(24px, env(safe-area-inset-bottom)); }
|
|
77
77
|
.team .inner { max-width: 34rem; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
|
|
78
78
|
.team .head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
79
79
|
.team h1 { margin: 0; font-size: clamp(23px, 6.2vw, 30px); line-height: 1.12; font-weight: 700; letter-spacing: -0.03em; }
|
|
80
|
-
.team .
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.team .
|
|
85
|
-
.team
|
|
86
|
-
.team .
|
|
80
|
+
.team .inner.wide { max-width: 64rem; }
|
|
81
|
+
/* the table: a line between every column and every row. at phone width the
|
|
82
|
+
box scrolls sideways and the name column stays put; no column ever gets
|
|
83
|
+
narrower than its words */
|
|
84
|
+
.team .grid { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; border: 1px solid var(--line); border-radius: 14px; }
|
|
85
|
+
.team table { width: 100%; min-width: 44rem; border-collapse: separate; border-spacing: 0; font-size: 17px; line-height: 1.35; }
|
|
86
|
+
.team th, .team td { padding: 10px 14px; text-align: left; vertical-align: top; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
87
|
+
.team th:last-child, .team td:last-child { border-right: 0; }
|
|
88
|
+
.team tbody tr:last-child td { border-bottom: 0; }
|
|
89
|
+
.team th { color: var(--text-2); font-weight: 400; white-space: nowrap; }
|
|
90
|
+
.team .row { cursor: pointer; }
|
|
91
|
+
@media (hover: hover) { .team .row:hover td { background: #1c1816; } }
|
|
92
|
+
.team .who { position: sticky; left: 0; z-index: 1; background: var(--bg); white-space: nowrap; }
|
|
93
|
+
.team .who button { font-size: 18px; font-weight: 700; text-align: left; }
|
|
94
|
+
.team .role { min-width: 9rem; color: var(--text-2); }
|
|
95
|
+
.team .ovr { white-space: nowrap; text-align: right; font-variant-numeric: tabular-nums; }
|
|
96
|
+
.team .eng { white-space: nowrap; }
|
|
97
|
+
.team td.eng button { text-align: left; text-decoration: underline; text-decoration-color: var(--line); text-underline-offset: 0.25em; min-height: 32px; }
|
|
98
|
+
/* the engine card: the line, who it is for, then every model on this machine by tool */
|
|
99
|
+
.team .done { margin: 0; color: var(--text); font-size: 18px; line-height: 1.4; }
|
|
100
|
+
.team .scopes { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
101
|
+
.team .scopes button { min-height: 44px; padding: 0 16px; border-radius: 22px; border: 1px solid var(--line); color: var(--text-2); font-size: 17px; }
|
|
102
|
+
.team .scopes button.chosen { border-color: var(--amber); background: rgba(255, 140, 66, 0.1); color: var(--text); }
|
|
103
|
+
.team .models { display: flex; flex-direction: column; gap: 8px; }
|
|
104
|
+
.team .models h2 { margin: 10px 0 0; color: var(--text-2); font-size: 17px; font-weight: 400; }
|
|
105
|
+
.team .models .choice { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
|
|
106
|
+
.team .models .choice .w { color: var(--text-2); font-size: 17px; white-space: nowrap; }
|
|
107
|
+
.team .models .choice[disabled] { cursor: default; }
|
|
108
|
+
.team td.st { min-width: 16rem; color: var(--text-2); }
|
|
109
|
+
.team td.st.working { color: var(--text); }
|
|
110
|
+
.team td.st.waiting-for-your-yes { color: var(--amber); }
|
|
111
|
+
/* the job deck: one running job at a time, in one spot, with its place in
|
|
112
|
+
the deck. the card is a .bet card, so a flip is the main card's own turn */
|
|
113
|
+
.deckspot { position: relative; }
|
|
114
|
+
.deckcard { display: flex; flex-direction: column; gap: 12px; touch-action: pan-y; }
|
|
115
|
+
.deckcard .count, .deckcard .label, .deckcard .time { font-size: 17px; color: var(--text-2); }
|
|
116
|
+
.deckcard .label.needs-you { color: var(--amber); }
|
|
117
|
+
.deckcard .why, .deckcard .act, .deckcard .mission { margin: 0; }
|
|
118
|
+
.jobs-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain; border: 1px solid var(--line); border-radius: 14px; }
|
|
119
|
+
.team table.jobs { min-width: 56rem; }
|
|
120
|
+
.jobs td.work { min-width: 14rem; }
|
|
121
|
+
.jobs td.done, .jobs td.member, .jobs td.engine, .jobs td.status { white-space: nowrap; }
|
|
122
|
+
.deckbar { display: flex; justify-content: space-between; gap: 12px; }
|
|
87
123
|
.sayline { margin: 10px 0 0; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
|
88
124
|
.sayline .thread, .sayline .reply { width: 100%; text-align: left; margin: 0; }
|
|
89
125
|
.unsent { margin: 0; text-align: center; }
|
|
@@ -662,15 +698,22 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
662
698
|
setTimeout(function () { tapOrb(false); }, AFTER_MS);
|
|
663
699
|
}
|
|
664
700
|
|
|
701
|
+
// A second tap, armed on this one button: one card and one command. A tap
|
|
702
|
+
// on another card, or a card drawn again after a flip, starts over, and
|
|
703
|
+
// the armed tap goes quiet after five seconds. Once sent, it takes no more.
|
|
665
704
|
function armedTap(go, label, armedLabel, run) {
|
|
705
|
+
var armed = false;
|
|
706
|
+
var quiet = null;
|
|
666
707
|
go.addEventListener('click', function () {
|
|
667
708
|
buzz();
|
|
668
|
-
if (!
|
|
669
|
-
|
|
670
|
-
setTimeout(function () {
|
|
709
|
+
if (!armed) {
|
|
710
|
+
armed = true; go.textContent = armedLabel; go.className = 'pill armed';
|
|
711
|
+
quiet = setTimeout(function () { armed = false; go.textContent = label; go.className = 'pill'; }, 5000);
|
|
671
712
|
return;
|
|
672
713
|
}
|
|
673
|
-
|
|
714
|
+
clearTimeout(quiet);
|
|
715
|
+
armed = false;
|
|
716
|
+
go.disabled = true;
|
|
674
717
|
run();
|
|
675
718
|
});
|
|
676
719
|
}
|
|
@@ -681,6 +724,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
681
724
|
return s && !/[.!?]$/.test(s) ? s + '.' : s;
|
|
682
725
|
}
|
|
683
726
|
|
|
727
|
+
// the yes on a yes card: the first tap arms it, the second sends it.
|
|
728
|
+
// done(line, reply) gets the line to show after it.
|
|
729
|
+
function yesPill(post, done) {
|
|
730
|
+
var yes = el('button', 'pill', 'yes');
|
|
731
|
+
yes.type = 'button';
|
|
732
|
+
armedTap(yes, 'yes', 'tap again to say yes', function () {
|
|
733
|
+
post().then(function (r) { done(r.ack || r.message || 'sent', r); });
|
|
734
|
+
});
|
|
735
|
+
return yes;
|
|
736
|
+
}
|
|
737
|
+
|
|
684
738
|
// a yes the owner owes, or a finished business move: what is going on,
|
|
685
739
|
// then why it matters, both written by the server
|
|
686
740
|
function drawJobCard(s) {
|
|
@@ -691,12 +745,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
691
745
|
if (j.why) box.appendChild(el('p', 'why', j.why));
|
|
692
746
|
var act = el('div', 'act');
|
|
693
747
|
if (s.kind === 'yes') {
|
|
694
|
-
|
|
695
|
-
yes.type = 'button';
|
|
696
|
-
armedTap(yes, 'yes', 'tap again to say yes', function () {
|
|
697
|
-
api('/bet/yes', { confirm: true }).then(function (r) { moveOn(r.ack || r.message || 'sent'); });
|
|
698
|
-
});
|
|
699
|
-
act.appendChild(yes);
|
|
748
|
+
act.appendChild(yesPill(function () { return api('/bet/yes', { confirm: true }); }, moveOn));
|
|
700
749
|
} else if (s.kind === 'wait') {
|
|
701
750
|
// one calm line: the idea turns in on its own once it is written
|
|
702
751
|
watchWait();
|
|
@@ -1301,7 +1350,14 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1301
1350
|
function crossfade(draw) {
|
|
1302
1351
|
var box = document.getElementById('bet');
|
|
1303
1352
|
box.classList.remove('holding');
|
|
1304
|
-
|
|
1353
|
+
turnInPlace(box, draw, function () { bet.busy = false; measured(); });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// one card turns into the next in its own spot: a still copy of the old
|
|
1357
|
+
// one fades over it while the new one drifts in, and the box eases to
|
|
1358
|
+
// the new height. after() runs once it is at rest.
|
|
1359
|
+
function turnInPlace(box, draw, after) {
|
|
1360
|
+
if (reduced) { box.style.minHeight = ''; draw(); after(); return; }
|
|
1305
1361
|
var from = box.offsetHeight;
|
|
1306
1362
|
box.style.height = from + 'px';
|
|
1307
1363
|
box.style.minHeight = '';
|
|
@@ -1331,8 +1387,7 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1331
1387
|
ghost.remove();
|
|
1332
1388
|
box.classList.remove('turning');
|
|
1333
1389
|
box.style.height = '';
|
|
1334
|
-
|
|
1335
|
-
measured();
|
|
1390
|
+
after();
|
|
1336
1391
|
}, 80 + TURN_IN_MS + 20);
|
|
1337
1392
|
}
|
|
1338
1393
|
|
|
@@ -1433,10 +1488,17 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1433
1488
|
// the way back from what is on top: the team closes, a talk card goes back to the team
|
|
1434
1489
|
var teamBack = null;
|
|
1435
1490
|
|
|
1491
|
+
// every redraw of the team box makes a new view; an answer that comes
|
|
1492
|
+
// back for an older view is dropped (a closed box is hidden, and the next
|
|
1493
|
+
// open is a new view)
|
|
1494
|
+
var teamView = 0;
|
|
1495
|
+
|
|
1436
1496
|
function closeTeam() { teamBox.hidden = true; }
|
|
1437
1497
|
|
|
1438
1498
|
function teamHead(title) {
|
|
1499
|
+
teamView += 1;
|
|
1439
1500
|
teamInner.textContent = '';
|
|
1501
|
+
teamInner.className = 'inner';
|
|
1440
1502
|
teamBack = closeTeam;
|
|
1441
1503
|
var head = el('div', 'head');
|
|
1442
1504
|
head.appendChild(el('h1', null, title));
|
|
@@ -1450,29 +1512,123 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1450
1512
|
function openTeam() {
|
|
1451
1513
|
teamBox.hidden = false;
|
|
1452
1514
|
teamHead('the team');
|
|
1453
|
-
|
|
1515
|
+
var view = teamView;
|
|
1516
|
+
api('/bet/team').then(function (s) { if (s.http === 200 && view === teamView) drawTeam(s.rows || []); });
|
|
1454
1517
|
}
|
|
1455
1518
|
|
|
1456
|
-
|
|
1519
|
+
// one row per member, five columns; a tap anywhere on a row opens its talk
|
|
1520
|
+
// card, and the name is a button so the keyboard reaches it too
|
|
1521
|
+
var TEAM_COLUMNS = ['member', 'role', 'ovr', 'engine', 'status'];
|
|
1522
|
+
var TEAM_CELLS = ['who', 'role', 'ovr', 'eng', 'st'];
|
|
1523
|
+
|
|
1524
|
+
// note is the one plain line a swap just said, shown over the table
|
|
1525
|
+
function drawTeam(rows, note) {
|
|
1457
1526
|
teamHead('the team');
|
|
1458
|
-
if (
|
|
1527
|
+
if (note) teamInner.appendChild(el('p', 'done', note));
|
|
1528
|
+
if (!rows.length) { teamInner.appendChild(el('p', 'why', 'nobody is on the team yet.')); return; }
|
|
1529
|
+
teamInner.className = 'inner wide';
|
|
1530
|
+
deckEntry(rows);
|
|
1531
|
+
var grid = el('div', 'grid');
|
|
1532
|
+
var table = el('table', 'roster');
|
|
1533
|
+
var head = el('tr');
|
|
1534
|
+
TEAM_COLUMNS.forEach(function (name, i) { head.appendChild(el('th', TEAM_CELLS[i], name)).scope = 'col'; });
|
|
1535
|
+
table.appendChild(el('thead')).appendChild(head);
|
|
1536
|
+
var body = table.appendChild(el('tbody'));
|
|
1459
1537
|
rows.forEach(function (r) {
|
|
1460
|
-
var
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1538
|
+
var tr = el('tr', 'row');
|
|
1539
|
+
var who = el('td', 'who');
|
|
1540
|
+
var open = who.appendChild(el('button', null, r.member));
|
|
1541
|
+
open.type = 'button';
|
|
1542
|
+
open.setAttribute('aria-label', 'talk to ' + r.member);
|
|
1543
|
+
tr.appendChild(who);
|
|
1544
|
+
tr.appendChild(el('td', 'role', r.role));
|
|
1545
|
+
tr.appendChild(el('td', 'ovr', r.ovr));
|
|
1546
|
+
// the engine cell is its own button: a tap opens the engine card, not the talk card
|
|
1547
|
+
var eng = tr.appendChild(el('td', 'eng'));
|
|
1548
|
+
if (r.engine === 'person') eng.textContent = r.engine;
|
|
1549
|
+
else {
|
|
1550
|
+
var swap = eng.appendChild(el('button', null, r.engine));
|
|
1551
|
+
swap.type = 'button';
|
|
1552
|
+
swap.setAttribute('aria-label', 'change what ' + r.member + ' runs on');
|
|
1553
|
+
swap.addEventListener('click', function (e) { e.stopPropagation(); drawEngine(r); });
|
|
1554
|
+
}
|
|
1555
|
+
tr.appendChild(el('td', 'st ' + r.status.replace(/ /g, '-'), r.sentence));
|
|
1556
|
+
tr.addEventListener('click', function () { drawTalk(r); });
|
|
1557
|
+
body.appendChild(tr);
|
|
1468
1558
|
});
|
|
1559
|
+
grid.appendChild(table);
|
|
1560
|
+
teamInner.appendChild(grid);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// the engine card: what the member runs on, who a swap is for, then every
|
|
1564
|
+
// model this machine offers by tool, the current one on top. One tap swaps
|
|
1565
|
+
// (the tap is the approval) and the team comes back with the new engine in
|
|
1566
|
+
// its row and one line saying what changed. Esc goes back to the team.
|
|
1567
|
+
function drawEngine(r) {
|
|
1568
|
+
teamHead(r.member);
|
|
1569
|
+
teamBack = openTeam;
|
|
1570
|
+
// this card's view and member: an answer for any other is not drawn, and
|
|
1571
|
+
// every swap button posts this member, never whoever is on screen later
|
|
1572
|
+
var view = teamView;
|
|
1573
|
+
var member = r.member;
|
|
1574
|
+
var mine = function (s) { return view === teamView && (!s || !s.member || s.member === member); };
|
|
1575
|
+
var said = teamInner.appendChild(el('p', 'why', member + ' runs on ' + r.engine + '.'));
|
|
1576
|
+
var back = el('button', 'quiet', 'back to the team');
|
|
1577
|
+
back.type = 'button';
|
|
1578
|
+
back.addEventListener('click', openTeam);
|
|
1579
|
+
var busy = false;
|
|
1580
|
+
api('/bet/engine', { member: member }).then(function (s) {
|
|
1581
|
+
if (!mine(s)) return;
|
|
1582
|
+
if (s.http !== 200 || !s.ok) { said.textContent = s.message || 'could not read the models on this machine.'; teamInner.appendChild(back); return; }
|
|
1583
|
+
said.textContent = s.line;
|
|
1584
|
+
var scope = 'member';
|
|
1585
|
+
var scopes = el('div', 'scopes');
|
|
1586
|
+
scopes.setAttribute('role', 'group');
|
|
1587
|
+
scopes.setAttribute('aria-label', 'who the swap is for');
|
|
1588
|
+
(s.scopes || []).forEach(function (sc) {
|
|
1589
|
+
var b = scopes.appendChild(el('button', sc.id === scope ? 'chosen' : null, sc.label));
|
|
1590
|
+
b.type = 'button';
|
|
1591
|
+
b.setAttribute('aria-pressed', sc.id === scope ? 'true' : 'false');
|
|
1592
|
+
b.addEventListener('click', function () {
|
|
1593
|
+
scope = sc.id;
|
|
1594
|
+
Array.prototype.forEach.call(scopes.children, function (x) { var on = x === b; x.classList.toggle('chosen', on); x.setAttribute('aria-pressed', on ? 'true' : 'false'); });
|
|
1595
|
+
});
|
|
1596
|
+
});
|
|
1597
|
+
if ((s.scopes || []).length > 1) teamInner.appendChild(scopes);
|
|
1598
|
+
(s.groups || []).forEach(function (g) {
|
|
1599
|
+
var box = teamInner.appendChild(el('section', 'models'));
|
|
1600
|
+
box.appendChild(el('h2', null, g.tool));
|
|
1601
|
+
g.models.forEach(function (m) {
|
|
1602
|
+
var b = box.appendChild(el('button', 'choice' + (m.current ? ' chosen' : '')));
|
|
1603
|
+
b.type = 'button';
|
|
1604
|
+
b.appendChild(el('span', 't', m.label));
|
|
1605
|
+
var w = m.current ? 'now' : m.record;
|
|
1606
|
+
if (w) b.appendChild(el('span', 'w', w));
|
|
1607
|
+
if (m.current) { b.disabled = true; return; }
|
|
1608
|
+
b.setAttribute('data-member', member);
|
|
1609
|
+
b.addEventListener('click', function () {
|
|
1610
|
+
if (busy || !mine()) return;
|
|
1611
|
+
busy = true;
|
|
1612
|
+
b.classList.add('chosen');
|
|
1613
|
+
api('/bet/swap', { member: b.getAttribute('data-member'), engine: m.engine, model: m.model, scope: scope }).then(function (out) {
|
|
1614
|
+
busy = false;
|
|
1615
|
+
if (!mine()) return;
|
|
1616
|
+
if (out.http === 200 && out.ok) { drawTeam(out.rows || [], out.ack); return; }
|
|
1617
|
+
b.classList.remove('chosen');
|
|
1618
|
+
said.textContent = out.message || 'that did not change. try again.';
|
|
1619
|
+
}, function () { busy = false; if (!mine()) return; b.classList.remove('chosen'); said.textContent = 'that did not reach the mac. try again.'; });
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
teamInner.appendChild(back);
|
|
1624
|
+
}, function () { if (!mine()) return; said.textContent = 'that did not reach the mac. try again.'; teamInner.appendChild(back); });
|
|
1469
1625
|
}
|
|
1470
1626
|
|
|
1471
1627
|
// a talk card: what the member is on, and a word to steer them
|
|
1472
1628
|
function drawTalk(r) {
|
|
1473
1629
|
teamHead(r.member);
|
|
1474
1630
|
teamBack = openTeam;
|
|
1475
|
-
teamInner.appendChild(el('p', 'why', r.
|
|
1631
|
+
teamInner.appendChild(el('p', 'why', r.sentence + '.'));
|
|
1476
1632
|
var talkBox = convo(function (body) { body.member = r.member; return api('/bet/steer', body); }, function (ack) {
|
|
1477
1633
|
talkBox.replaceWith(el('p', 'mission', ack));
|
|
1478
1634
|
}, 'tell ' + r.member + ' something');
|
|
@@ -1483,6 +1639,142 @@ body.has-card .foot { position: static; margin-top: 28px; }
|
|
|
1483
1639
|
teamInner.appendChild(back);
|
|
1484
1640
|
}
|
|
1485
1641
|
|
|
1642
|
+
// ---- the job deck: every running job is one card, the ones that need
|
|
1643
|
+
// the owner first. The arrows or a sideways swipe flip one card at a
|
|
1644
|
+
// time, and it turns in its own spot the way the main card does. Esc
|
|
1645
|
+
// goes back to talking. A card that needs the owner is that job's yes
|
|
1646
|
+
// card, and under the table one box talks to the job's member and keeps
|
|
1647
|
+
// the turns, even across flips.
|
|
1648
|
+
var deckCards = [];
|
|
1649
|
+
var deckColumns = [];
|
|
1650
|
+
var deckAt = 0;
|
|
1651
|
+
var deckTurning = false;
|
|
1652
|
+
var deckTalk = {};
|
|
1653
|
+
var DECK_LIVE = ['waiting for your yes', 'working', 'waiting to start'];
|
|
1654
|
+
|
|
1655
|
+
// the way in from the team page, there only while a job is running
|
|
1656
|
+
function deckEntry(rows) {
|
|
1657
|
+
if (!rows.some(function (r) { return DECK_LIVE.indexOf(r.status) >= 0; })) return;
|
|
1658
|
+
var open = el('button', 'quiet', 'the jobs, one at a time');
|
|
1659
|
+
open.type = 'button';
|
|
1660
|
+
open.addEventListener('click', openDeck);
|
|
1661
|
+
teamInner.appendChild(open);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function openDeck() {
|
|
1665
|
+
teamBox.hidden = false;
|
|
1666
|
+
teamHead('the jobs');
|
|
1667
|
+
var waiting = teamInner.appendChild(el('div', 'deckspot'));
|
|
1668
|
+
api('/bet/jobs').then(function (s) {
|
|
1669
|
+
// the owner moved on while it loaded: nothing is drawn over it
|
|
1670
|
+
if (s.http !== 200 || !waiting.isConnected || teamBox.hidden) return;
|
|
1671
|
+
deckCards = s.cards || [];
|
|
1672
|
+
deckColumns = s.columns || [];
|
|
1673
|
+
if (!deckCards.length) { openTeam(); return; }
|
|
1674
|
+
deckAt = Math.min(deckAt, deckCards.length - 1);
|
|
1675
|
+
drawDeck();
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function drawDeck() {
|
|
1680
|
+
teamHead('the jobs');
|
|
1681
|
+
teamInner.className = 'inner wide';
|
|
1682
|
+
var spot = teamInner.appendChild(el('div', 'deckspot'));
|
|
1683
|
+
// a card like the main one, so it turns the same way
|
|
1684
|
+
var card = spot.appendChild(el('article', 'bet'));
|
|
1685
|
+
card.classList.add('up', 'deckcard');
|
|
1686
|
+
fillDeckCard(card);
|
|
1687
|
+
var startX = null, startY = null;
|
|
1688
|
+
card.addEventListener('touchstart', function (e) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; }, { passive: true });
|
|
1689
|
+
card.addEventListener('touchend', function (e) {
|
|
1690
|
+
if (startX === null) return;
|
|
1691
|
+
var dx = e.changedTouches[0].clientX - startX, dy = e.changedTouches[0].clientY - startY;
|
|
1692
|
+
startX = null;
|
|
1693
|
+
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) flipDeck(dx < 0 ? 1 : -1);
|
|
1694
|
+
});
|
|
1695
|
+
var bar = el('div', 'deckbar');
|
|
1696
|
+
if (deckCards.length > 1) {
|
|
1697
|
+
var prev = bar.appendChild(el('button', 'quiet', 'previous'));
|
|
1698
|
+
prev.type = 'button';
|
|
1699
|
+
prev.addEventListener('click', function () { flipDeck(-1); });
|
|
1700
|
+
}
|
|
1701
|
+
var whole = bar.appendChild(el('button', 'quiet', 'the whole team'));
|
|
1702
|
+
whole.type = 'button';
|
|
1703
|
+
whole.addEventListener('click', openTeam);
|
|
1704
|
+
if (deckCards.length > 1) {
|
|
1705
|
+
var next = bar.appendChild(el('button', 'quiet', 'next'));
|
|
1706
|
+
next.type = 'button';
|
|
1707
|
+
next.addEventListener('click', function () { flipDeck(1); });
|
|
1708
|
+
}
|
|
1709
|
+
teamInner.appendChild(bar);
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// one job: its place in the deck, the yes card when it needs the owner,
|
|
1713
|
+
// the divided table, how long, and the talk box for its member
|
|
1714
|
+
function fillDeckCard(card) {
|
|
1715
|
+
var c = deckCards[deckAt];
|
|
1716
|
+
card.textContent = '';
|
|
1717
|
+
card.appendChild(el('span', 'count', 'job ' + (deckAt + 1) + ' of ' + deckCards.length));
|
|
1718
|
+
card.appendChild(el('span', 'label ' + c.label.replace(/ /g, '-'), c.label));
|
|
1719
|
+
if (c.yes) {
|
|
1720
|
+
card.appendChild(el('h1', null, c.yes.what));
|
|
1721
|
+
if (c.yes.why) card.appendChild(el('p', 'why', c.yes.why));
|
|
1722
|
+
var act = card.appendChild(el('div', 'act'));
|
|
1723
|
+
act.appendChild(yesPill(function () { return api('/bet/yes', { confirm: true, key: c.yes.key, job: c.job_id }); }, function (said, r) {
|
|
1724
|
+
if (r.ok || r.reason === 'gone') { c.yes = null; c.said = said; act.replaceWith(el('p', 'mission', said)); return; }
|
|
1725
|
+
c.unsent = said;
|
|
1726
|
+
fillDeckCard(card);
|
|
1727
|
+
}));
|
|
1728
|
+
if (c.unsent) act.appendChild(el('p', 'links unsent', c.unsent));
|
|
1729
|
+
} else if (c.said) card.appendChild(el('p', 'mission', c.said));
|
|
1730
|
+
var table = el('table', 'jobs');
|
|
1731
|
+
var head = el('tr');
|
|
1732
|
+
deckColumns.forEach(function (k) { head.appendChild(el('th', k, k)).scope = 'col'; });
|
|
1733
|
+
table.appendChild(el('thead')).appendChild(head);
|
|
1734
|
+
var body = table.appendChild(el('tbody'));
|
|
1735
|
+
c.rows.forEach(function (r) {
|
|
1736
|
+
var tr = body.appendChild(el('tr'));
|
|
1737
|
+
deckColumns.forEach(function (k) { tr.appendChild(el('td', k, r[k] || '')); });
|
|
1738
|
+
});
|
|
1739
|
+
card.appendChild(el('div', 'jobs-wrap')).appendChild(table);
|
|
1740
|
+
if (c.time) card.appendChild(el('span', 'time', c.time));
|
|
1741
|
+
card.appendChild(deckTalk[c.member] || (deckTalk[c.member] = talkBox(c.member)));
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// one talk box per member, made once and kept, so the turns and any
|
|
1745
|
+
// words being typed are still there after a flip; what came of a word
|
|
1746
|
+
// stays above a fresh box
|
|
1747
|
+
function talkBox(member) {
|
|
1748
|
+
var holder = el('div', 'jobtalk');
|
|
1749
|
+
var fresh = function () {
|
|
1750
|
+
var box = convo(function (body) { body.member = member; return api('/bet/steer', body); }, function (ack) {
|
|
1751
|
+
holder.insertBefore(el('p', 'mission', ack), box);
|
|
1752
|
+
box.replaceWith(fresh());
|
|
1753
|
+
}, 'talk to this job');
|
|
1754
|
+
return box;
|
|
1755
|
+
};
|
|
1756
|
+
holder.appendChild(fresh());
|
|
1757
|
+
return holder;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function flipDeck(step) {
|
|
1761
|
+
var card = teamInner.querySelector('.deckcard');
|
|
1762
|
+
if (!card || deckTurning || deckCards.length < 2) return;
|
|
1763
|
+
deckAt = (deckAt + step + deckCards.length) % deckCards.length;
|
|
1764
|
+
deckTurning = true;
|
|
1765
|
+
turnInPlace(card, function () { fillDeckCard(card); }, function () { deckTurning = false; });
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// the arrows flip the deck, unless the owner is typing in a box
|
|
1769
|
+
document.addEventListener('keydown', function (e) {
|
|
1770
|
+
var step = e.key === 'ArrowLeft' ? -1 : e.key === 'ArrowRight' ? 1 : 0;
|
|
1771
|
+
if (!step || e.isComposing || teamBox.hidden || !teamInner.querySelector('.deckcard')) return;
|
|
1772
|
+
var field = document.activeElement;
|
|
1773
|
+
if (field && (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA')) return;
|
|
1774
|
+
e.preventDefault();
|
|
1775
|
+
flipDeck(step);
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1486
1778
|
document.getElementById('working').addEventListener('click', openTeam);
|
|
1487
1779
|
|
|
1488
1780
|
// Esc on the team page steps back one layer, the way its own buttons do:
|
package/lib/gm-phone.js
CHANGED
|
@@ -518,6 +518,9 @@ function createPhoneHandler(deps) {
|
|
|
518
518
|
else if (p === '/bet/steer') out = await bet.talk({ member: String(body.member || ''), text: String(body.text || ''), confirm: body.confirm === true, note: body.note === true });
|
|
519
519
|
else if (p === '/bet/say') out = await bet.say({ text: String(body.text || ''), confirm: body.confirm === true, note: body.note === true, card: String(body.card || '') });
|
|
520
520
|
else if (p === '/bet/skip') out = bet.skip();
|
|
521
|
+
// the engine card and its one-tap swap; the tap is the owner's approval
|
|
522
|
+
else if (p === '/bet/engine') out = await bet.engineCard({ member: String(body.member || '') });
|
|
523
|
+
else if (p === '/bet/swap') out = await bet.swapEngine({ member: String(body.member || ''), engine: String(body.engine || ''), model: String(body.model || ''), scope: String(body.scope || '') });
|
|
521
524
|
else if (p === '/bet/do') {
|
|
522
525
|
if (body.confirm !== true) return { status: 409, body: { ok: false, reason: 'needs_confirm', confirm_ms: CONFIRM_MS } };
|
|
523
526
|
out = await bet.doIt();
|
|
@@ -526,7 +529,7 @@ function createPhoneHandler(deps) {
|
|
|
526
529
|
} else if (p === '/bet/yes') {
|
|
527
530
|
// a yes unblocks agents, so it needs a second tap
|
|
528
531
|
if (body.confirm !== true) return { status: 409, body: { ok: false, reason: 'needs_confirm', confirm_ms: CONFIRM_MS } };
|
|
529
|
-
out = await bet.answerYes();
|
|
532
|
+
out = await bet.answerYes(typeof body.key === 'string' && body.key ? { key: body.key.slice(0, 300), job: typeof body.job === 'string' ? body.job.slice(0, 300) : '' } : {});
|
|
530
533
|
} else return { status: 404, body: { ok: false, reason: 'not_found' } };
|
|
531
534
|
return { status: out.ok ? 200 : 409, body: out };
|
|
532
535
|
}
|
|
@@ -578,6 +581,7 @@ function createPhoneHandler(deps) {
|
|
|
578
581
|
if (req.method === 'GET' && check.path === '/state') return send(res, 200, state());
|
|
579
582
|
if (req.method === 'GET' && check.path === '/bet') return bet ? send(res, 200, bet.warm()) : send(res, 404, { ok: false, reason: 'not_found' });
|
|
580
583
|
if (req.method === 'GET' && check.path === '/bet/team') return bet ? send(res, 200, bet.teamTable()) : send(res, 404, { ok: false, reason: 'not_found' });
|
|
584
|
+
if (req.method === 'GET' && check.path === '/bet/jobs') return bet ? send(res, 200, bet.jobDeck()) : send(res, 404, { ok: false, reason: 'not_found' });
|
|
581
585
|
if (req.method === 'POST' && (check.path === '/action' || check.path === '/undo' || (bet && check.path.startsWith('/bet/')))) {
|
|
582
586
|
if (String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase() !== 'application/json') return send(res, 415, { ok: false, reason: 'json_only' });
|
|
583
587
|
let body;
|
package/lib/gm-season.js
CHANGED
|
@@ -58,6 +58,19 @@ function ovr(outcomes) {
|
|
|
58
58
|
return Math.round(60 + 39 * landed / finished);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Each member's rating over one window of plays, the board's own numbers:
|
|
62
|
+
// ovr is 60 to 99, or null for a rookie under 3 finished. A member with no
|
|
63
|
+
// plays in the window is not in the map.
|
|
64
|
+
function memberRatings(outcomes = []) {
|
|
65
|
+
const groups = new Map();
|
|
66
|
+
for (const o of outcomes || []) {
|
|
67
|
+
if (!o || !o.member) continue;
|
|
68
|
+
if (!groups.has(o.member)) groups.set(o.member, []);
|
|
69
|
+
groups.get(o.member).push(o);
|
|
70
|
+
}
|
|
71
|
+
return new Map([...groups].map(([name, list]) => [name, { ovr: ovr(list), finished: tally(list).finished }]));
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
// Last n finished jobs, oldest on the left, newest on the right.
|
|
62
75
|
function form(outcomes, n = 5) {
|
|
63
76
|
return [...(outcomes || [])]
|
|
@@ -1146,6 +1159,7 @@ module.exports = {
|
|
|
1146
1159
|
form,
|
|
1147
1160
|
lastPlays,
|
|
1148
1161
|
loadSeason,
|
|
1162
|
+
memberRatings,
|
|
1149
1163
|
outcomesFromEvents,
|
|
1150
1164
|
ovr,
|
|
1151
1165
|
ownerMood,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One place for how this CLI launches headless Chrome. Every spot that starts
|
|
4
|
+
// Chrome builds its args here so a new launch can't forget the rules:
|
|
5
|
+
//
|
|
6
|
+
// --use-mock-keychain Chrome never touches the macOS keychain, so nobody
|
|
7
|
+
// --password-store=basic gets a "Chrome Safe Storage" prompt
|
|
8
|
+
// --user-data-dir a throwaway profile under the system temp folder,
|
|
9
|
+
// never the person's real Chrome profile
|
|
10
|
+
//
|
|
11
|
+
// Make the profile with makeProfileDir, build args with headlessArgs, and
|
|
12
|
+
// remove the profile with removeProfileDir when the launch is done.
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const KEYCHAIN_FLAGS = Object.freeze(['--use-mock-keychain', '--password-store=basic']);
|
|
19
|
+
|
|
20
|
+
function makeProfileDir(prefix = 'atris-chrome-') {
|
|
21
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function removeProfileDir(dir) {
|
|
25
|
+
try { fs.rmSync(dir, { recursive: true, force: true, maxRetries: 3 }); } catch { /* best effort */ }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Args for one headless launch. `profileDir` must sit under the system temp
|
|
29
|
+
// folder; anything else throws, so a real profile can't be passed by mistake.
|
|
30
|
+
function headlessArgs(profileDir, extra = []) {
|
|
31
|
+
const tmpRoot = path.resolve(os.tmpdir());
|
|
32
|
+
const dir = profileDir ? path.resolve(String(profileDir)) : '';
|
|
33
|
+
if (!dir || !dir.startsWith(tmpRoot + path.sep)) {
|
|
34
|
+
throw new Error('headless chrome needs a throwaway profile under the temp folder (makeProfileDir)');
|
|
35
|
+
}
|
|
36
|
+
return ['--headless=new', ...KEYCHAIN_FLAGS, `--user-data-dir=${dir}`, ...extra];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { KEYCHAIN_FLAGS, makeProfileDir, removeProfileDir, headlessArgs };
|
package/lib/idea-gate.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The two gates every idea passes before it lands, kept apart from the tick
|
|
4
|
+
// so the loop cannot loosen them: a built change that edits this file is
|
|
5
|
+
// refused before landing (see JUDGE_PATHS in lib/idea-inbox.js).
|
|
6
|
+
|
|
7
|
+
const FORBIDDEN_SHIP_FLAGS = Object.freeze(['--force', '--force-with-lease', '--no-verify', '-f']);
|
|
8
|
+
|
|
9
|
+
// The builder and the reviewer must be two different engines, and the
|
|
10
|
+
// reviewer must have passed the change. One-lap enforces this already; the
|
|
11
|
+
// tick checks it again so a change can never land on its builder's word.
|
|
12
|
+
function secondReviewerSignedOff(lap) {
|
|
13
|
+
const validator = lap && lap.result && lap.result.validator_result;
|
|
14
|
+
return Boolean(validator
|
|
15
|
+
&& validator.passed === true
|
|
16
|
+
&& validator.independent === true
|
|
17
|
+
&& validator.engine
|
|
18
|
+
&& validator.executor_engine
|
|
19
|
+
&& validator.engine !== validator.executor_engine);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Landing never force-pushes and never skips hooks.
|
|
23
|
+
function assertSafeShipArgs(args) {
|
|
24
|
+
for (const flag of FORBIDDEN_SHIP_FLAGS) {
|
|
25
|
+
if (args.includes(flag)) throw new Error(`refusing to land with ${flag}`);
|
|
26
|
+
}
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
assertSafeShipArgs,
|
|
32
|
+
secondReviewerSignedOff,
|
|
33
|
+
};
|