@codyswann/lisa 2.299.4 → 2.300.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/upstream-evidence-manifest.js +4 -4
- package/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/lisa/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/lisa/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/lisa-agy/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/lisa-copilot/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/lisa-cursor/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/skills/playwright-selectors/SKILL.md +16 -0
- package/plugins/lisa-expo/skills/playwright-selectors/SKILL.md +16 -0
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-agy/skills/playwright-selectors/SKILL.md +16 -0
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/skills/playwright-selectors/SKILL.md +16 -0
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/skills/playwright-selectors/SKILL.md +16 -0
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-implement/SKILL.md +9 -0
- package/plugins/src/base/skills/lisa-verification-lifecycle/SKILL.md +9 -0
- package/plugins/src/expo/skills/playwright-selectors/SKILL.md +16 -0
- package/ui/index.html +3846 -168
package/ui/index.html
CHANGED
|
@@ -302,6 +302,10 @@
|
|
|
302
302
|
background: var(--surface2);
|
|
303
303
|
border-bottom: 1px solid var(--line);
|
|
304
304
|
}
|
|
305
|
+
/* display:flex above would otherwise beat the `hidden` attribute. */
|
|
306
|
+
.live-status-panel[hidden] {
|
|
307
|
+
display: none;
|
|
308
|
+
}
|
|
305
309
|
.live-status-panel h2 {
|
|
306
310
|
margin: 0;
|
|
307
311
|
font-size: 12px;
|
|
@@ -630,6 +634,11 @@
|
|
|
630
634
|
background: var(--accent-soft);
|
|
631
635
|
color: var(--accent);
|
|
632
636
|
}
|
|
637
|
+
.badge.na {
|
|
638
|
+
background: transparent;
|
|
639
|
+
color: var(--faint);
|
|
640
|
+
border: 1px dashed var(--line-strong);
|
|
641
|
+
}
|
|
633
642
|
|
|
634
643
|
.switch {
|
|
635
644
|
position: relative;
|
|
@@ -649,30 +658,60 @@
|
|
|
649
658
|
position: absolute;
|
|
650
659
|
inset: 0;
|
|
651
660
|
border-radius: 99px;
|
|
652
|
-
|
|
653
|
-
|
|
661
|
+
/* Off reads as an empty, recessed well; on reads as a filled accent
|
|
662
|
+
track. Border + inset shadow are what make the off state legible
|
|
663
|
+
against the card background instead of looking like a flat pill. */
|
|
664
|
+
background: var(--surface2);
|
|
665
|
+
border: 1px solid var(--line-strong);
|
|
666
|
+
box-shadow: inset 0 1px 2px rgba(22, 35, 38, 0.12);
|
|
667
|
+
transition:
|
|
668
|
+
background 0.15s,
|
|
669
|
+
border-color 0.15s;
|
|
654
670
|
}
|
|
655
671
|
.switch .trk::after {
|
|
656
672
|
content: "";
|
|
657
673
|
position: absolute;
|
|
658
674
|
top: 2px;
|
|
659
675
|
left: 2px;
|
|
660
|
-
width:
|
|
661
|
-
height:
|
|
676
|
+
width: 14px;
|
|
677
|
+
height: 14px;
|
|
662
678
|
border-radius: 50%;
|
|
663
|
-
background:
|
|
679
|
+
background: var(--faint);
|
|
664
680
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
665
|
-
transition:
|
|
681
|
+
transition:
|
|
682
|
+
transform 0.15s,
|
|
683
|
+
background 0.15s;
|
|
666
684
|
}
|
|
667
685
|
.switch input:checked + .trk {
|
|
668
686
|
background: var(--accent);
|
|
687
|
+
border-color: var(--accent);
|
|
688
|
+
box-shadow: none;
|
|
669
689
|
}
|
|
670
690
|
.switch input:checked + .trk::after {
|
|
671
691
|
transform: translateX(16px);
|
|
692
|
+
background: #fff;
|
|
672
693
|
}
|
|
673
694
|
.switch input:focus-visible + .trk {
|
|
674
695
|
box-shadow: 0 0 0 3px var(--accent-soft);
|
|
675
696
|
}
|
|
697
|
+
/* The state word is what actually makes on/off unambiguous — the track
|
|
698
|
+
alone reads as decoration at a glance. */
|
|
699
|
+
.switch-wrap {
|
|
700
|
+
display: inline-flex;
|
|
701
|
+
align-items: center;
|
|
702
|
+
gap: 8px;
|
|
703
|
+
}
|
|
704
|
+
.switch-state {
|
|
705
|
+
font-size: 11px;
|
|
706
|
+
font-weight: 650;
|
|
707
|
+
letter-spacing: 0.05em;
|
|
708
|
+
text-transform: uppercase;
|
|
709
|
+
color: var(--faint);
|
|
710
|
+
min-width: 22px;
|
|
711
|
+
}
|
|
712
|
+
.switch-state.on {
|
|
713
|
+
color: var(--accent);
|
|
714
|
+
}
|
|
676
715
|
|
|
677
716
|
select.ctl,
|
|
678
717
|
input.ctl {
|
|
@@ -1261,6 +1300,293 @@
|
|
|
1261
1300
|
animation: none !important;
|
|
1262
1301
|
}
|
|
1263
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
/* --- "how do I connect this?" modal --- */
|
|
1305
|
+
.badge-btn {
|
|
1306
|
+
border: 0;
|
|
1307
|
+
padding: 0;
|
|
1308
|
+
background: none;
|
|
1309
|
+
font: inherit;
|
|
1310
|
+
cursor: pointer;
|
|
1311
|
+
border-radius: 99px;
|
|
1312
|
+
}
|
|
1313
|
+
.badge-btn .badge {
|
|
1314
|
+
text-decoration: underline dotted currentColor;
|
|
1315
|
+
text-underline-offset: 3px;
|
|
1316
|
+
}
|
|
1317
|
+
.badge-btn:hover .badge,
|
|
1318
|
+
.badge-btn:focus-visible .badge {
|
|
1319
|
+
text-decoration: underline solid currentColor;
|
|
1320
|
+
outline: none;
|
|
1321
|
+
}
|
|
1322
|
+
.badge-btn:focus-visible {
|
|
1323
|
+
outline: 2px solid var(--accent);
|
|
1324
|
+
outline-offset: 2px;
|
|
1325
|
+
}
|
|
1326
|
+
.howto {
|
|
1327
|
+
width: min(560px, calc(100vw - 32px));
|
|
1328
|
+
border: 1px solid var(--line);
|
|
1329
|
+
border-radius: 12px;
|
|
1330
|
+
background: var(--surface);
|
|
1331
|
+
color: var(--ink);
|
|
1332
|
+
padding: 0;
|
|
1333
|
+
box-shadow: var(--shadow);
|
|
1334
|
+
}
|
|
1335
|
+
.howto::backdrop {
|
|
1336
|
+
background: rgba(22, 35, 38, 0.42);
|
|
1337
|
+
}
|
|
1338
|
+
.howto-head {
|
|
1339
|
+
display: flex;
|
|
1340
|
+
align-items: flex-start;
|
|
1341
|
+
gap: 12px;
|
|
1342
|
+
padding: 16px 18px 12px;
|
|
1343
|
+
border-bottom: 1px solid var(--line);
|
|
1344
|
+
}
|
|
1345
|
+
.howto-head h2 {
|
|
1346
|
+
margin: 0;
|
|
1347
|
+
font-size: 15px;
|
|
1348
|
+
line-height: 1.35;
|
|
1349
|
+
}
|
|
1350
|
+
.howto-head .sub {
|
|
1351
|
+
display: block;
|
|
1352
|
+
margin-top: 4px;
|
|
1353
|
+
font-size: 12px;
|
|
1354
|
+
font-weight: 400;
|
|
1355
|
+
color: var(--muted);
|
|
1356
|
+
}
|
|
1357
|
+
.howto-close {
|
|
1358
|
+
margin-left: auto;
|
|
1359
|
+
flex: none;
|
|
1360
|
+
border: 1px solid var(--line);
|
|
1361
|
+
background: var(--surface2);
|
|
1362
|
+
color: var(--muted);
|
|
1363
|
+
border-radius: 6px;
|
|
1364
|
+
width: 26px;
|
|
1365
|
+
height: 26px;
|
|
1366
|
+
font-size: 15px;
|
|
1367
|
+
line-height: 1;
|
|
1368
|
+
cursor: pointer;
|
|
1369
|
+
}
|
|
1370
|
+
.howto-close:hover {
|
|
1371
|
+
color: var(--ink);
|
|
1372
|
+
border-color: var(--line-strong);
|
|
1373
|
+
}
|
|
1374
|
+
.howto-body {
|
|
1375
|
+
padding: 14px 18px 18px;
|
|
1376
|
+
font-size: 13px;
|
|
1377
|
+
line-height: 1.55;
|
|
1378
|
+
}
|
|
1379
|
+
.howto-body p {
|
|
1380
|
+
margin: 0 0 12px;
|
|
1381
|
+
color: var(--muted);
|
|
1382
|
+
}
|
|
1383
|
+
.howto-body ol {
|
|
1384
|
+
margin: 0;
|
|
1385
|
+
padding-left: 18px;
|
|
1386
|
+
display: grid;
|
|
1387
|
+
gap: 10px;
|
|
1388
|
+
}
|
|
1389
|
+
.howto-body li {
|
|
1390
|
+
padding-left: 2px;
|
|
1391
|
+
}
|
|
1392
|
+
.howto-body code {
|
|
1393
|
+
display: block;
|
|
1394
|
+
margin-top: 6px;
|
|
1395
|
+
padding: 7px 9px;
|
|
1396
|
+
background: var(--surface2);
|
|
1397
|
+
border: 1px solid var(--line);
|
|
1398
|
+
border-radius: 6px;
|
|
1399
|
+
font-family: var(--mono);
|
|
1400
|
+
font-size: 12px;
|
|
1401
|
+
overflow-x: auto;
|
|
1402
|
+
white-space: pre;
|
|
1403
|
+
}
|
|
1404
|
+
.howto-foot {
|
|
1405
|
+
margin-top: 14px;
|
|
1406
|
+
padding-top: 12px;
|
|
1407
|
+
border-top: 1px solid var(--line);
|
|
1408
|
+
font-size: 12px;
|
|
1409
|
+
color: var(--muted);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/* --- gate matrix: clickable obligation + per-layer status dots --- */
|
|
1413
|
+
.linklike {
|
|
1414
|
+
border: 0;
|
|
1415
|
+
padding: 0;
|
|
1416
|
+
background: none;
|
|
1417
|
+
font: inherit;
|
|
1418
|
+
color: inherit;
|
|
1419
|
+
cursor: pointer;
|
|
1420
|
+
text-align: left;
|
|
1421
|
+
text-decoration: underline dotted var(--line-strong);
|
|
1422
|
+
text-underline-offset: 3px;
|
|
1423
|
+
}
|
|
1424
|
+
.linklike:hover,
|
|
1425
|
+
.linklike:focus-visible {
|
|
1426
|
+
text-decoration: underline solid var(--accent);
|
|
1427
|
+
color: var(--accent);
|
|
1428
|
+
outline: none;
|
|
1429
|
+
}
|
|
1430
|
+
.grid th.center,
|
|
1431
|
+
.grid td.gate-cell {
|
|
1432
|
+
text-align: center;
|
|
1433
|
+
}
|
|
1434
|
+
.gate-cell .dot {
|
|
1435
|
+
display: inline-block;
|
|
1436
|
+
}
|
|
1437
|
+
.dot.warn {
|
|
1438
|
+
background: var(--warn);
|
|
1439
|
+
}
|
|
1440
|
+
/* --- staff picker: which coding agent a factory page is read for --- */
|
|
1441
|
+
.staff-card {
|
|
1442
|
+
padding: 12px 16px;
|
|
1443
|
+
}
|
|
1444
|
+
.staff-head {
|
|
1445
|
+
display: flex;
|
|
1446
|
+
align-items: center;
|
|
1447
|
+
gap: 12px;
|
|
1448
|
+
flex-wrap: wrap;
|
|
1449
|
+
}
|
|
1450
|
+
.staff-label {
|
|
1451
|
+
font-size: 11px;
|
|
1452
|
+
font-weight: 650;
|
|
1453
|
+
letter-spacing: 0.06em;
|
|
1454
|
+
text-transform: uppercase;
|
|
1455
|
+
color: var(--muted);
|
|
1456
|
+
}
|
|
1457
|
+
.staff-opts {
|
|
1458
|
+
display: inline-flex;
|
|
1459
|
+
flex-wrap: wrap;
|
|
1460
|
+
gap: 4px;
|
|
1461
|
+
padding: 3px;
|
|
1462
|
+
background: var(--surface2);
|
|
1463
|
+
border: 1px solid var(--line);
|
|
1464
|
+
border-radius: 8px;
|
|
1465
|
+
}
|
|
1466
|
+
.staff-opt {
|
|
1467
|
+
border: 0;
|
|
1468
|
+
background: none;
|
|
1469
|
+
font: inherit;
|
|
1470
|
+
font-size: 12px;
|
|
1471
|
+
color: var(--muted);
|
|
1472
|
+
padding: 5px 11px;
|
|
1473
|
+
border-radius: 6px;
|
|
1474
|
+
cursor: pointer;
|
|
1475
|
+
white-space: nowrap;
|
|
1476
|
+
}
|
|
1477
|
+
.staff-opt:hover {
|
|
1478
|
+
color: var(--ink);
|
|
1479
|
+
}
|
|
1480
|
+
.staff-opt.active {
|
|
1481
|
+
background: var(--accent);
|
|
1482
|
+
color: var(--accent-ink);
|
|
1483
|
+
font-weight: 600;
|
|
1484
|
+
}
|
|
1485
|
+
/* The three environment cards are siblings of every other card on the
|
|
1486
|
+
page — display:contents keeps them in the section's own flow rather
|
|
1487
|
+
than nesting them in a box that would read as one grouped thing. */
|
|
1488
|
+
.env-cards {
|
|
1489
|
+
display: contents;
|
|
1490
|
+
}
|
|
1491
|
+
.card.env-gap {
|
|
1492
|
+
border-left: 3px solid var(--warn);
|
|
1493
|
+
}
|
|
1494
|
+
.staff-warn {
|
|
1495
|
+
margin-top: 8px;
|
|
1496
|
+
padding: 8px 11px;
|
|
1497
|
+
border-radius: 7px;
|
|
1498
|
+
background: var(--warn-soft);
|
|
1499
|
+
color: var(--warn);
|
|
1500
|
+
font-size: 12px;
|
|
1501
|
+
line-height: 1.5;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.howto-meta {
|
|
1505
|
+
display: grid;
|
|
1506
|
+
grid-template-columns: auto 1fr;
|
|
1507
|
+
gap: 4px 14px;
|
|
1508
|
+
margin: 0 0 14px;
|
|
1509
|
+
padding: 10px 12px;
|
|
1510
|
+
background: var(--surface2);
|
|
1511
|
+
border: 1px solid var(--line);
|
|
1512
|
+
border-radius: 8px;
|
|
1513
|
+
font-size: 12px;
|
|
1514
|
+
}
|
|
1515
|
+
.howto-meta .hm-k {
|
|
1516
|
+
color: var(--muted);
|
|
1517
|
+
}
|
|
1518
|
+
.howto-meta .hm-v {
|
|
1519
|
+
color: var(--ink);
|
|
1520
|
+
font-family: var(--mono);
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/* --- multi-select provider control --- */
|
|
1524
|
+
.ms {
|
|
1525
|
+
position: relative;
|
|
1526
|
+
display: inline-block;
|
|
1527
|
+
}
|
|
1528
|
+
.ms-summary {
|
|
1529
|
+
border: 1px solid var(--line);
|
|
1530
|
+
background: var(--surface);
|
|
1531
|
+
color: var(--ink);
|
|
1532
|
+
border-radius: 6px;
|
|
1533
|
+
padding: 4px 22px 4px 8px;
|
|
1534
|
+
font: inherit;
|
|
1535
|
+
font-size: 12px;
|
|
1536
|
+
cursor: pointer;
|
|
1537
|
+
text-align: left;
|
|
1538
|
+
max-width: 220px;
|
|
1539
|
+
white-space: nowrap;
|
|
1540
|
+
overflow: hidden;
|
|
1541
|
+
text-overflow: ellipsis;
|
|
1542
|
+
background-image:
|
|
1543
|
+
linear-gradient(45deg, transparent 50%, var(--faint) 50%),
|
|
1544
|
+
linear-gradient(135deg, var(--faint) 50%, transparent 50%);
|
|
1545
|
+
background-position:
|
|
1546
|
+
calc(100% - 12px) 50%,
|
|
1547
|
+
calc(100% - 8px) 50%;
|
|
1548
|
+
background-size:
|
|
1549
|
+
4px 4px,
|
|
1550
|
+
4px 4px;
|
|
1551
|
+
background-repeat: no-repeat;
|
|
1552
|
+
}
|
|
1553
|
+
.ms-summary:hover {
|
|
1554
|
+
border-color: var(--line-strong);
|
|
1555
|
+
}
|
|
1556
|
+
.ms-summary.ms-empty {
|
|
1557
|
+
color: var(--faint);
|
|
1558
|
+
}
|
|
1559
|
+
.ms-pop {
|
|
1560
|
+
display: none;
|
|
1561
|
+
position: absolute;
|
|
1562
|
+
z-index: 40;
|
|
1563
|
+
top: calc(100% + 4px);
|
|
1564
|
+
left: 0;
|
|
1565
|
+
min-width: 100%;
|
|
1566
|
+
max-height: 240px;
|
|
1567
|
+
overflow-y: auto;
|
|
1568
|
+
background: var(--surface);
|
|
1569
|
+
border: 1px solid var(--line);
|
|
1570
|
+
border-radius: 8px;
|
|
1571
|
+
box-shadow: var(--shadow);
|
|
1572
|
+
padding: 4px;
|
|
1573
|
+
}
|
|
1574
|
+
.ms.open .ms-pop {
|
|
1575
|
+
display: block;
|
|
1576
|
+
}
|
|
1577
|
+
.ms-opt {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
gap: 8px;
|
|
1581
|
+
padding: 5px 8px;
|
|
1582
|
+
border-radius: 5px;
|
|
1583
|
+
font-size: 12px;
|
|
1584
|
+
white-space: nowrap;
|
|
1585
|
+
cursor: pointer;
|
|
1586
|
+
}
|
|
1587
|
+
.ms-opt:hover {
|
|
1588
|
+
background: var(--surface2);
|
|
1589
|
+
}
|
|
1264
1590
|
</style>
|
|
1265
1591
|
</head>
|
|
1266
1592
|
<body>
|
|
@@ -1308,12 +1634,19 @@
|
|
|
1308
1634
|
◐
|
|
1309
1635
|
</button>
|
|
1310
1636
|
</header>
|
|
1637
|
+
<!-- Live status strip: HIDDEN for now. The probe fetch still runs and still
|
|
1638
|
+
paints the pages that consume it (Monitoring providers, CI quality
|
|
1639
|
+
jobs, Automations) — only this summary strip is hidden, because it
|
|
1640
|
+
renders raw probe payloads as unreadable JSON blobs. Unhide by removing
|
|
1641
|
+
the `hidden` attribute once each probe result has a human-readable
|
|
1642
|
+
rendering. -->
|
|
1311
1643
|
<section
|
|
1312
1644
|
class="live-status-panel"
|
|
1313
1645
|
aria-labelledby="liveStatusHeading"
|
|
1314
1646
|
aria-live="polite"
|
|
1315
1647
|
aria-atomic="true"
|
|
1316
1648
|
role="status"
|
|
1649
|
+
hidden
|
|
1317
1650
|
>
|
|
1318
1651
|
<h2 id="liveStatusHeading">Live status</h2>
|
|
1319
1652
|
<div class="live-status-list" id="liveStatusList" role="list">
|
|
@@ -1324,6 +1657,20 @@
|
|
|
1324
1657
|
<nav class="sidebar" id="nav" aria-label="Settings sections"></nav>
|
|
1325
1658
|
<main class="main" id="main"></main>
|
|
1326
1659
|
</div>
|
|
1660
|
+
<dialog class="howto" id="howto" aria-labelledby="howtoTitle">
|
|
1661
|
+
<div class="howto-head">
|
|
1662
|
+
<h2 id="howtoTitle"></h2>
|
|
1663
|
+
<button
|
|
1664
|
+
class="howto-close"
|
|
1665
|
+
id="howtoClose"
|
|
1666
|
+
type="button"
|
|
1667
|
+
aria-label="Close"
|
|
1668
|
+
>
|
|
1669
|
+
×
|
|
1670
|
+
</button>
|
|
1671
|
+
</div>
|
|
1672
|
+
<div class="howto-body" id="howtoBody"></div>
|
|
1673
|
+
</dialog>
|
|
1327
1674
|
<div class="savebar" id="savebar" role="status">
|
|
1328
1675
|
<span class="msg"
|
|
1329
1676
|
><b id="dirtyCount">0</b> unsaved change<span id="dirtyPlural"
|
|
@@ -1385,10 +1732,24 @@
|
|
|
1385
1732
|
"general",
|
|
1386
1733
|
"stacks",
|
|
1387
1734
|
"starters",
|
|
1388
|
-
"agents",
|
|
1389
1735
|
"remote",
|
|
1390
1736
|
],
|
|
1391
1737
|
},
|
|
1738
|
+
{
|
|
1739
|
+
label: "Factories",
|
|
1740
|
+
sections: [
|
|
1741
|
+
"factory-research",
|
|
1742
|
+
"factory-plan",
|
|
1743
|
+
"factory-implement",
|
|
1744
|
+
"factory-verify",
|
|
1745
|
+
"signals",
|
|
1746
|
+
"credentials",
|
|
1747
|
+
"environments",
|
|
1748
|
+
// The staff that run the factories — every factory page picks
|
|
1749
|
+
// one of these, and its capabilities change what that page shows.
|
|
1750
|
+
"agents",
|
|
1751
|
+
],
|
|
1752
|
+
},
|
|
1392
1753
|
{
|
|
1393
1754
|
label: "Delivery",
|
|
1394
1755
|
sections: [
|
|
@@ -1401,111 +1762,3200 @@
|
|
|
1401
1762
|
"monitor",
|
|
1402
1763
|
],
|
|
1403
1764
|
},
|
|
1404
|
-
{
|
|
1405
|
-
label: "Quality",
|
|
1406
|
-
sections: ["linting", "testing", "hooks", "ci", "qa"],
|
|
1407
|
-
},
|
|
1408
|
-
{ label: "Distribution", sections: ["repository", "plugins"] },
|
|
1409
|
-
{ label: "Advanced", sections: ["advanced"] },
|
|
1765
|
+
{
|
|
1766
|
+
label: "Quality",
|
|
1767
|
+
sections: ["linting", "testing", "hooks", "ci", "qa"],
|
|
1768
|
+
},
|
|
1769
|
+
{ label: "Distribution", sections: ["repository", "plugins"] },
|
|
1770
|
+
{ label: "Advanced", sections: ["advanced"] },
|
|
1771
|
+
],
|
|
1772
|
+
sections: {},
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
/* ---------------------------------- Overview ---------------------------------- */
|
|
1776
|
+
DATA.sections.overview = {
|
|
1777
|
+
title: "Overview",
|
|
1778
|
+
desc: "Lisa runs this project as a set of software factories — agent-operated production lines that build, verify, and ship with as few humans as possible. Everything below is what the factories are governed by.",
|
|
1779
|
+
src: ".lisa.config.json · .lisa.config.local.json",
|
|
1780
|
+
blocks: [
|
|
1781
|
+
{
|
|
1782
|
+
type: "flow",
|
|
1783
|
+
title:
|
|
1784
|
+
"The four factories — humans hand off at the gates, never inside",
|
|
1785
|
+
states: [
|
|
1786
|
+
{ name: "Research → PRDs" },
|
|
1787
|
+
{ name: "⛩ gate" },
|
|
1788
|
+
{ name: "Plan → work units" },
|
|
1789
|
+
{ name: "⛩ gate" },
|
|
1790
|
+
{ name: "Implement → software" },
|
|
1791
|
+
{ name: "⛩ gate" },
|
|
1792
|
+
{ name: "Verify → go/no-go", terminal: true },
|
|
1793
|
+
],
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
type: "callout",
|
|
1797
|
+
text: "<b>The gates are adversarial.</b> Agents, humans, and automations submit inputs; an intake agent admits only work that is unambiguous and that the factory has provable tooling access to execute — everything else is rejected and raised. Three loops feed the pipeline on schedule: <b>QA</b> (exploratory bugs), <b>Product Planning</b> (ideated PRDs), and <b>Monitoring</b> (observability regressions). Verify failures flow straight back into Implement as build-ready tickets. The goal: <b>non-technical people creating scalable software</b> — everything that crosses a gate is written for them, and end users never talk to a coding agent.",
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
type: "tiles",
|
|
1801
|
+
tiles: [
|
|
1802
|
+
{ k: "Project types", v: "2", s: "typescript · expo" },
|
|
1803
|
+
{ k: "Work tracker", v: "JIRA", s: "project ACME" },
|
|
1804
|
+
{ k: "PRD source", v: "Notion", s: "PRD queue database" },
|
|
1805
|
+
{ k: "Harness", v: "fleet", s: "all 6 coding agents" },
|
|
1806
|
+
{ k: "Plugins enabled", v: "11", s: "4 Lisa + 7 curated" },
|
|
1807
|
+
{
|
|
1808
|
+
k: "Coverage floor",
|
|
1809
|
+
v: "70%",
|
|
1810
|
+
s: "statements · branches · functions · lines",
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
k: "Required CI checks",
|
|
1814
|
+
v: "9",
|
|
1815
|
+
s: "incl. CodeRabbit + GitGuardian",
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
k: "Automations",
|
|
1819
|
+
v: "6",
|
|
1820
|
+
s: "intake, repair, loops — all healthy",
|
|
1821
|
+
},
|
|
1822
|
+
],
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
type: "flow",
|
|
1826
|
+
title:
|
|
1827
|
+
"Build lifecycle (fixed — only the status names are configurable)",
|
|
1828
|
+
states: [
|
|
1829
|
+
{ name: "ready" },
|
|
1830
|
+
{ name: "claimed" },
|
|
1831
|
+
{ name: "review?", opt: true },
|
|
1832
|
+
{ name: "done", terminal: true },
|
|
1833
|
+
],
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
type: "flow",
|
|
1837
|
+
title: "PRD lifecycle (fixed)",
|
|
1838
|
+
states: [
|
|
1839
|
+
{ name: "draft" },
|
|
1840
|
+
{ name: "ready" },
|
|
1841
|
+
{ name: "in_review" },
|
|
1842
|
+
{ name: "blocked | ticketed" },
|
|
1843
|
+
{ name: "shipped" },
|
|
1844
|
+
{ name: "verified", terminal: true },
|
|
1845
|
+
],
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
card: "How configuration resolves",
|
|
1849
|
+
rows: [
|
|
1850
|
+
{
|
|
1851
|
+
key: "",
|
|
1852
|
+
label: "Resolution order",
|
|
1853
|
+
desc: "Skill arguments always win, then the per-developer local file, then the committed file, then Lisa’s built-in default.",
|
|
1854
|
+
control: stat(
|
|
1855
|
+
"$ARGUMENTS → .lisa.config.local.json → .lisa.config.json → default",
|
|
1856
|
+
"mono"
|
|
1857
|
+
),
|
|
1858
|
+
},
|
|
1859
|
+
{
|
|
1860
|
+
key: "",
|
|
1861
|
+
label: "Secrets",
|
|
1862
|
+
desc: "Tokens never live in config — only IDs, keys and slugs. Secrets stay in env vars or the OS keychain.",
|
|
1863
|
+
control: stat(
|
|
1864
|
+
"ATLASSIAN_API_TOKEN · NOTION_API_TOKEN · LINEAR_API_KEY · GH_TOKEN",
|
|
1865
|
+
"mono"
|
|
1866
|
+
),
|
|
1867
|
+
},
|
|
1868
|
+
],
|
|
1869
|
+
},
|
|
1870
|
+
],
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1873
|
+
/* ---------------------------------- Factories ---------------------------------- */
|
|
1874
|
+
/* MOCK ONLY. The factory pages illustrate the readiness model —
|
|
1875
|
+
obligation × provider × enforcement point, then the same obligations
|
|
1876
|
+
re-asked across staff (which coding agent) and environment (where it
|
|
1877
|
+
runs). None of these cells are wired to a probe yet; every value below
|
|
1878
|
+
is illustrative. Wiring them means replacing the literal rows with
|
|
1879
|
+
probe-painted cells, the way Monitoring's "Connected providers" table
|
|
1880
|
+
already works. */
|
|
1881
|
+
|
|
1882
|
+
/* ===================================================================
|
|
1883
|
+
PROPOSED-CAPABILITY MARKER — read this before implementing anything
|
|
1884
|
+
these pages describe.
|
|
1885
|
+
===================================================================
|
|
1886
|
+
Some of this UI shows Lisa capabilities that DO NOT EXIST YET. The
|
|
1887
|
+
mock is the spec for them. Every such spot is marked with a comment
|
|
1888
|
+
beginning `PROPOSED:` so they can all be found with one grep:
|
|
1889
|
+
|
|
1890
|
+
rg "PROPOSED:" ui/index.html
|
|
1891
|
+
|
|
1892
|
+
A PROPOSED block always states four things, in this order:
|
|
1893
|
+
1. What to build — the command/behaviour the UI implies.
|
|
1894
|
+
2. Why — the problem it solves that today's surface
|
|
1895
|
+
does not.
|
|
1896
|
+
3. Today — what actually exists right now, so the
|
|
1897
|
+
implementer can see the delta.
|
|
1898
|
+
4. Replaces — existing skills/commands that should be
|
|
1899
|
+
folded in or retired, if any.
|
|
1900
|
+
|
|
1901
|
+
Rule: never quietly "fix" the UI to match what Lisa can do today.
|
|
1902
|
+
The gap is deliberate. Build the capability, then delete the marker.
|
|
1903
|
+
=================================================================== */
|
|
1904
|
+
|
|
1905
|
+
/* Cell states are deliberately tri-state-plus: green means proven, amber
|
|
1906
|
+
means partial, red means a real gap, grey means either not-applicable
|
|
1907
|
+
or never-exercised. Grey is NOT a pass — an obligation that has never
|
|
1908
|
+
been run must never render as green. */
|
|
1909
|
+
function fCell(v, how) {
|
|
1910
|
+
const cls =
|
|
1911
|
+
v === "yes" || v === "ready" || v === "proven"
|
|
1912
|
+
? "pass"
|
|
1913
|
+
: v === "partial"
|
|
1914
|
+
? "warn"
|
|
1915
|
+
: v === "no" || v === "gap"
|
|
1916
|
+
? "fail"
|
|
1917
|
+
: "default";
|
|
1918
|
+
return how ? { t: "badge", v, cls, how } : { t: "badge", v, cls };
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
/* Bypassability is its own vocabulary because "no" is the good answer:
|
|
1922
|
+
only server-side enforcement is unskippable, everything earlier is a
|
|
1923
|
+
fast-feedback mirror of the same rule. */
|
|
1924
|
+
function fBypass(v, how) {
|
|
1925
|
+
const cls =
|
|
1926
|
+
v === "blocked" ? "pass" : v === "possible" ? "warn" : "fail";
|
|
1927
|
+
return how ? { t: "badge", v, cls, how } : { t: "badge", v, cls };
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
/* Gate matrix cells. gObl = the obligation name (clickable when the gate
|
|
1931
|
+
has a gap). gGate = one enforcement layer's status: true enforced,
|
|
1932
|
+
"warn" runs-but-does-not-gate, false not-here. Bypassability is not a
|
|
1933
|
+
column any more — it is read off the row: the rightmost server-side
|
|
1934
|
+
layer being green is what makes an obligation unskippable. */
|
|
1935
|
+
function gObl(v, how) {
|
|
1936
|
+
return how ? { t: "obl", v, how } : { t: "obl", v };
|
|
1937
|
+
}
|
|
1938
|
+
function gGate(on) {
|
|
1939
|
+
return { t: "gate", on };
|
|
1940
|
+
}
|
|
1941
|
+
function gProv(values, options) {
|
|
1942
|
+
return { t: "multiselect", values, options, mono: true };
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
/* A single enforcement layer as a clickable yes/no. Setup and verify
|
|
1946
|
+
commands default per layer kind; the caller supplies the event binding
|
|
1947
|
+
(and, for CI, whether the check is required to merge) plus any
|
|
1948
|
+
obligation-specific note. When a wired version replaces this, these
|
|
1949
|
+
defaults become whatever the probe reports about the installed hook. */
|
|
1950
|
+
const LAYER_DEFS = {
|
|
1951
|
+
agent: {
|
|
1952
|
+
name: "Agent hook",
|
|
1953
|
+
setup: "/lisa:apply",
|
|
1954
|
+
verify: "/lisa:doctor",
|
|
1955
|
+
},
|
|
1956
|
+
git: {
|
|
1957
|
+
name: "Git hook",
|
|
1958
|
+
setup: "/lisa:apply",
|
|
1959
|
+
verify: "/lisa:doctor",
|
|
1960
|
+
},
|
|
1961
|
+
ci: {
|
|
1962
|
+
name: "CI",
|
|
1963
|
+
setup: "/lisa:apply",
|
|
1964
|
+
verify: "/lisa:verify:workflow-change",
|
|
1965
|
+
note: null,
|
|
1966
|
+
},
|
|
1967
|
+
};
|
|
1968
|
+
function layerCell(kind, on, opts) {
|
|
1969
|
+
opts = opts || {};
|
|
1970
|
+
const d = LAYER_DEFS[kind];
|
|
1971
|
+
// Per-gate setup/verify commands when a slug is supplied; the generic
|
|
1972
|
+
// apply/doctor commands otherwise.
|
|
1973
|
+
const setup = opts.slug ? "/lisa:setup:gate:" + opts.slug : d.setup;
|
|
1974
|
+
const verify = opts.slug ? "/lisa:verify:gate:" + opts.slug : d.verify;
|
|
1975
|
+
const enabled = on === true;
|
|
1976
|
+
const na = on === "na";
|
|
1977
|
+
const meta = [];
|
|
1978
|
+
if (enabled && opts.event) meta.push({ k: "Event", v: opts.event });
|
|
1979
|
+
if (kind === "ci" && enabled) {
|
|
1980
|
+
meta.push({
|
|
1981
|
+
k: "Required to merge",
|
|
1982
|
+
v: opts.required ? "Yes" : "No",
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
let steps;
|
|
1986
|
+
if (na) {
|
|
1987
|
+
steps = [
|
|
1988
|
+
{
|
|
1989
|
+
text: "Not applicable to this project — there is no deployed web or API surface to scan. It becomes a gate on a stack that has one.",
|
|
1990
|
+
cmd: setup,
|
|
1991
|
+
},
|
|
1992
|
+
];
|
|
1993
|
+
} else if (enabled) {
|
|
1994
|
+
steps = [
|
|
1995
|
+
{
|
|
1996
|
+
text: "Set up this gate — the command asks which layers to enforce it at, and with which provider.",
|
|
1997
|
+
cmd: setup,
|
|
1998
|
+
},
|
|
1999
|
+
{ text: "Verify it fires.", cmd: verify },
|
|
2000
|
+
];
|
|
2001
|
+
} else {
|
|
2002
|
+
steps = [
|
|
2003
|
+
{
|
|
2004
|
+
text: "Not enforced at this layer. Set the gate up and enable it here.",
|
|
2005
|
+
cmd: setup,
|
|
2006
|
+
},
|
|
2007
|
+
];
|
|
2008
|
+
}
|
|
2009
|
+
const subState = enabled
|
|
2010
|
+
? " — enforced here"
|
|
2011
|
+
: na
|
|
2012
|
+
? " — not applicable"
|
|
2013
|
+
: " — not enforced here";
|
|
2014
|
+
return {
|
|
2015
|
+
t: "layer",
|
|
2016
|
+
kind, // read at render time so the cell can defer to the selected agent
|
|
2017
|
+
on,
|
|
2018
|
+
how: {
|
|
2019
|
+
title: opts.for || d.name,
|
|
2020
|
+
sub: d.name + subState,
|
|
2021
|
+
intro: opts.intro,
|
|
2022
|
+
meta,
|
|
2023
|
+
steps,
|
|
2024
|
+
foot: opts.foot || (na ? null : d.note) || undefined,
|
|
2025
|
+
},
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/* An agent-layer cell is only as real as the agent running the factory.
|
|
2030
|
+
Where the selected agent has no working hook surface, the cell reports
|
|
2031
|
+
n/a with the capability reason — never "no", which would imply the
|
|
2032
|
+
operator forgot to turn something on. */
|
|
2033
|
+
function resolveLayerCell(cell) {
|
|
2034
|
+
if (cell.kind !== "agent") return cell;
|
|
2035
|
+
const caps = currentCaps();
|
|
2036
|
+
if (caps.agentHooks === true) return cell;
|
|
2037
|
+
|
|
2038
|
+
// Unproven. Never rendered as enforced, never as a flat capability gap.
|
|
2039
|
+
if (caps.agentHooks === "unverified") {
|
|
2040
|
+
return {
|
|
2041
|
+
...cell,
|
|
2042
|
+
on: "na",
|
|
2043
|
+
how: {
|
|
2044
|
+
title: cell.how ? cell.how.title : "Agent hook",
|
|
2045
|
+
sub: caps.label + " — unverified",
|
|
2046
|
+
intro: caps.hookWhy,
|
|
2047
|
+
meta: [
|
|
2048
|
+
{ k: "Agent", v: caps.label },
|
|
2049
|
+
{ k: "Agent-layer hooks", v: "unproven" },
|
|
2050
|
+
],
|
|
2051
|
+
steps: [
|
|
2052
|
+
{
|
|
2053
|
+
text: "Prove it either way by tripping the rule in a real headless run for this agent.",
|
|
2054
|
+
cmd: "/lisa:verify:gate:<name>",
|
|
2055
|
+
},
|
|
2056
|
+
],
|
|
2057
|
+
foot: "Unverified is not the same as unsupported. Rendering it as enforced would be a lie; rendering it as a capability gap would be a different lie.",
|
|
2058
|
+
},
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
return {
|
|
2063
|
+
...cell,
|
|
2064
|
+
on: "na",
|
|
2065
|
+
how: {
|
|
2066
|
+
title: cell.how ? cell.how.title : "Agent hook",
|
|
2067
|
+
sub: caps.label + " — capability gap",
|
|
2068
|
+
intro:
|
|
2069
|
+
caps.hookWhy +
|
|
2070
|
+
" This is a property of the agent, not of your configuration, so there is nothing here to switch on.",
|
|
2071
|
+
meta: [
|
|
2072
|
+
{ k: "Agent", v: caps.label },
|
|
2073
|
+
{ k: "Agent-layer hooks", v: "unsupported" },
|
|
2074
|
+
],
|
|
2075
|
+
steps: [
|
|
2076
|
+
{
|
|
2077
|
+
text: "Nothing to set up for this agent. The rule still holds — it is enforced at the layers below, which every agent shares.",
|
|
2078
|
+
cmd: "the Git hook and CI columns",
|
|
2079
|
+
},
|
|
2080
|
+
],
|
|
2081
|
+
foot: "Lisa's parity rule: where a capability cannot be represented on an agent, the gap is documented rather than silently dropped. This cell is that documentation.",
|
|
2082
|
+
},
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
/* Build a full gate row. layers = {agent, git, ci}, each an opts object
|
|
2086
|
+
carrying at least {on}. The obligation string is threaded into every
|
|
2087
|
+
cell's modal title so each layer modal names what it enforces. */
|
|
2088
|
+
function gateRow(obl, slug, provider, layers) {
|
|
2089
|
+
// o.on is tri-state: true | false | "na" — pass it through raw.
|
|
2090
|
+
const withFor = (kind, o) =>
|
|
2091
|
+
layerCell(kind, o.on, Object.assign({ for: obl, slug }, o));
|
|
2092
|
+
return [
|
|
2093
|
+
{ t: "text", v: obl },
|
|
2094
|
+
provider,
|
|
2095
|
+
withFor("agent", layers.agent),
|
|
2096
|
+
withFor("git", layers.git),
|
|
2097
|
+
withFor("ci", layers.ci),
|
|
2098
|
+
];
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
function fGates(rows) {
|
|
2102
|
+
return {
|
|
2103
|
+
type: "table",
|
|
2104
|
+
card: "Flows and gates",
|
|
2105
|
+
note: "The obligation is fixed; the provider is swappable. Change the tool, not the rule.",
|
|
2106
|
+
cols: [
|
|
2107
|
+
{
|
|
2108
|
+
v: "Obligation",
|
|
2109
|
+
tip: "What must be true. This does not change when you change tools.",
|
|
2110
|
+
},
|
|
2111
|
+
{
|
|
2112
|
+
v: "Provider",
|
|
2113
|
+
tip: "The tool currently satisfying the obligation. Swappable.",
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
v: "Enforced at",
|
|
2117
|
+
tip: "Where the gate actually fires. Only server-side enforcement cannot be skipped.",
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
v: "Bypass",
|
|
2121
|
+
tip: "blocked = the worker cannot skip it · possible = local-only, skippable · unenforced = nothing stops it",
|
|
2122
|
+
},
|
|
2123
|
+
{ v: "State" },
|
|
2124
|
+
],
|
|
2125
|
+
rows: rows.map(r => [
|
|
2126
|
+
{ t: "text", v: r[0] },
|
|
2127
|
+
{ t: "mono", v: r[1] },
|
|
2128
|
+
{ t: "text", v: r[2] },
|
|
2129
|
+
fBypass(r[3]),
|
|
2130
|
+
fCell(r[4]),
|
|
2131
|
+
]),
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function fStaff(rows) {
|
|
2136
|
+
return {
|
|
2137
|
+
type: "table",
|
|
2138
|
+
card: "Staff readiness — is this factory ready for each agent?",
|
|
2139
|
+
note: "A red cell here is sometimes not fixable by config: if the agent lacks the surface entirely, the gap is declared, never silently dropped.",
|
|
2140
|
+
cols: [
|
|
2141
|
+
{ v: "Agent", tip: "The staff that runs the factory." },
|
|
2142
|
+
{
|
|
2143
|
+
v: "Commands",
|
|
2144
|
+
tip: "Native slash-command surface for this factory's flows.",
|
|
2145
|
+
},
|
|
2146
|
+
{ v: "Skills", tip: "The factory's skills are installed." },
|
|
2147
|
+
{
|
|
2148
|
+
v: "Hooks enforce",
|
|
2149
|
+
tip: "Agent-level hooks actually fire for this agent in this factory.",
|
|
2150
|
+
},
|
|
2151
|
+
{
|
|
2152
|
+
v: "Unattended",
|
|
2153
|
+
tip: "Proven to complete a work item with no human in the loop.",
|
|
2154
|
+
},
|
|
2155
|
+
{ v: "Verdict" },
|
|
2156
|
+
],
|
|
2157
|
+
rows: rows.map(r => [
|
|
2158
|
+
{ t: "mono", v: r[0] },
|
|
2159
|
+
fCell(r[1]),
|
|
2160
|
+
fCell(r[2]),
|
|
2161
|
+
fCell(r[3]),
|
|
2162
|
+
fCell(r[4]),
|
|
2163
|
+
fCell(r[5]),
|
|
2164
|
+
]),
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function fEnv(rows) {
|
|
2169
|
+
return {
|
|
2170
|
+
type: "table",
|
|
2171
|
+
card: "Environment readiness — where can this factory run?",
|
|
2172
|
+
note: "Interactive OAuth is the usual killer: a factory that is green locally can be red headless for reasons no config inspection reveals.",
|
|
2173
|
+
cols: [
|
|
2174
|
+
{ v: "Where it runs" },
|
|
2175
|
+
{
|
|
2176
|
+
v: "Credentials",
|
|
2177
|
+
tip: "The factory's tokens and keys are present in this environment.",
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
v: "Interactive auth",
|
|
2181
|
+
tip: "Integrations that require a human OAuth session are reachable here.",
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
v: "Gate enforcement",
|
|
2185
|
+
tip: "The gates for this factory still fire in this environment.",
|
|
2186
|
+
},
|
|
2187
|
+
{ v: "Tooling", tip: "Every provider above is reachable here." },
|
|
2188
|
+
{ v: "Verdict" },
|
|
2189
|
+
],
|
|
2190
|
+
rows: rows.map(r => [
|
|
2191
|
+
{ t: "text", v: r[0] },
|
|
2192
|
+
fCell(r[1]),
|
|
2193
|
+
fCell(r[2]),
|
|
2194
|
+
fCell(r[3]),
|
|
2195
|
+
fCell(r[4]),
|
|
2196
|
+
fCell(r[5]),
|
|
2197
|
+
]),
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
function fLegend(text) {
|
|
2202
|
+
return { type: "callout", text: text };
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/* ===== Staff: which coding agent runs this factory =====================
|
|
2206
|
+
PROPOSED: per-agent factory readiness.
|
|
2207
|
+
1. What to build — every factory page is read FOR A PARTICULAR AGENT.
|
|
2208
|
+
The factories are the production line; the coding agents are the
|
|
2209
|
+
staff that run them. A factory that is fully wired for Claude can
|
|
2210
|
+
be partly unusable for Codex, because the agent lacks a surface —
|
|
2211
|
+
not because anything is misconfigured. Selecting an agent must
|
|
2212
|
+
repaint the page against that agent's real capabilities.
|
|
2213
|
+
2. Why — "is this factory ready?" is not answerable without naming the
|
|
2214
|
+
staff. Today the console implies one universal answer, which is
|
|
2215
|
+
wrong in a fleet: a green agent-hook cell is a lie for an agent
|
|
2216
|
+
whose hooks never fire.
|
|
2217
|
+
3. Today — the harness setting installs surfaces for every agent, and
|
|
2218
|
+
the parity subsystem knows which capabilities each one lacks, but
|
|
2219
|
+
none of that is surfaced as readiness. Nothing tells an operator
|
|
2220
|
+
that a gate they are relying on is inert for the agent that will
|
|
2221
|
+
actually run tonight.
|
|
2222
|
+
4. Replaces — nothing. This reads the same parity knowledge that
|
|
2223
|
+
already governs artifact generation.
|
|
2224
|
+
DISTINCTION worth preserving: a capability GAP (this agent cannot do
|
|
2225
|
+
this at all) is not a configuration gap (this is not set up yet). The
|
|
2226
|
+
first is never actionable by the operator, so it renders n/a with the
|
|
2227
|
+
reason — never red, which would imply something to fix. */
|
|
2228
|
+
const AGENTS = [
|
|
2229
|
+
"claude",
|
|
2230
|
+
"codex",
|
|
2231
|
+
"cursor",
|
|
2232
|
+
"agy",
|
|
2233
|
+
"copilot",
|
|
2234
|
+
"opencode",
|
|
2235
|
+
];
|
|
2236
|
+
/* agentHooks is tri-state:
|
|
2237
|
+
true — hooks fire. Whatever invocation flags that requires are
|
|
2238
|
+
Lisa's problem, not the operator's, so they live in
|
|
2239
|
+
code comments rather than on screen.
|
|
2240
|
+
"unverified"— docs neither confirm nor deny; last empirical check
|
|
2241
|
+
showed no fire. Never claim either way without proof.
|
|
2242
|
+
false — genuinely unsupported by the agent.
|
|
2243
|
+
Verified against vendor docs 2026-07-24. Codex and Copilot both moved:
|
|
2244
|
+
they previously appeared to have no headless hook support, and both now
|
|
2245
|
+
document it. Re-check on a cadence — this is the fastest-moving surface
|
|
2246
|
+
in the whole console, and a stale "false" here silently tells an
|
|
2247
|
+
operator a gate is inert when it is not. */
|
|
2248
|
+
const AGENT_CAPS = {
|
|
2249
|
+
claude: {
|
|
2250
|
+
label: "Claude Code",
|
|
2251
|
+
agentHooks: true,
|
|
2252
|
+
commands: true,
|
|
2253
|
+
},
|
|
2254
|
+
codex: {
|
|
2255
|
+
// Hooks fire in `codex exec`, but only when the invocation passes
|
|
2256
|
+
// `--dangerously-bypass-hook-trust` (hook trust is granted
|
|
2257
|
+
// interactively, so an unattended run without the flag silently runs
|
|
2258
|
+
// zero hooks while still reporting success). That is Lisa's job to
|
|
2259
|
+
// get right in how it invokes Codex — not something to surface to an
|
|
2260
|
+
// operator, who has no action to take. Kept here as an
|
|
2261
|
+
// implementation note for whoever wires the invocation.
|
|
2262
|
+
label: "Codex",
|
|
2263
|
+
agentHooks: true,
|
|
2264
|
+
commands: false,
|
|
2265
|
+
},
|
|
2266
|
+
cursor: {
|
|
2267
|
+
label: "Cursor",
|
|
2268
|
+
agentHooks: true,
|
|
2269
|
+
commands: true,
|
|
2270
|
+
},
|
|
2271
|
+
agy: {
|
|
2272
|
+
label: "Antigravity",
|
|
2273
|
+
agentHooks: "unverified",
|
|
2274
|
+
commands: true,
|
|
2275
|
+
hookWhy:
|
|
2276
|
+
"Antigravity documents plugin hooks (hooks.json) and a headless mode, but nothing states whether hooks fire in a non-TTY run, and the last empirical check showed they did not. Treat as unproven until a run proves otherwise.",
|
|
2277
|
+
},
|
|
2278
|
+
copilot: {
|
|
2279
|
+
label: "Copilot",
|
|
2280
|
+
agentHooks: true,
|
|
2281
|
+
commands: true,
|
|
2282
|
+
},
|
|
2283
|
+
opencode: {
|
|
2284
|
+
label: "OpenCode",
|
|
2285
|
+
agentHooks: true,
|
|
2286
|
+
commands: true,
|
|
2287
|
+
},
|
|
2288
|
+
};
|
|
2289
|
+
/* The currently-selected staff. Every factory page renders against it. */
|
|
2290
|
+
DATA.staff = "claude";
|
|
2291
|
+
function currentCaps() {
|
|
2292
|
+
return AGENT_CAPS[DATA.staff] || AGENT_CAPS.claude;
|
|
2293
|
+
}
|
|
2294
|
+
function fStaffPicker() {
|
|
2295
|
+
return { type: "staff" };
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
DATA.sections["factory-research"] = {
|
|
2299
|
+
title: "Research factory",
|
|
2300
|
+
desc: "Turns a problem or an idea into a PRD, and files it in the project's PRD source. Everything downstream starts here, so this factory needs two connections to be useful: somewhere to write the PRD, and enough project knowledge to write a good one.",
|
|
2301
|
+
src: ".lisa.config.json prd.* · source.*",
|
|
2302
|
+
blocks: [
|
|
2303
|
+
fStaffPicker(),
|
|
2304
|
+
/* PROPOSED: command renames — /lisa:setup:automations and
|
|
2305
|
+
/lisa:verify:automation (canonical marker; the console uses the
|
|
2306
|
+
new names EVERYWHERE, including on otherwise-real pages)
|
|
2307
|
+
------------------------------------------------------------
|
|
2308
|
+
1. What to build — rename two commands onto the standard
|
|
2309
|
+
<namespace>:<verb>:<noun> shape:
|
|
2310
|
+
/lisa:setup-automations → /lisa:setup:automations
|
|
2311
|
+
/lisa:automation-status → /lisa:verify:automation
|
|
2312
|
+
Ship the new names with the old ones kept as deprecated
|
|
2313
|
+
aliases that warn once, then retire the aliases.
|
|
2314
|
+
2. Why — every other surface is verb-scoped and colon-separated
|
|
2315
|
+
(/lisa:setup:jira, /lisa:setup:github, /lisa:improve:tests).
|
|
2316
|
+
These two are the odd ones out: one uses a hyphen where a
|
|
2317
|
+
colon belongs, the other is a bare noun-phrase with no verb,
|
|
2318
|
+
which makes it unguessable and sorts it away from its siblings.
|
|
2319
|
+
3. Today — both ship under their old names, and the old names are
|
|
2320
|
+
what the skills, docs and runbooks reference. Renaming means
|
|
2321
|
+
updating every caller, not just the skill directory: grep for
|
|
2322
|
+
both strings across .claude/, .agents/, plugins/ and wiki/.
|
|
2323
|
+
4. Replaces — nothing new is being built here; this is purely a
|
|
2324
|
+
rename plus alias-and-retire.
|
|
2325
|
+
VERB (decided): `verify` is the verb for proving that something
|
|
2326
|
+
works by exercising it; `validate` stays reserved for conformance
|
|
2327
|
+
checks on documents and config (ticket/PRD gates). Every
|
|
2328
|
+
connection- and automation-proving command in this console uses
|
|
2329
|
+
verify. The full rule is written out in the PROPOSED marker on the
|
|
2330
|
+
PRD-source row of the Connections table below. */
|
|
2331
|
+
{
|
|
2332
|
+
type: "table",
|
|
2333
|
+
card: "How to run it",
|
|
2334
|
+
note: "Start the scheduled loop with /lisa:setup:automations — it registers lisa-auto-<project>-exploratory-prds and writes its runbook to .lisa/automations/. Nothing here runs on a schedule until that happens.",
|
|
2335
|
+
cols: [
|
|
2336
|
+
{ v: "Mode" },
|
|
2337
|
+
{ v: "Command" },
|
|
2338
|
+
{ v: "Cadence" },
|
|
2339
|
+
{
|
|
2340
|
+
v: "Registered",
|
|
2341
|
+
tip: "Whether a scheduler is actually invoking this. Manual runs are never registered — you invoke them.",
|
|
2342
|
+
},
|
|
2343
|
+
],
|
|
2344
|
+
rows: [
|
|
2345
|
+
[
|
|
2346
|
+
{ t: "text", v: "Scheduled (automation)" },
|
|
2347
|
+
{ t: "mono", v: "/lisa:project-ideation prd_ready=false" },
|
|
2348
|
+
{ t: "text", v: "once a day" },
|
|
2349
|
+
fCell("no", {
|
|
2350
|
+
title: "Register the daily ideation loop",
|
|
2351
|
+
sub: "Nothing is scheduling this factory today",
|
|
2352
|
+
intro:
|
|
2353
|
+
"The loop exists and the command works — no scheduler is invoking it. Until it is registered, this factory only runs when a human asks it to.",
|
|
2354
|
+
steps: [
|
|
2355
|
+
{
|
|
2356
|
+
text: "Run setup and register this loop.",
|
|
2357
|
+
cmd: "/lisa:setup:automations",
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
2361
|
+
cmd: "/lisa:verify:automation",
|
|
2362
|
+
},
|
|
2363
|
+
],
|
|
2364
|
+
foot: "Registering a loop puts it under the automation runbook contract: every run must end in exactly one of the six named outcomes, so a silent failure is itself a contract violation.",
|
|
2365
|
+
}),
|
|
2366
|
+
],
|
|
2367
|
+
[
|
|
2368
|
+
{ t: "text", v: "Manual" },
|
|
2369
|
+
{ t: "mono", v: "/lisa:research <problem>" },
|
|
2370
|
+
{ t: "text", v: "on demand" },
|
|
2371
|
+
fCell("n/a"),
|
|
2372
|
+
],
|
|
2373
|
+
],
|
|
2374
|
+
},
|
|
2375
|
+
{
|
|
2376
|
+
type: "table",
|
|
2377
|
+
card: "What a run takes",
|
|
2378
|
+
cols: [{ v: "Input" }, { v: "Required" }, { v: "What it is" }],
|
|
2379
|
+
rows: [
|
|
2380
|
+
[
|
|
2381
|
+
{ t: "mono", v: "problem" },
|
|
2382
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
2383
|
+
{
|
|
2384
|
+
t: "text",
|
|
2385
|
+
v: "The problem statement or feature idea — free text, a feasibility card, or a URL.",
|
|
2386
|
+
},
|
|
2387
|
+
],
|
|
2388
|
+
[
|
|
2389
|
+
{ t: "mono", v: "prd_ready" },
|
|
2390
|
+
{ t: "badge", v: "optional", cls: "default" },
|
|
2391
|
+
{
|
|
2392
|
+
t: "text",
|
|
2393
|
+
v: "Default false — files the PRD as a draft for a human to review. True files it ready, so PRD intake claims it without anyone looking.",
|
|
2394
|
+
},
|
|
2395
|
+
],
|
|
2396
|
+
[
|
|
2397
|
+
{ t: "mono", v: "dedupe_key" },
|
|
2398
|
+
{ t: "badge", v: "optional", cls: "default" },
|
|
2399
|
+
{
|
|
2400
|
+
t: "text",
|
|
2401
|
+
v: "A stable marker so a re-run updates the existing PRD instead of filing a duplicate.",
|
|
2402
|
+
},
|
|
2403
|
+
],
|
|
2404
|
+
],
|
|
2405
|
+
},
|
|
2406
|
+
{
|
|
2407
|
+
type: "table",
|
|
2408
|
+
card: "Connections — is the factory wired up?",
|
|
2409
|
+
note: "Local is your workstation. Headless is a CI runner or a scheduled cloud run, where no human is present to complete an OAuth prompt.",
|
|
2410
|
+
cols: [
|
|
2411
|
+
{ v: "What it needs" },
|
|
2412
|
+
{ v: "Provider" },
|
|
2413
|
+
{ v: "Local", tip: "Reachable from your workstation." },
|
|
2414
|
+
{
|
|
2415
|
+
v: "Headless",
|
|
2416
|
+
tip: "Reachable from CI or a scheduled run, with no human to authenticate.",
|
|
2417
|
+
},
|
|
2418
|
+
],
|
|
2419
|
+
rows: [
|
|
2420
|
+
[
|
|
2421
|
+
{ t: "text", v: "Somewhere to write the PRD" },
|
|
2422
|
+
{
|
|
2423
|
+
t: "select",
|
|
2424
|
+
v: "Notion",
|
|
2425
|
+
mono: true,
|
|
2426
|
+
syncKey: "source.provider",
|
|
2427
|
+
options: [
|
|
2428
|
+
"Notion",
|
|
2429
|
+
"Confluence",
|
|
2430
|
+
"Linear",
|
|
2431
|
+
"GitHub",
|
|
2432
|
+
"Google Docs",
|
|
2433
|
+
],
|
|
2434
|
+
},
|
|
2435
|
+
/* PROPOSED: /lisa:setup:prd-source and /lisa:verify:prd-source
|
|
2436
|
+
------------------------------------------------------------
|
|
2437
|
+
1. What to build — ONE vendor-neutral setup command per
|
|
2438
|
+
connection type, and a matching verify command. Each is
|
|
2439
|
+
a two-question flow:
|
|
2440
|
+
Q1 "which provider?" → Notion | Confluence | Linear |
|
|
2441
|
+
GitHub | Google Docs
|
|
2442
|
+
Q2 "where should this be set up?" → local |
|
|
2443
|
+
github-actions | claude-remote | codex-remote | …
|
|
2444
|
+
Q2 is the important half and has no equivalent today. The
|
|
2445
|
+
chosen target decides WHICH credential kind is
|
|
2446
|
+
provisioned (interactive OAuth vs. non-interactive
|
|
2447
|
+
integration token) and WHERE it is installed (OS keychain,
|
|
2448
|
+
repo secret, remote-runtime secret store). Verify takes
|
|
2449
|
+
the same target and must EXECUTE FROM it — a local pass
|
|
2450
|
+
says nothing about a headless target.
|
|
2451
|
+
The same two-question shape applies to every connection
|
|
2452
|
+
Lisa owns, so build it as one shared flow with a
|
|
2453
|
+
connection-type parameter, not five copies:
|
|
2454
|
+
/lisa:setup:prd-source · /lisa:setup:tracker ·
|
|
2455
|
+
/lisa:setup:wiki · /lisa:setup:deploy …
|
|
2456
|
+
2. Why — today setup is per-vendor and implicitly local-only,
|
|
2457
|
+
which is exactly why a factory can be green locally and
|
|
2458
|
+
red headless with nothing in config to reveal it. Making
|
|
2459
|
+
the target an explicit answer turns that silent failure
|
|
2460
|
+
into a question the operator has to answer.
|
|
2461
|
+
3. Today — per-vendor skills exist (/lisa:setup:notion,
|
|
2462
|
+
:confluence, :linear, :github, :jira) and they assume the
|
|
2463
|
+
local workstation. Verification is a differently-named,
|
|
2464
|
+
tracker-only skill (/lisa:validate-tracker-mapping) with
|
|
2465
|
+
no target concept at all.
|
|
2466
|
+
4. Replaces — fold the per-vendor setup skills in behind Q1,
|
|
2467
|
+
and rename/absorb validate-tracker-mapping as
|
|
2468
|
+
/lisa:verify:tracker so setup and verify are symmetric
|
|
2469
|
+
across every connection type.
|
|
2470
|
+
VERB (decided): `verify`, never `validate`, for anything that
|
|
2471
|
+
proves a connection. The two words are not synonyms in Lisa's
|
|
2472
|
+
vocabulary — validate inspects a spec or a config for
|
|
2473
|
+
conformance, verify proves a thing works by exercising it.
|
|
2474
|
+
A connection can only be proven by using it, so it is always
|
|
2475
|
+
verify. Keep `validate` for what it already means elsewhere:
|
|
2476
|
+
gate checks on tickets and PRDs (lisa-tracker-validate,
|
|
2477
|
+
intake's PRD validation), which examine a document rather
|
|
2478
|
+
than exercise a system. Do not sweep those.
|
|
2479
|
+
NOTE ON NAMING: this row connects the PRD *source*, which
|
|
2480
|
+
Lisa configures separately from the work *tracker*, so the
|
|
2481
|
+
command here is :prd-source. If those two concepts should
|
|
2482
|
+
merge into one "tracker" notion, do that deliberately in
|
|
2483
|
+
config first — do not just rename the command. */
|
|
2484
|
+
fCell("yes", {
|
|
2485
|
+
title: "Notion — connected locally",
|
|
2486
|
+
sub: "Reachable from your workstation",
|
|
2487
|
+
intro:
|
|
2488
|
+
"Setup and verification are the same two commands for every provider and every target — the command walks you through the rest.",
|
|
2489
|
+
steps: [
|
|
2490
|
+
{
|
|
2491
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
2492
|
+
cmd: "/lisa:setup:prd-source",
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
text: "Verify from the same target.",
|
|
2496
|
+
cmd: "/lisa:verify:prd-source",
|
|
2497
|
+
},
|
|
2498
|
+
],
|
|
2499
|
+
foot: "Config holds IDs and role names only — the token itself lives in an env var or the OS keychain, never in .lisa.config.json.",
|
|
2500
|
+
}),
|
|
2501
|
+
fCell("no", {
|
|
2502
|
+
title: "Notion — not reachable headless",
|
|
2503
|
+
sub: "Fails in CI and on a schedule",
|
|
2504
|
+
intro:
|
|
2505
|
+
"The local connection is an interactive OAuth session tied to your desktop. A CI runner or a scheduled cloud run has no browser and no human to approve the prompt, so the same factory that works here fails there — and no config inspection would reveal it.",
|
|
2506
|
+
steps: [
|
|
2507
|
+
{
|
|
2508
|
+
text: "Run setup again and choose a headless target when it asks where.",
|
|
2509
|
+
cmd: "/lisa:setup:prd-source",
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
text: "Verify from that target, not from your laptop — a local pass proves nothing about a headless one.",
|
|
2513
|
+
cmd: "/lisa:verify:prd-source",
|
|
2514
|
+
},
|
|
2515
|
+
],
|
|
2516
|
+
foot: "This is the single most common reason a factory is green locally and red in production. Interactively-authenticated integrations are simply absent in headless runs.",
|
|
2517
|
+
}),
|
|
2518
|
+
],
|
|
2519
|
+
[
|
|
2520
|
+
{ t: "text", v: "Project knowledge to write it from" },
|
|
2521
|
+
{ t: "mono", v: "Lisa LLM Wiki" },
|
|
2522
|
+
fCell("no", {
|
|
2523
|
+
title: "No knowledge source connected",
|
|
2524
|
+
sub: "The PRD writer only has the code",
|
|
2525
|
+
intro:
|
|
2526
|
+
"Without a wiki, the factory re-derives project context on every run and cannot know what was already decided, what was tried and rejected, or why. The PRDs it writes will be plausible and uninformed.",
|
|
2527
|
+
steps: [
|
|
2528
|
+
{
|
|
2529
|
+
text: "Build the wiki from everything the agent can already reach — repository, git history, connected systems.",
|
|
2530
|
+
cmd: "/lisa:agent-ready",
|
|
2531
|
+
},
|
|
2532
|
+
{
|
|
2533
|
+
text: "Answer the questions it could not resolve itself. It writes them to a gaps file for a human.",
|
|
2534
|
+
cmd: "wiki/gaps.md",
|
|
2535
|
+
},
|
|
2536
|
+
{
|
|
2537
|
+
text: "Re-run in a fresh session so the answers are absorbed. Repeat until zero gaps remain.",
|
|
2538
|
+
cmd: "/lisa:agent-ready",
|
|
2539
|
+
},
|
|
2540
|
+
],
|
|
2541
|
+
foot: "The framing that makes this work: starting tomorrow you maintain this project with zero human input — today is your only chance to ask questions.",
|
|
2542
|
+
}),
|
|
2543
|
+
fCell("no", {
|
|
2544
|
+
title: "No knowledge source connected",
|
|
2545
|
+
sub: "Same gap, headless",
|
|
2546
|
+
intro:
|
|
2547
|
+
"The wiki lives in the repository, so once it exists it is reachable everywhere the repo is — including CI and scheduled runs. This cell turns green at the same moment the local one does.",
|
|
2548
|
+
steps: [
|
|
2549
|
+
{
|
|
2550
|
+
text: "Connect it once; there is no separate headless step.",
|
|
2551
|
+
cmd: "/lisa:agent-ready",
|
|
2552
|
+
},
|
|
2553
|
+
],
|
|
2554
|
+
foot: "A git-native knowledge source is the reason this row has no local/headless split in practice — unlike every hosted integration above it.",
|
|
2555
|
+
}),
|
|
2556
|
+
],
|
|
2557
|
+
[
|
|
2558
|
+
{ t: "text", v: "The codebase as ground truth" },
|
|
2559
|
+
{ t: "mono", v: "CodySwannGT/lisa" },
|
|
2560
|
+
fCell("yes", {
|
|
2561
|
+
title: "Repository — connected locally",
|
|
2562
|
+
sub: "CodySwannGT/lisa",
|
|
2563
|
+
intro:
|
|
2564
|
+
"The factory reads the working tree directly. This is the one input that is always available and always current.",
|
|
2565
|
+
steps: [
|
|
2566
|
+
{
|
|
2567
|
+
text: "Nothing to connect — the factory runs inside the checkout.",
|
|
2568
|
+
},
|
|
2569
|
+
],
|
|
2570
|
+
}),
|
|
2571
|
+
fCell("yes", {
|
|
2572
|
+
title: "Repository — connected headless",
|
|
2573
|
+
sub: "CodySwannGT/lisa",
|
|
2574
|
+
intro:
|
|
2575
|
+
"CI and scheduled runs check the repository out before the factory starts, so the code is present with no credential beyond the checkout itself.",
|
|
2576
|
+
steps: [
|
|
2577
|
+
{
|
|
2578
|
+
text: "Nothing to connect. Note that a scheduled run sees the default branch, not your local branch.",
|
|
2579
|
+
},
|
|
2580
|
+
],
|
|
2581
|
+
}),
|
|
2582
|
+
],
|
|
2583
|
+
],
|
|
2584
|
+
},
|
|
2585
|
+
fLegend(
|
|
2586
|
+
'<b>Without a context source, the PRDs are guesses.</b> The factory can reach the code but not the project\'s accumulated knowledge, so it re-derives context every run and cannot know what was already decided and why. Connect a wiki with <span class="mono">/lisa:agent-ready</span>, or point this factory at another knowledge source. Switching the PRD destination above changes where every PRD this factory produces is filed — the lifecycle roles for the new provider are configured on the <b>PRD source</b> page.'
|
|
2587
|
+
),
|
|
2588
|
+
],
|
|
2589
|
+
};
|
|
2590
|
+
|
|
2591
|
+
DATA.sections["factory-plan"] = {
|
|
2592
|
+
title: "Plan factory",
|
|
2593
|
+
desc: "Turns one PRD into ordered work items the Implement factory can dispatch without a human re-reading the PRD. It reads from the PRD source and writes to the tracker, so it is the one factory that needs both connected at once.",
|
|
2594
|
+
src: ".lisa.config.json tracker.* · source.*",
|
|
2595
|
+
blocks: [
|
|
2596
|
+
fStaffPicker(),
|
|
2597
|
+
{
|
|
2598
|
+
type: "table",
|
|
2599
|
+
card: "How to run it",
|
|
2600
|
+
note: "The scheduled mode is the PRD-side intake loop: each cycle claims one PRD marked ready — by a human, or by the Product Planning loop — and decomposes it. Register it with /lisa:setup:automations.",
|
|
2601
|
+
cols: [
|
|
2602
|
+
{ v: "Mode" },
|
|
2603
|
+
{ v: "Command" },
|
|
2604
|
+
{ v: "Cadence" },
|
|
2605
|
+
{
|
|
2606
|
+
v: "Registered",
|
|
2607
|
+
tip: "Whether a scheduler is actually invoking this. Manual runs are never registered — you invoke them.",
|
|
2608
|
+
},
|
|
2609
|
+
],
|
|
2610
|
+
rows: [
|
|
2611
|
+
[
|
|
2612
|
+
{ t: "text", v: "Scheduled (automation)" },
|
|
2613
|
+
{ t: "mono", v: "/lisa:intake <queue>" },
|
|
2614
|
+
{ t: "text", v: "every 60 minutes" },
|
|
2615
|
+
fCell("no", {
|
|
2616
|
+
title: "Register the PRD intake loop",
|
|
2617
|
+
sub: "Nothing is claiming ready PRDs today",
|
|
2618
|
+
intro:
|
|
2619
|
+
"Each cycle takes one PRD marked ready — by a human promoting a draft, or by the Product Planning loop filing one — and runs it through this factory. Until it is registered, a ready PRD sits there until someone notices.",
|
|
2620
|
+
steps: [
|
|
2621
|
+
{
|
|
2622
|
+
text: "Run setup and register this loop.",
|
|
2623
|
+
cmd: "/lisa:setup:automations",
|
|
2624
|
+
},
|
|
2625
|
+
{
|
|
2626
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
2627
|
+
cmd: "/lisa:verify:automation",
|
|
2628
|
+
},
|
|
2629
|
+
],
|
|
2630
|
+
foot: "Registering will not be enough on its own here: the loop runs headless, and this factory cannot read the PRD source headless yet. Fix that row in Connections below or every cycle will fail the same way.",
|
|
2631
|
+
}),
|
|
2632
|
+
],
|
|
2633
|
+
[
|
|
2634
|
+
{ t: "text", v: "Manual" },
|
|
2635
|
+
{ t: "mono", v: "/lisa:plan <source>" },
|
|
2636
|
+
{ t: "text", v: "on demand" },
|
|
2637
|
+
fCell("n/a"),
|
|
2638
|
+
],
|
|
2639
|
+
],
|
|
2640
|
+
},
|
|
2641
|
+
{
|
|
2642
|
+
type: "table",
|
|
2643
|
+
card: "What a run takes",
|
|
2644
|
+
note: "Each name below is the placeholder you see in the commands above. The scheduled loop takes a queue and resolves one PRD from it; the manual command takes that PRD directly.",
|
|
2645
|
+
cols: [{ v: "Input" }, { v: "Required" }, { v: "What it is" }],
|
|
2646
|
+
rows: [
|
|
2647
|
+
[
|
|
2648
|
+
{ t: "mono", v: "source" },
|
|
2649
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
2650
|
+
{
|
|
2651
|
+
t: "text",
|
|
2652
|
+
v: "Manual runs. A URL to a PRD living in the PRD provider.",
|
|
2653
|
+
},
|
|
2654
|
+
],
|
|
2655
|
+
[
|
|
2656
|
+
{ t: "mono", v: "queue" },
|
|
2657
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
2658
|
+
{
|
|
2659
|
+
t: "text",
|
|
2660
|
+
v: "Scheduled runs. A queue of PRDs in the PRD provider.",
|
|
2661
|
+
},
|
|
2662
|
+
],
|
|
2663
|
+
],
|
|
2664
|
+
},
|
|
2665
|
+
{
|
|
2666
|
+
type: "table",
|
|
2667
|
+
card: "Connections — is the factory wired up?",
|
|
2668
|
+
note: "This factory reads from one system and writes to another, so it needs both. Local is your workstation; headless is a CI runner or a scheduled cloud run, where no human is present to complete an OAuth prompt.",
|
|
2669
|
+
cols: [
|
|
2670
|
+
{ v: "What it needs" },
|
|
2671
|
+
{ v: "Provider" },
|
|
2672
|
+
{ v: "Local", tip: "Reachable from your workstation." },
|
|
2673
|
+
{
|
|
2674
|
+
v: "Headless",
|
|
2675
|
+
tip: "Reachable from CI or a scheduled run, with no human to authenticate.",
|
|
2676
|
+
},
|
|
2677
|
+
],
|
|
2678
|
+
rows: [
|
|
2679
|
+
[
|
|
2680
|
+
/* Same config value as the Research factory's output — bound
|
|
2681
|
+
through syncKey so the two dropdowns are one setting and
|
|
2682
|
+
cannot drift apart. Changing it here changes it there. */
|
|
2683
|
+
{ t: "text", v: "The PRD to decompose" },
|
|
2684
|
+
{
|
|
2685
|
+
t: "select",
|
|
2686
|
+
v: "Notion",
|
|
2687
|
+
mono: true,
|
|
2688
|
+
syncKey: "source.provider",
|
|
2689
|
+
options: [
|
|
2690
|
+
"Notion",
|
|
2691
|
+
"Confluence",
|
|
2692
|
+
"Linear",
|
|
2693
|
+
"GitHub",
|
|
2694
|
+
"Google Docs",
|
|
2695
|
+
],
|
|
2696
|
+
},
|
|
2697
|
+
fCell("yes", {
|
|
2698
|
+
title: "Notion — readable locally",
|
|
2699
|
+
sub: "Reachable from your workstation",
|
|
2700
|
+
intro:
|
|
2701
|
+
"Plan reads the PRD from the same source the Research factory writes it to. One connection, two factories — set it up once.",
|
|
2702
|
+
steps: [
|
|
2703
|
+
{
|
|
2704
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
2705
|
+
cmd: "/lisa:setup:prd-source",
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
text: "Verify from the same target.",
|
|
2709
|
+
cmd: "/lisa:verify:prd-source",
|
|
2710
|
+
},
|
|
2711
|
+
],
|
|
2712
|
+
foot: "This is the same setting as the Research factory's output, not a second copy of it — changing it here changes it there. One provider holds the PRDs; one factory writes them and this one reads them.",
|
|
2713
|
+
}),
|
|
2714
|
+
fCell("no", {
|
|
2715
|
+
title: "Notion — not readable headless",
|
|
2716
|
+
sub: "This is what keeps Plan manual-only",
|
|
2717
|
+
intro:
|
|
2718
|
+
"The scheduled loop runs with no human present, so it cannot complete an OAuth prompt — which means it can never claim a ready PRD. Registering the automation will not help until this is fixed; it will just fail every hour instead of never running.",
|
|
2719
|
+
steps: [
|
|
2720
|
+
{
|
|
2721
|
+
text: "Run setup again and choose a headless target when it asks where.",
|
|
2722
|
+
cmd: "/lisa:setup:prd-source",
|
|
2723
|
+
},
|
|
2724
|
+
{
|
|
2725
|
+
text: "Verify from that target, not from your laptop — a local pass proves nothing about a headless one.",
|
|
2726
|
+
cmd: "/lisa:verify:prd-source",
|
|
2727
|
+
},
|
|
2728
|
+
],
|
|
2729
|
+
foot: "Red here has a bigger blast radius than on the Research page: Research can still be run by hand, but an unattended pipeline stalls at this exact point.",
|
|
2730
|
+
}),
|
|
2731
|
+
],
|
|
2732
|
+
[
|
|
2733
|
+
/* PROPOSED: /lisa:setup:tracker and /lisa:verify:tracker —
|
|
2734
|
+
the same two-question (provider, then target) flow specified
|
|
2735
|
+
in the canonical marker on the Research factory page. The
|
|
2736
|
+
tracker instance also absorbs today's
|
|
2737
|
+
validate-tracker-mapping, so verifying a tracker must check
|
|
2738
|
+
the status-name mapping AND exercise the connection from the
|
|
2739
|
+
chosen target. */
|
|
2740
|
+
{ t: "text", v: "Somewhere to write the work items" },
|
|
2741
|
+
{
|
|
2742
|
+
t: "select",
|
|
2743
|
+
v: "JIRA",
|
|
2744
|
+
mono: true,
|
|
2745
|
+
syncKey: "tracker.provider",
|
|
2746
|
+
options: ["JIRA", "GitHub", "Linear"],
|
|
2747
|
+
},
|
|
2748
|
+
fCell("yes", {
|
|
2749
|
+
title: "JIRA — connected locally",
|
|
2750
|
+
sub: "Reachable from your workstation",
|
|
2751
|
+
intro:
|
|
2752
|
+
"Work items written from this machine reach the configured JIRA project, with the status names mapped onto Lisa's lifecycle roles.",
|
|
2753
|
+
steps: [
|
|
2754
|
+
{
|
|
2755
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
2756
|
+
cmd: "/lisa:setup:tracker",
|
|
2757
|
+
},
|
|
2758
|
+
{
|
|
2759
|
+
text: "Verify from the same target.",
|
|
2760
|
+
cmd: "/lisa:verify:tracker",
|
|
2761
|
+
},
|
|
2762
|
+
],
|
|
2763
|
+
foot: "Verifying the tracker also checks the status-name mapping, which drifts silently: JQL matches case-insensitively, so a config that says Done and a project that says DONE will look fine right up until a transition fails.",
|
|
2764
|
+
}),
|
|
2765
|
+
fCell("yes", {
|
|
2766
|
+
title: "JIRA — connected headless",
|
|
2767
|
+
sub: "Works in CI and on a schedule",
|
|
2768
|
+
intro:
|
|
2769
|
+
"The tracker connection uses an API token rather than an interactive session, so it survives in an environment with no human. This is what the PRD source row above still needs.",
|
|
2770
|
+
steps: [
|
|
2771
|
+
{
|
|
2772
|
+
text: "Run setup and choose the headless target you want.",
|
|
2773
|
+
cmd: "/lisa:setup:tracker",
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
text: "Verify from that target.",
|
|
2777
|
+
cmd: "/lisa:verify:tracker",
|
|
2778
|
+
},
|
|
2779
|
+
],
|
|
2780
|
+
foot: "Config holds the site, project key and role mapping only — the token itself lives in that target's secret store.",
|
|
2781
|
+
}),
|
|
2782
|
+
],
|
|
2783
|
+
[
|
|
2784
|
+
{ t: "text", v: "Project knowledge to size and sequence" },
|
|
2785
|
+
{ t: "mono", v: "Lisa LLM Wiki" },
|
|
2786
|
+
fCell("no", {
|
|
2787
|
+
title: "No knowledge source connected",
|
|
2788
|
+
sub: "Decomposition is guesswork without it",
|
|
2789
|
+
intro:
|
|
2790
|
+
"Splitting a PRD into the right work items depends on knowing how this project is actually built — what exists, what is shared, what was already tried. Without a wiki the factory re-derives that every run and produces plausible tickets that cut across the wrong seams.",
|
|
2791
|
+
steps: [
|
|
2792
|
+
{
|
|
2793
|
+
text: "Build the wiki from everything the agent can already reach — repository, git history, connected systems.",
|
|
2794
|
+
cmd: "/lisa:agent-ready",
|
|
2795
|
+
},
|
|
2796
|
+
{
|
|
2797
|
+
text: "Answer the questions it could not resolve itself. It writes them to a gaps file for a human.",
|
|
2798
|
+
cmd: "wiki/gaps.md",
|
|
2799
|
+
},
|
|
2800
|
+
{
|
|
2801
|
+
text: "Re-run in a fresh session so the answers are absorbed. Repeat until zero gaps remain.",
|
|
2802
|
+
cmd: "/lisa:agent-ready",
|
|
2803
|
+
},
|
|
2804
|
+
],
|
|
2805
|
+
foot: "The framing that makes this work: starting tomorrow you maintain this project with zero human input — today is your only chance to ask questions.",
|
|
2806
|
+
}),
|
|
2807
|
+
fCell("no", {
|
|
2808
|
+
title: "No knowledge source connected",
|
|
2809
|
+
sub: "Same gap, headless",
|
|
2810
|
+
intro:
|
|
2811
|
+
"The wiki lives in the repository, so once it exists it is reachable everywhere the repo is — including CI and scheduled runs. This cell turns green at the same moment the local one does.",
|
|
2812
|
+
steps: [
|
|
2813
|
+
{
|
|
2814
|
+
text: "Connect it once; there is no separate headless step.",
|
|
2815
|
+
cmd: "/lisa:agent-ready",
|
|
2816
|
+
},
|
|
2817
|
+
],
|
|
2818
|
+
foot: "A git-native knowledge source is the reason this row has no local/headless split in practice — unlike every hosted integration above it.",
|
|
2819
|
+
}),
|
|
2820
|
+
],
|
|
2821
|
+
[
|
|
2822
|
+
{ t: "text", v: "The codebase as ground truth" },
|
|
2823
|
+
{ t: "mono", v: "CodySwannGT/lisa" },
|
|
2824
|
+
fCell("yes", {
|
|
2825
|
+
title: "Repository — connected locally",
|
|
2826
|
+
sub: "CodySwannGT/lisa",
|
|
2827
|
+
intro:
|
|
2828
|
+
"The factory reads the working tree directly. This is the one input that is always available and always current.",
|
|
2829
|
+
steps: [
|
|
2830
|
+
{
|
|
2831
|
+
text: "Nothing to connect — the factory runs inside the checkout.",
|
|
2832
|
+
},
|
|
2833
|
+
],
|
|
2834
|
+
}),
|
|
2835
|
+
fCell("yes", {
|
|
2836
|
+
title: "Repository — connected headless",
|
|
2837
|
+
sub: "CodySwannGT/lisa",
|
|
2838
|
+
intro:
|
|
2839
|
+
"CI and scheduled runs check the repository out before the factory starts, so the code is present with no credential beyond the checkout itself.",
|
|
2840
|
+
steps: [
|
|
2841
|
+
{
|
|
2842
|
+
text: "Nothing to connect. Note that a scheduled run sees the default branch, not your local branch.",
|
|
2843
|
+
},
|
|
2844
|
+
],
|
|
2845
|
+
}),
|
|
2846
|
+
],
|
|
2847
|
+
],
|
|
2848
|
+
},
|
|
2849
|
+
{
|
|
2850
|
+
type: "table",
|
|
2851
|
+
card: "PRD lifecycle roles",
|
|
2852
|
+
note: "How a PRD's state is represented in the source. Lisa reasons in roles; each provider spells them differently — Notion uses a status property, Confluence uses parent pages, Linear and GitHub use labels. Created idempotently by the provider's setup command.",
|
|
2853
|
+
cols: [
|
|
2854
|
+
{ v: "Role", tip: "Lisa's vendor-neutral name for the state." },
|
|
2855
|
+
{
|
|
2856
|
+
v: "As stored",
|
|
2857
|
+
tip: "What the configured provider actually records. Changes with the PRD source.",
|
|
2858
|
+
},
|
|
2859
|
+
{ v: "What it means" },
|
|
2860
|
+
{ v: "Advanced by" },
|
|
2861
|
+
],
|
|
2862
|
+
rows: [
|
|
2863
|
+
[
|
|
2864
|
+
{ t: "mono", v: "draft" },
|
|
2865
|
+
{ t: "mono", v: "prd-draft" },
|
|
2866
|
+
{
|
|
2867
|
+
t: "text",
|
|
2868
|
+
v: "Being written. No factory touches it — this is the human's workspace.",
|
|
2869
|
+
},
|
|
2870
|
+
{ t: "text", v: "a human" },
|
|
2871
|
+
],
|
|
2872
|
+
[
|
|
2873
|
+
{ t: "mono", v: "ready" },
|
|
2874
|
+
{ t: "mono", v: "prd-ready" },
|
|
2875
|
+
{
|
|
2876
|
+
t: "text",
|
|
2877
|
+
v: "The gate. Whoever set it is asserting the PRD is ready to be built from; the Plan factory may now claim it. Set by a human promoting a draft, or by the Product Planning loop filing an ideated PRD ready.",
|
|
2878
|
+
},
|
|
2879
|
+
{ t: "text", v: "a human or a loop" },
|
|
2880
|
+
],
|
|
2881
|
+
[
|
|
2882
|
+
{ t: "mono", v: "in_review" },
|
|
2883
|
+
{ t: "mono", v: "prd-in-review" },
|
|
2884
|
+
{
|
|
2885
|
+
t: "text",
|
|
2886
|
+
v: "This factory has claimed it and is validating. Prevents a second cycle claiming the same PRD.",
|
|
2887
|
+
},
|
|
2888
|
+
{ t: "text", v: "Plan" },
|
|
2889
|
+
],
|
|
2890
|
+
[
|
|
2891
|
+
{ t: "mono", v: "blocked" },
|
|
2892
|
+
{ t: "mono", v: "prd-blocked" },
|
|
2893
|
+
{
|
|
2894
|
+
t: "text",
|
|
2895
|
+
v: "Validation failed — the PRD is ambiguous, or the factory cannot prove access to tooling it needs. Clarifying questions are posted for a human.",
|
|
2896
|
+
},
|
|
2897
|
+
{ t: "text", v: "Plan" },
|
|
2898
|
+
],
|
|
2899
|
+
[
|
|
2900
|
+
{ t: "mono", v: "ticketed" },
|
|
2901
|
+
{ t: "mono", v: "prd-ticketed" },
|
|
2902
|
+
{
|
|
2903
|
+
t: "text",
|
|
2904
|
+
v: "Decomposed into work items. This factory is done with it; the Implement factory takes over from the tickets.",
|
|
2905
|
+
},
|
|
2906
|
+
{ t: "text", v: "Plan" },
|
|
2907
|
+
],
|
|
2908
|
+
[
|
|
2909
|
+
{ t: "mono", v: "shipped" },
|
|
2910
|
+
{ t: "mono", v: "prd-shipped" },
|
|
2911
|
+
{
|
|
2912
|
+
t: "text",
|
|
2913
|
+
v: "Every work item is done. Awaiting verification.",
|
|
2914
|
+
},
|
|
2915
|
+
{ t: "text", v: "Implement" },
|
|
2916
|
+
],
|
|
2917
|
+
[
|
|
2918
|
+
{ t: "mono", v: "verified" },
|
|
2919
|
+
{ t: "mono", v: "prd-verified" },
|
|
2920
|
+
{
|
|
2921
|
+
t: "text",
|
|
2922
|
+
v: "UAT passed. The terminal state — the only one that means the PRD actually happened.",
|
|
2923
|
+
},
|
|
2924
|
+
{ t: "text", v: "Verify" },
|
|
2925
|
+
],
|
|
2926
|
+
[
|
|
2927
|
+
{ t: "mono", v: "sentinel" },
|
|
2928
|
+
{ t: "mono", v: "prd-sentinel" },
|
|
2929
|
+
{
|
|
2930
|
+
t: "text",
|
|
2931
|
+
v: "Marks the feedback item that carries blocked comments back to a human, on providers with nowhere else to put them.",
|
|
2932
|
+
},
|
|
2933
|
+
{ t: "text", v: "Plan" },
|
|
2934
|
+
],
|
|
2935
|
+
],
|
|
2936
|
+
},
|
|
2937
|
+
fLegend(
|
|
2938
|
+
"<b>The failure nobody sees is a requirement that never became a ticket.</b> Every other gate here checks the quality of the work items that exist; none of them notices one that is missing. Coverage — every PRD requirement traceable to at least one work item — is the only check that catches it, and it currently runs as an advisory audit rather than a gate."
|
|
2939
|
+
),
|
|
2940
|
+
],
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
DATA.sections["factory-implement"] = {
|
|
2944
|
+
title: "Implement factory",
|
|
2945
|
+
desc: "Turns one work item into merged, deployed software. This is where the gates live: almost everything Lisa enforces fires somewhere in this factory, and the thresholds below are the numbers those gates compare against.",
|
|
2946
|
+
src: ".lisa.config.json quality.* · ci.* · hooks.*",
|
|
2947
|
+
blocks: [
|
|
2948
|
+
fStaffPicker(),
|
|
2949
|
+
{
|
|
2950
|
+
type: "table",
|
|
2951
|
+
card: "How to run it",
|
|
2952
|
+
note: "The scheduled mode is the build-side intake loop: each cycle claims one work item marked ready — by a human, or by another factory or loop — and builds it end to end. Register it with /lisa:setup:automations.",
|
|
2953
|
+
cols: [
|
|
2954
|
+
{ v: "Mode" },
|
|
2955
|
+
{ v: "Command" },
|
|
2956
|
+
{ v: "Cadence" },
|
|
2957
|
+
{
|
|
2958
|
+
v: "Registered",
|
|
2959
|
+
tip: "Whether a scheduler is actually invoking this. Manual runs are never registered — you invoke them.",
|
|
2960
|
+
},
|
|
2961
|
+
],
|
|
2962
|
+
rows: [
|
|
2963
|
+
[
|
|
2964
|
+
{ t: "text", v: "Scheduled (automation)" },
|
|
2965
|
+
{ t: "mono", v: "/lisa:intake <queue>" },
|
|
2966
|
+
{ t: "text", v: "every 10 minutes" },
|
|
2967
|
+
fCell("no", {
|
|
2968
|
+
title: "Register the build intake loop",
|
|
2969
|
+
sub: "Nothing is claiming ready work items today",
|
|
2970
|
+
intro:
|
|
2971
|
+
"Each cycle takes one work item marked ready and runs it through this factory. Most of them are filed by other factories and loops rather than by a person. Until it is registered, a ready ticket sits there until someone notices.",
|
|
2972
|
+
steps: [
|
|
2973
|
+
{
|
|
2974
|
+
text: "Run setup and register this loop.",
|
|
2975
|
+
cmd: "/lisa:setup:automations",
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
2979
|
+
cmd: "/lisa:verify:automation",
|
|
2980
|
+
},
|
|
2981
|
+
],
|
|
2982
|
+
foot: "This is the fastest loop in the pipeline for a reason: it is the only one whose input queue humans and every other factory feed continuously.",
|
|
2983
|
+
}),
|
|
2984
|
+
],
|
|
2985
|
+
[
|
|
2986
|
+
{ t: "text", v: "Manual" },
|
|
2987
|
+
{ t: "mono", v: "/lisa:implement <work-item>" },
|
|
2988
|
+
{ t: "text", v: "on demand" },
|
|
2989
|
+
fCell("n/a"),
|
|
2990
|
+
],
|
|
2991
|
+
],
|
|
2992
|
+
},
|
|
2993
|
+
{
|
|
2994
|
+
type: "table",
|
|
2995
|
+
card: "What a run takes",
|
|
2996
|
+
note: "Each name below is the placeholder you see in the commands above.",
|
|
2997
|
+
cols: [{ v: "Input" }, { v: "Required" }, { v: "What it is" }],
|
|
2998
|
+
rows: [
|
|
2999
|
+
[
|
|
3000
|
+
{ t: "mono", v: "work-item" },
|
|
3001
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
3002
|
+
{
|
|
3003
|
+
t: "text",
|
|
3004
|
+
v: "Manual runs. A URL to a work item living in the tracker, or a plain description of the work.",
|
|
3005
|
+
},
|
|
3006
|
+
],
|
|
3007
|
+
[
|
|
3008
|
+
{ t: "mono", v: "queue" },
|
|
3009
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
3010
|
+
{
|
|
3011
|
+
t: "text",
|
|
3012
|
+
v: "Scheduled runs. A queue of work items in the tracker.",
|
|
3013
|
+
},
|
|
3014
|
+
],
|
|
3015
|
+
],
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
type: "table",
|
|
3019
|
+
card: "Connections — is the factory wired up?",
|
|
3020
|
+
note: "Local is your workstation. Headless is a CI runner or a scheduled cloud run, where no human is present to complete an OAuth prompt.",
|
|
3021
|
+
cols: [
|
|
3022
|
+
{ v: "What it needs" },
|
|
3023
|
+
{ v: "Provider" },
|
|
3024
|
+
{ v: "Local", tip: "Reachable from your workstation." },
|
|
3025
|
+
{
|
|
3026
|
+
v: "Headless",
|
|
3027
|
+
tip: "Reachable from CI or a scheduled run, with no human to authenticate.",
|
|
3028
|
+
},
|
|
3029
|
+
],
|
|
3030
|
+
rows: [
|
|
3031
|
+
[
|
|
3032
|
+
{ t: "text", v: "The work item to build" },
|
|
3033
|
+
{
|
|
3034
|
+
t: "select",
|
|
3035
|
+
v: "JIRA",
|
|
3036
|
+
mono: true,
|
|
3037
|
+
syncKey: "tracker.provider",
|
|
3038
|
+
options: ["JIRA", "GitHub", "Linear"],
|
|
3039
|
+
},
|
|
3040
|
+
fCell("yes", {
|
|
3041
|
+
title: "JIRA — connected locally",
|
|
3042
|
+
sub: "Reachable from your workstation",
|
|
3043
|
+
intro:
|
|
3044
|
+
"This factory reads the work item the Plan factory wrote, and posts progress and evidence back to it. Same connection, both directions.",
|
|
3045
|
+
steps: [
|
|
3046
|
+
{
|
|
3047
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
3048
|
+
cmd: "/lisa:setup:tracker",
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
text: "Verify from the same target.",
|
|
3052
|
+
cmd: "/lisa:verify:tracker",
|
|
3053
|
+
},
|
|
3054
|
+
],
|
|
3055
|
+
foot: "This is the same setting as the Plan factory's output, not a second copy of it — changing it here changes it there.",
|
|
3056
|
+
}),
|
|
3057
|
+
fCell("yes", {
|
|
3058
|
+
title: "JIRA — connected headless",
|
|
3059
|
+
sub: "Works in CI and on a schedule",
|
|
3060
|
+
intro:
|
|
3061
|
+
"The tracker connection uses an API token rather than an interactive session, so the build loop can claim and close work items with no human present.",
|
|
3062
|
+
steps: [
|
|
3063
|
+
{
|
|
3064
|
+
text: "Run setup and choose the headless target you want.",
|
|
3065
|
+
cmd: "/lisa:setup:tracker",
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
text: "Verify from that target.",
|
|
3069
|
+
cmd: "/lisa:verify:tracker",
|
|
3070
|
+
},
|
|
3071
|
+
],
|
|
3072
|
+
foot: "Config holds the site, project key and role mapping only — the token itself lives in that target's secret store.",
|
|
3073
|
+
}),
|
|
3074
|
+
],
|
|
3075
|
+
[
|
|
3076
|
+
{
|
|
3077
|
+
t: "text",
|
|
3078
|
+
v: "The codebase, and somewhere to open the pull request",
|
|
3079
|
+
},
|
|
3080
|
+
{ t: "mono", v: "CodySwannGT/lisa" },
|
|
3081
|
+
fCell("yes", {
|
|
3082
|
+
title: "Repository — connected locally",
|
|
3083
|
+
sub: "CodySwannGT/lisa",
|
|
3084
|
+
intro:
|
|
3085
|
+
"The factory works in a branch off the checkout it is running in, and opens the pull request against this repository.",
|
|
3086
|
+
steps: [
|
|
3087
|
+
{
|
|
3088
|
+
text: "Nothing to connect for the code itself. Opening and merging pull requests needs the GitHub CLI authenticated as you.",
|
|
3089
|
+
cmd: "gh auth status",
|
|
3090
|
+
},
|
|
3091
|
+
],
|
|
3092
|
+
}),
|
|
3093
|
+
fCell("yes", {
|
|
3094
|
+
title: "Repository — connected headless",
|
|
3095
|
+
sub: "CodySwannGT/lisa",
|
|
3096
|
+
intro:
|
|
3097
|
+
"Scheduled and CI runs check the repository out and use a token-scoped identity to open pull requests, so no interactive login is involved.",
|
|
3098
|
+
steps: [
|
|
3099
|
+
{
|
|
3100
|
+
text: "Nothing to connect. Note that a scheduled run branches from the default branch, not your local branch.",
|
|
3101
|
+
},
|
|
3102
|
+
],
|
|
3103
|
+
}),
|
|
3104
|
+
],
|
|
3105
|
+
[
|
|
3106
|
+
/* PROPOSED: CI provider is swappable — /lisa:setup:ci should
|
|
3107
|
+
generate the gate workflows for the chosen system, not just
|
|
3108
|
+
GitHub Actions. The gate obligations below are identical
|
|
3109
|
+
whichever CI runs them; only the workflow syntax differs.
|
|
3110
|
+
syncKey ci.provider ties this to the "CI" column in Gates. */
|
|
3111
|
+
{ t: "text", v: "Somewhere to run the gates" },
|
|
3112
|
+
{
|
|
3113
|
+
t: "select",
|
|
3114
|
+
v: "GitHub Actions",
|
|
3115
|
+
mono: true,
|
|
3116
|
+
syncKey: "ci.provider",
|
|
3117
|
+
options: [
|
|
3118
|
+
"GitHub Actions",
|
|
3119
|
+
"GitLab CI",
|
|
3120
|
+
"Jenkins",
|
|
3121
|
+
"CircleCI",
|
|
3122
|
+
"Buildkite",
|
|
3123
|
+
],
|
|
3124
|
+
},
|
|
3125
|
+
fCell("partial", {
|
|
3126
|
+
title: "Gates run, but not all of them locally",
|
|
3127
|
+
sub: "Local runs mirror CI — they do not replace it",
|
|
3128
|
+
intro:
|
|
3129
|
+
"The fast checks run on your machine through git hooks, but the authoritative gates run in CI. A green local run is feedback, not a verdict, and the two can disagree if the mirrors drift.",
|
|
3130
|
+
steps: [
|
|
3131
|
+
{
|
|
3132
|
+
text: "Run the same suite CI runs, to catch drift before you push.",
|
|
3133
|
+
cmd: "bun run lint && bun run typecheck && bun run test",
|
|
3134
|
+
},
|
|
3135
|
+
{
|
|
3136
|
+
text: "See which gates are actually required to merge, and which only warn.",
|
|
3137
|
+
cmd: "the Gates table below",
|
|
3138
|
+
},
|
|
3139
|
+
],
|
|
3140
|
+
foot: "Anything a local hook can enforce, a developer can skip with --no-verify. That is why the local tier is a mirror and the server-side tier is the gate.",
|
|
3141
|
+
}),
|
|
3142
|
+
fCell("yes", {
|
|
3143
|
+
title: "GitHub Actions — connected",
|
|
3144
|
+
sub: "The authoritative tier",
|
|
3145
|
+
intro:
|
|
3146
|
+
"Every gate marked blocked below fires here and cannot be skipped by the worker, whether that worker is a person or an agent.",
|
|
3147
|
+
steps: [
|
|
3148
|
+
{
|
|
3149
|
+
text: "Provision the secrets the scanners need, or their gates degrade to skipped.",
|
|
3150
|
+
cmd: "gh secret set …",
|
|
3151
|
+
},
|
|
3152
|
+
],
|
|
3153
|
+
foot: "A gate whose credential is missing usually does not fail loudly — it skips, and a skipped gate renders identically to a passing one unless the job is required.",
|
|
3154
|
+
}),
|
|
3155
|
+
],
|
|
3156
|
+
[
|
|
3157
|
+
{ t: "text", v: "Project knowledge to build from" },
|
|
3158
|
+
{ t: "mono", v: "Lisa LLM Wiki" },
|
|
3159
|
+
fCell("no", {
|
|
3160
|
+
title: "No knowledge source connected",
|
|
3161
|
+
sub: "The builder only has the code",
|
|
3162
|
+
intro:
|
|
3163
|
+
"Without a wiki the factory re-derives how this project is built on every run — which conventions are load-bearing, what was already tried, why a thing is the way it is. The code shows what, never why.",
|
|
3164
|
+
steps: [
|
|
3165
|
+
{
|
|
3166
|
+
text: "Build the wiki from everything the agent can already reach — repository, git history, connected systems.",
|
|
3167
|
+
cmd: "/lisa:agent-ready",
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
text: "Answer the questions it could not resolve itself. It writes them to a gaps file for a human.",
|
|
3171
|
+
cmd: "wiki/gaps.md",
|
|
3172
|
+
},
|
|
3173
|
+
{
|
|
3174
|
+
text: "Re-run in a fresh session so the answers are absorbed. Repeat until zero gaps remain.",
|
|
3175
|
+
cmd: "/lisa:agent-ready",
|
|
3176
|
+
},
|
|
3177
|
+
],
|
|
3178
|
+
foot: "The framing that makes this work: starting tomorrow you maintain this project with zero human input — today is your only chance to ask questions.",
|
|
3179
|
+
}),
|
|
3180
|
+
fCell("no", {
|
|
3181
|
+
title: "No knowledge source connected",
|
|
3182
|
+
sub: "Same gap, headless",
|
|
3183
|
+
intro:
|
|
3184
|
+
"The wiki lives in the repository, so once it exists it is reachable everywhere the repo is — including CI and scheduled runs. This cell turns green at the same moment the local one does.",
|
|
3185
|
+
steps: [
|
|
3186
|
+
{
|
|
3187
|
+
text: "Connect it once; there is no separate headless step.",
|
|
3188
|
+
cmd: "/lisa:agent-ready",
|
|
3189
|
+
},
|
|
3190
|
+
],
|
|
3191
|
+
foot: "A git-native knowledge source is the reason this row has no local/headless split in practice — unlike every hosted integration above it.",
|
|
3192
|
+
}),
|
|
3193
|
+
],
|
|
3194
|
+
],
|
|
3195
|
+
},
|
|
3196
|
+
{
|
|
3197
|
+
/* PROPOSED: per-gate setup and verify — /lisa:setup:gate:<name>
|
|
3198
|
+
and /lisa:verify:gate:<name>
|
|
3199
|
+
------------------------------------------------------------
|
|
3200
|
+
1. What to build — one setup and one verify command PER GATE,
|
|
3201
|
+
slug-named after the obligation:
|
|
3202
|
+
/lisa:setup:gate:unit-tests · :integration-tests ·
|
|
3203
|
+
:e2e-tests · :coverage · :mutation · :secret-scanning …
|
|
3204
|
+
setup:gate is the same two-question flow as setup:tracker —
|
|
3205
|
+
(a) which provider(s), (b) which layers to enforce it at
|
|
3206
|
+
(agent hook / git hook / CI, and whether the CI check is
|
|
3207
|
+
required to merge) — and it writes the hooks + workflow jobs
|
|
3208
|
+
for the chosen layers. verify:gate exercises the gate: it
|
|
3209
|
+
makes the gate fire on a known-bad input and confirms it
|
|
3210
|
+
actually blocks, rather than only checking that config exists.
|
|
3211
|
+
2. Why — today a gate is configured by editing several files at
|
|
3212
|
+
once (a hook entry, a workflow job, a threshold, a required-
|
|
3213
|
+
check setting) with nothing that owns the whole gate. A gate
|
|
3214
|
+
the operator can name is a gate the operator can turn on,
|
|
3215
|
+
move a layer of, or prove — without knowing which files back
|
|
3216
|
+
it. It also makes "is this gate real?" answerable by running
|
|
3217
|
+
one command instead of reading four config surfaces.
|
|
3218
|
+
3. Today — gates are assembled by /lisa:apply (installs hooks
|
|
3219
|
+
and workflows wholesale) plus manual branch-protection and
|
|
3220
|
+
threshold edits. There is no per-gate command and no gate
|
|
3221
|
+
that verifies by being tripped.
|
|
3222
|
+
4. Replaces — nothing is deleted; setup:gate/verify:gate sit on
|
|
3223
|
+
top of apply as the per-gate control surface. The gate roster
|
|
3224
|
+
is not hardcoded — it is derived from the obligations Lisa
|
|
3225
|
+
knows how to enforce, so adding an obligation adds its
|
|
3226
|
+
commands automatically.
|
|
3227
|
+
VERB: verify, not validate — a gate is proven by being tripped,
|
|
3228
|
+
not by inspecting its config. Same rule as the rest of the
|
|
3229
|
+
console. */
|
|
3230
|
+
type: "table",
|
|
3231
|
+
card: "Gates",
|
|
3232
|
+
note: "The obligation is fixed; the provider is swappable — pick one or more per row. Each layer is a yes/no you can click for its event binding, its setup and verify commands, and (for CI) whether the check is required to merge. The two local layers are mirrors a worker can skip; an obligation is only unskippable when its CI check is required.",
|
|
3233
|
+
cols: [
|
|
3234
|
+
{
|
|
3235
|
+
v: "Enforcement",
|
|
3236
|
+
tip: "What must be true. This does not change when you change tools.",
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
v: "Provider",
|
|
3240
|
+
tip: "The tool(s) satisfying it. Multi-select — swap or stack them.",
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
v: "Agent hook",
|
|
3244
|
+
center: true,
|
|
3245
|
+
tip: "Fires inside the coding agent. Advisory — one harness only, and never in a headless run. Click for the event and commands.",
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
v: "Git hook",
|
|
3249
|
+
center: true,
|
|
3250
|
+
tip: "Fires on commit or push. Advisory — skippable with --no-verify. Click for the event and commands.",
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
v: "CI",
|
|
3254
|
+
center: true,
|
|
3255
|
+
tip: "Runs in the pipeline. Click to see the event, the commands, and whether the check is required to merge — that last is what makes the obligation unskippable.",
|
|
3256
|
+
},
|
|
3257
|
+
],
|
|
3258
|
+
rows: [
|
|
3259
|
+
gateRow(
|
|
3260
|
+
"Unit testing enforcement",
|
|
3261
|
+
"unit-tests",
|
|
3262
|
+
gProv(["vitest"], ["vitest", "jest", "bun test", "mocha"]),
|
|
3263
|
+
{
|
|
3264
|
+
agent: { on: false },
|
|
3265
|
+
git: { on: true, event: "pre-push" },
|
|
3266
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3267
|
+
}
|
|
3268
|
+
),
|
|
3269
|
+
gateRow(
|
|
3270
|
+
"Integration testing enforcement",
|
|
3271
|
+
"integration-tests",
|
|
3272
|
+
gProv(
|
|
3273
|
+
["vitest", "Testcontainers"],
|
|
3274
|
+
["vitest", "jest", "Testcontainers", "RSpec"]
|
|
3275
|
+
),
|
|
3276
|
+
{
|
|
3277
|
+
agent: { on: false },
|
|
3278
|
+
git: {
|
|
3279
|
+
on: false,
|
|
3280
|
+
intro:
|
|
3281
|
+
"Integration tests stand up real dependencies (a database, a container), which is too heavy for a local hook — so they wait for CI.",
|
|
3282
|
+
},
|
|
3283
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3284
|
+
}
|
|
3285
|
+
),
|
|
3286
|
+
gateRow(
|
|
3287
|
+
"E2E testing enforcement",
|
|
3288
|
+
"e2e-tests",
|
|
3289
|
+
gProv(
|
|
3290
|
+
["Playwright"],
|
|
3291
|
+
["Playwright", "Cypress", "Selenium", "Maestro", "Detox"]
|
|
3292
|
+
),
|
|
3293
|
+
{
|
|
3294
|
+
agent: { on: false },
|
|
3295
|
+
git: { on: false },
|
|
3296
|
+
ci: {
|
|
3297
|
+
on: true,
|
|
3298
|
+
event: "pull_request",
|
|
3299
|
+
required: true,
|
|
3300
|
+
foot: "Web journeys run headless in CI and gate the merge. Native journeys (Maestro / Detox) are too heavy to gate and run nightly on a device instead — a route-coverage floor, not a per-PR check.",
|
|
3301
|
+
},
|
|
3302
|
+
}
|
|
3303
|
+
),
|
|
3304
|
+
gateRow(
|
|
3305
|
+
"Property-based testing enforcement",
|
|
3306
|
+
"property-tests",
|
|
3307
|
+
gProv(["fast-check"], ["fast-check", "Hypothesis", "PropEr"]),
|
|
3308
|
+
{
|
|
3309
|
+
agent: { on: false },
|
|
3310
|
+
git: { on: false },
|
|
3311
|
+
ci: {
|
|
3312
|
+
on: true,
|
|
3313
|
+
event: "pull_request",
|
|
3314
|
+
required: true,
|
|
3315
|
+
intro:
|
|
3316
|
+
"Property tests run inside the unit-test job, so when they exist they gate the merge like any other test.",
|
|
3317
|
+
foot: "What is NOT enforced is that you write them: nothing requires a given module to have a property test. This row is green because the runner is wired, not because coverage of properties is measured.",
|
|
3318
|
+
},
|
|
3319
|
+
}
|
|
3320
|
+
),
|
|
3321
|
+
gateRow(
|
|
3322
|
+
"Contract testing enforcement",
|
|
3323
|
+
"contract-tests",
|
|
3324
|
+
gProv(
|
|
3325
|
+
[],
|
|
3326
|
+
["Pact", "Spring Cloud Contract", "Schemathesis", "Dredd"]
|
|
3327
|
+
),
|
|
3328
|
+
{
|
|
3329
|
+
agent: { on: false },
|
|
3330
|
+
git: { on: false },
|
|
3331
|
+
ci: {
|
|
3332
|
+
on: false,
|
|
3333
|
+
intro:
|
|
3334
|
+
"No contract testing is configured. Unit tests prove one service in isolation and e2e proves the whole system, but neither catches two services drifting out of agreement at their shared boundary — that is the gap a contract test fills.",
|
|
3335
|
+
foot: "Pick a provider to add it. Most valuable the moment this repo talks to a service it does not deploy in lockstep with.",
|
|
3336
|
+
},
|
|
3337
|
+
}
|
|
3338
|
+
),
|
|
3339
|
+
gateRow(
|
|
3340
|
+
"Coverage enforcement",
|
|
3341
|
+
"coverage",
|
|
3342
|
+
gProv(
|
|
3343
|
+
["vitest thresholds"],
|
|
3344
|
+
["vitest thresholds", "c8", "nyc", "SimpleCov"]
|
|
3345
|
+
),
|
|
3346
|
+
{
|
|
3347
|
+
agent: { on: false },
|
|
3348
|
+
git: { on: false },
|
|
3349
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3350
|
+
}
|
|
3351
|
+
),
|
|
3352
|
+
gateRow(
|
|
3353
|
+
"Mutation testing enforcement",
|
|
3354
|
+
"mutation",
|
|
3355
|
+
gProv(["Stryker"], ["Stryker", "PIT", "mutmut"]),
|
|
3356
|
+
{
|
|
3357
|
+
agent: { on: false },
|
|
3358
|
+
git: { on: false },
|
|
3359
|
+
ci: {
|
|
3360
|
+
on: true,
|
|
3361
|
+
event: "schedule (nightly)",
|
|
3362
|
+
required: false,
|
|
3363
|
+
intro:
|
|
3364
|
+
"Mutation runs nightly, so it does not gate a merge — a pull request can lower the score and nobody finds out until tomorrow. Coverage proves a line ran; mutation proves a test would have failed if that line were wrong.",
|
|
3365
|
+
foot: "Turn on the diff-scoped gate (quality.mutation.gate.enabled) so only changed code is scored, which is fast enough to run on pull_request and be required.",
|
|
3366
|
+
},
|
|
3367
|
+
}
|
|
3368
|
+
),
|
|
3369
|
+
gateRow(
|
|
3370
|
+
"Format enforcement",
|
|
3371
|
+
"format",
|
|
3372
|
+
gProv(["prettier"], ["prettier", "biome", "dprint"]),
|
|
3373
|
+
{
|
|
3374
|
+
agent: {
|
|
3375
|
+
on: true,
|
|
3376
|
+
event: "PostToolUse — after Edit / Write",
|
|
3377
|
+
},
|
|
3378
|
+
git: { on: true, event: "pre-commit" },
|
|
3379
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3380
|
+
}
|
|
3381
|
+
),
|
|
3382
|
+
gateRow(
|
|
3383
|
+
"Lint: correctness rules",
|
|
3384
|
+
"lint-correctness",
|
|
3385
|
+
gProv(["oxlint"], ["oxlint", "eslint", "biome"]),
|
|
3386
|
+
{
|
|
3387
|
+
agent: {
|
|
3388
|
+
on: true,
|
|
3389
|
+
event: "PostToolUse — after Edit / Write",
|
|
3390
|
+
intro:
|
|
3391
|
+
"The fast correctness pass (oxlint) runs on every edit inside the agent, so a mistake is caught before the next tool call.",
|
|
3392
|
+
},
|
|
3393
|
+
git: { on: true, event: "pre-commit" },
|
|
3394
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3395
|
+
}
|
|
3396
|
+
),
|
|
3397
|
+
gateRow(
|
|
3398
|
+
"Lint: complexity & size budgets",
|
|
3399
|
+
"lint-complexity",
|
|
3400
|
+
gProv(["eslint"], ["eslint", "oxlint"]),
|
|
3401
|
+
{
|
|
3402
|
+
agent: {
|
|
3403
|
+
on: false,
|
|
3404
|
+
intro:
|
|
3405
|
+
"The complexity and size rules are eslint's, not oxlint's, and are too slow to run on every keystroke — so they wait for pre-push rather than firing in the agent.",
|
|
3406
|
+
},
|
|
3407
|
+
git: { on: true, event: "pre-push" },
|
|
3408
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3409
|
+
}
|
|
3410
|
+
),
|
|
3411
|
+
gateRow(
|
|
3412
|
+
"Lint: structure & docs rules",
|
|
3413
|
+
"lint-structure",
|
|
3414
|
+
gProv(["eslint"], ["eslint"]),
|
|
3415
|
+
{
|
|
3416
|
+
agent: { on: false },
|
|
3417
|
+
git: {
|
|
3418
|
+
on: false,
|
|
3419
|
+
intro:
|
|
3420
|
+
"These are the type-aware and cross-file rules (component structure, import order, JSDoc). They need the whole project in memory, which is too expensive for a local hook — so CI is the only place they run.",
|
|
3421
|
+
},
|
|
3422
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3423
|
+
}
|
|
3424
|
+
),
|
|
3425
|
+
gateRow(
|
|
3426
|
+
"Type-safety enforcement",
|
|
3427
|
+
"typecheck",
|
|
3428
|
+
gProv(["tsc"], ["tsc", "mypy", "pyright"]),
|
|
3429
|
+
{
|
|
3430
|
+
agent: { on: false },
|
|
3431
|
+
git: { on: true, event: "pre-push" },
|
|
3432
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3433
|
+
}
|
|
3434
|
+
),
|
|
3435
|
+
gateRow(
|
|
3436
|
+
"Structural-rule enforcement",
|
|
3437
|
+
"structural-rules",
|
|
3438
|
+
gProv(["ast-grep"], ["ast-grep", "Semgrep", "Comby"]),
|
|
3439
|
+
{
|
|
3440
|
+
agent: {
|
|
3441
|
+
on: true,
|
|
3442
|
+
event: "PreToolUse — before Bash",
|
|
3443
|
+
intro:
|
|
3444
|
+
"The Safety Net rules also run as an agent guard, blocking dangerous commands (rm -rf, force-push, hard-reset) before the tool call executes.",
|
|
3445
|
+
},
|
|
3446
|
+
git: { on: false },
|
|
3447
|
+
ci: {
|
|
3448
|
+
on: true,
|
|
3449
|
+
event: "pull_request",
|
|
3450
|
+
required: true,
|
|
3451
|
+
foot: "ast-grep enforces the project's custom structural rules — the ones a general linter cannot express — as a required CI job.",
|
|
3452
|
+
},
|
|
3453
|
+
}
|
|
3454
|
+
),
|
|
3455
|
+
gateRow(
|
|
3456
|
+
"Dead-code enforcement",
|
|
3457
|
+
"dead-code",
|
|
3458
|
+
gProv(["knip"], ["knip", "ts-prune", "depcheck"]),
|
|
3459
|
+
{
|
|
3460
|
+
agent: { on: false },
|
|
3461
|
+
git: { on: false },
|
|
3462
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3463
|
+
}
|
|
3464
|
+
),
|
|
3465
|
+
gateRow(
|
|
3466
|
+
"Secret-scanning enforcement",
|
|
3467
|
+
"secret-scanning",
|
|
3468
|
+
gProv(
|
|
3469
|
+
["GitGuardian"],
|
|
3470
|
+
["GitGuardian", "gitleaks", "trufflehog"]
|
|
3471
|
+
),
|
|
3472
|
+
{
|
|
3473
|
+
agent: { on: false },
|
|
3474
|
+
git: { on: false },
|
|
3475
|
+
ci: {
|
|
3476
|
+
on: true,
|
|
3477
|
+
event: "pull_request",
|
|
3478
|
+
required: true,
|
|
3479
|
+
foot: "Also enforced by GitHub push protection, which blocks the push server-side before the secret ever reaches a pull request.",
|
|
3480
|
+
},
|
|
3481
|
+
}
|
|
3482
|
+
),
|
|
3483
|
+
gateRow(
|
|
3484
|
+
"Dependency-audit enforcement",
|
|
3485
|
+
"dependency-audit",
|
|
3486
|
+
gProv(
|
|
3487
|
+
["npm audit", "Dependabot"],
|
|
3488
|
+
["npm audit", "Dependabot", "Snyk", "Trivy"]
|
|
3489
|
+
),
|
|
3490
|
+
{
|
|
3491
|
+
agent: { on: false },
|
|
3492
|
+
git: { on: true, event: "pre-push" },
|
|
3493
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3494
|
+
}
|
|
3495
|
+
),
|
|
3496
|
+
gateRow(
|
|
3497
|
+
"Static-analysis enforcement",
|
|
3498
|
+
"static-analysis",
|
|
3499
|
+
gProv(
|
|
3500
|
+
["SonarCloud"],
|
|
3501
|
+
["SonarCloud", "CodeQL", "Semgrep", "Snyk Code"]
|
|
3502
|
+
),
|
|
3503
|
+
{
|
|
3504
|
+
agent: { on: false },
|
|
3505
|
+
git: { on: false },
|
|
3506
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3507
|
+
}
|
|
3508
|
+
),
|
|
3509
|
+
/* Running-app scans. These need a deployed target, so they run in
|
|
3510
|
+
CI against an ephemeral preview — never at a local layer. This
|
|
3511
|
+
project is a CLI with no web or API surface, so all three are
|
|
3512
|
+
n/a here (n/a, not a gap): they become real gates on a
|
|
3513
|
+
frontend or service stack. They are Implement gates, NOT Verify
|
|
3514
|
+
— Verify is human UAT, and an automated scanner is not a human
|
|
3515
|
+
using the product. */
|
|
3516
|
+
gateRow(
|
|
3517
|
+
"Dynamic security scan (DAST)",
|
|
3518
|
+
"dast",
|
|
3519
|
+
gProv(
|
|
3520
|
+
["OWASP ZAP"],
|
|
3521
|
+
["OWASP ZAP", "Burp Suite", "Nuclei", "StackHawk"]
|
|
3522
|
+
),
|
|
3523
|
+
{
|
|
3524
|
+
agent: { on: "na" },
|
|
3525
|
+
git: { on: "na" },
|
|
3526
|
+
ci: {
|
|
3527
|
+
on: "na",
|
|
3528
|
+
intro:
|
|
3529
|
+
"A dynamic scan drives a running app looking for common web vulnerabilities. In CI it runs against an ephemeral preview deploy, and only when a target URL is configured. This project has no deployed surface, so there is nothing to scan.",
|
|
3530
|
+
},
|
|
3531
|
+
}
|
|
3532
|
+
),
|
|
3533
|
+
gateRow(
|
|
3534
|
+
"Accessibility enforcement",
|
|
3535
|
+
"accessibility",
|
|
3536
|
+
gProv(
|
|
3537
|
+
["axe-core"],
|
|
3538
|
+
["axe-core", "Pa11y", "Lighthouse a11y", "IBM Equal Access"]
|
|
3539
|
+
),
|
|
3540
|
+
{
|
|
3541
|
+
agent: { on: "na" },
|
|
3542
|
+
git: { on: "na" },
|
|
3543
|
+
ci: {
|
|
3544
|
+
on: "na",
|
|
3545
|
+
intro:
|
|
3546
|
+
"Accessibility checks run against the rendered UI. They apply to frontend stacks; this project renders no UI, so the gate has nothing to measure.",
|
|
3547
|
+
},
|
|
3548
|
+
}
|
|
3549
|
+
),
|
|
3550
|
+
gateRow(
|
|
3551
|
+
"Performance-budget enforcement",
|
|
3552
|
+
"performance",
|
|
3553
|
+
gProv(
|
|
3554
|
+
["Lighthouse CI"],
|
|
3555
|
+
["Lighthouse CI", "SpeedCurve", "WebPageTest"]
|
|
3556
|
+
),
|
|
3557
|
+
{
|
|
3558
|
+
agent: { on: "na" },
|
|
3559
|
+
git: { on: "na" },
|
|
3560
|
+
ci: {
|
|
3561
|
+
on: "na",
|
|
3562
|
+
intro:
|
|
3563
|
+
"A performance budget holds the built site to Core Web Vitals thresholds. Frontend stacks only — there is no site to measure here.",
|
|
3564
|
+
foot: "On a stack where it applies, a performance budget is usually advisory rather than required: the right number is a judgement call, like cost.",
|
|
3565
|
+
},
|
|
3566
|
+
}
|
|
3567
|
+
),
|
|
3568
|
+
gateRow(
|
|
3569
|
+
"License-compliance enforcement",
|
|
3570
|
+
"license-compliance",
|
|
3571
|
+
gProv(
|
|
3572
|
+
["license-checker"],
|
|
3573
|
+
["license-checker", "FOSSA", "ScanCode", "Black Duck"]
|
|
3574
|
+
),
|
|
3575
|
+
{
|
|
3576
|
+
agent: { on: false },
|
|
3577
|
+
git: { on: false },
|
|
3578
|
+
ci: {
|
|
3579
|
+
on: true,
|
|
3580
|
+
event: "pull_request",
|
|
3581
|
+
required: true,
|
|
3582
|
+
foot: "Fails the build when a dependency arrives under a license outside the project's allowlist — a legal gate, not a quality one.",
|
|
3583
|
+
},
|
|
3584
|
+
}
|
|
3585
|
+
),
|
|
3586
|
+
gateRow(
|
|
3587
|
+
"Infrastructure-scanning enforcement",
|
|
3588
|
+
"infra-scanning",
|
|
3589
|
+
gProv([], ["cdk-nag", "checkov", "tfsec", "Terrascan"]),
|
|
3590
|
+
{
|
|
3591
|
+
agent: { on: false },
|
|
3592
|
+
git: { on: false },
|
|
3593
|
+
ci: {
|
|
3594
|
+
on: false,
|
|
3595
|
+
intro:
|
|
3596
|
+
"No scanner is selected, so infrastructure merges unscanned — an over-permissive policy or an unencrypted bucket passes as easily as a typo fix, while application code goes through several security gates.",
|
|
3597
|
+
foot: "Pick a scanner in the Provider column, then make its job a required check. A scanner that runs but is not required is indistinguishable from one that is not installed.",
|
|
3598
|
+
},
|
|
3599
|
+
}
|
|
3600
|
+
),
|
|
3601
|
+
gateRow(
|
|
3602
|
+
"Code-review enforcement",
|
|
3603
|
+
"code-review",
|
|
3604
|
+
gProv(
|
|
3605
|
+
["CodeRabbit", "/code-review"],
|
|
3606
|
+
["CodeRabbit", "Greptile", "Qodo", "/code-review"]
|
|
3607
|
+
),
|
|
3608
|
+
{
|
|
3609
|
+
agent: { on: false },
|
|
3610
|
+
git: { on: false },
|
|
3611
|
+
ci: { on: true, event: "pull_request", required: true },
|
|
3612
|
+
}
|
|
3613
|
+
),
|
|
3614
|
+
gateRow(
|
|
3615
|
+
"Review-resolution enforcement",
|
|
3616
|
+
"review-resolution",
|
|
3617
|
+
gProv(["GitHub thread check"], ["GitHub thread check"]),
|
|
3618
|
+
{
|
|
3619
|
+
agent: { on: false },
|
|
3620
|
+
git: { on: false },
|
|
3621
|
+
ci: {
|
|
3622
|
+
on: true,
|
|
3623
|
+
event: "pull_request",
|
|
3624
|
+
required: true,
|
|
3625
|
+
foot: "There is no local mirror for this one — whether every review thread is resolved is only knowable server-side, so branch protection is the only place it can be enforced.",
|
|
3626
|
+
},
|
|
3627
|
+
}
|
|
3628
|
+
),
|
|
3629
|
+
],
|
|
3630
|
+
},
|
|
3631
|
+
{
|
|
3632
|
+
card: "Thresholds — coverage",
|
|
3633
|
+
note: "The floors the coverage gate compares against, for this project's stack. Same settings as the Testing page — change them here and they change there. Thresholds may only tighten: loosening one requires its own reviewed change.",
|
|
3634
|
+
rows: [
|
|
3635
|
+
{
|
|
3636
|
+
key: "quality.testCoverage.global.statements",
|
|
3637
|
+
label: "Statements",
|
|
3638
|
+
desc: "Statement coverage below this fails the build.",
|
|
3639
|
+
control: num(70, "%"),
|
|
3640
|
+
},
|
|
3641
|
+
{
|
|
3642
|
+
key: "quality.testCoverage.global.branches",
|
|
3643
|
+
label: "Branches",
|
|
3644
|
+
desc: "The floor that usually bites first — a branch is only covered when both sides are taken.",
|
|
3645
|
+
control: num(70, "%"),
|
|
3646
|
+
},
|
|
3647
|
+
{
|
|
3648
|
+
key: "quality.testCoverage.global.functions",
|
|
3649
|
+
label: "Functions",
|
|
3650
|
+
desc: "Share of functions entered by at least one test.",
|
|
3651
|
+
control: num(70, "%"),
|
|
3652
|
+
},
|
|
3653
|
+
{
|
|
3654
|
+
key: "quality.testCoverage.global.lines",
|
|
3655
|
+
label: "Lines",
|
|
3656
|
+
desc: "Share of executable lines run by the suite.",
|
|
3657
|
+
control: num(70, "%"),
|
|
3658
|
+
},
|
|
3659
|
+
],
|
|
3660
|
+
},
|
|
3661
|
+
{
|
|
3662
|
+
card: "Thresholds — test depth",
|
|
3663
|
+
note: "How hard the tests are made to work. Coverage says code ran; these say whether running it proved anything.",
|
|
3664
|
+
rows: [
|
|
3665
|
+
{
|
|
3666
|
+
key: "quality.e2eCoverage.playwright.routes",
|
|
3667
|
+
label: "E2E route coverage (web)",
|
|
3668
|
+
desc: "Share of user-facing routes that must be exercised by a Playwright journey.",
|
|
3669
|
+
control: num(80, "%"),
|
|
3670
|
+
},
|
|
3671
|
+
{
|
|
3672
|
+
key: "quality.e2eCoverage.maestro.routes",
|
|
3673
|
+
label: "E2E route coverage (native)",
|
|
3674
|
+
desc: "Same floor for native journeys. Applies to Expo stacks; ignored where there is no native surface.",
|
|
3675
|
+
control: num(80, "%"),
|
|
3676
|
+
},
|
|
3677
|
+
{
|
|
3678
|
+
key: "quality.mutation.strykerThresholds.break",
|
|
3679
|
+
label: "Mutation score — break",
|
|
3680
|
+
desc: "Below this the mutation run fails. The only one of the three that can fail a build.",
|
|
3681
|
+
control: num(60, "%"),
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
key: "quality.mutation.strykerThresholds.low",
|
|
3685
|
+
label: "Mutation score — low",
|
|
3686
|
+
desc: "Reporting boundary: scores under this are flagged red in the report without failing anything.",
|
|
3687
|
+
control: num(60, "%"),
|
|
3688
|
+
},
|
|
3689
|
+
{
|
|
3690
|
+
key: "quality.mutation.strykerThresholds.high",
|
|
3691
|
+
label: "Mutation score — high",
|
|
3692
|
+
desc: "Reporting boundary: at or above this the report reads green.",
|
|
3693
|
+
control: num(80, "%"),
|
|
3694
|
+
},
|
|
3695
|
+
{
|
|
3696
|
+
key: "quality.mutation.gate.enabled",
|
|
3697
|
+
label: "Mutation gates merges",
|
|
3698
|
+
desc: "Off means the score is measured nightly but never blocks a pull request — the gap flagged in the Gates table above.",
|
|
3699
|
+
control: tog(false),
|
|
3700
|
+
},
|
|
3701
|
+
{
|
|
3702
|
+
key: "quality.mutation.gate.since",
|
|
3703
|
+
label: "Mutation diff base",
|
|
3704
|
+
desc: "The ref changed code is measured against, so only the diff is scored rather than the whole codebase.",
|
|
3705
|
+
control: txt("main", { mono: true }),
|
|
3706
|
+
},
|
|
3707
|
+
],
|
|
3708
|
+
},
|
|
3709
|
+
{
|
|
3710
|
+
card: "Thresholds — code budgets",
|
|
3711
|
+
note: "Per-unit ceilings enforced by the lint gates. Same settings as the Linting page.",
|
|
3712
|
+
rows: [
|
|
3713
|
+
{
|
|
3714
|
+
key: "quality.lintBudgets.cognitiveComplexity",
|
|
3715
|
+
label: "Cognitive complexity",
|
|
3716
|
+
desc: "Per-function ceiling. The most common reason a build goes red on otherwise working code.",
|
|
3717
|
+
control: num(10),
|
|
3718
|
+
},
|
|
3719
|
+
{
|
|
3720
|
+
key: "quality.lintBudgets.maxLines",
|
|
3721
|
+
label: "Max lines per file",
|
|
3722
|
+
desc: "Files above this must be split.",
|
|
3723
|
+
control: num(300),
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
key: "quality.lintBudgets.maxLinesPerFunction",
|
|
3727
|
+
label: "Max lines per function",
|
|
3728
|
+
desc: "Functions above this must be decomposed.",
|
|
3729
|
+
control: num(75),
|
|
3730
|
+
},
|
|
3731
|
+
{
|
|
3732
|
+
key: "body-max-line-length",
|
|
3733
|
+
label: "Commit body line length",
|
|
3734
|
+
desc: "Enforced by commitlint at the commit hook, not by a merge gate.",
|
|
3735
|
+
control: num(300, "chars"),
|
|
3736
|
+
},
|
|
3737
|
+
],
|
|
3738
|
+
},
|
|
3739
|
+
{
|
|
3740
|
+
type: "table",
|
|
3741
|
+
card: "Work-unit lifecycle roles",
|
|
3742
|
+
note: "How a work item's state is represented in the tracker. Lisa reasons in roles; each tracker spells them differently — JIRA uses workflow statuses, GitHub and Linear use labels. Created idempotently by the tracker's setup command.",
|
|
3743
|
+
cols: [
|
|
3744
|
+
{ v: "Role", tip: "Lisa's vendor-neutral name for the state." },
|
|
3745
|
+
{
|
|
3746
|
+
v: "As stored",
|
|
3747
|
+
tip: "What the configured tracker actually records. Changes with the tracker.",
|
|
3748
|
+
},
|
|
3749
|
+
{ v: "What it means" },
|
|
3750
|
+
{ v: "Advanced by" },
|
|
3751
|
+
],
|
|
3752
|
+
rows: [
|
|
3753
|
+
[
|
|
3754
|
+
{ t: "mono", v: "ready" },
|
|
3755
|
+
{ t: "mono", v: "status:ready" },
|
|
3756
|
+
{
|
|
3757
|
+
t: "text",
|
|
3758
|
+
v: "The gate. Whoever set it is asserting the work item is ready to build; the build loop may now claim it. Set by a human, by Plan decomposing a PRD, by Verify filing a UAT failure, or by the QA and Monitoring loops filing a bug or regression.",
|
|
3759
|
+
},
|
|
3760
|
+
{ t: "text", v: "a human or a factory" },
|
|
3761
|
+
],
|
|
3762
|
+
[
|
|
3763
|
+
{ t: "mono", v: "claimed" },
|
|
3764
|
+
{ t: "mono", v: "status:in-progress" },
|
|
3765
|
+
{
|
|
3766
|
+
t: "text",
|
|
3767
|
+
v: "This factory has it. Prevents a second cycle dispatching the same item.",
|
|
3768
|
+
},
|
|
3769
|
+
{ t: "text", v: "Implement" },
|
|
3770
|
+
],
|
|
3771
|
+
[
|
|
3772
|
+
{ t: "mono", v: "blocked" },
|
|
3773
|
+
{ t: "mono", v: "status:blocked" },
|
|
3774
|
+
{
|
|
3775
|
+
t: "text",
|
|
3776
|
+
v: "Triage rejected it, or the build could not finish. Needs an answer before it can move.",
|
|
3777
|
+
},
|
|
3778
|
+
{ t: "text", v: "Implement" },
|
|
3779
|
+
],
|
|
3780
|
+
[
|
|
3781
|
+
{ t: "mono", v: "human_needed" },
|
|
3782
|
+
{ t: "mono", v: "status:human-needed" },
|
|
3783
|
+
{
|
|
3784
|
+
t: "text",
|
|
3785
|
+
v: "The factory reached a boundary it may not cross alone and is waiting on a person — an approval, not a failure.",
|
|
3786
|
+
},
|
|
3787
|
+
{ t: "text", v: "Implement" },
|
|
3788
|
+
],
|
|
3789
|
+
[
|
|
3790
|
+
{ t: "mono", v: "done" },
|
|
3791
|
+
{ t: "mono", v: "status:done" },
|
|
3792
|
+
{
|
|
3793
|
+
t: "text",
|
|
3794
|
+
v: "Merged and deployed. Environment-aware: a project with a deploy chain marks done per environment.",
|
|
3795
|
+
},
|
|
3796
|
+
{ t: "text", v: "Implement" },
|
|
3797
|
+
],
|
|
3798
|
+
],
|
|
3799
|
+
},
|
|
3800
|
+
fLegend(
|
|
3801
|
+
'<b>The CI column is the one that gates.</b> Agent-hook and git-hook are fast-feedback mirrors a worker can skip; whether an obligation is actually unskippable is inside its CI cell — click it and read <i>Required to merge</i>. Three rows fall short: mutation testing runs in CI but nightly and unrequired, and infrastructure scanning and contract testing have no provider selected at all. Those are the honest gaps — each is one choice away from being a real gate. The <span class="mono">n/a</span> rows are different: those gates need a deployed web or API surface, and this project is a CLI, so they do not apply here rather than being gaps — they become real gates on a frontend or service stack. The agent-hook and git-hook mirrors must be generated from the same config as CI, or they drift and people learn to distrust them.'
|
|
3802
|
+
),
|
|
3803
|
+
],
|
|
3804
|
+
};
|
|
3805
|
+
|
|
3806
|
+
DATA.sections["factory-verify"] = {
|
|
3807
|
+
title: "Verify factory",
|
|
3808
|
+
desc: "User acceptance testing, run by an agent. It drives the shipped software the way a human would, reaches a go/no-go decision, and files every failure as a build-ready ticket straight back into Implement. No scanners live here — an automated check is not a human using the product; those are Implement gates.",
|
|
3809
|
+
src: ".lisa.config.json verify.* · qa.*",
|
|
3810
|
+
blocks: [
|
|
3811
|
+
fStaffPicker(),
|
|
3812
|
+
{
|
|
3813
|
+
type: "table",
|
|
3814
|
+
card: "How to run it",
|
|
3815
|
+
note: "The scheduled driver is the same PRD intake loop as the Plan factory — a later phase of it. Once a PRD's work is shipped, the loop dispatches verification for it: shipped → verified on pass, or re-opened with build-ready fix tickets on fail.",
|
|
3816
|
+
cols: [
|
|
3817
|
+
{ v: "Mode" },
|
|
3818
|
+
{ v: "Command" },
|
|
3819
|
+
{ v: "Cadence" },
|
|
3820
|
+
{
|
|
3821
|
+
v: "Registered",
|
|
3822
|
+
tip: "Whether a scheduler is actually invoking this. Manual runs are never registered — you invoke them.",
|
|
3823
|
+
},
|
|
3824
|
+
],
|
|
3825
|
+
rows: [
|
|
3826
|
+
[
|
|
3827
|
+
{ t: "text", v: "Scheduled (automation)" },
|
|
3828
|
+
{ t: "mono", v: "/lisa:intake <queue>" },
|
|
3829
|
+
{ t: "text", v: "every 60 minutes" },
|
|
3830
|
+
fCell("no", {
|
|
3831
|
+
title: "Register the PRD intake loop",
|
|
3832
|
+
sub: "Nothing is verifying shipped work today",
|
|
3833
|
+
intro:
|
|
3834
|
+
"Verification runs as a phase of the PRD intake loop — the same loop the Plan factory uses. Until it is registered, shipped work is never checked, and a regression can sit in production until a human notices.",
|
|
3835
|
+
steps: [
|
|
3836
|
+
{
|
|
3837
|
+
text: "Run setup and register this loop.",
|
|
3838
|
+
cmd: "/lisa:setup:automations",
|
|
3839
|
+
},
|
|
3840
|
+
{
|
|
3841
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
3842
|
+
cmd: "/lisa:verify:automation",
|
|
3843
|
+
},
|
|
3844
|
+
],
|
|
3845
|
+
foot: "On a fail the loop does not block — it re-opens the PRD and files build-ready fix tickets that flow straight back into Implement, then re-verifies. A no-go is never a dead end.",
|
|
3846
|
+
}),
|
|
3847
|
+
],
|
|
3848
|
+
[
|
|
3849
|
+
{ t: "text", v: "Manual" },
|
|
3850
|
+
{ t: "mono", v: "/lisa:verify <target>" },
|
|
3851
|
+
{ t: "text", v: "on demand" },
|
|
3852
|
+
fCell("n/a"),
|
|
3853
|
+
],
|
|
3854
|
+
],
|
|
3855
|
+
},
|
|
3856
|
+
{
|
|
3857
|
+
type: "table",
|
|
3858
|
+
card: "What a run takes",
|
|
3859
|
+
note: "Each name below is the placeholder you see in the commands above. Use /lisa:verify-prd instead to verify a whole PRD's shipped work in one pass.",
|
|
3860
|
+
cols: [{ v: "Input" }, { v: "Required" }, { v: "What it is" }],
|
|
3861
|
+
rows: [
|
|
3862
|
+
[
|
|
3863
|
+
{ t: "mono", v: "target" },
|
|
3864
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
3865
|
+
{
|
|
3866
|
+
t: "text",
|
|
3867
|
+
v: "Manual runs. A URL to a shipped work item or PRD to put through UAT.",
|
|
3868
|
+
},
|
|
3869
|
+
],
|
|
3870
|
+
[
|
|
3871
|
+
{ t: "mono", v: "queue" },
|
|
3872
|
+
{ t: "badge", v: "required", cls: "required" },
|
|
3873
|
+
{
|
|
3874
|
+
t: "text",
|
|
3875
|
+
v: "Scheduled runs. A queue of shipped PRDs awaiting verification.",
|
|
3876
|
+
},
|
|
3877
|
+
],
|
|
3878
|
+
],
|
|
3879
|
+
},
|
|
3880
|
+
{
|
|
3881
|
+
type: "table",
|
|
3882
|
+
card: "Connections — is the factory wired up?",
|
|
3883
|
+
note: "Verify needs a running copy of the software to drive — that is what makes it different from every other factory. Local is your workstation; headless is a CI runner or a scheduled cloud run, where no human is present to complete an OAuth prompt.",
|
|
3884
|
+
cols: [
|
|
3885
|
+
{ v: "What it needs" },
|
|
3886
|
+
{ v: "Provider" },
|
|
3887
|
+
{ v: "Local", tip: "Reachable from your workstation." },
|
|
3888
|
+
{
|
|
3889
|
+
v: "Headless",
|
|
3890
|
+
tip: "Reachable from CI or a scheduled run, with no human to authenticate.",
|
|
3891
|
+
},
|
|
3892
|
+
],
|
|
3893
|
+
rows: [
|
|
3894
|
+
[
|
|
3895
|
+
{ t: "text", v: "A running copy of the software to drive" },
|
|
3896
|
+
{ t: "mono", v: "build output · CodySwannGT/lisa" },
|
|
3897
|
+
fCell("yes", {
|
|
3898
|
+
title: "Build output — runnable locally",
|
|
3899
|
+
sub: "What you get from building the repo",
|
|
3900
|
+
intro:
|
|
3901
|
+
"UAT needs something to actually use. For this project that is the built CLI from this repo; for a web or service stack it is a deployed URL or an ephemeral preview built from the same repo. Without a running copy there is nothing to verify.",
|
|
3902
|
+
steps: [
|
|
3903
|
+
{
|
|
3904
|
+
text: "Build the repo, then point verification at the copy to exercise.",
|
|
3905
|
+
cmd: "verify.target",
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
text: "Confirm the copy is reachable and is the build you think it is, before a run.",
|
|
3909
|
+
cmd: "/lisa:verify:automation",
|
|
3910
|
+
},
|
|
3911
|
+
],
|
|
3912
|
+
foot: "Verifying against a stale or wrong build is the quiet failure here — a green run that proved something you did not ship. The target is recorded in config so a run cannot silently drift onto the wrong copy.",
|
|
3913
|
+
}),
|
|
3914
|
+
fCell("partial", {
|
|
3915
|
+
title: "Build output — partial headless",
|
|
3916
|
+
sub: "Depends where the build can run",
|
|
3917
|
+
intro:
|
|
3918
|
+
"A scheduled run can build the repo and drive the output, or reach a copy already deployed from it, with no human present. It cannot reach anything that only exists on your workstation — a copy behind your desktop session, or a build you never pushed.",
|
|
3919
|
+
steps: [
|
|
3920
|
+
{
|
|
3921
|
+
text: "Verify against a build the headless runner can produce or reach, not one only on your machine.",
|
|
3922
|
+
cmd: "verify.target",
|
|
3923
|
+
},
|
|
3924
|
+
],
|
|
3925
|
+
foot: "This is the Verify-specific version of the interactive-auth trap: a run that is green locally can be meaningless headless if the only reachable copy was on your machine.",
|
|
3926
|
+
}),
|
|
3927
|
+
],
|
|
3928
|
+
[
|
|
3929
|
+
{
|
|
3930
|
+
t: "text",
|
|
3931
|
+
v: "A way to drive it the way a human would",
|
|
3932
|
+
},
|
|
3933
|
+
{
|
|
3934
|
+
t: "multiselect",
|
|
3935
|
+
values: ["CLI harness"],
|
|
3936
|
+
options: [
|
|
3937
|
+
"CLI harness",
|
|
3938
|
+
"Playwright",
|
|
3939
|
+
"Cypress",
|
|
3940
|
+
"Maestro",
|
|
3941
|
+
"Detox",
|
|
3942
|
+
"manual walkthrough",
|
|
3943
|
+
],
|
|
3944
|
+
mono: true,
|
|
3945
|
+
},
|
|
3946
|
+
fCell("yes", {
|
|
3947
|
+
title: "Driver — present locally",
|
|
3948
|
+
sub: "How UAT actually touches the product",
|
|
3949
|
+
intro:
|
|
3950
|
+
"The driver is what stands in for a human's hands. Web journeys use Playwright, native journeys use Maestro, and a CLI is driven through its own harness. Pick whatever matches the product's surface — more than one where it has several.",
|
|
3951
|
+
steps: [
|
|
3952
|
+
{
|
|
3953
|
+
text: "Installed with the project's E2E tooling.",
|
|
3954
|
+
cmd: "/lisa:apply",
|
|
3955
|
+
},
|
|
3956
|
+
{
|
|
3957
|
+
text: "Confirm the driver can reach the target.",
|
|
3958
|
+
cmd: "/lisa:verify:automation",
|
|
3959
|
+
},
|
|
3960
|
+
],
|
|
3961
|
+
foot: "The driver is swappable and stackable — it does not change what Verify proves, only how it touches the product.",
|
|
3962
|
+
}),
|
|
3963
|
+
fCell("yes", {
|
|
3964
|
+
title: "Driver — present headless",
|
|
3965
|
+
sub: "Runs in CI and on a schedule",
|
|
3966
|
+
intro:
|
|
3967
|
+
"The E2E drivers run headless by design, so a scheduled UAT pass drives the product with no human present.",
|
|
3968
|
+
steps: [
|
|
3969
|
+
{
|
|
3970
|
+
text: "Nothing extra — the driver ships with the E2E tooling.",
|
|
3971
|
+
cmd: "/lisa:apply",
|
|
3972
|
+
},
|
|
3973
|
+
],
|
|
3974
|
+
foot: "Native drivers (Maestro / Detox) need a device or emulator, which is why native UAT usually runs nightly on a device farm rather than on every cycle.",
|
|
3975
|
+
}),
|
|
3976
|
+
],
|
|
3977
|
+
[
|
|
3978
|
+
/* Same tracker setting as Plan and Implement — bound through
|
|
3979
|
+
syncKey so all three factory pages show one value. */
|
|
3980
|
+
{
|
|
3981
|
+
t: "text",
|
|
3982
|
+
v: "The work item, and where to file failures",
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
t: "select",
|
|
3986
|
+
v: "JIRA",
|
|
3987
|
+
mono: true,
|
|
3988
|
+
syncKey: "tracker.provider",
|
|
3989
|
+
options: ["JIRA", "GitHub", "Linear"],
|
|
3990
|
+
},
|
|
3991
|
+
fCell("yes", {
|
|
3992
|
+
title: "JIRA — connected locally",
|
|
3993
|
+
sub: "Reachable from your workstation",
|
|
3994
|
+
intro:
|
|
3995
|
+
"Verify reads the acceptance criteria from the work item, posts the go/no-go decision back to it with evidence, and — on a no-go — files the failure as a new build-ready ticket.",
|
|
3996
|
+
steps: [
|
|
3997
|
+
{
|
|
3998
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
3999
|
+
cmd: "/lisa:setup:tracker",
|
|
4000
|
+
},
|
|
4001
|
+
{
|
|
4002
|
+
text: "Verify from the same target.",
|
|
4003
|
+
cmd: "/lisa:verify:tracker",
|
|
4004
|
+
},
|
|
4005
|
+
],
|
|
4006
|
+
foot: "Same setting as the Plan and Implement factories — changing it here changes it there.",
|
|
4007
|
+
}),
|
|
4008
|
+
fCell("yes", {
|
|
4009
|
+
title: "JIRA — connected headless",
|
|
4010
|
+
sub: "Works in CI and on a schedule",
|
|
4011
|
+
intro:
|
|
4012
|
+
"Token auth, so the scheduled loop can read the work item and file failure tickets with no human present.",
|
|
4013
|
+
steps: [
|
|
4014
|
+
{
|
|
4015
|
+
text: "Run setup and choose the headless target you want.",
|
|
4016
|
+
cmd: "/lisa:setup:tracker",
|
|
4017
|
+
},
|
|
4018
|
+
{
|
|
4019
|
+
text: "Verify from that target.",
|
|
4020
|
+
cmd: "/lisa:verify:tracker",
|
|
4021
|
+
},
|
|
4022
|
+
],
|
|
4023
|
+
foot: "Config holds the site, project key and role mapping only — the token lives in that target's secret store.",
|
|
4024
|
+
}),
|
|
4025
|
+
],
|
|
4026
|
+
[
|
|
4027
|
+
{
|
|
4028
|
+
t: "text",
|
|
4029
|
+
v: "The spec to check the result against",
|
|
4030
|
+
},
|
|
4031
|
+
{
|
|
4032
|
+
t: "select",
|
|
4033
|
+
v: "Notion",
|
|
4034
|
+
mono: true,
|
|
4035
|
+
syncKey: "source.provider",
|
|
4036
|
+
options: [
|
|
4037
|
+
"Notion",
|
|
4038
|
+
"Confluence",
|
|
4039
|
+
"Linear",
|
|
4040
|
+
"GitHub",
|
|
4041
|
+
"Google Docs",
|
|
4042
|
+
],
|
|
4043
|
+
},
|
|
4044
|
+
fCell("yes", {
|
|
4045
|
+
title: "Notion — readable locally",
|
|
4046
|
+
sub: "The PRD, for spec conformance",
|
|
4047
|
+
intro:
|
|
4048
|
+
"Beyond acceptance criteria, Verify checks the shipped result against the original PRD section by section — Out of Scope, Technical Approach, the Validation Journey. That needs the PRD, from the same source the Research factory wrote it to.",
|
|
4049
|
+
steps: [
|
|
4050
|
+
{
|
|
4051
|
+
text: "Run setup and choose this workstation when it asks where.",
|
|
4052
|
+
cmd: "/lisa:setup:prd-source",
|
|
4053
|
+
},
|
|
4054
|
+
{
|
|
4055
|
+
text: "Verify from the same target.",
|
|
4056
|
+
cmd: "/lisa:verify:prd-source",
|
|
4057
|
+
},
|
|
4058
|
+
],
|
|
4059
|
+
foot: "Same setting as the Research factory's output — one provider holds the PRDs, and three factories touch it.",
|
|
4060
|
+
}),
|
|
4061
|
+
fCell("no", {
|
|
4062
|
+
title: "Notion — not readable headless",
|
|
4063
|
+
sub: "Spec conformance degrades without it",
|
|
4064
|
+
intro:
|
|
4065
|
+
"A scheduled run cannot complete an OAuth prompt, so it cannot open the PRD — which means the spec-conformance check is skipped and verification falls back to acceptance criteria alone.",
|
|
4066
|
+
steps: [
|
|
4067
|
+
{
|
|
4068
|
+
text: "Run setup again and choose a headless target when it asks where.",
|
|
4069
|
+
cmd: "/lisa:setup:prd-source",
|
|
4070
|
+
},
|
|
4071
|
+
{
|
|
4072
|
+
text: "Verify from that target, not from your laptop.",
|
|
4073
|
+
cmd: "/lisa:verify:prd-source",
|
|
4074
|
+
},
|
|
4075
|
+
],
|
|
4076
|
+
foot: "Unlike the tracker, losing this does not stop a run — it narrows it. That is worth knowing: a headless go/no-go may be a weaker verdict than a local one.",
|
|
4077
|
+
}),
|
|
4078
|
+
],
|
|
4079
|
+
[
|
|
4080
|
+
{
|
|
4081
|
+
t: "text",
|
|
4082
|
+
v: "Project knowledge for what correct looks like",
|
|
4083
|
+
},
|
|
4084
|
+
{ t: "mono", v: "Lisa LLM Wiki" },
|
|
4085
|
+
fCell("no", {
|
|
4086
|
+
title: "No knowledge source connected",
|
|
4087
|
+
sub: "UAT is guesswork without it",
|
|
4088
|
+
intro:
|
|
4089
|
+
"To judge whether the product behaves correctly, the agent has to know what correct means here — the intended flows, the edge cases that matter, the decisions already made. Without a wiki it verifies against the ticket alone and misses everything the ticket assumed you already knew.",
|
|
4090
|
+
steps: [
|
|
4091
|
+
{
|
|
4092
|
+
text: "Build the wiki from everything the agent can already reach — repository, git history, connected systems.",
|
|
4093
|
+
cmd: "/lisa:agent-ready",
|
|
4094
|
+
},
|
|
4095
|
+
{
|
|
4096
|
+
text: "Answer the questions it could not resolve itself. It writes them to a gaps file for a human.",
|
|
4097
|
+
cmd: "wiki/gaps.md",
|
|
4098
|
+
},
|
|
4099
|
+
{
|
|
4100
|
+
text: "Re-run in a fresh session so the answers are absorbed. Repeat until zero gaps remain.",
|
|
4101
|
+
cmd: "/lisa:agent-ready",
|
|
4102
|
+
},
|
|
4103
|
+
],
|
|
4104
|
+
foot: "The framing that makes this work: starting tomorrow you maintain this project with zero human input — today is your only chance to ask questions.",
|
|
4105
|
+
}),
|
|
4106
|
+
fCell("no", {
|
|
4107
|
+
title: "No knowledge source connected",
|
|
4108
|
+
sub: "Same gap, headless",
|
|
4109
|
+
intro:
|
|
4110
|
+
"The wiki lives in the repository, so once it exists it is reachable everywhere the repo is. This cell turns green at the same moment the local one does.",
|
|
4111
|
+
steps: [
|
|
4112
|
+
{
|
|
4113
|
+
text: "Connect it once; there is no separate headless step.",
|
|
4114
|
+
cmd: "/lisa:agent-ready",
|
|
4115
|
+
},
|
|
4116
|
+
],
|
|
4117
|
+
foot: "A git-native knowledge source is the reason this row has no local/headless split in practice.",
|
|
4118
|
+
}),
|
|
4119
|
+
],
|
|
4120
|
+
[
|
|
4121
|
+
{ t: "text", v: "The codebase as ground truth" },
|
|
4122
|
+
{ t: "mono", v: "CodySwannGT/lisa" },
|
|
4123
|
+
fCell("yes", {
|
|
4124
|
+
title: "Repository — connected locally",
|
|
4125
|
+
sub: "CodySwannGT/lisa",
|
|
4126
|
+
intro:
|
|
4127
|
+
"Verify reads the code to write regression tests that pin each passing check, and to know what shipped in the first place.",
|
|
4128
|
+
steps: [
|
|
4129
|
+
{
|
|
4130
|
+
text: "Nothing to connect — the factory runs inside the checkout.",
|
|
4131
|
+
},
|
|
4132
|
+
],
|
|
4133
|
+
}),
|
|
4134
|
+
fCell("yes", {
|
|
4135
|
+
title: "Repository — connected headless",
|
|
4136
|
+
sub: "CodySwannGT/lisa",
|
|
4137
|
+
intro:
|
|
4138
|
+
"CI and scheduled runs check the repository out before the factory starts.",
|
|
4139
|
+
steps: [
|
|
4140
|
+
{
|
|
4141
|
+
text: "Nothing to connect. A scheduled run sees the default branch.",
|
|
4142
|
+
},
|
|
4143
|
+
],
|
|
4144
|
+
}),
|
|
4145
|
+
],
|
|
4146
|
+
],
|
|
4147
|
+
},
|
|
4148
|
+
fLegend(
|
|
4149
|
+
"<b>Verify and the QA loop are the same competence in two positions.</b> Verify drives the product to produce a bounded go/no-go on one work item at a gate; the QA loop drives it continuously to produce unbounded exploratory signal. Both re-enter the pipeline through intake — a finding from either gets no privileged path around the adversarial gate. And neither is a scanner: an automated check that inspects a diff or a running app is an Implement gate, not UAT."
|
|
4150
|
+
),
|
|
4151
|
+
],
|
|
4152
|
+
};
|
|
4153
|
+
/* ---------------------------------- Signals (Observe) ---------------------------------- */
|
|
4154
|
+
/* Observe is deliberately NOT a factory — it transforms no work unit and
|
|
4155
|
+
has no gate of its own. It is the sensor layer that watches deployed
|
|
4156
|
+
software and turns what it sees into inputs that re-enter the factories
|
|
4157
|
+
at whichever gate matches the signal. It lives under Factories in the
|
|
4158
|
+
nav because that is where an operator looks for it, not because it is
|
|
4159
|
+
one. */
|
|
4160
|
+
DATA.sections.signals = {
|
|
4161
|
+
title: "Signals",
|
|
4162
|
+
desc: "The Observe layer — not a factory. It watches the deployed software and turns what it sees into inputs that re-enter the factories at the gate that matches the signal: a crash becomes an Implement ticket, a usage pattern becomes Research material. Every finding submits to the same adversarial intake as any other work; sensing earns no privileged path around the gate.",
|
|
4163
|
+
src: ".lisa.config.json monitor.* · qa.*",
|
|
4164
|
+
blocks: [
|
|
4165
|
+
{
|
|
4166
|
+
type: "table",
|
|
4167
|
+
card: "How it senses",
|
|
4168
|
+
note: "Two ways to sense, both scheduled loops. Reading telemetry catches what the software reports about itself; using the product catches what it does not.",
|
|
4169
|
+
cols: [
|
|
4170
|
+
{ v: "Sensor" },
|
|
4171
|
+
{ v: "Command" },
|
|
4172
|
+
{ v: "Cadence" },
|
|
4173
|
+
{
|
|
4174
|
+
v: "Registered",
|
|
4175
|
+
tip: "Whether a scheduler is actually invoking this. Manual runs are never registered — you invoke them.",
|
|
4176
|
+
},
|
|
4177
|
+
],
|
|
4178
|
+
rows: [
|
|
4179
|
+
[
|
|
4180
|
+
{ t: "text", v: "Reading telemetry (Monitoring)" },
|
|
4181
|
+
{ t: "mono", v: "/lisa:monitor" },
|
|
4182
|
+
{ t: "text", v: "once a day" },
|
|
4183
|
+
fCell("no", {
|
|
4184
|
+
title: "Register the monitoring loop",
|
|
4185
|
+
sub: "Nothing is watching telemetry today",
|
|
4186
|
+
intro:
|
|
4187
|
+
"The monitoring loop scans errors, latency and fault rates across every connected provider and files a regression ticket when a threshold is crossed. Until it is registered, a production regression waits for a human to notice.",
|
|
4188
|
+
steps: [
|
|
4189
|
+
{
|
|
4190
|
+
text: "Run setup and register this loop.",
|
|
4191
|
+
cmd: "/lisa:setup:automations",
|
|
4192
|
+
},
|
|
4193
|
+
{
|
|
4194
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
4195
|
+
cmd: "/lisa:verify:automation",
|
|
4196
|
+
},
|
|
4197
|
+
],
|
|
4198
|
+
foot: "A sensor that is not running produces no findings — which looks exactly like a healthy system with nothing to report. That ambiguity is the whole risk of this layer.",
|
|
4199
|
+
}),
|
|
4200
|
+
],
|
|
4201
|
+
[
|
|
4202
|
+
{ t: "text", v: "Using the product (Exploratory QA)" },
|
|
4203
|
+
{ t: "mono", v: "/lisa:exploratory-qa" },
|
|
4204
|
+
{ t: "text", v: "once a day" },
|
|
4205
|
+
fCell("no", {
|
|
4206
|
+
title: "Register the exploratory-QA loop",
|
|
4207
|
+
sub: "Nothing is exercising the product today",
|
|
4208
|
+
intro:
|
|
4209
|
+
"The QA loop drives the product the way a curious user would, hunting for bugs no acceptance criterion named. It is the same competence as the Verify factory, but continuous and unbounded rather than a bounded go/no-go.",
|
|
4210
|
+
steps: [
|
|
4211
|
+
{
|
|
4212
|
+
text: "Run setup and register this loop.",
|
|
4213
|
+
cmd: "/lisa:setup:automations",
|
|
4214
|
+
},
|
|
4215
|
+
{
|
|
4216
|
+
text: "Confirm the registration took, and see when it last ran.",
|
|
4217
|
+
cmd: "/lisa:verify:automation",
|
|
4218
|
+
},
|
|
4219
|
+
],
|
|
4220
|
+
foot: "Whatever it finds is filed straight to the build-ready role and enters intake like any other work — no human promotes it first. Autonomy is the default; the adversarial gate is the quality control, not a person's approval.",
|
|
4221
|
+
}),
|
|
4222
|
+
],
|
|
4223
|
+
],
|
|
4224
|
+
},
|
|
4225
|
+
{
|
|
4226
|
+
type: "table",
|
|
4227
|
+
card: "Signal coverage",
|
|
4228
|
+
note: "Whether the deployed software actually emits each signal, and which gate the signal re-enters the pipeline at. Provider is swappable; instrumentation is what makes the signal real. Click a red instrumented cell for how to close it.",
|
|
4229
|
+
cols: [
|
|
4230
|
+
{ v: "Signal" },
|
|
4231
|
+
{
|
|
4232
|
+
v: "Provider",
|
|
4233
|
+
tip: "The tool that carries this signal. Swappable — the pipeline is provider-neutral.",
|
|
4234
|
+
},
|
|
4235
|
+
{
|
|
4236
|
+
v: "Instrumented",
|
|
4237
|
+
tip: "Does the deployed software actually emit this signal? A provider with no instrumentation carries nothing.",
|
|
4238
|
+
},
|
|
4239
|
+
{
|
|
4240
|
+
v: "Routes back to",
|
|
4241
|
+
tip: "Which factory this signal re-enters the pipeline at when it fires — through that factory’s intake gate.",
|
|
4242
|
+
},
|
|
4243
|
+
],
|
|
4244
|
+
rows: [
|
|
4245
|
+
[
|
|
4246
|
+
{ t: "text", v: "Errors & crashes" },
|
|
4247
|
+
{
|
|
4248
|
+
t: "select",
|
|
4249
|
+
v: "Sentry",
|
|
4250
|
+
mono: true,
|
|
4251
|
+
options: ["Sentry", "Rollbar", "Bugsnag", "Crashlytics"],
|
|
4252
|
+
},
|
|
4253
|
+
fCell("yes"),
|
|
4254
|
+
{ t: "text", v: "Implement" },
|
|
4255
|
+
],
|
|
4256
|
+
[
|
|
4257
|
+
{ t: "text", v: "Logs" },
|
|
4258
|
+
{
|
|
4259
|
+
t: "select",
|
|
4260
|
+
v: "CloudWatch Logs",
|
|
4261
|
+
mono: true,
|
|
4262
|
+
options: [
|
|
4263
|
+
"CloudWatch Logs",
|
|
4264
|
+
"Datadog",
|
|
4265
|
+
"Splunk",
|
|
4266
|
+
"Loki",
|
|
4267
|
+
"Axiom",
|
|
4268
|
+
],
|
|
4269
|
+
},
|
|
4270
|
+
fCell("yes"),
|
|
4271
|
+
{ t: "text", v: "Implement" },
|
|
4272
|
+
],
|
|
4273
|
+
[
|
|
4274
|
+
{ t: "text", v: "Metrics & alarms" },
|
|
4275
|
+
{
|
|
4276
|
+
t: "select",
|
|
4277
|
+
v: "CloudWatch Alarms",
|
|
4278
|
+
mono: true,
|
|
4279
|
+
options: [
|
|
4280
|
+
"CloudWatch Alarms",
|
|
4281
|
+
"Datadog",
|
|
4282
|
+
"Prometheus",
|
|
4283
|
+
"Grafana Cloud",
|
|
4284
|
+
],
|
|
4285
|
+
},
|
|
4286
|
+
fCell("yes"),
|
|
4287
|
+
{ t: "text", v: "Implement" },
|
|
4288
|
+
],
|
|
4289
|
+
[
|
|
4290
|
+
{ t: "text", v: "Traces" },
|
|
4291
|
+
{
|
|
4292
|
+
t: "select",
|
|
4293
|
+
v: "AWS X-Ray",
|
|
4294
|
+
mono: true,
|
|
4295
|
+
options: ["AWS X-Ray", "Datadog", "Honeycomb", "Jaeger"],
|
|
4296
|
+
},
|
|
4297
|
+
fCell("no", {
|
|
4298
|
+
title: "Traces are not instrumented",
|
|
4299
|
+
sub: "Provider connected, nothing emitting",
|
|
4300
|
+
intro:
|
|
4301
|
+
"X-Ray is available but the code emits no spans, so trace fault rates and latency percentiles are invisible. A connected provider with no instrumentation is the sensor version of a gate that never fires — it looks configured and reports nothing.",
|
|
4302
|
+
steps: [
|
|
4303
|
+
{
|
|
4304
|
+
text: "Instrument the code to emit spans — ideally through OpenTelemetry, so the backend stays swappable.",
|
|
4305
|
+
cmd: "OTel SDK · X-Ray",
|
|
4306
|
+
},
|
|
4307
|
+
{
|
|
4308
|
+
text: "Confirm traces are arriving before trusting the silence.",
|
|
4309
|
+
cmd: "/lisa:verify:automation",
|
|
4310
|
+
},
|
|
4311
|
+
],
|
|
4312
|
+
foot: "OpenTelemetry is the Observe layer's version of the control/provider split: instrument once, swap the backend freely.",
|
|
4313
|
+
}),
|
|
4314
|
+
{ t: "text", v: "Implement" },
|
|
4315
|
+
],
|
|
4316
|
+
[
|
|
4317
|
+
{ t: "text", v: "Frontend real-user monitoring" },
|
|
4318
|
+
{ t: "mono", v: "—" },
|
|
4319
|
+
fCell("n/a", {
|
|
4320
|
+
title: "RUM — not applicable",
|
|
4321
|
+
sub: "No frontend in this stack",
|
|
4322
|
+
intro:
|
|
4323
|
+
"Real-user monitoring measures a rendered UI. This project is a CLI, so there is nothing to instrument. It becomes a real signal on a frontend stack, routing to the Implement gate for perf and error regressions.",
|
|
4324
|
+
steps: [
|
|
4325
|
+
{
|
|
4326
|
+
text: "On a frontend stack, connect a RUM provider.",
|
|
4327
|
+
cmd: "Datadog RUM · Sentry · PostHog",
|
|
4328
|
+
},
|
|
4329
|
+
],
|
|
4330
|
+
}),
|
|
4331
|
+
{ t: "text", v: "—" },
|
|
4332
|
+
],
|
|
4333
|
+
[
|
|
4334
|
+
{ t: "text", v: "Uptime & synthetics" },
|
|
4335
|
+
{ t: "mono", v: "—" },
|
|
4336
|
+
fCell("n/a", {
|
|
4337
|
+
title: "Synthetics — not applicable",
|
|
4338
|
+
sub: "No served endpoint to probe",
|
|
4339
|
+
intro:
|
|
4340
|
+
"Synthetic checks ping a running endpoint on a schedule. A CLI has none. On a service or web stack this is one of the highest-value sensors — it catches an outage before any user reports it.",
|
|
4341
|
+
steps: [
|
|
4342
|
+
{
|
|
4343
|
+
text: "On a service stack, add a synthetic monitor for the key journeys.",
|
|
4344
|
+
cmd: "Checkly · Datadog Synthetics · Pingdom",
|
|
4345
|
+
},
|
|
4346
|
+
],
|
|
4347
|
+
}),
|
|
4348
|
+
{ t: "text", v: "—" },
|
|
4349
|
+
],
|
|
4350
|
+
[
|
|
4351
|
+
{ t: "text", v: "Product analytics" },
|
|
4352
|
+
{ t: "mono", v: "—" },
|
|
4353
|
+
fCell("n/a", {
|
|
4354
|
+
title: "Product analytics — not applicable",
|
|
4355
|
+
sub: "No product surface to measure",
|
|
4356
|
+
intro:
|
|
4357
|
+
"Usage analytics show what people try to do and where they give up — the richest Research input there is, and the one that routes to the Research gate rather than Implement. It needs a product surface this CLI does not have.",
|
|
4358
|
+
steps: [
|
|
4359
|
+
{
|
|
4360
|
+
text: "On a product stack, connect an analytics provider.",
|
|
4361
|
+
cmd: "PostHog · Amplitude · Mixpanel",
|
|
4362
|
+
},
|
|
4363
|
+
],
|
|
4364
|
+
foot: "This is the sensor that closes the whole loop: usage becomes PRDs, which become work, which becomes software, which is observed again.",
|
|
4365
|
+
}),
|
|
4366
|
+
{ t: "text", v: "Research" },
|
|
4367
|
+
],
|
|
4368
|
+
],
|
|
4369
|
+
},
|
|
4370
|
+
fLegend(
|
|
4371
|
+
"<b>Factories fail loudly; sensors fail silently.</b> A factory that cannot run emits a recovery-required outcome. A dead sensor emits nothing — which is pixel-identical to a healthy system with no problems. So a sensor needs a liveness signal independent of its findings: “no bugs this week” and “the bug pipe is broken” must never render the same. The disagreement between sensors is itself a signal — a crash a customer reported that never appeared in errors means the instrumentation has a hole; a spike no customer felt means you are alerting on something users do not."
|
|
4372
|
+
),
|
|
1410
4373
|
],
|
|
1411
|
-
sections: {},
|
|
1412
4374
|
};
|
|
1413
|
-
|
|
1414
|
-
/*
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
4375
|
+
/* ---------------------------------- Credentials ---------------------------------- */
|
|
4376
|
+
/* PROPOSED: the credential map — /lisa:setup:credentials and
|
|
4377
|
+
/lisa:verify:credentials, plus per-connection identity provisioning
|
|
4378
|
+
------------------------------------------------------------------
|
|
4379
|
+
1. What to build — a central inventory of every credential the
|
|
4380
|
+
factories use, keyed by (connection × surface), recording: does the
|
|
4381
|
+
AGENT have its own identity for this connection, which store holds
|
|
4382
|
+
it, which identity path each surface uses (OIDC federation /
|
|
4383
|
+
human session / service-account token), and when it was last
|
|
4384
|
+
PROVEN by being exercised from that surface. Plus the two
|
|
4385
|
+
infrastructure choices below: which credential store, and whether
|
|
4386
|
+
an egress proxy substitutes values so the agent never reads them.
|
|
4387
|
+
2. Why — Lisa already refuses to put secret values in config, but it
|
|
4388
|
+
has no idea whether a given surface can actually authenticate. That
|
|
4389
|
+
gap is the whole "green locally, red headless" failure class, and
|
|
4390
|
+
it is what makes intake's "provable access to tooling" gate
|
|
4391
|
+
aspirational rather than real. A vault can tell you a secret
|
|
4392
|
+
exists; only exercising it from the surface tells you the factory
|
|
4393
|
+
can run there.
|
|
4394
|
+
3. Today — credentials live in env vars, the OS keychain, and CI
|
|
4395
|
+
secrets, provisioned by hand per vendor. Nothing enumerates them,
|
|
4396
|
+
nothing knows which surface each serves, and nothing proves them.
|
|
4397
|
+
4. Replaces — nothing. Lisa does NOT become a vault; storage stays
|
|
4398
|
+
with whatever store is chosen below. Lisa owns the map and the
|
|
4399
|
+
proof, not the secrets.
|
|
4400
|
+
DEFAULT STANCE (load-bearing): the agent gets its own identity for
|
|
4401
|
+
every connection, on EVERY surface including the local workstation.
|
|
4402
|
+
The human's credentials are not the agent's ambient authority.
|
|
4403
|
+
"Driving" — the agent acting as the human — is an explicit, bounded
|
|
4404
|
+
elevation, never the default. This is what makes a local run predict a
|
|
4405
|
+
headless run: same identity, same path, same result. */
|
|
4406
|
+
DATA.sections.credentials = {
|
|
4407
|
+
title: "Credentials",
|
|
4408
|
+
desc: "Where every credential the factories use lives, and whether each surface can actually authenticate with it. Lisa stores no secret values — only which identity is used where, and when it was last proven by being exercised. The agent gets its own identity by default, everywhere; borrowing yours is the exception, not the baseline.",
|
|
4409
|
+
src: ".lisa.config.json credentials.*",
|
|
1419
4410
|
blocks: [
|
|
1420
4411
|
{
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
4412
|
+
card: "Where secrets live",
|
|
4413
|
+
note: "Lisa never stores a secret value — config holds IDs, slugs and references only. These two choices decide where the values actually live and whether the agent can ever read them.",
|
|
4414
|
+
rows: [
|
|
4415
|
+
{
|
|
4416
|
+
key: "credentials.store",
|
|
4417
|
+
label: "Credential store",
|
|
4418
|
+
desc: "The vault of record. Everything below fetches from here rather than from a copy pasted per surface.",
|
|
4419
|
+
control: sel("1Password", [
|
|
4420
|
+
"1Password",
|
|
4421
|
+
"Bitwarden Secrets Manager",
|
|
4422
|
+
"Doppler",
|
|
4423
|
+
"Infisical",
|
|
4424
|
+
"HashiCorp Vault",
|
|
4425
|
+
"AWS Secrets Manager",
|
|
4426
|
+
"Azure Key Vault",
|
|
4427
|
+
"GCP Secret Manager",
|
|
4428
|
+
]),
|
|
4429
|
+
},
|
|
4430
|
+
{
|
|
4431
|
+
key: "credentials.rotation",
|
|
4432
|
+
label: "Rotation window",
|
|
4433
|
+
desc: "How long a static credential may live before it must be replaced. Short-lived credentials matter more than hidden ones: a token that expires on its own has a small blast radius whether or not it leaked.",
|
|
4434
|
+
control: num(90, "days"),
|
|
4435
|
+
},
|
|
1432
4436
|
],
|
|
1433
4437
|
},
|
|
1434
4438
|
{
|
|
1435
|
-
type: "
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
tiles: [
|
|
1441
|
-
{ k: "Project types", v: "2", s: "typescript · expo" },
|
|
1442
|
-
{ k: "Work tracker", v: "JIRA", s: "project ACME" },
|
|
1443
|
-
{ k: "PRD source", v: "Notion", s: "PRD queue database" },
|
|
1444
|
-
{ k: "Harness", v: "fleet", s: "all 6 coding agents" },
|
|
1445
|
-
{ k: "Plugins enabled", v: "11", s: "4 Lisa + 7 curated" },
|
|
4439
|
+
type: "table",
|
|
4440
|
+
card: "Agent identities",
|
|
4441
|
+
note: "One row per connection. Identity answers whether the agent authenticates as itself; the three surface columns answer whether that identity is provisioned and proven from each place a factory runs. Click a surface cell for its identity path and commands.",
|
|
4442
|
+
cols: [
|
|
4443
|
+
{ v: "Connection" },
|
|
1446
4444
|
{
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
s: "statements · branches · functions · lines",
|
|
4445
|
+
v: "Identity",
|
|
4446
|
+
tip: "own = the agent authenticates as itself · yours = it borrows your credential, which is a gap · human only = deliberately not delegable.",
|
|
1450
4447
|
},
|
|
1451
4448
|
{
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
s: "incl. CodeRabbit + GitGuardian",
|
|
4449
|
+
v: "Stored in",
|
|
4450
|
+
tip: "Where the value lives. OIDC means no stored secret at all — the runtime's own identity is the credential.",
|
|
1455
4451
|
},
|
|
1456
4452
|
{
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
4453
|
+
v: "Local",
|
|
4454
|
+
center: true,
|
|
4455
|
+
tip: "Provisioned and proven from your workstation. Local is not privileged — it is just another runner.",
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
v: "CI",
|
|
4459
|
+
center: true,
|
|
4460
|
+
tip: "Provisioned and proven from the pipeline. This is the surface that can usually be made keyless via OIDC federation.",
|
|
4461
|
+
},
|
|
4462
|
+
{
|
|
4463
|
+
v: "Remote headless",
|
|
4464
|
+
center: true,
|
|
4465
|
+
tip: "Provisioned and proven from a scheduled cloud run. Structurally the weakest — no human, and usually no federation, so a static token.",
|
|
1460
4466
|
},
|
|
1461
4467
|
],
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
4468
|
+
rows: [
|
|
4469
|
+
[
|
|
4470
|
+
{ t: "text", v: "Tracker — JIRA" },
|
|
4471
|
+
{ t: "badge", v: "own", cls: "pass" },
|
|
4472
|
+
{ t: "mono", v: "1Password" },
|
|
4473
|
+
fCell("yes", {
|
|
4474
|
+
title: "JIRA — agent identity, local",
|
|
4475
|
+
sub: "Bot user, not your account",
|
|
4476
|
+
intro:
|
|
4477
|
+
"The agent authenticates as its own JIRA bot user, so every transition and comment is attributable to it rather than smeared into your account — and its permissions can be cut to what the factories need instead of inheriting your admin rights.",
|
|
4478
|
+
meta: [
|
|
4479
|
+
{ k: "Identity", v: "service account (bot user)" },
|
|
4480
|
+
{ k: "Path", v: "API token from the store" },
|
|
4481
|
+
{ k: "Last proven", v: "never" },
|
|
4482
|
+
],
|
|
4483
|
+
steps: [
|
|
4484
|
+
{
|
|
4485
|
+
text: "Provision the agent's own identity for this connection and surface.",
|
|
4486
|
+
cmd: "/lisa:setup:credentials",
|
|
4487
|
+
},
|
|
4488
|
+
{
|
|
4489
|
+
text: "Prove it by exercising it from this surface.",
|
|
4490
|
+
cmd: "/lisa:verify:credentials",
|
|
4491
|
+
},
|
|
4492
|
+
],
|
|
4493
|
+
foot: "Per-seat licensing is the real cost of agent identity on JIRA, Linear and Notion — a bot user may consume a seat. Worth deciding deliberately rather than discovering at renewal.",
|
|
4494
|
+
}),
|
|
4495
|
+
fCell("yes"),
|
|
4496
|
+
fCell("yes"),
|
|
4497
|
+
],
|
|
4498
|
+
[
|
|
4499
|
+
{ t: "text", v: "PRD source — Notion" },
|
|
4500
|
+
{
|
|
4501
|
+
t: "badge",
|
|
4502
|
+
v: "yours",
|
|
4503
|
+
cls: "warn",
|
|
4504
|
+
how: {
|
|
4505
|
+
title: "The agent is borrowing your credential",
|
|
4506
|
+
sub: "This is the gap behind every red headless cell",
|
|
4507
|
+
intro:
|
|
4508
|
+
"Notion is reached through an OAuth session that authenticates as you. That is why it works at your desk and fails everywhere else — and why every PRD the factory files is attributed to you rather than to the agent.",
|
|
4509
|
+
meta: [
|
|
4510
|
+
{ k: "Identity", v: "human session (yours)" },
|
|
4511
|
+
{
|
|
4512
|
+
k: "Should be",
|
|
4513
|
+
v: "internal integration (agent's own)",
|
|
4514
|
+
},
|
|
4515
|
+
],
|
|
4516
|
+
steps: [
|
|
4517
|
+
{
|
|
4518
|
+
text: "Give the agent its own Notion integration and share the PRD database with it.",
|
|
4519
|
+
cmd: "/lisa:setup:credentials",
|
|
4520
|
+
},
|
|
4521
|
+
{
|
|
4522
|
+
text: "Prove it from each surface — a local pass no longer implies the others once identities differ.",
|
|
4523
|
+
cmd: "/lisa:verify:credentials",
|
|
4524
|
+
},
|
|
4525
|
+
],
|
|
4526
|
+
foot: "Fixing this turns three cells green at once: same identity, same path, every surface. That is the whole argument for agent-identity-by-default.",
|
|
4527
|
+
},
|
|
4528
|
+
},
|
|
4529
|
+
{ t: "mono", v: "— your session —" },
|
|
4530
|
+
fCell("yes"),
|
|
4531
|
+
fCell("no"),
|
|
4532
|
+
fCell("no"),
|
|
4533
|
+
],
|
|
4534
|
+
[
|
|
4535
|
+
{ t: "text", v: "Repository & pull requests — GitHub" },
|
|
4536
|
+
{ t: "badge", v: "own", cls: "pass" },
|
|
4537
|
+
{ t: "mono", v: "GitHub App · OIDC in CI" },
|
|
4538
|
+
fCell("yes", {
|
|
4539
|
+
title: "GitHub — agent identity, local",
|
|
4540
|
+
sub: "GitHub App installation token",
|
|
4541
|
+
intro:
|
|
4542
|
+
"A GitHub App is the right primitive here: scoped per repository, tokens that expire in an hour, and commits and pull requests attributable to the app rather than to a machine user wearing a person's name.",
|
|
4543
|
+
meta: [
|
|
4544
|
+
{ k: "Identity", v: "GitHub App installation" },
|
|
4545
|
+
{ k: "Path", v: "short-lived installation token" },
|
|
4546
|
+
{ k: "Last proven", v: "never" },
|
|
4547
|
+
],
|
|
4548
|
+
steps: [
|
|
4549
|
+
{
|
|
4550
|
+
text: "Provision the app installation for this surface.",
|
|
4551
|
+
cmd: "/lisa:setup:credentials",
|
|
4552
|
+
},
|
|
4553
|
+
{
|
|
4554
|
+
text: "Prove it by opening and closing a scratch pull request.",
|
|
4555
|
+
cmd: "/lisa:verify:credentials",
|
|
4556
|
+
},
|
|
4557
|
+
],
|
|
4558
|
+
foot: "This also lets the agent be the actual committer rather than a Co-authored-by trailer on your commit.",
|
|
4559
|
+
}),
|
|
4560
|
+
fCell("yes", {
|
|
4561
|
+
title: "GitHub — agent identity, CI",
|
|
4562
|
+
sub: "No stored secret at all",
|
|
4563
|
+
intro:
|
|
4564
|
+
"In the pipeline the runtime federates: it mints a short-lived OIDC token the provider already trusts. Nothing is stored, so there is nothing to rotate and nothing to leak. This is the strongest of the three surfaces.",
|
|
4565
|
+
meta: [
|
|
4566
|
+
{ k: "Identity", v: "OIDC federation" },
|
|
4567
|
+
{ k: "Stored secret", v: "none" },
|
|
4568
|
+
{ k: "Last proven", v: "never" },
|
|
4569
|
+
],
|
|
4570
|
+
steps: [
|
|
4571
|
+
{
|
|
4572
|
+
text: "Nothing to provision — federation is configured once at the provider.",
|
|
4573
|
+
cmd: "/lisa:setup:credentials",
|
|
4574
|
+
},
|
|
4575
|
+
{
|
|
4576
|
+
text: "Prove the federation is trusted from the pipeline.",
|
|
4577
|
+
cmd: "/lisa:verify:credentials",
|
|
4578
|
+
},
|
|
4579
|
+
],
|
|
4580
|
+
foot: "Where a surface can federate, it should. Keyless beats hidden.",
|
|
4581
|
+
}),
|
|
4582
|
+
fCell("yes"),
|
|
4583
|
+
],
|
|
4584
|
+
[
|
|
4585
|
+
{ t: "text", v: "Cloud & deploys — AWS" },
|
|
4586
|
+
{ t: "badge", v: "own", cls: "pass" },
|
|
4587
|
+
{ t: "mono", v: "IAM role · OIDC in CI" },
|
|
4588
|
+
fCell("partial", {
|
|
4589
|
+
title: "AWS — role assumption incomplete locally",
|
|
4590
|
+
sub: "The agent can authenticate, but not assume the role",
|
|
4591
|
+
intro:
|
|
4592
|
+
"The agent has its own IAM identity, but the local trust policy does not yet let it assume the deploy role — so it can read but not ship from this surface. CI can, because federation is already trusted there.",
|
|
4593
|
+
meta: [
|
|
4594
|
+
{ k: "Identity", v: "IAM role (assumed)" },
|
|
4595
|
+
{ k: "Path", v: "role assumption from agent principal" },
|
|
4596
|
+
{ k: "Last proven", v: "never" },
|
|
4597
|
+
],
|
|
4598
|
+
steps: [
|
|
4599
|
+
{
|
|
4600
|
+
text: "Add this surface's principal to the role's trust policy.",
|
|
4601
|
+
cmd: "/lisa:setup:credentials",
|
|
4602
|
+
},
|
|
4603
|
+
{
|
|
4604
|
+
text: "Prove it by assuming the role and calling a read-only API.",
|
|
4605
|
+
cmd: "/lisa:verify:credentials",
|
|
4606
|
+
},
|
|
4607
|
+
],
|
|
4608
|
+
}),
|
|
4609
|
+
fCell("yes"),
|
|
4610
|
+
fCell("no"),
|
|
4611
|
+
],
|
|
4612
|
+
[
|
|
4613
|
+
{ t: "text", v: "Observability — Sentry" },
|
|
4614
|
+
{ t: "badge", v: "own", cls: "pass" },
|
|
4615
|
+
{ t: "mono", v: "1Password" },
|
|
4616
|
+
fCell("yes"),
|
|
4617
|
+
fCell("yes"),
|
|
4618
|
+
fCell("yes"),
|
|
4619
|
+
],
|
|
4620
|
+
[
|
|
4621
|
+
{ t: "text", v: "Package registry — npm" },
|
|
4622
|
+
{ t: "badge", v: "own", cls: "pass" },
|
|
4623
|
+
{ t: "mono", v: "1Password" },
|
|
4624
|
+
fCell("yes"),
|
|
4625
|
+
fCell("yes"),
|
|
4626
|
+
fCell("no", {
|
|
4627
|
+
title: "npm publish — not provisioned headless",
|
|
4628
|
+
sub: "Deliberate, for now",
|
|
4629
|
+
intro:
|
|
4630
|
+
"The publish token is not available to scheduled runs, so a cron cannot cut a release. Whether that is a gap or a boundary is a decision: releases may be something you want a proven pipeline to do and an unattended loop not to.",
|
|
4631
|
+
meta: [
|
|
4632
|
+
{ k: "Identity", v: "service account (automation token)" },
|
|
4633
|
+
{ k: "Path", v: "token from the store" },
|
|
4634
|
+
],
|
|
4635
|
+
steps: [
|
|
4636
|
+
{
|
|
4637
|
+
text: "Provision it for this surface if unattended releases are intended.",
|
|
4638
|
+
cmd: "/lisa:setup:credentials",
|
|
4639
|
+
},
|
|
4640
|
+
],
|
|
4641
|
+
foot: "A missing credential is a weak way to enforce a policy — it looks like a gap. If this is intentional, say so with an approval gate instead.",
|
|
4642
|
+
}),
|
|
4643
|
+
],
|
|
4644
|
+
[
|
|
4645
|
+
{ t: "text", v: "Protected deploy approval" },
|
|
4646
|
+
{ t: "badge", v: "human only", cls: "na" },
|
|
4647
|
+
{ t: "mono", v: "—" },
|
|
4648
|
+
fCell("n/a", {
|
|
4649
|
+
title: "Human authority — not delegable",
|
|
4650
|
+
sub: "By design, on every surface",
|
|
4651
|
+
intro:
|
|
4652
|
+
"Approving a protected deploy is not a credential the agent should ever hold. The point of the gate is that a person accepted the risk — an agent approving on your behalf would make the gate ceremonial.",
|
|
4653
|
+
meta: [
|
|
4654
|
+
{ k: "Identity", v: "human, always" },
|
|
4655
|
+
{ k: "Delegable", v: "no" },
|
|
4656
|
+
],
|
|
4657
|
+
steps: [
|
|
4658
|
+
{
|
|
4659
|
+
text: "Nothing to provision. The agent requests approval and waits.",
|
|
4660
|
+
cmd: "approval-requested",
|
|
4661
|
+
},
|
|
4662
|
+
],
|
|
4663
|
+
foot: "This is also where driving belongs: if you want the agent to act with your authority, that is a deliberate, bounded elevation — never ambient.",
|
|
4664
|
+
}),
|
|
4665
|
+
fCell("n/a"),
|
|
4666
|
+
fCell("n/a"),
|
|
4667
|
+
],
|
|
1472
4668
|
],
|
|
1473
4669
|
},
|
|
4670
|
+
fLegend(
|
|
4671
|
+
'<b>The agent has its own identity by default — everywhere, including your workstation.</b> Your laptop is not a privileged environment; it is just another runner with better latency and worse uptime. When the agent authenticates as itself on every surface, a local pass genuinely predicts a headless pass, because it is the same identity taking the same path. The <span class="mono">yours</span> row above is the exception that proves it: borrowing your session is exactly why that connection works at your desk and nowhere else. Where a surface can federate (CI, via OIDC) it should — keyless beats hidden. The remote-headless static token is the structural weak link, so scope it narrowly and rotate it often; a short-lived credential is worth more than a well-hidden one.'
|
|
4672
|
+
),
|
|
4673
|
+
],
|
|
4674
|
+
};
|
|
4675
|
+
/* ---------------------------------- Environments ---------------------------------- */
|
|
4676
|
+
/* PROPOSED: environments as a first-class, per-agent concept.
|
|
4677
|
+
1. What to build — a roster of the places a factory can run, where
|
|
4678
|
+
"Remote" is NOT one thing: every coding agent ships its own hosted
|
|
4679
|
+
runtime, so the remote row changes identity with the staff. Lisa
|
|
4680
|
+
needs to know which environments are provisioned, how credentials
|
|
4681
|
+
reach each one, and which scheduler drives unattended work there.
|
|
4682
|
+
2. Why — the console keeps asking "local or headless?" as if headless
|
|
4683
|
+
were a single place. It is not: Claude's hosted runtime, Codex's,
|
|
4684
|
+
and a GitHub Actions runner are different substrates with different
|
|
4685
|
+
credential paths and different schedulers. Collapsing them hides the
|
|
4686
|
+
question an operator actually has — "will tonight's run work, on the
|
|
4687
|
+
thing that will actually run it?"
|
|
4688
|
+
3. Today — `harness` picks which agents get surfaces installed, and CI
|
|
4689
|
+
is configured per workflow. Nothing models the vendor remotes at
|
|
4690
|
+
all, and nothing records which environments are proven.
|
|
4691
|
+
4. Replaces — nothing. This is the axis the factory Connections tables
|
|
4692
|
+
already imply with their Local/Headless columns; those columns
|
|
4693
|
+
should eventually be generated from this roster.
|
|
4694
|
+
NOTE: OpenCode has no vendor cloud — it is self-hosted, so its remote
|
|
4695
|
+
row is whatever infrastructure you provide. That is a real difference
|
|
4696
|
+
in kind, not a gap, and the page must say so rather than showing a
|
|
4697
|
+
missing product as a red cell. */
|
|
4698
|
+
const REMOTE_ENVS = {
|
|
4699
|
+
claude: {
|
|
4700
|
+
name: "Claude Code on the web",
|
|
4701
|
+
manual: "Run now",
|
|
4702
|
+
scheduled: "routine schedule (1 hour minimum)",
|
|
4703
|
+
event: "GitHub PR / release · API POST /fire",
|
|
4704
|
+
state: "ready",
|
|
4705
|
+
blurb:
|
|
4706
|
+
"Anthropic-hosted sessions, each a fresh isolated run with no prior context. Routines are the scheduler, and they take all three trigger types — a single routine can run nightly, fire from a deploy script, and react to every new PR.",
|
|
4707
|
+
},
|
|
4708
|
+
codex: {
|
|
4709
|
+
name: "Codex cloud",
|
|
4710
|
+
manual: "dispatch a task",
|
|
4711
|
+
scheduled: "cloud automations",
|
|
4712
|
+
event: "— none —",
|
|
4713
|
+
state: "ready",
|
|
4714
|
+
blurb:
|
|
4715
|
+
"OpenAI-hosted container provisioned per task, checked out from the repository. Codex has both local and cloud automations — the cloud ones are the set-and-forget kind that do not need your machine running.",
|
|
4716
|
+
},
|
|
4717
|
+
cursor: {
|
|
4718
|
+
name: "Cursor Cloud Agents",
|
|
4719
|
+
manual: "dispatch a cloud agent",
|
|
4720
|
+
scheduled: "— none —",
|
|
4721
|
+
event: "— none —",
|
|
4722
|
+
state: "gap",
|
|
4723
|
+
blurb:
|
|
4724
|
+
"Cursor's hosted agents, with a self-hosted variant on Enterprise for codebases that must stay on your own infrastructure. Not provisioned for this project.",
|
|
4725
|
+
},
|
|
4726
|
+
agy: {
|
|
4727
|
+
name: "Antigravity background tasks",
|
|
4728
|
+
manual: "dispatch a task",
|
|
4729
|
+
scheduled: "Antigravity schedules",
|
|
4730
|
+
event: "— none —",
|
|
4731
|
+
state: "gap",
|
|
4732
|
+
blurb:
|
|
4733
|
+
"Antigravity 2.0 added scheduled background tasks. Not provisioned for this project, and its agent-layer enforcement is unproven — see the picker above.",
|
|
4734
|
+
},
|
|
4735
|
+
copilot: {
|
|
4736
|
+
name: "Copilot coding agent",
|
|
4737
|
+
manual: "assign an issue to the agent",
|
|
4738
|
+
scheduled: "— none —",
|
|
4739
|
+
event: "issue assignment",
|
|
4740
|
+
state: "gap",
|
|
4741
|
+
blurb:
|
|
4742
|
+
"GitHub-hosted. Runs in an ephemeral Linux sandbox provisioned per job, non-interactive with constrained network, destroyed when the job ends. Not provisioned for this project.",
|
|
4743
|
+
},
|
|
4744
|
+
opencode: {
|
|
4745
|
+
name: "— no vendor cloud —",
|
|
4746
|
+
manual: "yours to provide",
|
|
4747
|
+
scheduled: "cron you run",
|
|
4748
|
+
event: "yours to provide",
|
|
4749
|
+
state: "na",
|
|
4750
|
+
blurb:
|
|
4751
|
+
"OpenCode is self-hosted and provider-agnostic, so there is no vendor remote to provision. Remote here means whatever infrastructure you stand up — which is a difference in kind, not a gap.",
|
|
4752
|
+
},
|
|
4753
|
+
};
|
|
4754
|
+
|
|
4755
|
+
/* Each environment gets its own card, because each one is independently
|
|
4756
|
+
configured — most importantly its own credential gateway. A gateway
|
|
4757
|
+
deployed locally but not in the remote leaves the remote unprotected,
|
|
4758
|
+
and a single shared setting would hide exactly that.
|
|
4759
|
+
|
|
4760
|
+
Every environment is described by the same three trigger types, because
|
|
4761
|
+
"where it runs" and "what starts it" are independent questions that get
|
|
4762
|
+
conflated constantly. Local is NOT manual-only: every agent can be
|
|
4763
|
+
scheduled on your workstation, it just needs the machine awake. And the
|
|
4764
|
+
vendor remotes are not schedule-only: Claude routines, for instance,
|
|
4765
|
+
also fire on GitHub events and on an authenticated POST.
|
|
4766
|
+
Verified against vendor docs 2026-07-24. */
|
|
4767
|
+
const GATEWAYS = [
|
|
4768
|
+
"none — direct injection",
|
|
4769
|
+
"Vault Agent",
|
|
4770
|
+
"1Password Connect",
|
|
4771
|
+
"Infisical Agent",
|
|
4772
|
+
"custom egress proxy",
|
|
4773
|
+
];
|
|
4774
|
+
/* Local schedulers are per-agent: some ship a native one, and cron always
|
|
4775
|
+
works. All of them share one constraint — the machine has to be awake. */
|
|
4776
|
+
const LOCAL_SCHED = {
|
|
4777
|
+
claude: "Local routines · /loop · cron",
|
|
4778
|
+
codex: "local automations (~/.codex/automations) · cron",
|
|
4779
|
+
agy: "Antigravity schedules · cron",
|
|
4780
|
+
cursor: "cron",
|
|
4781
|
+
copilot: "cron",
|
|
4782
|
+
opencode: "cron",
|
|
4783
|
+
};
|
|
4784
|
+
function buildEnvironments() {
|
|
4785
|
+
const caps = currentCaps();
|
|
4786
|
+
const remote = REMOTE_ENVS[DATA.staff] || REMOTE_ENVS.claude;
|
|
4787
|
+
const wrap = el("div", "env-cards");
|
|
4788
|
+
const trig = (label, desc, value) => ({
|
|
4789
|
+
key: "",
|
|
4790
|
+
label,
|
|
4791
|
+
desc,
|
|
4792
|
+
control: stat(value, "mono"),
|
|
4793
|
+
});
|
|
4794
|
+
const gatewayRows = (scope, gatewayDesc, egressDesc) => [
|
|
1474
4795
|
{
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
{ name: "ready" },
|
|
1480
|
-
{ name: "in_review" },
|
|
1481
|
-
{ name: "blocked | ticketed" },
|
|
1482
|
-
{ name: "shipped" },
|
|
1483
|
-
{ name: "verified", terminal: true },
|
|
1484
|
-
],
|
|
4796
|
+
key: "environments." + scope + ".gateway",
|
|
4797
|
+
label: "Credential gateway",
|
|
4798
|
+
desc: gatewayDesc,
|
|
4799
|
+
control: sel("none — direct injection", GATEWAYS),
|
|
1485
4800
|
},
|
|
1486
4801
|
{
|
|
1487
|
-
|
|
4802
|
+
key: "environments." + scope + ".egressLock",
|
|
4803
|
+
label: "Restrict egress to the gateway",
|
|
4804
|
+
desc: egressDesc,
|
|
4805
|
+
control: tog(false),
|
|
4806
|
+
},
|
|
4807
|
+
];
|
|
4808
|
+
|
|
4809
|
+
wrap.appendChild(
|
|
4810
|
+
buildCard({
|
|
4811
|
+
card: "Local — your workstation",
|
|
4812
|
+
note: "Not a privileged environment. The same factories, the same agent identity — just better latency, worse uptime, and a human nearby who could intervene.",
|
|
1488
4813
|
rows: [
|
|
4814
|
+
trig(
|
|
4815
|
+
"Manual runs",
|
|
4816
|
+
"You invoke a factory in a session.",
|
|
4817
|
+
"a CLI session"
|
|
4818
|
+
),
|
|
4819
|
+
trig(
|
|
4820
|
+
"Scheduled runs",
|
|
4821
|
+
"Local scheduling is a first-class feature, not a workaround — Claude creates these from the same Routines page, choosing Local instead of Cloud. It only runs while the app is open and the machine is awake; a run that falls during sleep is skipped, then caught up once on wake. In exchange it gets access to local files and a 1-minute minimum interval.",
|
|
4822
|
+
LOCAL_SCHED[DATA.staff] || "cron"
|
|
4823
|
+
),
|
|
4824
|
+
trig(
|
|
4825
|
+
"Event-driven runs",
|
|
4826
|
+
"Local routines take a schedule only. Anything that has to start from an API call or a repository event needs the hosted runtime instead — that is the documented reason to reach for a remote routine rather than a local one.",
|
|
4827
|
+
"— none —"
|
|
4828
|
+
),
|
|
1489
4829
|
{
|
|
1490
4830
|
key: "",
|
|
1491
|
-
label: "
|
|
1492
|
-
desc: "
|
|
1493
|
-
control: stat(
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
4831
|
+
label: "Credential path",
|
|
4832
|
+
desc: "The agent authenticates as itself, not as you. That is what makes a local pass predict a headless pass.",
|
|
4833
|
+
control: stat("agent service account", "mono"),
|
|
4834
|
+
},
|
|
4835
|
+
...gatewayRows(
|
|
4836
|
+
"local",
|
|
4837
|
+
"Substitutes real values into outbound requests so the agent only handles placeholders. None means secrets land in the agent's environment where it — and anything reaching its context — can read them.",
|
|
4838
|
+
"A gateway only protects what passes through it. Without this, an agent that can run shell reaches the network directly and bypasses it."
|
|
4839
|
+
),
|
|
4840
|
+
],
|
|
4841
|
+
})
|
|
4842
|
+
);
|
|
4843
|
+
|
|
4844
|
+
wrap.appendChild(
|
|
4845
|
+
buildCard({
|
|
4846
|
+
card: "CI — the authoritative tier",
|
|
4847
|
+
note: "Where the gates actually gate: every check marked required to merge fires here, server-side, where the worker cannot skip it. Also the one environment that can usually hold no secret at all.",
|
|
4848
|
+
rows: [
|
|
4849
|
+
{
|
|
4850
|
+
key: "ci.provider",
|
|
4851
|
+
label: "Provider",
|
|
4852
|
+
desc: "The gate workflows are generated for whichever system is selected.",
|
|
4853
|
+
control: sel("GitHub Actions", [
|
|
4854
|
+
"GitHub Actions",
|
|
4855
|
+
"GitLab CI",
|
|
4856
|
+
"Jenkins",
|
|
4857
|
+
"CircleCI",
|
|
4858
|
+
"Buildkite",
|
|
4859
|
+
]),
|
|
1497
4860
|
},
|
|
4861
|
+
trig(
|
|
4862
|
+
"Manual runs",
|
|
4863
|
+
"A workflow can be dispatched by hand from the UI or the CLI, with inputs.",
|
|
4864
|
+
"workflow_dispatch"
|
|
4865
|
+
),
|
|
4866
|
+
trig(
|
|
4867
|
+
"Scheduled runs",
|
|
4868
|
+
"Cron in the workflow itself. Runs against the default branch, not your local one.",
|
|
4869
|
+
"schedule (cron)"
|
|
4870
|
+
),
|
|
4871
|
+
trig(
|
|
4872
|
+
"Event-driven runs",
|
|
4873
|
+
"The common case — repository activity starts the run. This is what makes CI the tier that can gate a merge.",
|
|
4874
|
+
"push · pull_request · release"
|
|
4875
|
+
),
|
|
1498
4876
|
{
|
|
1499
4877
|
key: "",
|
|
1500
|
-
label: "
|
|
1501
|
-
desc: "
|
|
1502
|
-
control: stat(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
4878
|
+
label: "Credential path",
|
|
4879
|
+
desc: "Federation mints a short-lived token the provider already trusts — nothing stored, nothing to rotate, nothing to leak.",
|
|
4880
|
+
control: stat("OIDC federation", "mono"),
|
|
4881
|
+
},
|
|
4882
|
+
...gatewayRows(
|
|
4883
|
+
"ci",
|
|
4884
|
+
"Less load-bearing here than elsewhere: where federation is in use there is no long-lived secret for a gateway to hide.",
|
|
4885
|
+
"Runners have broad network access by default. Locking egress is what turns a gateway from advisory into enforcement."
|
|
4886
|
+
),
|
|
1507
4887
|
],
|
|
1508
|
-
}
|
|
4888
|
+
})
|
|
4889
|
+
);
|
|
4890
|
+
|
|
4891
|
+
const remoteCard = buildCard({
|
|
4892
|
+
card: "Remote — " + remote.name,
|
|
4893
|
+
note: remote.blurb,
|
|
4894
|
+
rows: [
|
|
4895
|
+
{
|
|
4896
|
+
key: "",
|
|
4897
|
+
label: "Provider",
|
|
4898
|
+
desc:
|
|
4899
|
+
"Vendor-provided by " +
|
|
4900
|
+
caps.label +
|
|
4901
|
+
". This is the card that changes with the agent selected above.",
|
|
4902
|
+
control: stat(remote.name, "mono"),
|
|
4903
|
+
},
|
|
4904
|
+
trig(
|
|
4905
|
+
"Manual runs",
|
|
4906
|
+
"Dispatching a one-off run to the hosted runtime without waiting for a trigger.",
|
|
4907
|
+
remote.manual
|
|
4908
|
+
),
|
|
4909
|
+
trig(
|
|
4910
|
+
"Scheduled runs",
|
|
4911
|
+
remote.scheduled === "— none —"
|
|
4912
|
+
? "No vendor scheduler for this runtime."
|
|
4913
|
+
: "Runs on the vendor's own scheduler, so it keeps working with your laptop closed — the thing local scheduling cannot do. The tradeoff runs the other way on granularity: hosted schedulers impose a floor that local scheduling does not.",
|
|
4914
|
+
remote.scheduled
|
|
4915
|
+
),
|
|
4916
|
+
trig(
|
|
4917
|
+
"Event-driven runs",
|
|
4918
|
+
remote.event === "— none —"
|
|
4919
|
+
? "Nothing external can start a run here."
|
|
4920
|
+
: "External systems can start a run. This is what lets a monitor or a deploy pipeline hand work to a factory directly.",
|
|
4921
|
+
remote.event
|
|
4922
|
+
),
|
|
4923
|
+
{
|
|
4924
|
+
key: "",
|
|
4925
|
+
label: "Credential path",
|
|
4926
|
+
desc:
|
|
4927
|
+
remote.state === "na"
|
|
4928
|
+
? "No vendor runtime, so no vendor secret store — the identity is whatever your own infrastructure provides."
|
|
4929
|
+
: "The structural weak link: no human to authenticate and usually no federation, so a long-lived token in the runtime's secret store. Scope it narrowly and rotate it often.",
|
|
4930
|
+
control: stat(
|
|
4931
|
+
remote.state === "na"
|
|
4932
|
+
? "yours to provide"
|
|
4933
|
+
: "service account token",
|
|
4934
|
+
"mono"
|
|
4935
|
+
),
|
|
4936
|
+
},
|
|
4937
|
+
...gatewayRows(
|
|
4938
|
+
"remote",
|
|
4939
|
+
"Matters most here, precisely because this is where the long-lived credentials live. A gateway configured locally does nothing for this environment.",
|
|
4940
|
+
"Vendor sandboxes vary in how much network they allow. Without a restriction the gateway is advisory."
|
|
4941
|
+
),
|
|
4942
|
+
],
|
|
4943
|
+
});
|
|
4944
|
+
if (remote.state === "gap") remoteCard.classList.add("env-gap");
|
|
4945
|
+
wrap.appendChild(remoteCard);
|
|
4946
|
+
return wrap;
|
|
4947
|
+
}
|
|
4948
|
+
|
|
4949
|
+
DATA.sections.environments = {
|
|
4950
|
+
title: "Environments",
|
|
4951
|
+
desc: "The places a factory can run. Local and CI are the same wherever you look; Remote is not — each coding agent ships its own hosted runtime, so pick the agent first and the remote card becomes whatever that vendor provides. Each environment is configured independently — most importantly its own credential gateway.",
|
|
4952
|
+
src: ".lisa.config.json environments.*",
|
|
4953
|
+
blocks: [
|
|
4954
|
+
fStaffPicker(),
|
|
4955
|
+
{ type: "environments" },
|
|
4956
|
+
fLegend(
|
|
4957
|
+
"<b>Remote is not one place, and local is not manual-only.</b> Every agent ships its own hosted runtime with its own credential store and its own scheduler, so “does this work headless?” has a different answer per agent — which is why this page and the factory pages share one staff selector. Every environment can be scheduled; what differs is durability and granularity. A hosted scheduler survives a closed laptop but imposes a floor on how often it can fire — <b>which is why a loop specified at every 10 minutes cannot run as a Claude cloud routine at all</b>, and has to live locally or in CI. On credentials the ranking is: CI is strongest because it can federate and hold no secret at all; local is next because a human is nearby; the vendor remotes are weakest, since they have neither a human nor federation and fall back to a long-lived token. Scope those narrowly, rotate them often, and put a gateway in front of them — a short-lived credential is worth more than a well-hidden one."
|
|
4958
|
+
),
|
|
1509
4959
|
],
|
|
1510
4960
|
};
|
|
1511
4961
|
|
|
@@ -1574,7 +5024,9 @@
|
|
|
1574
5024
|
id: "setup.automations",
|
|
1575
5025
|
label: "Schedule the automations",
|
|
1576
5026
|
desc: "Creates the six-automation fleet — intake, repair, and the QA / Product Planning / Monitoring loops — on the harness’s native scheduler.",
|
|
1577
|
-
|
|
5027
|
+
// Renamed from /lisa:setup-automations — see the PROPOSED
|
|
5028
|
+
// command-renames marker on the Research factory page.
|
|
5029
|
+
command: "/lisa:setup:automations",
|
|
1578
5030
|
},
|
|
1579
5031
|
{
|
|
1580
5032
|
id: "setup.exploration",
|
|
@@ -1616,7 +5068,7 @@
|
|
|
1616
5068
|
// the column carries a hover tooltip defining it.
|
|
1617
5069
|
const AUTOMATION_COL = {
|
|
1618
5070
|
v: "Automation",
|
|
1619
|
-
tip: "The scheduled automation (created by /lisa:setup
|
|
5071
|
+
tip: "The scheduled automation (created by /lisa:setup:automations) that already runs this command on a cadence. — means manual-only; “via intake” / “via implement” means it runs inside that command’s cycle. Invoke the command yourself to force a cycle now.",
|
|
1620
5072
|
};
|
|
1621
5073
|
DATA.sections.workflow = {
|
|
1622
5074
|
title: "Core workflow",
|
|
@@ -2360,54 +5812,31 @@
|
|
|
2360
5812
|
{
|
|
2361
5813
|
type: "table",
|
|
2362
5814
|
card: "Fleet targets",
|
|
2363
|
-
|
|
5815
|
+
note: "Which agents Lisa installs governance into. Enabling one does not make every factory work for it — see the capability row on each factory page, which changes with the agent selected there.",
|
|
5816
|
+
cols: ["Agent", "Enabled"],
|
|
2364
5817
|
rows: [
|
|
2365
5818
|
[
|
|
2366
5819
|
{ t: "mono", v: "claude" },
|
|
2367
|
-
{
|
|
2368
|
-
t: "text",
|
|
2369
|
-
v: ".claude/, .claude-plugin/, CLAUDE.md; plugins installed from the lisa marketplace at project scope",
|
|
2370
|
-
},
|
|
2371
5820
|
{ t: "toggle", v: true },
|
|
2372
5821
|
],
|
|
2373
5822
|
[
|
|
2374
5823
|
{ t: "mono", v: "codex" },
|
|
2375
|
-
{
|
|
2376
|
-
t: "text",
|
|
2377
|
-
v: ".codex/, .codex-plugin/, .agents/, AGENTS.md; hooks merged into ~/.codex/hooks.json",
|
|
2378
|
-
},
|
|
2379
5824
|
{ t: "toggle", v: true },
|
|
2380
5825
|
],
|
|
2381
5826
|
[
|
|
2382
5827
|
{ t: "mono", v: "cursor" },
|
|
2383
|
-
{
|
|
2384
|
-
t: "text",
|
|
2385
|
-
v: "No per-project writes — Cursor natively reads the .claude-plugin loader via lisa-cursor variants",
|
|
2386
|
-
},
|
|
2387
5828
|
{ t: "toggle", v: true },
|
|
2388
5829
|
],
|
|
2389
5830
|
[
|
|
2390
5831
|
{ t: "mono", v: "agy" },
|
|
2391
|
-
{
|
|
2392
|
-
t: "text",
|
|
2393
|
-
v: "Antigravity: lisa-agy plugin variants; MCP mirrored user-globally; rules baked into AGENTS.md",
|
|
2394
|
-
},
|
|
2395
5832
|
{ t: "toggle", v: true },
|
|
2396
5833
|
],
|
|
2397
5834
|
[
|
|
2398
5835
|
{ t: "mono", v: "copilot" },
|
|
2399
|
-
{
|
|
2400
|
-
t: "text",
|
|
2401
|
-
v: ".github/copilot-instructions.md; agents emitted as *.agent.md with camelCase hook events",
|
|
2402
|
-
},
|
|
2403
5836
|
{ t: "toggle", v: true },
|
|
2404
5837
|
],
|
|
2405
5838
|
[
|
|
2406
5839
|
{ t: "mono", v: "opencode" },
|
|
2407
|
-
{
|
|
2408
|
-
t: "text",
|
|
2409
|
-
v: ".opencode/skills/lisa/ + AGENTS.md, read natively; project plugin hooks",
|
|
2410
|
-
},
|
|
2411
5840
|
{ t: "toggle", v: true },
|
|
2412
5841
|
],
|
|
2413
5842
|
],
|
|
@@ -3316,11 +6745,13 @@
|
|
|
3316
6745
|
desc: "All agent work that runs on a schedule, whichever machine fires it: the fleet that runs the factories \u2014 pipeline movers (intake, repair) and the three feeding loops (QA, Product Planning, Monitoring) \u2014 on the harness\u2019s native scheduler, plus the nightly improvement agents that run on the pipeline host. Harness names carry a stable lisa-auto-<project>- prefix so teardown is precise.",
|
|
3317
6746
|
src: "plugins/src/base/skills/lisa-setup-automations \u00b7 claude-nightly-*.yml",
|
|
3318
6747
|
blocks: [
|
|
6748
|
+
// Both command names below were renamed — see the PROPOSED
|
|
6749
|
+
// command-renames marker on the Research factory page.
|
|
3319
6750
|
{
|
|
3320
6751
|
type: "table",
|
|
3321
6752
|
card: "Scheduled jobs",
|
|
3322
6753
|
id: "automations-scheduled-jobs",
|
|
3323
|
-
note: "Live from the harness scheduler (Codex automations or Claude /schedule). Only lisa-auto-<project>-* jobs are listed. Cadence is read-only in this phase \u2014 change jobs with /lisa:setup
|
|
6754
|
+
note: "Live from the harness scheduler (Codex automations or Claude /schedule). Only lisa-auto-<project>-* jobs are listed. Cadence is read-only in this phase \u2014 change jobs with /lisa:setup:automations. Nightly agents are pipeline files, inventoried under Pipelines.",
|
|
3324
6755
|
cols: ["Automation", "Runs on", "Cadence", "Last run", "Status"],
|
|
3325
6756
|
rows: [],
|
|
3326
6757
|
empty: {
|
|
@@ -3334,7 +6765,7 @@
|
|
|
3334
6765
|
{
|
|
3335
6766
|
key: "",
|
|
3336
6767
|
label: "Fleet health",
|
|
3337
|
-
desc: "From /lisa:automation
|
|
6768
|
+
desc: "From /lisa:verify:automation \u2014 whether every expected automation exists, matches the setup contract, and shows healthy recent activity.",
|
|
3338
6769
|
control: stat(
|
|
3339
6770
|
"6 of 6 present \u00b7 healthy \u00b7 checked 12:58",
|
|
3340
6771
|
"mono"
|
|
@@ -4762,44 +8193,6 @@
|
|
|
4762
8193
|
},
|
|
4763
8194
|
],
|
|
4764
8195
|
},
|
|
4765
|
-
{
|
|
4766
|
-
type: "table",
|
|
4767
|
-
card: "Lifecycle labels",
|
|
4768
|
-
note: "Created idempotently by /lisa:setup:github",
|
|
4769
|
-
cols: ["Label", "Role", "Color"],
|
|
4770
|
-
rows: [
|
|
4771
|
-
[
|
|
4772
|
-
{ t: "mono", v: "status:ready" },
|
|
4773
|
-
{ t: "text", v: "build · ready" },
|
|
4774
|
-
{ t: "color", v: "#FBCA04" },
|
|
4775
|
-
],
|
|
4776
|
-
[
|
|
4777
|
-
{ t: "mono", v: "status:in-progress" },
|
|
4778
|
-
{ t: "text", v: "build · claimed" },
|
|
4779
|
-
{ t: "color", v: "#0E8A16" },
|
|
4780
|
-
],
|
|
4781
|
-
[
|
|
4782
|
-
{ t: "mono", v: "status:blocked" },
|
|
4783
|
-
{ t: "text", v: "build · blocked" },
|
|
4784
|
-
{ t: "color", v: "#D93F0B" },
|
|
4785
|
-
],
|
|
4786
|
-
[
|
|
4787
|
-
{ t: "mono", v: "status:on-dev / status:on-stg" },
|
|
4788
|
-
{ t: "text", v: "build · done (dev/staging)" },
|
|
4789
|
-
{ t: "color", v: "#1D76DB" },
|
|
4790
|
-
],
|
|
4791
|
-
[
|
|
4792
|
-
{ t: "mono", v: "status:done" },
|
|
4793
|
-
{ t: "text", v: "build · done (production)" },
|
|
4794
|
-
{ t: "color", v: "#0E8A16" },
|
|
4795
|
-
],
|
|
4796
|
-
[
|
|
4797
|
-
{ t: "mono", v: "prd-draft → prd-verified" },
|
|
4798
|
-
{ t: "text", v: "PRD lifecycle (8 labels)" },
|
|
4799
|
-
{ t: "color", v: "#5319E7" },
|
|
4800
|
-
],
|
|
4801
|
-
],
|
|
4802
|
-
},
|
|
4803
8196
|
{
|
|
4804
8197
|
type: "table",
|
|
4805
8198
|
card: "Secrets",
|
|
@@ -4877,7 +8270,7 @@
|
|
|
4877
8270
|
|
|
4878
8271
|
/* ---------------------------------- Plugins ---------------------------------- */
|
|
4879
8272
|
/* Catalog copy only — enablement comes from the enabled-plugins probe.
|
|
4880
|
-
|
|
8273
|
+
Demo toggles are never rendered as a fallback. */
|
|
4881
8274
|
DATA.sections.plugins = {
|
|
4882
8275
|
title: "Plugins & MCP",
|
|
4883
8276
|
desc: "Lisa’s own plugin set (base + one per detected stack), curated third-party plugins, and the MCP servers each one carries. Non-Claude agents receive generated equivalents of everything here.",
|
|
@@ -5063,18 +8456,18 @@
|
|
|
5063
8456
|
const dirty = new Set();
|
|
5064
8457
|
|
|
5065
8458
|
/* Live enabled-plugins state, hydrated from GET /api/status. Starts as
|
|
5066
|
-
|
|
8459
|
+
"loading" so the Plugins section never presents demo toggles as truth. */
|
|
5067
8460
|
let pluginsLive = {
|
|
5068
8461
|
status: "loading",
|
|
5069
8462
|
settingsPresent: false,
|
|
5070
8463
|
plugins: [],
|
|
5071
8464
|
};
|
|
5072
8465
|
/* Live detected-stacks state, hydrated from GET /api/status. Starts as
|
|
5073
|
-
|
|
5074
|
-
|
|
8466
|
+
"detecting" so the Stacks section never presents the metadata catalog
|
|
8467
|
+
as if it were the detection result. */
|
|
5075
8468
|
let stackDetection = { status: "detecting", ids: [] };
|
|
5076
8469
|
/* Version context and Health v1 verdict are painted together. Health
|
|
5077
|
-
|
|
8470
|
+
takes priority once measured without erasing the update context. */
|
|
5078
8471
|
let lisaVersionChipState = {
|
|
5079
8472
|
dotClass: "",
|
|
5080
8473
|
label: "@codyswann/lisa · checking…",
|
|
@@ -5113,6 +8506,42 @@
|
|
|
5113
8506
|
if (html !== undefined) node.innerHTML = html;
|
|
5114
8507
|
return node;
|
|
5115
8508
|
}
|
|
8509
|
+
|
|
8510
|
+
/* Connection cells carry a `how` payload: the steps that would turn a
|
|
8511
|
+
red cell green. Shape: {title, sub, intro, steps:[{text, cmd}], foot}.
|
|
8512
|
+
Mock content for now — a wired version would generate these from the
|
|
8513
|
+
same probe that decided the cell's colour, so the instructions can
|
|
8514
|
+
never drift from the verdict they explain. */
|
|
8515
|
+
function openHowto(how) {
|
|
8516
|
+
const dlg = document.getElementById("howto");
|
|
8517
|
+
if (!dlg || !how) return;
|
|
8518
|
+
document.getElementById("howtoTitle").innerHTML =
|
|
8519
|
+
esc(how.title) +
|
|
8520
|
+
(how.sub ? '<span class="sub">' + esc(how.sub) + "</span>" : "");
|
|
8521
|
+
const body = document.getElementById("howtoBody");
|
|
8522
|
+
body.innerHTML = "";
|
|
8523
|
+
if (how.intro) body.appendChild(el("p", "", esc(how.intro)));
|
|
8524
|
+
if (Array.isArray(how.meta) && how.meta.length > 0) {
|
|
8525
|
+
const dl = el("div", "howto-meta");
|
|
8526
|
+
how.meta.forEach(m => {
|
|
8527
|
+
dl.appendChild(el("span", "hm-k", esc(m.k)));
|
|
8528
|
+
dl.appendChild(el("span", "hm-v", esc(m.v)));
|
|
8529
|
+
});
|
|
8530
|
+
body.appendChild(dl);
|
|
8531
|
+
}
|
|
8532
|
+
if (Array.isArray(how.steps) && how.steps.length > 0) {
|
|
8533
|
+
const ol = el("ol");
|
|
8534
|
+
how.steps.forEach(step => {
|
|
8535
|
+
const li = el("li", "", esc(step.text));
|
|
8536
|
+
if (step.cmd) li.appendChild(el("code", "", esc(step.cmd)));
|
|
8537
|
+
ol.appendChild(li);
|
|
8538
|
+
});
|
|
8539
|
+
body.appendChild(ol);
|
|
8540
|
+
}
|
|
8541
|
+
if (how.foot) body.appendChild(el("div", "howto-foot", esc(how.foot)));
|
|
8542
|
+
if (typeof dlg.showModal === "function") dlg.showModal();
|
|
8543
|
+
else dlg.setAttribute("open", "");
|
|
8544
|
+
}
|
|
5116
8545
|
function esc(s) {
|
|
5117
8546
|
return String(s)
|
|
5118
8547
|
.replace(/&/g, "&")
|
|
@@ -5148,6 +8577,48 @@
|
|
|
5148
8577
|
}
|
|
5149
8578
|
|
|
5150
8579
|
let ctlSeq = 0;
|
|
8580
|
+
/* Propagate a shared setting to every control bound to the same syncKey:
|
|
8581
|
+
first into DATA so a re-render keeps the new value, then into any
|
|
8582
|
+
matching control already on screen. A setting surfaced on more than
|
|
8583
|
+
one page (the PRD provider, the tracker, the quality thresholds) is
|
|
8584
|
+
ONE setting — the surfaces must never be able to disagree. Covers both
|
|
8585
|
+
table cells ({t, v, syncKey}) and card rows ({control:{value,
|
|
8586
|
+
syncKey}}). */
|
|
8587
|
+
function syncControlValue(syncKey, value) {
|
|
8588
|
+
Object.values(DATA.sections).forEach(section => {
|
|
8589
|
+
(section.blocks || []).forEach(block => {
|
|
8590
|
+
if (!Array.isArray(block.rows)) return;
|
|
8591
|
+
block.rows.forEach(row => {
|
|
8592
|
+
if (Array.isArray(row)) {
|
|
8593
|
+
row.forEach(cell => {
|
|
8594
|
+
if (cell && cell.syncKey === syncKey) cell.v = value;
|
|
8595
|
+
});
|
|
8596
|
+
} else if (
|
|
8597
|
+
row &&
|
|
8598
|
+
row.control &&
|
|
8599
|
+
(row.control.syncKey === syncKey || row.key === syncKey)
|
|
8600
|
+
) {
|
|
8601
|
+
row.control.value = value;
|
|
8602
|
+
}
|
|
8603
|
+
});
|
|
8604
|
+
});
|
|
8605
|
+
});
|
|
8606
|
+
document
|
|
8607
|
+
.querySelectorAll('[data-sync-key="' + syncKey + '"]')
|
|
8608
|
+
.forEach(node => {
|
|
8609
|
+
if (node.value !== String(value)) node.value = value;
|
|
8610
|
+
});
|
|
8611
|
+
}
|
|
8612
|
+
|
|
8613
|
+
/* Mark a control as one surface of a shared setting. */
|
|
8614
|
+
function bindSync(input, control) {
|
|
8615
|
+
if (!control || !control.syncKey) return;
|
|
8616
|
+
input.dataset.syncKey = control.syncKey;
|
|
8617
|
+
input.addEventListener("change", () =>
|
|
8618
|
+
syncControlValue(control.syncKey, input.value)
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
|
|
5151
8622
|
function buildControl(control, dirtyId) {
|
|
5152
8623
|
const id = dirtyId || "ctl-" + ++ctlSeq;
|
|
5153
8624
|
const wrap = el("div", "control");
|
|
@@ -5161,14 +8632,25 @@
|
|
|
5161
8632
|
};
|
|
5162
8633
|
if (!control) return wrap;
|
|
5163
8634
|
if (control.type === "toggle") {
|
|
8635
|
+
const box = el("div", "switch-wrap");
|
|
5164
8636
|
const label = el("label", "switch");
|
|
5165
8637
|
const input = document.createElement("input");
|
|
5166
8638
|
input.type = "checkbox";
|
|
5167
8639
|
input.checked = !!control.value;
|
|
5168
8640
|
label.appendChild(input);
|
|
5169
8641
|
label.appendChild(el("span", "trk"));
|
|
8642
|
+
// Spell the state out — a bare track is ambiguous at a glance.
|
|
8643
|
+
const state = el("span", "switch-state");
|
|
8644
|
+
const paintState = () => {
|
|
8645
|
+
state.textContent = input.checked ? "on" : "off";
|
|
8646
|
+
state.classList.toggle("on", input.checked);
|
|
8647
|
+
};
|
|
8648
|
+
paintState();
|
|
8649
|
+
input.addEventListener("change", paintState);
|
|
5170
8650
|
bind(input, i => i.closest(".row") || i.closest("tr"));
|
|
5171
|
-
|
|
8651
|
+
box.appendChild(label);
|
|
8652
|
+
box.appendChild(state);
|
|
8653
|
+
wrap.appendChild(box);
|
|
5172
8654
|
} else if (control.type === "select") {
|
|
5173
8655
|
const s = document.createElement("select");
|
|
5174
8656
|
s.className = "ctl" + (control.mono ? " mono" : "");
|
|
@@ -5180,6 +8662,7 @@
|
|
|
5180
8662
|
s.appendChild(opt);
|
|
5181
8663
|
});
|
|
5182
8664
|
bind(s, i => i.closest(".row") || i.closest("tr"));
|
|
8665
|
+
bindSync(s, control);
|
|
5183
8666
|
wrap.appendChild(s);
|
|
5184
8667
|
} else if (control.type === "text") {
|
|
5185
8668
|
const i = document.createElement("input");
|
|
@@ -5198,9 +8681,57 @@
|
|
|
5198
8681
|
i.className = "ctl num";
|
|
5199
8682
|
i.value = control.value;
|
|
5200
8683
|
bind(i);
|
|
8684
|
+
bindSync(i, control);
|
|
5201
8685
|
wrap.appendChild(i);
|
|
5202
8686
|
if (control.unit)
|
|
5203
8687
|
wrap.appendChild(el("span", "unit", esc(control.unit)));
|
|
8688
|
+
} else if (control.type === "multiselect") {
|
|
8689
|
+
// Pick one or more providers for a single obligation. Summary shows
|
|
8690
|
+
// the current selection; the popover is the checklist of options.
|
|
8691
|
+
const box = el("div", "ms");
|
|
8692
|
+
const values = new Set(control.values || []);
|
|
8693
|
+
const summary = el(
|
|
8694
|
+
"button",
|
|
8695
|
+
"ms-summary" + (control.mono ? " mono" : "")
|
|
8696
|
+
);
|
|
8697
|
+
summary.type = "button";
|
|
8698
|
+
const paint = () => {
|
|
8699
|
+
summary.textContent = values.size ? [...values].join(", ") : "none";
|
|
8700
|
+
summary.classList.toggle("ms-empty", values.size === 0);
|
|
8701
|
+
};
|
|
8702
|
+
paint();
|
|
8703
|
+
const pop = el("div", "ms-pop");
|
|
8704
|
+
(control.options || []).forEach(o => {
|
|
8705
|
+
const lab = el("label", "ms-opt");
|
|
8706
|
+
const cb = document.createElement("input");
|
|
8707
|
+
cb.type = "checkbox";
|
|
8708
|
+
cb.checked = values.has(o);
|
|
8709
|
+
cb.addEventListener("change", () => {
|
|
8710
|
+
if (cb.checked) values.add(o);
|
|
8711
|
+
else values.delete(o);
|
|
8712
|
+
paint();
|
|
8713
|
+
markDirty(id + "·" + ++ctlSeq, box.closest("tr"));
|
|
8714
|
+
});
|
|
8715
|
+
lab.appendChild(cb);
|
|
8716
|
+
lab.appendChild(el("span", "", esc(o)));
|
|
8717
|
+
pop.appendChild(lab);
|
|
8718
|
+
});
|
|
8719
|
+
summary.addEventListener("click", e => {
|
|
8720
|
+
e.stopPropagation();
|
|
8721
|
+
const open = box.classList.toggle("open");
|
|
8722
|
+
if (open) {
|
|
8723
|
+
const close = ev => {
|
|
8724
|
+
if (!box.contains(ev.target)) {
|
|
8725
|
+
box.classList.remove("open");
|
|
8726
|
+
document.removeEventListener("click", close);
|
|
8727
|
+
}
|
|
8728
|
+
};
|
|
8729
|
+
document.addEventListener("click", close);
|
|
8730
|
+
}
|
|
8731
|
+
});
|
|
8732
|
+
box.appendChild(summary);
|
|
8733
|
+
box.appendChild(pop);
|
|
8734
|
+
wrap.appendChild(box);
|
|
5204
8735
|
} else if (control.type === "tags") {
|
|
5205
8736
|
const box = el("div", "tagbox");
|
|
5206
8737
|
(control.values || []).forEach(v => {
|
|
@@ -5326,7 +8857,14 @@
|
|
|
5326
8857
|
if (row.desc) left.appendChild(el("div", "desc", row.desc));
|
|
5327
8858
|
if (row.key) left.appendChild(el("span", "keychip", esc(row.key)));
|
|
5328
8859
|
r.appendChild(left);
|
|
5329
|
-
|
|
8860
|
+
// A row's config key IS its sync key: the same setting surfaced on two
|
|
8861
|
+
// pages (a factory page and its owning settings page) stays in step
|
|
8862
|
+
// without either surface having to know about the other.
|
|
8863
|
+
const control =
|
|
8864
|
+
row.key && row.control && !row.control.syncKey
|
|
8865
|
+
? { ...row.control, syncKey: row.key }
|
|
8866
|
+
: row.control;
|
|
8867
|
+
r.appendChild(buildControl(control, row.key));
|
|
5330
8868
|
return r;
|
|
5331
8869
|
}
|
|
5332
8870
|
|
|
@@ -5439,6 +8977,7 @@
|
|
|
5439
8977
|
}
|
|
5440
8978
|
const th = el("th", c.tip ? "th-help" : "", esc(c.v));
|
|
5441
8979
|
if (c.tip) th.title = c.tip;
|
|
8980
|
+
if (c.center) th.classList.add("center");
|
|
5442
8981
|
trh.appendChild(th);
|
|
5443
8982
|
});
|
|
5444
8983
|
thead.appendChild(trh);
|
|
@@ -5458,10 +8997,78 @@
|
|
|
5458
8997
|
if (cell.t === "mono") td.className = "mono";
|
|
5459
8998
|
if (cell.t === "text" || cell.t === "mono") {
|
|
5460
8999
|
td.textContent = cell.v;
|
|
9000
|
+
} else if (cell.t === "obl") {
|
|
9001
|
+
// An obligation cell. Clickable only when the gate has a gap —
|
|
9002
|
+
// clicking opens "how to close it". Fully-enforced rows are
|
|
9003
|
+
// plain text with nothing to act on.
|
|
9004
|
+
if (cell.how) {
|
|
9005
|
+
const btn = el("button", "linklike");
|
|
9006
|
+
btn.type = "button";
|
|
9007
|
+
btn.title = "How to close this gap";
|
|
9008
|
+
btn.textContent = cell.v;
|
|
9009
|
+
btn.addEventListener("click", () => openHowto(cell.how));
|
|
9010
|
+
td.appendChild(btn);
|
|
9011
|
+
} else {
|
|
9012
|
+
td.textContent = cell.v;
|
|
9013
|
+
}
|
|
9014
|
+
} else if (cell.t === "gate") {
|
|
9015
|
+
// One enforcement layer for one obligation. The dot says whether
|
|
9016
|
+
// the rule fires here; the rightmost such column being green is
|
|
9017
|
+
// what makes an obligation unskippable.
|
|
9018
|
+
td.className = "gate-cell";
|
|
9019
|
+
const cls =
|
|
9020
|
+
cell.on === true ? " ok" : cell.on === "warn" ? " warn" : "";
|
|
9021
|
+
const dot = el("span", "dot" + cls);
|
|
9022
|
+
dot.title =
|
|
9023
|
+
cell.title ||
|
|
9024
|
+
(cell.on === true
|
|
9025
|
+
? "enforced here"
|
|
9026
|
+
: cell.on === "warn"
|
|
9027
|
+
? "runs here, but does not gate the merge"
|
|
9028
|
+
: "not enforced here");
|
|
9029
|
+
td.appendChild(dot);
|
|
9030
|
+
} else if (cell.t === "layer") {
|
|
9031
|
+
// One enforcement layer, as a clickable yes / no / n-a. The modal
|
|
9032
|
+
// carries the event it is bound to, the setup and verify
|
|
9033
|
+
// commands, and — for CI — whether the check is required to
|
|
9034
|
+
// merge. n/a is distinct from no: no is a fixable gap, n/a means
|
|
9035
|
+
// the gate does not apply to this project's stack — or to the
|
|
9036
|
+
// coding agent currently selected as this factory's staff.
|
|
9037
|
+
td.className = "gate-cell";
|
|
9038
|
+
const shown = resolveLayerCell(cell);
|
|
9039
|
+
const label =
|
|
9040
|
+
shown.on === true ? "yes" : shown.on === "na" ? "n/a" : "no";
|
|
9041
|
+
const bcls =
|
|
9042
|
+
shown.on === true
|
|
9043
|
+
? "pass"
|
|
9044
|
+
: shown.on === "na"
|
|
9045
|
+
? "na"
|
|
9046
|
+
: "default";
|
|
9047
|
+
const chip = el("span", "badge " + bcls, label);
|
|
9048
|
+
const btn = el("button", "badge-btn");
|
|
9049
|
+
btn.type = "button";
|
|
9050
|
+
btn.title = "Setup, verify and details";
|
|
9051
|
+
btn.appendChild(chip);
|
|
9052
|
+
btn.addEventListener("click", () => openHowto(shown.how));
|
|
9053
|
+
td.appendChild(btn);
|
|
5461
9054
|
} else if (cell.t === "badge") {
|
|
5462
|
-
|
|
5463
|
-
|
|
9055
|
+
const chip = el(
|
|
9056
|
+
"span",
|
|
9057
|
+
"badge " + (cell.cls || "default"),
|
|
9058
|
+
esc(cell.v)
|
|
5464
9059
|
);
|
|
9060
|
+
if (cell.how) {
|
|
9061
|
+
// Clickable verdict: the badge answers "is it connected?",
|
|
9062
|
+
// the modal answers "so how do I connect it?"
|
|
9063
|
+
const btn = el("button", "badge-btn");
|
|
9064
|
+
btn.type = "button";
|
|
9065
|
+
btn.title = "How to connect this";
|
|
9066
|
+
btn.appendChild(chip);
|
|
9067
|
+
btn.addEventListener("click", () => openHowto(cell.how));
|
|
9068
|
+
td.appendChild(btn);
|
|
9069
|
+
} else {
|
|
9070
|
+
td.appendChild(chip);
|
|
9071
|
+
}
|
|
5465
9072
|
} else if (cell.t === "color") {
|
|
5466
9073
|
const chip = el("span", "colorchip");
|
|
5467
9074
|
const sw = el("span", "swatch");
|
|
@@ -5500,8 +9107,13 @@
|
|
|
5500
9107
|
if (cell.reason) {
|
|
5501
9108
|
td.appendChild(el("span", "status-why", esc(cell.reason)));
|
|
5502
9109
|
}
|
|
5503
|
-
} else if (
|
|
9110
|
+
} else if (
|
|
9111
|
+
cell.t === "toggle" ||
|
|
9112
|
+
cell.t === "select" ||
|
|
9113
|
+
cell.t === "multiselect"
|
|
9114
|
+
) {
|
|
5504
9115
|
// Table cells use {t, v}; buildControl expects {type, value}.
|
|
9116
|
+
// multiselect carries {values, options} which spread through.
|
|
5505
9117
|
td.appendChild(
|
|
5506
9118
|
buildControl({ ...cell, type: cell.t, value: cell.v }, null)
|
|
5507
9119
|
);
|
|
@@ -5545,8 +9157,8 @@
|
|
|
5545
9157
|
}
|
|
5546
9158
|
|
|
5547
9159
|
/* The 8-entry stack metadata, keyed by id. This is a catalog of what each
|
|
5548
|
-
|
|
5549
|
-
|
|
9160
|
+
stack installs — NOT the detection result; only ids the probe reports
|
|
9161
|
+
are rendered from it. */
|
|
5550
9162
|
function stackCatalog(block) {
|
|
5551
9163
|
const catalog = new Map();
|
|
5552
9164
|
block.items.forEach(s => {
|
|
@@ -5556,7 +9168,7 @@
|
|
|
5556
9168
|
}
|
|
5557
9169
|
|
|
5558
9170
|
/* A detected id with no catalog entry still gets a card (id as name) so
|
|
5559
|
-
|
|
9171
|
+
detection truth is never dropped just because copy is missing. */
|
|
5560
9172
|
function minimalStackMeta(id) {
|
|
5561
9173
|
return {
|
|
5562
9174
|
id,
|
|
@@ -5933,9 +9545,65 @@
|
|
|
5933
9545
|
if (block.type === "tabs") return buildTabs(block);
|
|
5934
9546
|
if (block.type === "checklist") return buildChecklist(block);
|
|
5935
9547
|
if (block.type === "plugins-live") return buildPluginsLive(block);
|
|
9548
|
+
if (block.type === "staff") return buildStaff(block);
|
|
9549
|
+
if (block.type === "environments") return buildEnvironments();
|
|
5936
9550
|
return buildCard(block);
|
|
5937
9551
|
}
|
|
5938
9552
|
|
|
9553
|
+
/* The staff picker: which coding agent this factory page is read for.
|
|
9554
|
+
Single-select — a factory is run by one agent at a time, and its
|
|
9555
|
+
readiness is only meaningful with that agent named. */
|
|
9556
|
+
function buildStaff() {
|
|
9557
|
+
const card = el("div", "card staff-card");
|
|
9558
|
+
const head = el("div", "staff-head");
|
|
9559
|
+
head.appendChild(el("span", "staff-label", "Run by"));
|
|
9560
|
+
const group = el("div", "staff-opts");
|
|
9561
|
+
group.setAttribute("role", "radiogroup");
|
|
9562
|
+
group.setAttribute("aria-label", "Coding agent running this factory");
|
|
9563
|
+
AGENTS.forEach(id => {
|
|
9564
|
+
const caps = AGENT_CAPS[id];
|
|
9565
|
+
const btn = el("button", "staff-opt");
|
|
9566
|
+
btn.type = "button";
|
|
9567
|
+
btn.setAttribute("role", "radio");
|
|
9568
|
+
const selected = DATA.staff === id;
|
|
9569
|
+
btn.setAttribute("aria-checked", selected ? "true" : "false");
|
|
9570
|
+
if (selected) btn.classList.add("active");
|
|
9571
|
+
btn.textContent = caps.label;
|
|
9572
|
+
btn.addEventListener("click", () => {
|
|
9573
|
+
if (DATA.staff === id) return;
|
|
9574
|
+
DATA.staff = id;
|
|
9575
|
+
renderAll();
|
|
9576
|
+
showSection(activeSection);
|
|
9577
|
+
});
|
|
9578
|
+
group.appendChild(btn);
|
|
9579
|
+
});
|
|
9580
|
+
head.appendChild(group);
|
|
9581
|
+
card.appendChild(head);
|
|
9582
|
+
// No per-agent "how it works" blurb: how the surface is installed is
|
|
9583
|
+
// implementation detail, not something the operator acts on. Only a
|
|
9584
|
+
// capability gap earns a line here, because it changes what the page
|
|
9585
|
+
// below actually means.
|
|
9586
|
+
const caps = currentCaps();
|
|
9587
|
+
if (caps.agentHooks !== true) {
|
|
9588
|
+
const warn = el("div", "staff-warn");
|
|
9589
|
+
if (caps.agentHooks === "unverified") {
|
|
9590
|
+
warn.textContent =
|
|
9591
|
+
caps.label +
|
|
9592
|
+
" has unproven agent-layer enforcement. " +
|
|
9593
|
+
caps.hookWhy +
|
|
9594
|
+
" Those cells read n/a below — unproven, which is not the same as unsupported.";
|
|
9595
|
+
} else {
|
|
9596
|
+
warn.textContent =
|
|
9597
|
+
caps.label +
|
|
9598
|
+
" cannot enforce anything at the agent layer. " +
|
|
9599
|
+
caps.hookWhy +
|
|
9600
|
+
" Those cells read n/a below — a capability gap, not something to fix.";
|
|
9601
|
+
}
|
|
9602
|
+
card.appendChild(warn);
|
|
9603
|
+
}
|
|
9604
|
+
return card;
|
|
9605
|
+
}
|
|
9606
|
+
|
|
5939
9607
|
function renderAll() {
|
|
5940
9608
|
syncHealthViewModel();
|
|
5941
9609
|
$main.innerHTML = "";
|
|
@@ -7175,8 +10843,8 @@
|
|
|
7175
10843
|
}
|
|
7176
10844
|
}
|
|
7177
10845
|
/* Map the detected-stacks probe into stackDetection. A missing probe or a
|
|
7178
|
-
|
|
7179
|
-
|
|
10846
|
+
non-value/non-array result becomes an explicit unknown state — never a
|
|
10847
|
+
false empty and never the metadata catalog. */
|
|
7180
10848
|
function applyDetectedStacks(probes) {
|
|
7181
10849
|
const raw =
|
|
7182
10850
|
probes && Object.hasOwn(probes, "detected-stacks")
|
|
@@ -7409,6 +11077,16 @@
|
|
|
7409
11077
|
showSection(activeSection);
|
|
7410
11078
|
});
|
|
7411
11079
|
|
|
11080
|
+
document.getElementById("howtoClose").addEventListener("click", () => {
|
|
11081
|
+
document.getElementById("howto").close();
|
|
11082
|
+
});
|
|
11083
|
+
|
|
11084
|
+
// Clicking the backdrop closes: on a <dialog> the click target is the
|
|
11085
|
+
// dialog itself only when the press landed outside its content box.
|
|
11086
|
+
document.getElementById("howto").addEventListener("click", event => {
|
|
11087
|
+
if (event.target === event.currentTarget) event.currentTarget.close();
|
|
11088
|
+
});
|
|
11089
|
+
|
|
7412
11090
|
document.getElementById("healthChip").addEventListener("click", () => {
|
|
7413
11091
|
showSection("health");
|
|
7414
11092
|
});
|