@nanocollective/roster 0.1.0-alpha.5 → 0.1.0-alpha.50

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.
Files changed (102) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4833 -2752
  3. package/docs/README.md +9 -6
  4. package/docs/agents.md +24 -20
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/analyst.md +65 -0
  7. package/docs/charters/cmo.md +69 -0
  8. package/docs/charters/community.md +63 -0
  9. package/docs/charters/cto.md +71 -0
  10. package/docs/charters/designer.md +65 -0
  11. package/docs/charters/devops.md +65 -0
  12. package/docs/charters/pm.md +70 -0
  13. package/docs/charters/qa.md +65 -0
  14. package/docs/charters/support.md +60 -0
  15. package/docs/charters/writer.md +63 -0
  16. package/docs/commands.md +93 -7
  17. package/docs/concepts.md +61 -14
  18. package/docs/cost.md +36 -1
  19. package/docs/developing.md +16 -21
  20. package/docs/doctor-codes.md +10 -2
  21. package/docs/export.md +2 -0
  22. package/docs/extending.md +2 -2
  23. package/docs/getting-started.md +128 -78
  24. package/docs/images/brain.jpg +0 -0
  25. package/docs/images/org.jpg +0 -0
  26. package/docs/images/prompt.jpg +0 -0
  27. package/docs/images/setup-org.jpg +0 -0
  28. package/docs/images/setup-plan.jpg +0 -0
  29. package/docs/images/staff.jpg +0 -0
  30. package/docs/manual-steps.md +94 -123
  31. package/docs/memory.md +21 -3
  32. package/docs/org-yaml.md +40 -2
  33. package/docs/portal.md +165 -48
  34. package/docs/prompts.md +31 -4
  35. package/docs/security.md +37 -5
  36. package/docs/session-workflow.md +63 -17
  37. package/docs/staff-yaml.md +30 -3
  38. package/docs/troubleshooting.md +8 -8
  39. package/docs/upgrading.md +9 -3
  40. package/docs/writing-a-charter.md +28 -0
  41. package/package.json +18 -20
  42. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +26 -1
  43. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +41 -7
  44. package/templates/brain/CHARTER.md +3 -3
  45. package/templates/brain/README.md +1 -0
  46. package/templates/brain/log/decisions.md +3 -0
  47. package/templates/brain/staff.yaml +4 -1
  48. package/templates/brain/strategy/ideas.md +7 -0
  49. package/templates/briefs/priorities.md +46 -0
  50. package/templates/ops/.github/workflows/session.yaml +236 -15
  51. package/templates/ops/agents.mjs +7 -3
  52. package/templates/ops/compose.mjs +31 -3
  53. package/templates/ops/inflight.mjs +157 -0
  54. package/templates/ops/org/operating.md +43 -4
  55. package/templates/ops/org/voice.md +9 -0
  56. package/templates/ops/prompts/_inflight.md +14 -0
  57. package/templates/ops/prompts/_paths.md +2 -1
  58. package/templates/ops/prompts/daily.md +37 -9
  59. package/templates/ops/prompts/mention.md +21 -0
  60. package/templates/ops/run-record.mjs +146 -0
  61. package/templates/portal/css/base.css +167 -73
  62. package/templates/portal/css/brain.css +23 -20
  63. package/templates/portal/css/diff.css +10 -9
  64. package/templates/portal/css/graph.css +12 -7
  65. package/templates/portal/css/health.css +13 -11
  66. package/templates/portal/css/home.css +93 -0
  67. package/templates/portal/css/inbox.css +45 -25
  68. package/templates/portal/css/layout.css +90 -46
  69. package/templates/portal/css/markdown.css +36 -14
  70. package/templates/portal/css/runs.css +13 -0
  71. package/templates/portal/css/setup.css +116 -34
  72. package/templates/portal/index.html +21 -9
  73. package/templates/portal/js/api.js +44 -4
  74. package/templates/portal/js/app.js +94 -9
  75. package/templates/portal/js/dialog.js +83 -0
  76. package/templates/portal/js/homesort.js +174 -0
  77. package/templates/portal/js/icons.js +37 -0
  78. package/templates/portal/js/inflight.js +18 -0
  79. package/templates/portal/js/md.js +5 -2
  80. package/templates/portal/js/mdedit.js +84 -0
  81. package/templates/portal/js/readiness.js +70 -0
  82. package/templates/portal/js/refresh.js +10 -2
  83. package/templates/portal/js/state.js +11 -5
  84. package/templates/portal/js/views/app.js +24 -7
  85. package/templates/portal/js/views/brain.js +1 -1
  86. package/templates/portal/js/views/checklist.js +10 -4
  87. package/templates/portal/js/views/credential.js +84 -0
  88. package/templates/portal/js/views/graph.js +1 -1
  89. package/templates/portal/js/views/health.js +17 -4
  90. package/templates/portal/js/views/hire.js +593 -0
  91. package/templates/portal/js/views/home.js +546 -0
  92. package/templates/portal/js/views/inbox.js +226 -70
  93. package/templates/portal/js/views/org.js +46 -106
  94. package/templates/portal/js/views/orgedit.js +234 -0
  95. package/templates/portal/js/views/paste.js +87 -21
  96. package/templates/portal/js/views/prompt.js +11 -4
  97. package/templates/portal/js/views/repos.js +20 -15
  98. package/templates/portal/js/views/runonce.js +94 -0
  99. package/templates/portal/js/views/runs.js +170 -0
  100. package/templates/portal/js/views/setup.js +261 -75
  101. package/templates/portal/js/views/staff.js +170 -243
  102. package/templates/portal/js/views/todo.js +62 -0
