@bongos/core 1.20.59 → 1.20.61

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 (96) hide show
  1. package/.bongos-core.json +151 -91
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +10 -0
  4. package/clients/bongos-client/index.cjs +10 -0
  5. package/clients/bongos-client/index.d.ts +14 -1
  6. package/clients/bongos-client/index.mjs +10 -0
  7. package/docs/adr/0358-a-hall-wears-a-look-and-its-night-takes-a-tint.md +52 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +324 -3
  10. package/docs/api-reference.md +8 -3
  11. package/docs/copy-inventory.md +343 -333
  12. package/docs/copy-registry.json +527 -433
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/page-inventory.json +4 -1
  15. package/docs/page-readings.json +758 -707
  16. package/modules/autonomy/db.js +13 -6
  17. package/modules/autonomy/migrations/autonomy_004_runner_identity.sql +30 -0
  18. package/modules/autonomy/routes/autonomy.js +16 -0
  19. package/modules/autonomy/runner-health.js +66 -5
  20. package/modules/hall-ui/public/atlas.html +1 -1
  21. package/modules/hall-ui/public/blockers.html +1 -1
  22. package/modules/hall-ui/public/board-room.html +1 -1
  23. package/modules/hall-ui/public/collab.html +1 -1
  24. package/modules/hall-ui/public/commands.html +1 -1
  25. package/modules/hall-ui/public/copy-desk.html +1 -1
  26. package/modules/hall-ui/public/deploy.html +1 -1
  27. package/modules/hall-ui/public/diagrams.html +1 -1
  28. package/modules/hall-ui/public/drachmae.html +1 -1
  29. package/modules/hall-ui/public/fleet.html +1 -1
  30. package/modules/hall-ui/public/gate.html +1 -1
  31. package/modules/hall-ui/public/gate.js +24 -8
  32. package/modules/hall-ui/public/goal-map.html +1 -1
  33. package/modules/hall-ui/public/goals.html +1 -1
  34. package/modules/hall-ui/public/government.html +1 -1
  35. package/modules/hall-ui/public/idea.html +1 -1
  36. package/modules/hall-ui/public/ideas.html +1 -1
  37. package/modules/hall-ui/public/index.html +1 -1
  38. package/modules/hall-ui/public/modules.html +1 -1
  39. package/modules/hall-ui/public/primer.html +1 -1
  40. package/modules/hall-ui/public/profile.html +1 -1
  41. package/modules/hall-ui/public/project-settings.html +1 -1
  42. package/modules/hall-ui/public/ranks.html +1 -1
  43. package/modules/hall-ui/public/roadmap.html +1 -1
  44. package/modules/hall-ui/public/roster.html +1 -1
  45. package/modules/hall-ui/public/sessions.html +1 -1
  46. package/modules/hall-ui/public/settings.html +1 -1
  47. package/modules/hall-ui/public/shell.js +5 -1
  48. package/modules/hall-ui/public/studio.html +1 -1
  49. package/modules/hall-ui/public/style.css +8 -0
  50. package/modules/hall-ui/public/task.html +1 -1
  51. package/modules/hall-ui/public/thinking.html +1 -1
  52. package/modules/hall-ui/public/tweak-editor.html +1 -1
  53. package/modules/hall-ui/public/watch.html +1 -1
  54. package/modules/hall-ui/public/work.html +1 -1
  55. package/modules/provisioning/look.js +169 -0
  56. package/modules/provisioning/migrations/provisioning_034_project_logo.sql +35 -0
  57. package/modules/provisioning/module.json +2 -1
  58. package/modules/provisioning/provisioning.js +4 -3
  59. package/modules/provisioning/routes/look.js +204 -0
  60. package/modules/provisioning/routes/provisioning.js +5 -59
  61. package/modules/provisioning/settings-push.js +76 -0
  62. package/modules/public-landing/public/assets/cosmos.css +1 -1
  63. package/modules/public-landing/public/projects.html +499 -28
  64. package/modules/public-landing/public/projects.probes.json +2 -2
  65. package/modules/public-landing/public/projects.states.json +6 -3
  66. package/modules/ui-design/kit/serve.js +50 -0
  67. package/package-lock.json +2 -2
  68. package/package.json +1 -1
  69. package/release-notes.json +12 -0
  70. package/scripts/gds/autobongos-run.js +43 -2
  71. package/scripts/gds/fitness.js +4 -0
  72. package/src/bongos/routes.js +4 -1
  73. package/src/bongos/serve-internal.js +6 -0
  74. package/src/branding.js +26 -1
  75. package/src/hall-look.js +99 -0
  76. package/src/module-api.js +1 -1
  77. package/src/night-tint.js +26 -0
  78. package/tests/autonomy_runner_identity.mjs +215 -0
  79. package/tests/currency_label.mjs +7 -4
  80. package/tests/hall_tokens.mjs +10 -1
  81. package/tests/hall_tweak_editor.mjs +1 -1
  82. package/tests/look_night_tint.mjs +169 -0
  83. package/tests/nav_permission_atoms.mjs +1 -1
  84. package/tests/projects_hub.mjs +5 -4
  85. package/tests/projects_hub_app_status.mjs +1 -1
  86. package/tests/projects_hub_app_step.mjs +8 -7
  87. package/tests/projects_hub_look.mjs +255 -0
  88. package/tests/projects_hub_module_picker.mjs +2 -2
  89. package/tests/provision_settings_apply.mjs +12 -4
  90. package/tests/provisioning_look.mjs +376 -0
  91. package/tests/provisioning_settings.mjs +10 -7
  92. package/tests/provisioning_settings_apply.mjs +4 -4
  93. package/tests/provisioning_settings_env.mjs +4 -0
  94. package/tests/wizard_draft_resume.mjs +2 -2
  95. package/tests/wizard_front_door.mjs +2 -2
  96. package/tests/wizard_intent_resume.mjs +16 -15