@@ -1,56 +1,63 @@
1
1
  /* The setup screen, and the copy-a-prompt panel it shares with the authoring screens. */
2
2
 
3
- .step{background:var(--panel);border:1px solid var(--line);border-radius:10px;
4
- padding:18px 20px 20px;margin-bottom:14px}
5
- .step.done{border-color:var(--accent-dim)}
3
+ .step{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);
4
+ padding:20px 22px 22px;margin-bottom:14px;box-shadow:var(--shadow-sm)}
5
+ .step.done{border-color:color-mix(in srgb,var(--accent) 30%,var(--line))}
6
6
  .stephead{display:flex;align-items:center;gap:11px;margin-bottom:10px}
7
- .stephead h2{margin:0;font:600 16px var(--sans);letter-spacing:-.01em}
8
- .stepn{flex:none;width:23px;height:23px;border-radius:50%;display:grid;place-items:center;
9
- background:var(--panel-2);border:1px solid var(--line);color:var(--ink-faint);
7
+ .stephead h2{margin:0;font:600 16px var(--sans);letter-spacing:-.018em}
8
+ .stepn{flex:none;width:24px;height:24px;border-radius:50%;display:grid;place-items:center;
9
+ background:var(--surface-2);border:1px solid transparent;color:var(--ink-dim);
10
10
  font:600 11px var(--mono)}
11
- .step.done .stepn{background:var(--accent-dim);border-color:var(--accent-dim);color:var(--bg)}
11
+ .step.done .stepn{background:var(--accent-fill);border-color:transparent;color:var(--accent-ink)}
12
12
 
13
13
  /* ---------- the form ---------- */
14
14
 
15
15
  .setupform{display:grid;gap:13px;margin:14px 0 18px;max-width:520px}
16
16
  .field{display:grid;gap:4px}
17
17
  .field>span{font:600 12px var(--sans);color:var(--ink-dim)}
18
- .field input,.field select{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
19
- border-radius:7px;padding:8px 11px;font:13px var(--sans);outline:none;width:100%}
20
- .field input:focus,.field select:focus{border-color:var(--accent-dim)}
18
+ .field input,.field select{background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
19
+ border-radius:var(--r);padding:8px 11px;font:13px var(--sans);outline:none;width:100%;
20
+ transition:border-color .15s var(--ease),box-shadow .15s var(--ease)}
21
+ .field input:focus,.field select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
21
22
  .field small{color:var(--ink-faint);font-size:11.5px;line-height:1.45}
22
23
 
23
24
  .choices{display:grid;gap:8px;margin:10px 0 4px;max-width:640px}
24
25
  .choice{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;align-items:start;
25
- background:var(--panel-2);border:1px solid var(--line);border-radius:8px;
26
- padding:11px 13px;cursor:pointer}
27
- .choice:has(input:checked){border-color:var(--accent-dim)}
26
+ background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
27
+ padding:12px 14px;cursor:pointer;
28
+ transition:border-color .15s var(--ease),background .15s var(--ease),box-shadow .15s var(--ease)}
29
+ .choice:hover{border-color:var(--border-strong)}
30
+ .choice:has(input:checked){border-color:var(--accent-dim);background:var(--accent-soft)}
28
31
  .choice input{grid-row:1/3;margin-top:3px;accent-color:var(--accent)}
29
32
  .choice b{font:600 13px var(--sans)}
30
33
  .choice small{color:var(--ink-dim);font-size:12px;line-height:1.45}
31
34
  .choice code{grid-column:2;font:10.5px var(--mono);color:var(--ink-faint)}
32
35
 
33
36
  .row{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:6px}
34
- .btn{background:var(--panel-2);border:1px solid var(--line);color:var(--ink);border-radius:7px;
35
- padding:8px 13px;font:13px var(--sans);cursor:pointer;text-decoration:none;display:inline-block}
36
- .btn:hover{border-color:var(--accent-dim)}
37
+ .btn{background:var(--surface-2);border:1px solid transparent;color:var(--ink);border-radius:8px;
38
+ padding:7px 14px;font:500 13px var(--sans);cursor:pointer;text-decoration:none;display:inline-block;
39
+ transition:background .15s var(--ease),opacity .15s var(--ease)}
40
+ .btn:hover{background:color-mix(in srgb,var(--surface-2) 88%,var(--ink) 12%)}
41
+ .btn:active{opacity:.8}
37
42
  /* The full accent, not the dim one. Dim on the page background is pale-green-on-white with
38
43
  near-white text: the button that does the main thing on the screen read as disabled. */
39
- .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--bg);font-weight:600}
40
- .btn.primary:hover{filter:brightness(1.08);background:var(--accent);border-color:var(--accent)}
41
- .btn[disabled]{opacity:.5;cursor:default}
44
+ .btn.primary{background:var(--accent-fill);border-color:transparent;color:var(--accent-ink);font-weight:600}
45
+ .btn.primary:hover{background:var(--accent-fill);opacity:.88}
46
+ .btn[disabled]{opacity:.45;cursor:default}
42
47
 
43
- .cmd{background:var(--panel-2);border:1px solid var(--line);border-radius:7px;padding:10px 12px;
48
+ .cmd{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;
44
49
  font:12px/1.6 var(--mono);color:var(--ink);overflow-x:auto;white-space:pre-wrap;
45
50
  word-break:break-word;margin:9px 0}
46
51
 
47
52
  .planout{margin-top:12px}
48
- .filelist{display:grid;gap:2px;margin:9px 0;max-height:240px;overflow-y:auto;
49
- background:var(--panel-2);border:1px solid var(--line);border-radius:7px;padding:10px 12px}
53
+ .filelist{display:grid;gap:2px;margin:9px 0;
54
+ background:var(--bg);border:1px solid var(--line);border-radius:var(--r);padding:10px 12px}
50
55
  .filelist code{font:11px var(--mono);color:var(--ink-dim)}
51
56
 
52
- .manual{background:var(--panel-2);border:1px solid var(--line);border-radius:8px;
57
+ .manual{background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
53
58
  padding:13px 15px;margin:11px 0}
59
+ /* A file step that is written says so in the border too, the way a finished step does. */
60
+ .manual.done{border-color:color-mix(in srgb,var(--accent) 30%,var(--line))}
54
61
  .manual b{display:block;font:600 13px var(--sans);margin-bottom:4px}
55
62
  .manual p{margin:0 0 6px;font-size:13px}
56
63
  .manual small{display:block;color:var(--ink-faint);font-size:11.5px;line-height:1.5;margin-bottom:9px}
@@ -59,32 +66,33 @@
59
66
 
60
67
  .paste{border-top:1px solid var(--line);margin-top:16px;padding-top:15px}
61
68
  .paste h3{margin:0 0 5px;font:600 14px var(--sans)}
62
- .briefpreview{background:var(--panel-2);border:1px solid var(--line);border-radius:7px;
69
+ .briefpreview{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);
63
70
  padding:12px 14px;font:11.5px/1.6 var(--mono);max-height:340px;overflow:auto;
64
71
  white-space:pre-wrap;word-break:break-word;margin:10px 0}
65
- .pastebox{width:100%;background:var(--panel-2);border:1px solid var(--line);color:var(--ink);
66
- border-radius:7px;padding:10px 12px;font:12px/1.6 var(--mono);outline:none;resize:vertical;
72
+ .pastebox{width:100%;background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
73
+ border-radius:var(--r);padding:10px 12px;font:12px/1.6 var(--mono);outline:none;resize:vertical;
67
74
  margin:4px 0}
68
- .pastebox:focus{border-color:var(--accent-dim)}
75
+ .pastebox:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
69
76
 
70
77
  .pasteresult{margin-top:12px}
71
78
  /* A model that replied in prose is the common case and not the person's mistake, so this reads
72
79
  as a next step rather than as a failure. */
73
- .problem{background:var(--panel-2);border:1px solid var(--warn);border-radius:8px;
80
+ .problem{background:color-mix(in srgb,var(--warn) 9%,transparent);
81
+ border:1px solid transparent;border-radius:var(--r);
74
82
  padding:12px 14px;margin-bottom:10px}
75
83
  .problem b{display:block;font:600 13px var(--sans);margin-bottom:7px}
76
84
 
77
- .pastefile{background:var(--panel-2);border:1px solid var(--line);border-radius:8px;
85
+ .pastefile{background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
78
86
  padding:12px 14px;margin-bottom:10px}
79
87
  .pastefile.saved{border-color:var(--accent-dim)}
80
88
  .pastefile b{font:600 13px var(--mono)}
81
89
  .pastefile .meta{margin-left:9px}
82
- .pastefile .diff{max-height:300px;overflow:auto;margin:9px 0}
90
+ .pastefile .diff{margin:9px 0}
83
91
 
84
92
  /* ---------- the doctor checklist ---------- */
85
93
 
86
- .check{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;align-items:start;
87
- padding:10px 0;border-top:1px solid var(--line)}
94
+ .check{display:grid;grid-template-columns:auto 1fr;gap:2px 12px;align-items:start;
95
+ padding:12px 0;border-top:1px solid var(--line)}
88
96
  .check:first-child{border-top:none}
89
97
  .checkmark{font:600 13px var(--mono);line-height:1.5}
90
98
  .check.fail .checkmark{color:var(--err)}
@@ -95,7 +103,7 @@
95
103
  /* ---------- the repo picker ---------- */
96
104
 
97
105
  .repolist{display:grid;gap:1px;max-height:280px;overflow-y:auto;
98
- border:1px solid var(--line);border-radius:7px;background:var(--line)}
106
+ border:1px solid var(--line);border-radius:var(--r);background:var(--line)}
99
107
  /* Scoped to the picker. Bare `.repo` also matched the project label on every inbox row,
100
108
  which is where the white box and the stray padding behind MARKETING came from. */
101
109
  .repolist .repo{display:flex;align-items:center;justify-content:space-between;gap:12px;
@@ -106,6 +114,80 @@
106
114
  .repolist .repo .sub{margin:2px 0 0;font-size:11.5px}
107
115
 
108
116
  /* Which of the two things this is: a new org, or one that already runs roster. */
109
- .verdict{background:var(--panel-2);border:1px solid var(--line);border-radius:7px;
117
+ .verdict{background:var(--accent-soft);border:1px solid transparent;border-radius:var(--r);
110
118
  padding:9px 12px;font-size:12.5px}
111
119
  .verdict p{margin:0}
120
+
121
+ .plansteps{margin:6px 0 12px;padding-left:20px;font-size:13.5px;line-height:1.7}
122
+ .planfiles{margin:0 0 12px}
123
+ .planfiles summary{cursor:pointer;color:var(--ink-dim);font-size:12.5px}
124
+
125
+ /* ---------- the numbered to-do list ---------- */
126
+
127
+ .todos{display:grid;gap:10px;margin-bottom:14px}
128
+ .todo{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);padding:16px 18px}
129
+ .todo.done{padding:11px 18px;border-color:color-mix(in srgb,var(--accent) 30%,var(--line))}
130
+ .todohead{display:flex;align-items:center;gap:11px}
131
+ .todohead h3{margin:0;font:600 14.5px var(--sans);flex:1}
132
+ .todon{flex:none;width:24px;height:24px;border-radius:50%;display:grid;place-items:center;
133
+ background:var(--surface-2);color:var(--ink-dim);font:600 11px var(--mono)}
134
+ .todo.done .todon{background:var(--accent-fill);color:var(--accent-ink)}
135
+ .todopill{font:600 11px var(--sans);padding:2px 8px;border-radius:99px;background:var(--surface-2);color:var(--ink-dim)}
136
+ .todo.done .todopill{background:var(--accent-soft);color:var(--accent)}
137
+ .todohint{margin:8px 0 0 35px}
138
+ .todobody{margin:12px 0 0 35px}
139
+ .todobody .row{margin-top:10px}
140
+ .todo.locked{background:transparent;border-style:dashed}
141
+ .todo.locked h3{color:var(--ink-dim)}
142
+ .todo.locked .todopill{background:none;border:1px solid var(--line);color:var(--ink-faint)}
143
+
144
+ /* ---------- hiring on the Staff screen ---------- */
145
+
146
+ .hirepane > h3{margin:0 0 10px;font:600 14.5px var(--sans)}
147
+ .roles{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px;max-width:920px}
148
+ .role{display:grid;gap:5px;align-content:start;text-align:left;cursor:pointer;
149
+ background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);padding:14px 16px;
150
+ color:var(--ink);font:inherit;
151
+ transition:border-color .15s var(--ease),box-shadow .15s var(--ease),transform .15s var(--ease)}
152
+ .role:hover{border-color:var(--accent-dim);box-shadow:var(--shadow-sm);transform:translateY(-1px)}
153
+ .role:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
154
+ .role b{font:600 14px var(--sans);letter-spacing:-.01em}
155
+ .role span{color:var(--ink-dim);font:12.5px/1.5 var(--sans)}
156
+ .roleform{max-width:560px;margin:14px 0 0}
157
+ .roleform textarea{width:100%}
158
+ .hireflow{max-width:920px}
159
+ .hireflowhead{justify-content:space-between;margin:0 0 12px}
160
+ .hireflowhead h2{margin:0;font:600 17px var(--sans);letter-spacing:-.015em}
161
+ .hiresummary{margin:0;font:13.5px/1.55 var(--sans);color:var(--ink)}
162
+ .hiresummary.err{color:var(--err)}
163
+ .hireadvanced{margin-top:12px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--r);max-width:560px}
164
+ .qfield{display:grid;gap:5px;margin:0 0 12px}
165
+ .qfield span{font:600 12.5px var(--sans)}
166
+ .qfield input{background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
167
+ border-radius:var(--r);padding:8px 10px;font:13px var(--sans)}
168
+ .modes{display:inline-flex;gap:2px;padding:3px;margin:0 0 14px;background:var(--surface-2);
169
+ border-radius:var(--r);flex-wrap:wrap}
170
+ .mode{border:0;background:none;color:var(--ink-dim);font:600 12.5px var(--sans);padding:6px 12px;
171
+ border-radius:calc(var(--r) - 2px);cursor:pointer}
172
+ .mode:hover{color:var(--ink)}
173
+ .mode.on{background:var(--bg-elev);color:var(--ink);box-shadow:var(--shadow-sm)}
174
+ .modes + div > .paste{border-top:0;margin-top:0;padding-top:0}
175
+
176
+ /* ---------- a checked paste ---------- */
177
+ .verdict{margin:0 0 10px;font:600 13.5px var(--sans)}
178
+ .verdict.ok{color:var(--accent)}
179
+ .verdict.bad{color:var(--err)}
180
+ .pastepreview{border:1px solid var(--line);border-radius:var(--r);padding:14px 16px;margin-bottom:12px;background:var(--bg)}
181
+ .pastenotes{margin:0 0 12px}
182
+ .pastenotes summary{cursor:pointer;font:600 12.5px var(--sans);color:var(--ink-dim);margin-bottom:6px}
183
+ .pastenotes .md{border-left:3px solid var(--line);padding-left:12px}
184
+ .pastefile .diff{white-space:pre-wrap;overflow-wrap:anywhere}
185
+ .warnline{margin:10px 0 0;padding:8px 11px;border-radius:var(--r);background:color-mix(in srgb,var(--warn) 12%,transparent);color:var(--ink);font-size:13px}
186
+
187
+ /* A single-line text box, matching the question fields. */
188
+ .textfield{width:100%;background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
189
+ border-radius:var(--r);padding:8px 10px;font:13px var(--sans);outline:none}
190
+ .textfield:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
191
+ .credform{display:grid;gap:10px}
192
+ .credform .row{margin:0}
193
+ .okline{margin:0 0 8px;color:var(--accent);font:600 13px var(--sans)}
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>roster</title>
7
7
  <!-- Inline, so a local page with no network still has a tab icon and the console stays clean. -->
8
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='7' fill='%237cc7a4'/%3E%3C/svg%3E">
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2334c759'/%3E%3C/svg%3E">
9
9
  <!-- Stylesheets and modules are served from /assets by `roster portal`. No build step:
10
10
  editing one of these files and reloading is the whole edit loop. -->
11
11
  <link rel="stylesheet" href="/assets/css/base.css">
@@ -13,9 +13,11 @@
13
13
  <link rel="stylesheet" href="/assets/css/markdown.css">
14
14
  <link rel="stylesheet" href="/assets/css/brain.css">
15
15
  <link rel="stylesheet" href="/assets/css/inbox.css">
16
+ <link rel="stylesheet" href="/assets/css/home.css">
16
17
  <link rel="stylesheet" href="/assets/css/graph.css">
17
18
  <link rel="stylesheet" href="/assets/css/diff.css">
18
19
  <link rel="stylesheet" href="/assets/css/health.css">
20
+ <link rel="stylesheet" href="/assets/css/runs.css">
19
21
  <link rel="stylesheet" href="/assets/css/setup.css">
20
22
  </head>
21
23
  <body>
@@ -26,15 +28,22 @@
26
28
  <span class="sync" data-icon="refresh"></span><span id="loaded">data now</span>
27
29
  </button>
28
30
 
29
- <button class="nav top" id="inboxnav" data-view="inbox">
30
- <span class="ic" data-icon="inbox"></span> Inbox
31
+ <!-- Only while something is left to set up; app.js decides. -->
32
+ <button class="nav top" id="setupnav" data-view="setup" hidden>
33
+ <span class="ic" data-icon="task-done"></span> Getting started
34
+ <span class="n" id="setupcount"></span>
35
+ </button>
36
+ <button class="nav top" id="homenav" data-view="home" title="What needs you, and who is working">
37
+ <span class="ic" data-icon="inbox"></span> Home
38
+ <span class="n" id="homecount"></span>
39
+ </button>
40
+ <button class="nav" id="inboxnav" data-view="inbox" title="Every open issue on each staff member's tracker">
41
+ <span class="ic" data-icon="issue-open"></span> Trackers
31
42
  <span class="n" id="inboxcount"></span>
32
43
  </button>
33
- <!-- "Pending work", not "Pull requests": what is behind it is finished work waiting on you,
34
- and the word for how it is delivered is not the word for what it is. -->
35
- <button class="nav" id="prsnav" data-view="prs" title="Finished work waiting on you to merge">
36
- <span class="ic" data-icon="merged"></span> Pending work
37
- <span class="n" id="prcount"></span>
44
+ <div class="staffviews" id="inboxsub" hidden></div>
45
+ <button class="nav" id="runsnav" data-view="runs" title="What ran, how it ended, and what it cost">
46
+ <span class="ic" data-icon="play"></span> Runs
38
47
  </button>
39
48
  <button class="nav" id="orgnav" data-view="org">
40
49
  <span class="ic" data-icon="file"></span> Org
@@ -48,7 +57,10 @@
48
57
 
49
58
  <!-- The heading is the fold. Collapsed, it carries the count, because a hidden list still
50
59
  has to say how much it is hiding. -->
51
- <button class="sect sectfold" id="stafffold" aria-expanded="false" aria-controls="stafflist">
60
+ <!-- Named apart from the Staff screen above it: two controls both called "Staff" is how a
61
+ click meant for the screen landed on the fold, for people and for automation alike. -->
62
+ <button class="sect sectfold" id="stafffold" aria-expanded="false" aria-controls="stafflist"
63
+ aria-label="Staff list" title="Show or hide each staff member's own screens">
52
64
  <span class="caret" data-icon="chevron"></span>Staff<span class="n" id="staffcount" hidden></span>
53
65
  </button>
54
66
  <div id="stafflist"></div>
@@ -7,6 +7,12 @@ export const getDocs = () => json("/api/docs");
7
7
  export const getInbox = (force) => json("/api/inbox" + (force ? "?refresh=1" : ""));
8
8
  export const getSync = () => json("/api/sync").catch(() => null);
9
9
 
10
+ /** Every staff member's recent runs and 30-day spend. Online only; offline it says so. */
11
+ export const getRuns = (force) => json("/api/runs" + (force ? "?refresh=1" : ""));
12
+
13
+ /** Who is running now, what started it, and what finished in the last few hours. Never cached. */
14
+ export const getLive = () => json("/api/live");
15
+
10
16
  /** The repos org.yaml lists. Off disk, so the new-issue form does not wait on the inbox. */
11
17
  export const getRepos = () => json("/api/repos");
12
18
 
@@ -47,7 +53,12 @@ export async function post(payload, url = "/api/act") {
47
53
  body: JSON.stringify(payload),
48
54
  });
49
55
  const data = await res.json().catch(() => ({}));
50
- if (!res.ok || data.error) throw new Error(data.error || "failed with " + res.status);
56
+ if (!res.ok || data.error) {
57
+ // The body travels with the error: a refusal often carries the next step, like a link.
58
+ const err = new Error(data.error || "failed with " + res.status);
59
+ err.data = data;
60
+ throw err;
61
+ }
51
62
  return data;
52
63
  }
53
64
 
@@ -78,8 +89,21 @@ export const planTenant = (params) =>
78
89
  export const createTenant = (params) => post(params, "/api/setup/apply");
79
90
 
80
91
  /** The copyable prompt, with every file it refers to carried inside it. */
81
- export const getBrief = (kind, staff) =>
82
- json("/api/brief?kind=" + encodeURIComponent(kind) + (staff ? "&staff=" + encodeURIComponent(staff) : ""));
92
+ export const getBrief = (kind, staff, example, about) =>
93
+ json(
94
+ "/api/brief?kind=" + encodeURIComponent(kind) +
95
+ (staff ? "&staff=" + encodeURIComponent(staff) : "") +
96
+ (example ? "&example=" + encodeURIComponent(example) : "") +
97
+ (about ? "&about=" + encodeURIComponent(about) : ""),
98
+ );
99
+
100
+ /** The worked charter that matches a staff member, with the business's name in it. */
101
+ export const getCharterExample = (staff) =>
102
+ json("/api/charter-example?staff=" + encodeURIComponent(staff));
103
+
104
+ /** Whether a staff member's App secrets are set and a daily run has succeeded. Online only. */
105
+ export const getStaffProgress = (staff, fresh) =>
106
+ json("/api/staff/progress?staff=" + encodeURIComponent(staff) + (fresh ? "&fresh=1" : ""));
83
107
 
84
108
  /** Parse what came back from the model. Reads only: saving is a second, deliberate step. */
85
109
  export const parsePaste = (kind, staff, answer) =>
@@ -98,10 +122,26 @@ export const getDoctor = (offline) => json("/api/doctor" + (offline ? "?offline=
98
122
  export const getFix = (offline) => json("/api/fix" + (offline ? "?offline=1" : ""));
99
123
 
100
124
  export const listRepos = (org) => json("/api/setup/repos?org=" + encodeURIComponent(org));
101
- export const addRepo = (name, role) => post({ name, role }, "/api/setup/add-repo");
125
+ /** Commits org.yaml and pushes, as every other write here does. */
126
+ export const addRepo = (name, role, visibility) =>
127
+ post({ name, role, visibility }, "/api/setup/add-repo");
102
128
  export const startApp = (staff, scope) => post({ staff, scope }, "/api/setup/app");
103
129
  export const appResult = (state) => json("/api/setup/app-result?state=" + encodeURIComponent(state));
104
130
 
105
131
  /** Does this org already run roster. "Create" and "join" are different answers. */
106
132
  export const checkOrg = (org) => json("/api/setup/check-org?org=" + encodeURIComponent(org));
107
133
  export const joinOrg = (org) => post({ org }, "/api/setup/join");
134
+
135
+ /** The ops repo's Actions access: read it, or ask the server to set it. */
136
+ export const getAccess = () => json("/api/setup/access");
137
+ export const setAccess = () => post({}, "/api/setup/access");
138
+
139
+ /** Where the agent credential goes, and storing it. The value only ever travels in a POST. */
140
+ export const getCredential = () => json("/api/setup/credential");
141
+ export const storeCredential = (value, repoSecrets) =>
142
+ post({ value, repoSecrets }, "/api/setup/credential");
143
+
144
+ /** One daily run, now, and where it has got to. */
145
+ export const startRun = (staff) => post({ staff }, "/api/run/start");
146
+ export const runStatus = (staff, id) =>
147
+ json("/api/run/state?staff=" + encodeURIComponent(staff) + "&id=" + encodeURIComponent(id));
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { getOrg } from "./api.js";
4
4
  import { $, el, store } from "./dom.js";
5
- import { icon, iconHTML } from "./icons.js";
5
+ import { icon, iconHTML, staffIcon } from "./icons.js";
6
6
  import { installLightbox } from "./lightbox.js";
7
7
  import { setPeople } from "./md.js";
8
8
  import {
@@ -14,21 +14,27 @@ import {
14
14
  syncNotice,
15
15
  } from "./refresh.js";
16
16
  import { onRender } from "./router.js";
17
- import { ORG_WIDE, S, VIEWS, applyHash, writeHash } from "./state.js";
17
+ import { ORG_WIDE, S, VIEWS, applyHash, humanLabel, readHash, writeHash } from "./state.js";
18
18
  import { viewBrain } from "./views/brain.js";
19
19
  import { viewChanged } from "./views/changed.js";
20
20
  import { viewDocs } from "./views/docs.js";
21
21
  import { viewGraph } from "./views/graph.js";
22
22
  import { viewHealth } from "./views/health.js";
23
+ import { viewHome } from "./views/home.js";
23
24
  import { viewInbox, viewPrs } from "./views/inbox.js";
24
25
  import { viewOrg } from "./views/org.js";
25
26
  import { viewPrompt } from "./views/prompt.js";
26
- import { viewSetup } from "./views/setup.js";
27
+ import { viewRuns } from "./views/runs.js";
28
+ import { sentence, whatsLeft } from "./readiness.js";
29
+ import { paintSetupNav, viewGettingStarted, viewSetup } from "./views/setup.js";
27
30
  import { viewStaff } from "./views/staff.js";
28
31
 
29
32
  const SCREEN = {
33
+ setup: viewGettingStarted,
34
+ home: viewHome,
30
35
  inbox: viewInbox,
31
36
  prs: viewPrs,
37
+ runs: viewRuns,
32
38
  org: viewOrg,
33
39
  staff: viewStaff,
34
40
  docs: viewDocs,
@@ -50,8 +56,38 @@ const SCREEN = {
50
56
  */
51
57
  const NEEDS = ["org", "name", "opsName", "staff"];
52
58
 
59
+ /** The org-wide screens in the sidebar, by button. */
60
+ const NAV = [
61
+ ["#setupnav", "setup"],
62
+ ["#homenav", "home"],
63
+ ["#inboxnav", "inbox"],
64
+ ["#runsnav", "runs"],
65
+ ["#staffnav", "staff"],
66
+ ["#orgnav", "org"],
67
+ ["#docsnav", "docs"],
68
+ ];
69
+
53
70
  export async function boot() {
54
71
  initTheme();
72
+
73
+ /* Bound before the first request rather than after it. The sidebar is markup, so it is on
74
+ screen and clickable while /api/org is still building the export, and a click in that
75
+ window used to do nothing at all: the first click on Staff after a load was simply lost.
76
+ Now it is remembered and honoured once there is something to render. */
77
+ let early = null;
78
+ for (const [id, view] of NAV) {
79
+ const b = $(id);
80
+ if (!b) continue;
81
+ b.onclick = () => {
82
+ if (!S.data) {
83
+ early = view;
84
+ return;
85
+ }
86
+ S.view = view;
87
+ render();
88
+ };
89
+ }
90
+
55
91
  /* Before the first render, and once: it is delegated, so it covers every image any screen
56
92
  draws from here on, including the setup screen. */
57
93
  installLightbox();
@@ -66,7 +102,7 @@ export async function boot() {
66
102
  /* Every one of these reads a tenant, so during setup they answer 409 and the sidebar is a
67
103
  row of four dead ends beside an empty Staff heading. There is one thing to do on this
68
104
  screen; the navigation comes back with the org. */
69
- for (const el of document.querySelectorAll("#inboxnav, #prsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
105
+ for (const el of document.querySelectorAll("#homenav, #inboxnav, #runsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
70
106
  el.hidden = true;
71
107
  }
72
108
  for (const slot of document.querySelectorAll("[data-icon]")) {
@@ -78,6 +114,8 @@ export async function boot() {
78
114
 
79
115
  S.data = first;
80
116
  S.loadedAt = new Date();
117
+ S.staffOpen = null; // an open hire list survives a refresh, not a page load
118
+ S.readiness = null;
81
119
  S.staffHandle = S.data.staff[0]?.handle ?? null;
82
120
  $("#orgname").textContent = S.data.name + " · " + S.data.staff.length + " staff";
83
121
 
@@ -88,15 +126,15 @@ export async function boot() {
88
126
  slot.innerHTML = iconHTML(slot.dataset.icon);
89
127
  }
90
128
  onRender(render);
129
+ const linked = readHash() !== null;
91
130
  applyHash();
131
+ /* An org nobody has been hired into has an empty Inbox, which says nothing about what to do
132
+ next. It opens on what is left instead, unless a link or a click asked for somewhere else. */
133
+ if (early) S.view = early;
134
+ else if (!linked && (S.data.unfinished ?? []).includes("hire")) S.view = "setup";
92
135
  initStaffFold();
93
136
  paintSidebar();
94
137
 
95
- $("#inboxnav").onclick = () => { S.view = "inbox"; render(); };
96
- $("#prsnav").onclick = () => { S.view = "prs"; render(); };
97
- $("#staffnav").onclick = () => { S.view = "staff"; render(); };
98
- $("#orgnav").onclick = () => { S.view = "org"; render(); };
99
- $("#docsnav").onclick = () => { S.view = "docs"; render(); };
100
138
  $("#refreshall").onclick = () => refreshAll(false);
101
139
 
102
140
  /* Coming back to the tab after a run should show the run — and coming back to it thirty
@@ -218,6 +256,7 @@ function initStaffFold() {
218
256
  }
219
257
 
220
258
  function paintSidebar() {
259
+ paintInboxLinks();
221
260
  const host = $("#stafflist");
222
261
  host.replaceChildren();
223
262
  paintedRoster = S.data.staff.map((s) => s.handle).join(" ");
@@ -228,11 +267,20 @@ function paintSidebar() {
228
267
  const head = el("button", { className: "nav staffrow" });
229
268
  head.dataset.staff = s.handle;
230
269
  head.setAttribute("aria-expanded", String(open));
270
+ const dot = el("span", { className: "readydot", hidden: true });
231
271
  head.append(
232
272
  icon("chevron", "caret"),
273
+ icon(staffIcon(s), "ic"),
233
274
  el("span", { className: "t", textContent: s.name }),
275
+ dot,
234
276
  el("span", { className: "hh", textContent: s.handle }),
235
277
  );
278
+ whatsLeft(s).then((left) => {
279
+ paintSetupNav();
280
+ if (!left.length) return;
281
+ dot.hidden = false;
282
+ head.title = "Not ready to run: " + sentence(left);
283
+ });
236
284
 
237
285
  const views = el("div", { className: "staffviews" });
238
286
  views.hidden = !open;
@@ -271,7 +319,43 @@ function paintSidebar() {
271
319
  }
272
320
 
273
321
  /** Which rows are lit, and which staff member is unfolded. */
322
+ /* The inbox's filters, as links under Inbox: everything, what is on you, decisions, then one
323
+ per staff member. They replaced two dropdowns on the Inbox screen itself. */
324
+ function paintInboxLinks() {
325
+ const sub = $("#inboxsub");
326
+ if (!sub) return;
327
+ sub.replaceChildren();
328
+ // Inbox itself is everything. Under it: each staff member, then the product repos' issues.
329
+ const links = [
330
+ ["", "All", "inbox"],
331
+ ...S.data.staff.map((s) => [s.handle, s.name, staffIcon(s)]),
332
+ ["@issues", "Issues", "issue-open"],
333
+ ];
334
+ for (const [who, label, glyph] of links) {
335
+ const b = el("button", { className: "nav subnav inboxlink" }, [
336
+ icon(glyph, "ic"),
337
+ el("span", { className: "t", textContent: label }),
338
+ ...(who === "@unread" ? [el("span", { className: "n", id: "unreadcount" })] : []),
339
+ ]);
340
+ b.dataset.who = who;
341
+ b.onclick = () => {
342
+ S.view = "inbox";
343
+ S.inboxStaff = who;
344
+ S.inboxFilter = "";
345
+ S.inboxOpen = null;
346
+ render();
347
+ };
348
+ sub.append(b);
349
+ }
350
+ }
351
+
274
352
  function markSidebar() {
353
+ const sub = $("#inboxsub");
354
+ if (sub) sub.hidden = S.view !== "inbox";
355
+ for (const b of document.querySelectorAll(".inboxlink")) {
356
+ const on = S.view === "inbox" && b.dataset.who === (S.inboxStaff || "");
357
+ b.setAttribute("aria-current", String(on));
358
+ }
275
359
  for (const b of document.querySelectorAll(".nav")) {
276
360
  if (!b.dataset.view) continue;
277
361
  const mine = !b.dataset.staff || b.dataset.staff === S.staffHandle;
@@ -296,6 +380,7 @@ function render() {
296
380
  // A refresh can add or remove a staff member, and the sidebar is built once.
297
381
  const roster = S.data.staff.map((s) => s.handle).join(" ");
298
382
  if (roster !== paintedRoster) paintSidebar();
383
+ paintSetupNav();
299
384
  markSidebar();
300
385
  stampCounts();
301
386
  writeHash(true);
@@ -111,3 +111,86 @@ export function askText({
111
111
  ta.setSelectionRange?.(ta.value.length, ta.value.length);
112
112
  });
113
113
  }
114
+
115
+ /**
116
+ * Asking before something that cannot be taken back: a merge, a push, a new repo, a paid run.
117
+ *
118
+ * Native `confirm()` looked like the browser rather than this page, and it blocks the whole
119
+ * tab, which froze browser automation mid-run. This is the same `<dialog>` as askText, so the
120
+ * backdrop, Escape and the focus trap come free. The wording is the caller's: `title` is the
121
+ * question, `hint` says what will happen.
122
+ *
123
+ * @returns true only for the confirm button. Escape, Cancel and the backdrop are all no.
124
+ */
125
+ export function askYes({ title, hint, confirm = "Continue" }) {
126
+ const box = document.createElement("dialog");
127
+ // The test shim has no dialog element; see askText.
128
+ if (typeof box.showModal !== "function") {
129
+ return Promise.resolve(Boolean(globalThis.confirm?.(hint ? title + "\n\n" + hint : title)));
130
+ }
131
+
132
+ box.className = "ask";
133
+ box.append(el("h3", { textContent: title }));
134
+ if (hint) box.append(el("p", { className: "askhint", textContent: hint }));
135
+
136
+ const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
137
+ const go = el("button", { className: "ghbtn primary", textContent: confirm });
138
+ box.append(el("div", { className: "row askrow" }, [cancel, go]));
139
+
140
+ return new Promise((resolve) => {
141
+ let answer = false;
142
+ const done = (yes) => {
143
+ answer = yes;
144
+ box.close();
145
+ };
146
+ cancel.onclick = () => done(false);
147
+ go.onclick = () => done(true);
148
+ box.addEventListener("close", () => {
149
+ box.remove();
150
+ resolve(answer);
151
+ });
152
+ box.addEventListener("click", (e) => {
153
+ if (onBackdrop(e, box.getBoundingClientRect?.())) done(false);
154
+ });
155
+ document.body.append(box);
156
+ box.showModal();
157
+ // Cancel has the focus, so an Enter pressed out of habit does not merge anything.
158
+ cancel.focus();
159
+ });
160
+ }
161
+
162
+ /**
163
+ * A larger dialog for a whole task: hiring someone, or finishing their setup. Native <dialog>
164
+ * again, appended to <body> rather than to the screen, so a background refresh repainting the
165
+ * screen leaves it open with whatever was typed into it.
166
+ *
167
+ * @returns {{set: (node: Node) => void, close: () => void, isOpen: () => boolean} | null}
168
+ * null where there is no dialog element (the test shim), and the caller draws inline instead.
169
+ */
170
+ export function sheet({ node, onClose, side = false }) {
171
+ const box = document.createElement("dialog");
172
+ if (typeof box.showModal !== "function") return null;
173
+ // `side` slides in from the right at full height, for reading a thread beside the page.
174
+ box.className = side ? "sheet side" : "sheet";
175
+ const body = el("div", { className: "sheetbody" });
176
+ body.append(node);
177
+ box.append(body);
178
+ let open = true;
179
+ const close = () => {
180
+ if (!open) return;
181
+ open = false;
182
+ box.close();
183
+ box.remove();
184
+ onClose?.();
185
+ };
186
+ box.addEventListener("cancel", (e) => {
187
+ e.preventDefault();
188
+ close();
189
+ });
190
+ box.addEventListener("click", (e) => {
191
+ if (onBackdrop(e, box.getBoundingClientRect())) close();
192
+ });
193
+ document.body.append(box);
194
+ box.showModal();
195
+ return { set: (n) => body.replaceChildren(n), close, isOpen: () => open };
196
+ }