@@ -162,11 +162,11 @@ async function disallowGoal(goalId) {
162
162
  // recordRunnerHeartbeat — upsert one host's check-in. ON CONFLICT so the normal
163
163
  // case is one write, and `host` is the key so a second runner appears as a second
164
164
  // row rather than silently overwriting the first.
165
- async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt = null, mode = null, consecutiveFailures = null, lastEvent = null, workingTaskId = null, workingGoalId = null }) {
165
+ async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt = null, mode = null, consecutiveFailures = null, lastEvent = null, workingTaskId = null, workingGoalId = null, head = null, diskHead = null, mainHead = null, claudeAccount = null }) {
166
166
  const { rows } = await pool.query(
167
167
  `INSERT INTO autonomy_runner_heartbeat
168
- (builder_id, host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, last_seen_at, updated_at)
169
- VALUES ($8, $1, $2, $3, $4, $5, $6, $7, $9, now(), now())
168
+ (builder_id, host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, head, disk_head, main_head, claude_account, last_seen_at, updated_at)
169
+ VALUES ($8, $1, $2, $3, $4, $5, $6, $7, $9, $10, $11, $12, $13, now(), now())
170
170
  ON CONFLICT (builder_id, host) DO UPDATE SET
171
171
  pid = EXCLUDED.pid,
172
172
  started_at = COALESCE(EXCLUDED.started_at, autonomy_runner_heartbeat.started_at),
@@ -175,10 +175,16 @@ async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt =
175
175
  last_event = EXCLUDED.last_event,
176
176
  working_task_id = EXCLUDED.working_task_id,
177
177
  working_goal_id = EXCLUDED.working_goal_id,
178
+ -- Overwritten, never COALESCEd (task 1004407): a fresh process that cannot
179
+ -- read its own commit must say "unknown", not inherit the last process's.
180
+ head = EXCLUDED.head,
181
+ disk_head = EXCLUDED.disk_head,
182
+ main_head = EXCLUDED.main_head,
183
+ claude_account = EXCLUDED.claude_account,
178
184
  last_seen_at = now(),
179
185
  updated_at = now()
180
- RETURNING host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, last_seen_at`,
181
- [String(host).toLowerCase().slice(0, 255), pid, startedAt, mode, consecutiveFailures, lastEvent, workingTaskId, builderId, workingGoalId]
186
+ RETURNING host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, head, disk_head, main_head, claude_account, last_seen_at`,
187
+ [String(host).toLowerCase().slice(0, 255), pid, startedAt, mode, consecutiveFailures, lastEvent, workingTaskId, builderId, workingGoalId, head, diskHead, mainHead, claudeAccount]
182
188
  );
183
189
  return rows[0] || null;
184
190
  }
@@ -193,7 +199,8 @@ async function recordRunnerHeartbeat({ builderId, host, pid = null, startedAt =
193
199
  // liveness view entirely (grader finding, task 1003905).
194
200
  async function readRunnerHeartbeats(builderId) {
195
201
  const { rows } = await pool.query(
196
- `SELECT host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id, last_seen_at
202
+ `SELECT host, pid, started_at, mode, consecutive_failures, last_event, working_task_id, working_goal_id,
203
+ head, disk_head, main_head, claude_account, last_seen_at
197
204
  FROM autonomy_runner_heartbeat
198
205
  WHERE builder_id = $1
199
206
  ORDER BY last_seen_at DESC
@@ -0,0 +1,30 @@
1
+ -- autonomy_004_runner_identity.sql — what the runner card can say about a runner
2
+ -- (task 1004407).
3
+ --
4
+ -- Owner ruling, 2026-10-01: the gate page must show which code each runner is
5
+ -- running, whether it is live or restarting, which always-on PC it is on, and
6
+ -- which Claude account its workers spend. `host` already names the PC
7
+ -- (autonomy_002); these four columns carry the rest.
8
+ --
9
+ -- head the commit the supervisor process is RUNNING (40-char hex)
10
+ -- disk_head the commit on disk, when the checkout was refreshed in place
11
+ -- past what is running (task 1004406) — null otherwise
12
+ -- main_head the newest main the runner has seen, so the card can say
13
+ -- "behind main" without the server reaching GitHub
14
+ -- claude_account a LABEL for the Claude login its workers use (an email, or
15
+ -- "API key" / "Claude token" when one is set). Never a token or
16
+ -- a key: the route caps it at 200 characters and the runner only
17
+ -- ever sends the label.
18
+ --
19
+ -- Nullable, no defaults: a runner that predates this reports nothing, and the
20
+ -- card says nothing rather than guessing. Additive and idempotent.
21
+
22
+ BEGIN;
23
+
24
+ ALTER TABLE autonomy_runner_heartbeat
25
+ ADD COLUMN IF NOT EXISTS head text,
26
+ ADD COLUMN IF NOT EXISTS disk_head text,
27
+ ADD COLUMN IF NOT EXISTS main_head text,
28
+ ADD COLUMN IF NOT EXISTS claude_account text;
29
+
30
+ COMMIT;
@@ -52,6 +52,11 @@ function runsLimit(raw) {
52
52
  return Math.min(RUNS_MAX_LIMIT, Math.max(1, n));
53
53
  }
54
54
 
55
+ // commitOrNull — a full lowercase commit sha, or null (task 1004407).
56
+ function commitOrNull(v) {
57
+ return typeof v === 'string' && /^[0-9a-f]{40}$/.test(v) ? v : null;
58
+ }
59
+
55
60
  module.exports = function buildAutonomyRouter() {
56
61
  const router = express.Router();
57
62
 
@@ -219,6 +224,11 @@ module.exports = function buildAutonomyRouter() {
219
224
  last_event: { type: 'string', maxLength: 64 },
220
225
  working_task_id: { type: 'number' },
221
226
  working_goal_id: { type: 'number' },
227
+ // What the runner card says about the runner (task 1004407).
228
+ head: { type: 'string', maxLength: 40 },
229
+ disk_head: { type: 'string', maxLength: 40 },
230
+ main_head: { type: 'string', maxLength: 40 },
231
+ claude_account: { type: 'string', maxLength: 200 },
222
232
  })) return;
223
233
  const b = req.body || {};
224
234
  try {
@@ -234,6 +244,12 @@ module.exports = function buildAutonomyRouter() {
234
244
  lastEvent: b.last_event || null,
235
245
  workingTaskId: Number.isInteger(b.working_task_id) ? b.working_task_id : null,
236
246
  workingGoalId: Number.isInteger(b.working_goal_id) ? b.working_goal_id : null,
247
+ // A commit is stored only if it IS one: anything that is not 40 hex
248
+ // characters is dropped to null rather than rendered as a version.
249
+ head: commitOrNull(b.head),
250
+ diskHead: commitOrNull(b.disk_head),
251
+ mainHead: commitOrNull(b.main_head),
252
+ claudeAccount: typeof b.claude_account === 'string' && b.claude_account.trim() ? b.claude_account.trim() : null,
237
253
  });
238
254
  res.json({ ok: true, heartbeat: row });
239
255
  } catch (err) {
@@ -19,6 +19,11 @@
19
19
  // exactly what it is for, and crying dead every time it works is worse than
20
20
  // saying nothing.
21
21
  // · dead — quiet longer than any legitimate explanation.
22
+ // · restarting — its last word was "exiting to pick up new code" (upgrade_exit,
23
+ // task 1004406), and that was recently. The wrapper fetches, resets and
24
+ // relaunches, which takes a minute or two, and the owner ruled that a restart
25
+ // must read as a restart, never as a death (task 1004407). Past RESTART_S the
26
+ // relaunch has failed and it is dead again — with a detail that says so.
22
27
 
23
28
  // Reasoned from the runner's own numbers, not picked. The loop beats at the top of
24
29
  // every pass and again on each poll inside a wait (every 60s), so a healthy runner
@@ -28,6 +33,43 @@ const ALIVE_S = 15 * 60;
28
33
  // worker holds the loop for its whole run. Two hours is that ceiling plus room for
29
34
  // the ship, the grade and the verify that follow it.
30
35
  const LATE_S = 2 * 60 * 60;
36
+ // A relaunch is fetch + reset + boot + the first beat. Ten minutes is several
37
+ // times that, so a slow fetch is not called a death, and short enough that a
38
+ // relaunch which never happened is noticed the same evening.
39
+ const RESTART_S = 10 * 60;
40
+
41
+ const SHA_RE = /^[0-9a-f]{40}$/;
42
+ const sha = (v) => (typeof v === 'string' && SHA_RE.test(v) ? v : null);
43
+
44
+ // describeCode — what the card says about the code a runner is on (task 1004407).
45
+ // `behind` is true only when BOTH sides are known and differ: the runner reports
46
+ // the newest main it has seen, and an unknown is never drawn as "out of date".
47
+ // A checkout refreshed in place counts by what is on disk, because that is what
48
+ // every worker and child process runs (task 1004406).
49
+ function describeCode(hb) {
50
+ const head = sha(hb && hb.head);
51
+ if (!head) return null;
52
+ const disk = sha(hb.disk_head);
53
+ const main = sha(hb.main_head);
54
+ const current = disk || head;
55
+ return {
56
+ head, short: head.slice(0, 8),
57
+ diskShort: disk && disk !== head ? disk.slice(0, 8) : null,
58
+ mainShort: main ? main.slice(0, 8) : null,
59
+ behind: main ? main !== current : null,
60
+ };
61
+ }
62
+
63
+ // What the runner was last doing, in words. Keyed on the run-log event names the
64
+ // runner sends as last_event; an unknown event says nothing rather than guessing.
65
+ const ACTIVITY = {
66
+ boot: 'just started',
67
+ waiting: 'waiting for work',
68
+ code_refreshed: 'updated its code in place',
69
+ nothing_claimable: 'waiting — nothing to claim',
70
+ fenced: 'held by the fence',
71
+ hold: 'pausing',
72
+ };
31
73
 
32
74
  function ageS(lastSeenAt, nowMs) {
33
75
  const t = Date.parse(lastSeenAt || '');
@@ -68,19 +110,38 @@ function describeRunner(heartbeat, { nowMs = Date.now(), aliveS = ALIVE_S, lateS
68
110
  const mode = heartbeat.mode === 'probing' ? 'probing' : (heartbeat.mode === 'full' ? 'full' : null);
69
111
  const working = heartbeat.working_task_id ? String(heartbeat.working_task_id) : null;
70
112
  const where = host ? ` on ${host}` : '';
113
+ const ident = {
114
+ host, mode, workingTaskId: working,
115
+ account: typeof heartbeat.claude_account === 'string' && heartbeat.claude_account ? heartbeat.claude_account : null,
116
+ code: describeCode(heartbeat),
117
+ };
118
+
119
+ if (heartbeat.last_event === 'upgrade_exit') {
120
+ if (age <= RESTART_S) {
121
+ return {
122
+ state: 'restarting', ok: true, ageS: age, age: humanAge(age), ...ident,
123
+ detail: `restarting${where} to pick up new code`,
124
+ };
125
+ }
126
+ return {
127
+ state: 'dead', ok: false, ageS: age, age: humanAge(age), ...ident,
128
+ detail: `exited${where} ${humanAge(age)} ago to restart on new code and has not come back`,
129
+ };
130
+ }
71
131
 
72
132
  if (age <= aliveS) {
133
+ const doing = ACTIVITY[heartbeat.last_event];
73
134
  return {
74
- state: 'alive', ok: true, ageS: age, age: humanAge(age), host, mode, workingTaskId: working,
135
+ state: 'alive', ok: true, ageS: age, age: humanAge(age), ...ident,
75
136
  detail: working
76
137
  ? `working task ${working}${where}`
77
- : `${mode === 'probing' ? 'probing after failures' : 'running'}${where}`,
138
+ : `${mode === 'probing' ? 'probing after failures' : (doing || 'running')}${where}`,
78
139
  };
79
140
  }
80
141
 
81
142
  if (age <= lateS) {
82
143
  return {
83
- state: 'late', ok: true, ageS: age, age: humanAge(age), host, mode, workingTaskId: working,
144
+ state: 'late', ok: true, ageS: age, age: humanAge(age), ...ident,
84
145
  // Deliberately NOT an alarm. This is what a runner doing its job looks like.
85
146
  detail: working
86
147
  ? `quiet for ${humanAge(age)} — expected while task ${working} builds`
@@ -89,9 +150,9 @@ function describeRunner(heartbeat, { nowMs = Date.now(), aliveS = ALIVE_S, lateS
89
150
  }
90
151
 
91
152
  return {
92
- state: 'dead', ok: false, ageS: age, age: humanAge(age), host, mode, workingTaskId: working,
153
+ state: 'dead', ok: false, ageS: age, age: humanAge(age), ...ident,
93
154
  detail: `no check-in for ${humanAge(age)}${where} — longer than any worker takes, so the runner is not running`,
94
155
  };
95
156
  }
96
157
 
97
- module.exports = { ALIVE_S, LATE_S, ageS, humanAge, describeRunner };
158
+ module.exports = { ALIVE_S, LATE_S, RESTART_S, ageS, humanAge, describeRunner };
@@ -91,7 +91,7 @@
91
91
 
92
92
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
93
93
 
94
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
94
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
95
95
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
96
96
  <script src="/builders/palette.js" defer></script>
97
97
  <script src="/builders/atlas.js?v=2026-09-02-index" defer></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
53
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -60,7 +60,7 @@
60
60
 
61
61
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
62
62
 
63
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
63
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
64
64
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
65
65
  <script src="/builders/palette.js" defer></script>
66
66
  <script src="/builders/dom-utils.js?v=2026-06-16-board"></script>
@@ -148,7 +148,7 @@
148
148
 
149
149
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
150
150
 
151
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
151
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
152
152
  <script src="/builders/palette.js" defer></script>
153
153
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
154
154
  <script src="/builders/collab-lib.js?v=2026-09-24-archive"></script>
@@ -54,7 +54,7 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
57
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
58
58
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
59
59
  <script src="/builders/palette.js" defer></script>
60
60
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -177,7 +177,7 @@
177
177
 
178
178
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
179
179
 
180
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
180
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
181
181
  <script src="/builders/palette.js" defer></script>
182
182
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
183
183
  <script src="/builders/hall-kit.js"></script>
@@ -74,7 +74,7 @@
74
74
 
75
75
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
76
76
 
77
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
77
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
78
78
  <script src="/builders/palette.js" defer></script>
79
79
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
80
80
  <script src="/builders/hall-kit.js"></script>
@@ -97,7 +97,7 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
100
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -98,7 +98,7 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
101
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
102
102
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
103
103
  <script src="/builders/palette.js" defer></script>
104
104
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -54,7 +54,7 @@
54
54
 
55
55
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
56
56
 
57
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
57
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
58
58
  <script src="/builders/palette.js" defer></script>
59
59
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
60
60
  <script src="/builders/hall-kit.js"></script>
@@ -76,7 +76,7 @@
76
76
  <div id="gate-modal-root"></div>
77
77
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
78
78
 
79
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
79
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
80
80
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
81
  <script src="/builders/palette.js" defer></script>
82
82
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -358,7 +358,7 @@
358
358
  // exist here, resolved to inherited grey, and drew a dead runner in exactly the
359
359
  // same colour as a healthy one. Named classes cannot fail that way silently —
360
360
  // a missing class is visible in the stylesheet, a missing var is not.
361
- const HEALTH_CLASS = { alive: 'fact-pill--ok', late: 'fact-pill--warn', dead: 'fact-pill--danger', never_seen: 'fact-pill--info' };
361
+ const HEALTH_CLASS = { alive: 'fact-pill--ok', late: 'fact-pill--warn', dead: 'fact-pill--danger', never_seen: 'fact-pill--info', restarting: 'fact-pill--info' };
362
362
 
363
363
  async function loadRunners() {
364
364
  const el = document.getElementById('fence-runner');
@@ -384,13 +384,29 @@
384
384
  // NOT the word "running": the switch below already uses it for a different
385
385
  // thing (allowed to work), and the two lines sit one above the other. A
386
386
  // reader should never have to work out which sense is meant.
387
- const label = { dead: 'Not checking in', late: 'Quiet', alive: 'Checking in' }[h.state] || 'Unknown';
388
- return `<p class="ov-foot" style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;">
389
- <span class="fact-pill ${cls}">${escapeHtml(label)}</span>
390
- <span>${escapeHtml(h.detail || '')}</span>
391
- ${r.working_goal_id ? `<span class="ov-fact">from goal ${escapeHtml(String(r.working_goal_id))}</span>` : ''}
392
- <span class="ov-fact">last check-in ${escapeHtml(h.age || 'unknown')} ago</span>
393
- </p>`;
387
+ const label = { dead: 'Not checking in', late: 'Quiet', alive: 'Checking in', restarting: 'Restarting' }[h.state] || 'Unknown';
388
+ // Which PC, which Claude account, which code (task 1004407). Each line is
389
+ // drawn only when the runner reported it, so an older runner shows less
390
+ // rather than a guess.
391
+ const facts = [];
392
+ if (h.host) facts.push(`<span class="ov-fact">PC <strong>${escapeHtml(h.host)}</strong></span>`);
393
+ if (h.account) facts.push(`<span class="ov-fact">Claude account ${escapeHtml(h.account)}</span>`);
394
+ if (h.code) {
395
+ const c = h.code;
396
+ const status = c.behind === true ? ` — behind main (${escapeHtml(c.mainShort)})`
397
+ : c.behind === false ? ' — up to date' : '';
398
+ const disk = c.diskShort ? `, updated in place to ${escapeHtml(c.diskShort)}` : '';
399
+ facts.push(`<span class="ov-fact">code <code>${escapeHtml(c.short)}</code>${disk}${status}</span>`);
400
+ }
401
+ return `<div class="ov-foot">
402
+ <p style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin:0;">
403
+ <span class="fact-pill ${cls}">${escapeHtml(label)}</span>
404
+ <span>${escapeHtml(h.detail || '')}</span>
405
+ ${r.working_goal_id ? `<span class="ov-fact">from goal ${escapeHtml(String(r.working_goal_id))}</span>` : ''}
406
+ <span class="ov-fact">last check-in ${escapeHtml(h.age || 'unknown')} ago</span>
407
+ </p>
408
+ ${facts.length ? `<p style="display:flex;gap:14px;flex-wrap:wrap;margin:4px 0 0;">${facts.join('')}</p>` : ''}
409
+ </div>`;
394
410
  }).join('');
395
411
  }
396
412
 
@@ -137,7 +137,7 @@
137
137
  </div>
138
138
  </div>
139
139
 
140
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
140
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
141
141
  <script src="/builders/palette.js" defer></script>
142
142
 
143
143
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -230,7 +230,7 @@
230
230
 
231
231
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
232
232
 
233
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
233
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
234
234
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
235
235
  <script src="/builders/palette.js" defer></script>
236
236
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
137
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
55
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -225,7 +225,7 @@
225
225
 
226
226
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
227
227
 
228
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
228
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
229
229
  <script src="/builders/palette.js" defer></script>
230
230
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
231
231
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -106,7 +106,7 @@
106
106
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
107
107
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
108
108
  BEFORE builders.js so the chrome ids exist when events are wired. -->
109
- <script src="/builders/shell.js?v=2026-09-30-navback2" defer></script>
109
+ <script src="/builders/shell.js?v=2026-10-01-look" defer></script>
110
110
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
111
111
  <script src="/builders/palette.js" defer></script>
112
112
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -101,7 +101,7 @@
101
101
 
102
102
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
103
103
 
104
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
104
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
105
105
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
106
106
  <script src="/builders/palette.js" defer></script>
107
107
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
118
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
190
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
61
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -77,7 +77,7 @@
77
77
 
78
78
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
79
79
  chrome exists before the page script runs. -->
80
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
80
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
81
81
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
82
82
  <script src="/builders/palette.js" defer></script>
83
83
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
44
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
53
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
102
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -400,7 +400,7 @@
400
400
 
401
401
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
402
402
  chrome exists before the page scripts run. -->
403
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
403
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
404
404
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
405
405
  <script src="/builders/palette.js" defer></script>
406
406
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -442,7 +442,11 @@
442
442
 
443
443
  el.innerHTML =
444
444
  '<div class="app-sidebar__brand"><a class="brand-mark" href="/" id="brand-mark">' +
445
- '<span class="brand-mark__dot" aria-hidden="true"></span>' +
445
+ // A project's logo (ADR 0358, task 1004421) takes the dot's place; the
446
+ // platform validated it as an https image address before it was pushed.
447
+ (brand.theme && brand.theme.logo
448
+ ? '<img class="brand-mark__logo" src="' + esc(brand.theme.logo) + '" alt="" width="24" height="24" decoding="async">'
449
+ : '<span class="brand-mark__dot" aria-hidden="true"></span>') +
446
450
  '<span class="brand-mark__name" id="brand-name"></span></a></div>' +
447
451
  '<nav class="app-sidebar__nav" aria-label="Primary">' + groups + '</nav>' +
448
452
  '<div class="app-sidebar__foot">' +
@@ -176,7 +176,7 @@
176
176
 
177
177
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
178
178
 
179
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
179
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
180
180
  <script src="/builders/palette.js" defer></script>
181
181
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
182
182
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -561,6 +561,14 @@ main:focus { outline: none; }
561
561
  background: none;
562
562
  flex-shrink: 0;
563
563
  }
564
+ /* A project's logo in the dot's place (ADR 0358): the image is the founder's, so
565
+ it gets a soft square, never a recolour. */
566
+ .brand-mark__logo {
567
+ width: 24px; height: 24px;
568
+ border-radius: 6px;
569
+ object-fit: contain;
570
+ flex-shrink: 0;
571
+ }
564
572
  .brand-mark__name {
565
573
  display: none;
566
574
  white-space: nowrap;
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
60
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -138,7 +138,7 @@
138
138
  </div>
139
139
  </div>
140
140
 
141
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
141
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
142
142
  <script src="/builders/palette.js" defer></script>
143
143
 
144
144
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -140,7 +140,7 @@
140
140
 
141
141
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
142
142
 
143
- <script src="/builders/shell.js?v=2026-09-30-navback2"></script>
143
+ <script src="/builders/shell.js?v=2026-10-01-look"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
146
146
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>