loki-mode 11.72.4 → 11.73.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.
Files changed (74) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/dashboard/__init__.py +1 -1
  4. package/docs/CLI-REFERENCE.md +1 -1
  5. package/docs/INSTALLATION.md +1 -1
  6. package/docs/dev/architecture-reference.md +26 -0
  7. package/docs/v10/OPERATING-MODEL.md +14 -8
  8. package/docs/v10/RETRO-2026.md +650 -0
  9. package/loki-ts/dist/loki.js +1636 -1637
  10. package/loki-ts/package.json +2 -2
  11. package/mcp/__init__.py +1 -1
  12. package/package.json +3 -3
  13. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  14. package/tools/cockpit/SPEC.md +275 -0
  15. package/tools/cockpit/index.html +144 -0
  16. package/tools/cockpit/roi-basis.json +33 -0
  17. package/tools/cockpit/server.py +891 -0
  18. package/web-app/dist/assets/{AdminPage-xzGuOr-n.js → AdminPage-DLxU8cWs.js} +1 -1
  19. package/web-app/dist/assets/{Avatar-TI-tKKlA.js → Avatar-FpXoF8Iv.js} +1 -1
  20. package/web-app/dist/assets/{Badge-Bpsvr32Y.js → Badge-BpPOaQ1Z.js} +1 -1
  21. package/web-app/dist/assets/{Button-Bs6Ksvez.js → Button-BwM7fgQH.js} +1 -1
  22. package/web-app/dist/assets/{CockpitPage-DO265aOO.js → CockpitPage-nc-CvHy4.js} +1 -1
  23. package/web-app/dist/assets/{ComparePage-CdFol2W2.js → ComparePage-30XVTYmd.js} +1 -1
  24. package/web-app/dist/assets/{ErrorBoundary-BFNHnRlC.js → ErrorBoundary-CfKqjdpH.js} +1 -1
  25. package/web-app/dist/assets/{EvidenceReceiptPanel-BYowSNZf.js → EvidenceReceiptPanel-BVIX0kAo.js} +1 -1
  26. package/web-app/dist/assets/{GitHubIssuesPanel-Q4n1PnKJ.js → GitHubIssuesPanel-CDmHswpW.js} +1 -1
  27. package/web-app/dist/assets/{GitHubPRsPanel-3YAhTdNH.js → GitHubPRsPanel-E-OqBH5f.js} +1 -1
  28. package/web-app/dist/assets/{HomePage-5_s0DB-R.js → HomePage-CESGv5fC.js} +2 -2
  29. package/web-app/dist/assets/{LoginPage-4lLnOEw7.js → LoginPage-BY0y6aE6.js} +1 -1
  30. package/web-app/dist/assets/{MagicPage-B-YdYnHb.js → MagicPage-BpjToOBX.js} +1 -1
  31. package/web-app/dist/assets/{MetricsPage-WTZUrQnh.js → MetricsPage-DpO52H9O.js} +1 -1
  32. package/web-app/dist/assets/{NotFoundPage-DHgW0oAH.js → NotFoundPage-BXU0NGlM.js} +1 -1
  33. package/web-app/dist/assets/{ProjectPage-BekgCDq_.js → ProjectPage-CewcEfOG.js} +3 -3
  34. package/web-app/dist/assets/{ProjectsPage-DK03jcxC.js → ProjectsPage-C7LZkpcR.js} +1 -1
  35. package/web-app/dist/assets/{SettingsPage-CQAz-Z-c.js → SettingsPage-DHp1BCgG.js} +1 -1
  36. package/web-app/dist/assets/{ShowcasePage-BCvi6l1R.js → ShowcasePage-C_E4yQad.js} +1 -1
  37. package/web-app/dist/assets/{SystemSettingsPage-D-pa6BMI.js → SystemSettingsPage-wK7KRqw1.js} +1 -1
  38. package/web-app/dist/assets/{TeamsPage-CXUc1cy-.js → TeamsPage-CiMcY4GR.js} +1 -1
  39. package/web-app/dist/assets/{TemplatesPage-ZBMUi0-a.js → TemplatesPage-pGUCVtD6.js} +1 -1
  40. package/web-app/dist/assets/{TerminalOutput-CrG-Nh0k.js → TerminalOutput-CvD75-gW.js} +1 -1
  41. package/web-app/dist/assets/{activity-B4OW45Iy.js → activity-DSy_WjFp.js} +1 -1
  42. package/web-app/dist/assets/{bell-C9RlDVIm.js → bell-BRHWRNQ-.js} +1 -1
  43. package/web-app/dist/assets/{bot-3LjewrK4.js → bot-CacHJXqJ.js} +1 -1
  44. package/web-app/dist/assets/{check-BTbBe1Gj.js → check-DcGs1awd.js} +1 -1
  45. package/web-app/dist/assets/{chevron-left-DCBZHrXv.js → chevron-left-BpKToVdV.js} +1 -1
  46. package/web-app/dist/assets/{circle-alert-BXEglG-b.js → circle-alert-Cvg68BPU.js} +1 -1
  47. package/web-app/dist/assets/{clock-C-hS5UgA.js → clock-Bm-746fH.js} +1 -1
  48. package/web-app/dist/assets/{cloud-DvapCBZB.js → cloud-DkpICGV1.js} +1 -1
  49. package/web-app/dist/assets/{code-xml-Cp86TqsZ.js → code-xml-Bnps1g8p.js} +1 -1
  50. package/web-app/dist/assets/{database-DhSK5kEY.js → database-DXpzjteu.js} +1 -1
  51. package/web-app/dist/assets/{dollar-sign-Ci4MCIIw.js → dollar-sign-B03JgYAV.js} +1 -1
  52. package/web-app/dist/assets/{file-code-corner-DnjCjsh8.js → file-code-corner-BuEiz3XW.js} +1 -1
  53. package/web-app/dist/assets/{file-plus-B9bsG5Tq.js → file-plus-DhM25BHK.js} +1 -1
  54. package/web-app/dist/assets/{globe-D1S4CTjQ.js → globe-_Xv1tJB8.js} +1 -1
  55. package/web-app/dist/assets/{hammer-BY9BjPRM.js → hammer-D9ZYHuG8.js} +1 -1
  56. package/web-app/dist/assets/{index-w2zPXwIV.js → index-BD-nW46P.js} +3 -3
  57. package/web-app/dist/assets/{layers-CoKx45jP.js → layers-B5nOPH68.js} +1 -1
  58. package/web-app/dist/assets/{loader-circle-BMKEYndi.js → loader-circle-c1OVmB63.js} +1 -1
  59. package/web-app/dist/assets/{lock-BZH8DCdh.js → lock-B5_SQpPS.js} +1 -1
  60. package/web-app/dist/assets/{package-ZDk8UPD_.js → package-Cx_KC4IW.js} +1 -1
  61. package/web-app/dist/assets/{plus-DIbN38-M.js → plus-BCoCE3wz.js} +1 -1
  62. package/web-app/dist/assets/{refresh-cw-Dsl3xoHX.js → refresh-cw-ComZ_-zw.js} +1 -1
  63. package/web-app/dist/assets/{rotate-ccw-DIexIxUg.js → rotate-ccw-D-2b38Jn.js} +1 -1
  64. package/web-app/dist/assets/{scroll-text-Bk3d7iYV.js → scroll-text-BIupqEqW.js} +1 -1
  65. package/web-app/dist/assets/{server-DrTdGxpu.js → server-jYMEfnFP.js} +1 -1
  66. package/web-app/dist/assets/{shield-alert-CTLoIHTS.js → shield-alert-DQmUPlgL.js} +1 -1
  67. package/web-app/dist/assets/{thumbs-up-Dzhq17Pn.js → thumbs-up-CaMdkKBO.js} +1 -1
  68. package/web-app/dist/assets/{trash-2-aapXLPeI.js → trash-2-DUGxfddn.js} +1 -1
  69. package/web-app/dist/assets/{trending-down-Cn_lODPb.js → trending-down-DK-pYPoa.js} +1 -1
  70. package/web-app/dist/assets/{trending-up-eQoFN3XW.js → trending-up-BPhgmVjq.js} +1 -1
  71. package/web-app/dist/assets/{upload-Bg3wIqZl.js → upload-zGWNLWJm.js} +1 -1
  72. package/web-app/dist/assets/{usePolling-gcJn98tf.js → usePolling-CYApkja5.js} +1 -1
  73. package/web-app/dist/assets/{user-hDdryAY2.js → user-D7GR1bID.js} +1 -1
  74. package/web-app/dist/index.html +1 -1
@@ -22,8 +22,8 @@
22
22
  "typescript": "^5.6.0"
23
23
  },
24
24
  "dependencies": {
25
- "@anthropic-ai/claude-agent-sdk": "0.3.293",
26
- "@anthropic-ai/sdk": "^0.124.0",
25
+ "@anthropic-ai/claude-agent-sdk": "0.3.309",
26
+ "@anthropic-ai/sdk": "1.2.0",
27
27
  "@resvg/resvg-wasm": "^2.6.2"
28
28
  },
29
29
  "optionalDependencies": {
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '11.72.4'
78
+ __version__ = '11.73.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "11.72.4",
4
+ "version": "11.73.0",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",
@@ -134,7 +134,7 @@
134
134
  "linux"
135
135
  ],
136
136
  "optionalDependencies": {
137
- "@anthropic-ai/claude-agent-sdk": "0.3.293",
137
+ "@anthropic-ai/claude-agent-sdk": "0.3.309",
138
138
  "@opentelemetry/api": "^1.9.1",
139
139
  "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
140
140
  "@opentelemetry/sdk-trace-base": "^2.10.0",
@@ -160,5 +160,5 @@
160
160
  "dependencies": {
161
161
  "@resvg/resvg-wasm": "^2.6.2"
162
162
  },
163
- "gitHead": "4fc5cbe65193140f9f97040645c0acd724428a4c"
163
+ "gitHead": "2594eed73bca88cf08462bd6f7cc769da29e04bc"
164
164
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "11.72.4",
5
+ "version": "11.73.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -0,0 +1,275 @@
1
+ # CEO cockpit: specification
2
+
3
+ Status: spec only (owner: platform-lead to build; chief-of-staff to accept).
4
+ Source of the ask: docs/v10/RETRO-2026.md section 6 item 6 (CEO console) and
5
+ item 7 (ROI ledger).
6
+
7
+ New-file justification (CLAUDE.md "extend, never duplicate"): no existing file
8
+ owns company operations for the founder. `dashboard/server.py` is the
9
+ user-facing per-run product dashboard and must stay free of company state;
10
+ `scripts/v10-pulse.sh`, `tools/cost-history.py` and `scripts/usage-governor.py`
11
+ already own pulse, run cost and usage, so the cockpit only invokes them and
12
+ renders their output. It re-implements none of their logic.
13
+
14
+ ## 1. Goals and non-goals
15
+ Goals:
16
+ - One page where the CEO sees, within 5 seconds of opening, what the company
17
+ is doing, what shipped, what is blocked and what it costs against value.
18
+ - One write path: a founder command box that appends a row to
19
+ docs/v10/FOUNDER-QUEUE.md for the chief-of-staff to act on.
20
+ Non-goals:
21
+ - No direct control. Stop-all, dispatch and release actions are queue rows
22
+ ("STOP ALL", "pause release") that the chief-of-staff executes by recorded
23
+ PID or through release-reliability-lead. The cockpit never signals a
24
+ process, edits BOARD.md, runs git writes or calls npm publish.
25
+ - No hosted mode, no auth provider, no telemetry.
26
+
27
+ ## 2. Hard constraints
28
+ 1. Server: Python 3 standard library only (`http.server`,
29
+ `ThreadingHTTPServer`, `json`, `subprocess`, `fcntl`, `secrets`). No pip
30
+ dependency.
31
+ 2. Client: one static file `tools/cockpit/index.html` with inline CSS and JS.
32
+ No build step, no CDN, no framework. Works offline.
33
+ 3. Bind 127.0.0.1 only. The `--host` flag accepts only `127.0.0.1` or `::1`;
34
+ any other value exits 64 with a message. Never `0.0.0.0`.
35
+ 4. Read-only except `POST /api/queue`, which only appends to FOUNDER-QUEUE.md.
36
+ 5. Testable headless: `--once --json` prints the full snapshot and exits; no
37
+ browser is ever opened (honour LOKI_NO_BROWSER; no `webbrowser` import).
38
+ 6. Every shell-out has a timeout (default 20 s; pulse 60 s) and a cache, so a
39
+ hung `gh` or `npm` never blocks the page. A failed source renders
40
+ UNKNOWN with the error line, never a blank or a 0.
41
+ 7. No emojis, no em or en dashes in code, UI text or output.
42
+
43
+ ## 3. Files
44
+ - `tools/cockpit/server.py`: server, collectors, snapshot builder, CLI.
45
+ - `tools/cockpit/index.html`: the page.
46
+ - `tools/cockpit/roi-basis.json`: the salary basis and ROI assumptions (section
47
+ 5.6). Tracked, edited by hand or by the chief-of-staff, versioned in git.
48
+ - `tests/test-cockpit.sh`: headless test, registered in
49
+ `tests/run-all-tests.sh` and the shard map (retro 3.3.21).
50
+ - Runtime only (untracked, under `.loki/`): `.loki/company/status/<slug>.json`
51
+ (written by each seat), `.loki/cockpit/cache/*.json`, `.loki/cockpit/token`.
52
+
53
+ ## 4. CLI
54
+ ```
55
+ python3 tools/cockpit/server.py [--port 8787] [--host 127.0.0.1]
56
+ [--repo-root PATH] [--once] [--json]
57
+ [--panel NAME]
58
+ ```
59
+ - `--once --json`: build one snapshot, print it, exit 0 (exit 2 if any panel
60
+ is UNKNOWN, mirroring the pulse contract 0 clean, 1 violation, 2 unknown;
61
+ exit 1 if the pulse reports a VIOLATION).
62
+ - `--panel NAME`: restrict `--once` to one panel (pulse, releases, board,
63
+ agents, okrs, roi, queue).
64
+ - Env overrides (for tests and fixtures): `COCKPIT_NPM_CMD`, `COCKPIT_GH_CMD`,
65
+ `COCKPIT_PULSE_CMD`, `BOARD_MD`, `FOUNDER_QUEUE_MD`, `RETRO_MD`,
66
+ `USAGE_READINGS_TSV`, `COST_HISTORY_FILE`, `COCKPIT_STATUS_DIR`,
67
+ `COCKPIT_NOW` (ISO8601 UTC, for deterministic ages).
68
+ - On start the server prints `cockpit: http://127.0.0.1:<port>/` and its PID,
69
+ and writes the PID to `.loki/cockpit/server.pid` so it is stopped by
70
+ recorded PID only.
71
+
72
+ ## 5. Panels and data sources
73
+ Each panel in the snapshot has the shape
74
+ `{status: OK|VIOLATION|UNKNOWN|UNMEASURED, as_of, source_cmd, data, error}`.
75
+ `source_cmd` is the exact command or file read, shown on the page under each
76
+ panel (evidence rule, retro 3.1.1).
77
+
78
+ ### 5.1 Pulse
79
+ - Source: `bash scripts/v10-pulse.sh` (override `COCKPIT_PULSE_CMD`), run with
80
+ `PULSE_REPO_ROOT` set to the repo root. Cached 90 s; refreshed in a
81
+ background thread so a request never waits on it.
82
+ - Parse: every `Key: value` line into `metrics`; every line under
83
+ `=== VIOLATIONS ===` starting `VIOLATION:` into `violations`; the
84
+ `UNKNOWN metrics:` and `STALE metrics` lines into lists. Exit code 0, 1 or 2
85
+ maps to OK, VIOLATION, UNKNOWN.
86
+ - Render: violations first in a red band, top one largest; then the metric
87
+ table; STALE values shown with their age.
88
+
89
+ ### 5.2 Releases
90
+ - npm: `npm view loki-mode time dist-tags --json` (override
91
+ `COCKPIT_NPM_CMD`). Show `dist-tags.latest`, `dist-tags.next`, the last 20
92
+ versions with publish time, releases in the last hour and 24 h, and minutes
93
+ since the last publish.
94
+ - Release runs: `gh run list --workflow release.yml --limit 20 --json
95
+ databaseId,status,conclusion,headSha,createdAt,displayTitle,url` (override
96
+ `COCKPIT_GH_CMD`). Show each run with conclusion and link.
97
+ - Local: `VERSION` file and `git -C <root> describe --tags --abbrev=0`.
98
+ - Derived flags: VERSION vs next vs latest divergence (retro 3.2.27); a
99
+ release run with conclusion failure on the newest VERSION SHA is a
100
+ VIOLATION. A cached packument is not proof: the panel labels npm data
101
+ "registry view" and never claims a tarball is fetchable (retro 3.2.7).
102
+
103
+ ### 5.3 BOARD flow
104
+ - Source: `docs/v10/BOARD.md` (override `BOARD_MD`). Parse table rows with
105
+ columns `ID | Owner | Branch @ SHA | File set | Tier | Status | Notes`.
106
+ Status cell is `<token>@YYYY-MM-DDTHH:MMZ`; tokens: ready, building, review,
107
+ review-blocked, approved, merged, released, blocked, rejected, parked.
108
+ - Render: count per token (flow strip ready, building, review, approved,
109
+ merged, released), the active rows (building, review, review-blocked,
110
+ approved, blocked) with age in minutes against their tier budget (LOW 15,
111
+ MEDIUM 30, HIGH 60 per OPERATING-MODEL.md), and an "unparsed rows" list.
112
+ Ready count below 30 is flagged (D91).
113
+
114
+ ### 5.4 Agents
115
+ - Source: `.loki/company/status/<slug>.json` for the 10 slugs in
116
+ `.claude/agents/` (list derived from that directory, never hard-coded).
117
+ Schema: `{seat, model, doing, slice, since, next, blocked_on, updated}`.
118
+ - Render one card per seat: model, what it is doing, slice ID linking to the
119
+ BOARD row, minutes since `updated`. A seat with no file is UNKNOWN; a file
120
+ older than 60 min is STALE; `blocked_on` non-empty is highlighted.
121
+ - Also show the seat's owned root causes, parsed from the "Root causes
122
+ owned" section of its agent file.
123
+
124
+ ### 5.5 OKRs
125
+ - Source: `docs/v10/RETRO-2026.md` section 6 (override `RETRO_MD`): the 20
126
+ numbered items, each parsed into title (first sentence), owner (text after
127
+ `Owner:` up to the next period) and metric (text after `Metric:`).
128
+ - Current values: `.loki/company/okrs.json`, written by the owning seats,
129
+ shape `{"<item number>": {value, target, status, source_cmd, as_of}}`.
130
+ - Render the 20 rows ranked as in the retro. An item with no current value
131
+ shows UNMEASURED, never 0 and never green. A value without `source_cmd` is
132
+ shown as UNCITED and counts against verification-scientist.
133
+
134
+ ### 5.6 ROI
135
+ Two sides, always labelled with their measurement state.
136
+
137
+ Agent cost (left side):
138
+ - Plan cost: `roi-basis.json` field `plan_usd_per_month` (default 200, the
139
+ founder's stated spend, retro RC9 evidence), prorated per day.
140
+ - Usage share: `docs/v10/usage-readings.tsv` (columns `utc_time
141
+ window_percent weekly_percent`) and `python3 scripts/usage-governor.py
142
+ --json`. These are plan-usage percentages, not tokens or dollars; the panel
143
+ says so and shows weekly percent used.
144
+ - Metered run cost: `python3 tools/cost-history.py report --json` (file
145
+ override `COST_HISTORY_FILE`). Shows measured runs, unmeasured runs (usd
146
+ null) as a separate count, and total measured USD. A missing history file
147
+ is UNMEASURED.
148
+ - Budget line: the $50 per day budget from FOUNDER-QUEUE row 3, as a
149
+ configurable `daily_budget_usd`.
150
+ - Cost is labelled ESTIMATE when derived from plan proration, MEASURED only
151
+ when it comes from cost-history measured rows.
152
+
153
+ Human-equivalent value (right side):
154
+ - Salary basis in `roi-basis.json`, versioned, every figure with source URL
155
+ and snapshot date:
156
+ ```json
157
+ {
158
+ "source_url": "https://www.levels.fyi/companies/anthropic/salaries/software-engineer",
159
+ "snapshot_date": "2026-09-24",
160
+ "annual_total_comp_usd": {
161
+ "entry": 483000, "senior": 621000, "lead": 865000,
162
+ "staff": 1410000, "median": 883000
163
+ },
164
+ "research_scientist_median_usd": {"value": null, "status": "UNVERIFIED"},
165
+ "research_engineer_usd": {"value": null, "status": "NOT FOUND"},
166
+ "overhead_multiplier": {"value": 1.25, "status": "ASSUMPTION"},
167
+ "work_hours_per_year": {"value": 2000, "status": "ASSUMPTION"},
168
+ "seat_tier": {"chief-of-staff": "lead", "cto": "staff", "...": "..."},
169
+ "hours_per_outcome": {"verified_slice": {"value": null, "status": "ASSUMPTION, unset"}},
170
+ "plan_usd_per_month": 200,
171
+ "daily_budget_usd": 50
172
+ }
173
+ ```
174
+ The figures match RETRO-2026.md 3.8.10 and were re-fetched from the URL on
175
+ 2026-10-23. The basis is flagged STALE when `snapshot_date` is older than
176
+ 90 days.
177
+ - Value formula, shown on the page: hourly rate = annual total comp x
178
+ overhead multiplier / work hours per year; human-equivalent value = sum over
179
+ released slices (BOARD released tokens in the window) of hours per outcome
180
+ x hourly rate for the owning seat's tier.
181
+ - Value is UNMEASURED while `hours_per_outcome` is unset. The page then shows
182
+ only "standing 10-seat payroll equivalent" (sum of tier comp x overhead for
183
+ 10 seats, labelled ASSUMPTION) next to agent cost, and never prints an ROI
184
+ multiple.
185
+ - ROI multiple is shown only when both sides are non-null, with the label of
186
+ the weaker side (ESTIMATE or ASSUMPTION). Per verified outcome cost comes
187
+ from the outcome scoreboard (`.loki/company/scoreboard/*.json`) when it
188
+ exists, else UNMEASURED.
189
+ - Every assumption is editable only in `roi-basis.json`; the page shows its
190
+ path and git SHA, never an inline editor.
191
+
192
+ ### 5.7 Founder command box
193
+ - UI: one textarea (max 2000 characters), one "Why only you" optional field,
194
+ one submit button. After submit, the page shows the appended row number
195
+ and the line written.
196
+ - `POST /api/queue`, body JSON `{item, why}`; returns `{row, line}`.
197
+ - Append rules:
198
+ 1. Take an exclusive `fcntl.flock` on `FOUNDER-QUEUE.md.lock` (sidecar,
199
+ under `.loki/cockpit/`), with a 5 s timeout returning 503.
200
+ 2. Read the file; allocate the next number as max existing `#` + 1.
201
+ 3. Sanitize: strip control characters, replace newlines with spaces and
202
+ `|` with `/`, trim, reject empty (400) or over the cap (413).
203
+ 4. Write one row in the existing header shape
204
+ `| # | Date | Item | Why only you | Loop does meanwhile |` with Date as
205
+ today UTC `YYYY-MM-DD`, Item prefixed `[cockpit]`, Why from the field or
206
+ `founder command`, Loop cell `chief-of-staff to triage`, plus trailing
207
+ status cell `open`.
208
+ 5. Append only (`open(path, "a")`), then fsync, then re-read and verify the
209
+ file did not shrink and the new row is the last line (shrink guard,
210
+ retro 3.3.17). On failure return 500 and leave the file unchanged.
211
+ - The cockpit never commits the change; the chief-of-staff triages and
212
+ commits.
213
+
214
+ ## 6. HTTP API
215
+ | Method | Path | Returns |
216
+ |---|---|---|
217
+ | GET | `/` | index.html |
218
+ | GET | `/api/snapshot` | all panels |
219
+ | GET | `/api/panel/<name>` | one panel |
220
+ | GET | `/healthz` | `{"ok": true, "pid": N}` |
221
+ | POST | `/api/queue` | append result |
222
+ Every other method or path returns 405 or 404.
223
+
224
+ Security:
225
+ - Reject any request whose `Host` header is not `127.0.0.1:<port>`,
226
+ `localhost:<port>` or `[::1]:<port>` (DNS rebinding).
227
+ - `POST /api/queue` requires header `X-Cockpit-Token` equal to a per-process
228
+ random token (`secrets.token_urlsafe(32)`), written to
229
+ `.loki/cockpit/token` with mode 600 and injected into the served page; and
230
+ an `Origin` equal to the served origin. Missing or wrong: 403.
231
+ - Request body capped at 8 KB. Responses set `Cache-Control: no-store`,
232
+ `X-Content-Type-Options: nosniff` and a CSP of `default-src 'self';
233
+ script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'`.
234
+ - Subprocesses run with an argument list (never `shell=True`) and an env built
235
+ deny-by-default (PATH, HOME, USER, LOGNAME, LANG, gh and npm config paths
236
+ only), per security-lead rule 3.9.2.
237
+ - No file path from a request is ever opened; all paths come from flags or
238
+ env.
239
+
240
+ ## 7. Page behaviour
241
+ - Polls `/api/snapshot` every 15 s; panels update in place without layout
242
+ shift (fixed card heights, skeleton rows); a "last refreshed" time and the
243
+ age of each source are always visible.
244
+ - Order: pulse violations band, agents, BOARD flow, releases, OKRs, ROI,
245
+ command box.
246
+ - Light and dark from `prefers-color-scheme`; readable at 1280 px and 390 px.
247
+ - Every number has a hover or caption with its `source_cmd` and `as_of`.
248
+
249
+ ## 8. Headless test (`tests/test-cockpit.sh`)
250
+ Uses `loki_run_tmp_create` and `loki_run_tmp_cleanup`, stops the server by its
251
+ recorded PID only, sets `LOKI_NO_BROWSER=1`. Fixtures under the run tmp dir:
252
+ - A BOARD.md with one row per token plus one unparsed row; assert counts.
253
+ - A FOUNDER-QUEUE.md copy; POST an item containing `|`, a newline and 2001
254
+ characters; assert 413, then a valid item, assert row number = max + 1, the
255
+ pipe replaced, the file only grew, and two concurrent POSTs get distinct
256
+ numbers.
257
+ - Stub `COCKPIT_NPM_CMD` and `COCKPIT_GH_CMD` scripts emitting fixed JSON,
258
+ and one that sleeps past the timeout; assert UNKNOWN, not a hang.
259
+ - Missing cost-history and empty status dir: assert ROI cost UNMEASURED and
260
+ agents UNKNOWN, and that no panel prints `0` for an unmeasured value.
261
+ - `--host 0.0.0.0` exits 64. POST without token: 403. Bad Host: 403.
262
+ - `--once --json` output parses and its exit code matches the pulse stub's
263
+ exit code mapping.
264
+ - Positive control for every absence assertion (retro 3.1.6): the same probe
265
+ run on a fixture that does contain the value must find it.
266
+
267
+ ## 9. Acceptance (Wall checks)
268
+ 1. `python3 tools/cockpit/server.py --once --json | python3 -m json.tool`
269
+ exits 0 or 2 on a clean checkout, with all 7 panels present.
270
+ 2. `bash tests/test-cockpit.sh` passes, and fails when the append sanitizer
271
+ or the Host check is removed (mutation).
272
+ 3. `lsof -nP -iTCP:<port> -sTCP:LISTEN` shows 127.0.0.1 only.
273
+ 4. No file under the repo changes after an hour of page polling except
274
+ FOUNDER-QUEUE.md on submit (`git status --porcelain` diff).
275
+ 5. Screenshot at 1280 px and 390 px reviewed by developer-experience-lead.
@@ -0,0 +1,144 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Loki Cockpit</title>
7
+ <style>
8
+ :root{--bg:#f4f5f8;--card:#fff;--ink:#14161c;--mute:#5b6270;--line:#d9dde6;--ok:#17653a;--bad:#a4161a;--warn:#8a5a00;--unk:#4a4f6a;--badbg:#fde8e8;--accent:#2b4ed8}
9
+ @media (prefers-color-scheme:dark){:root{--bg:#0f1115;--card:#171a21;--ink:#e8eaf0;--mute:#9aa3b5;--line:#2a2f3a;--ok:#5fd08f;--bad:#ff8083;--warn:#f0b955;--unk:#9fa6c8;--badbg:#3a1618;--accent:#8aa2ff}}
10
+ *{box-sizing:border-box}
11
+ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,Segoe UI,sans-serif}
12
+ header{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:baseline;padding:12px 16px;border-bottom:1px solid var(--line)}
13
+ header h1{font-size:16px;margin:0}
14
+ #refreshed{color:var(--mute);font-size:12px}
15
+ main{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr));gap:12px;padding:12px 16px}
16
+ section{background:var(--card);border:1px solid var(--line);border-radius:8px;padding:12px;min-height:160px;overflow:hidden}
17
+ section.wide{grid-column:1/-1}
18
+ h2{font-size:13px;margin:0 0 8px;text-transform:uppercase;letter-spacing:.04em;display:flex;gap:8px;align-items:center}
19
+ .badge{font-size:11px;padding:1px 6px;border-radius:10px;border:1px solid currentColor}
20
+ .OK{color:var(--ok)}.VIOLATION{color:var(--bad)}.UNKNOWN,.UNMEASURED,.UNCITED{color:var(--unk)}.STALE{color:var(--warn)}
21
+ .src{margin-top:8px;color:var(--mute);font:11px ui-monospace,Menlo,monospace;word-break:break-all}
22
+ .err{color:var(--warn);font-size:12px;margin:4px 0}
23
+ .band{background:var(--badbg);color:var(--bad);border:1px solid var(--bad);border-radius:6px;padding:8px;margin-bottom:8px}
24
+ .band .top{font-size:16px;font-weight:600}
25
+ table{width:100%;border-collapse:collapse;font-size:12px}
26
+ td,th{text-align:left;padding:3px 6px 3px 0;border-bottom:1px solid var(--line);vertical-align:top;word-break:break-word}
27
+ .flow{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
28
+ .flow div{border:1px solid var(--line);border-radius:6px;padding:4px 8px;min-width:78px}
29
+ .flow b{display:block;font-size:18px}
30
+ .cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}
31
+ .card{border:1px solid var(--line);border-radius:6px;padding:8px;font-size:12px}
32
+ .card.blocked{border-color:var(--bad)}
33
+ .mute{color:var(--mute)}
34
+ textarea,input[type=text]{width:100%;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:6px;padding:8px;font:inherit}
35
+ button{background:var(--accent);color:#fff;border:0;border-radius:6px;padding:8px 14px;font:inherit;cursor:pointer}
36
+ button:disabled{opacity:.5}
37
+ #qres{margin-top:8px;font-size:12px;min-height:18px;word-break:break-word}
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <header><h1>Loki Cockpit</h1><span id="refreshed">loading</span><span class="mute" id="fail"></span></header>
42
+ <main>
43
+ <section class="wide" id="p-pulse"><h2>Pulse</h2><div class="body">loading</div></section>
44
+ <section class="wide" id="p-agents"><h2>Agents</h2><div class="body">loading</div></section>
45
+ <section id="p-board"><h2>BOARD flow</h2><div class="body">loading</div></section>
46
+ <section id="p-releases"><h2>Releases</h2><div class="body">loading</div></section>
47
+ <section class="wide" id="p-okrs"><h2>OKRs</h2><div class="body">loading</div></section>
48
+ <section class="wide" id="p-roi"><h2>ROI</h2><div class="body">loading</div></section>
49
+ <section class="wide" id="p-queue"><h2>Founder command box</h2><div class="body">loading</div>
50
+ <div>
51
+ <textarea id="item" rows="3" maxlength="2000" placeholder="Command or decision for the chief of staff (max 2000 characters)"></textarea>
52
+ <input type="text" id="why" maxlength="500" placeholder="Why only you (optional)">
53
+ <p><button id="send">Append to FOUNDER-QUEUE</button> <span class="mute" id="count">0/2000</span></p>
54
+ <div id="qres"></div>
55
+ </div>
56
+ </section>
57
+ </main>
58
+ <script>
59
+ (function(){
60
+ "use strict";
61
+ var TOKEN="__COCKPIT_TOKEN__";
62
+ function h(tag,attrs){var e=document.createElement(tag);if(attrs){for(var k in attrs){if(k==="class")e.className=attrs[k];else if(k==="title")e.title=attrs[k];else e.setAttribute(k,attrs[k]);}}
63
+ for(var i=2;i<arguments.length;i++){var c=arguments[i];if(c===null||c===undefined)continue;e.appendChild(typeof c==="object"?c:document.createTextNode(String(c)));}return e;}
64
+ function show(v){return v===null||v===undefined?"UNKNOWN":String(v);}
65
+ function badge(s){return h("span",{"class":"badge "+s},s);}
66
+ function src(p){return h("div",{"class":"src",title:"as of "+p.as_of},"source: "+p.source_cmd+" as of "+p.as_of+(p.cache_age_s!==undefined?" cached "+p.cache_age_s+"s":""));}
67
+ function err(p){return p.error?h("div",{"class":"err"},p.error):null;}
68
+ function table(rows,head){var t=h("table");if(head){var tr=h("tr");head.forEach(function(x){tr.appendChild(h("th",null,x));});t.appendChild(tr);}
69
+ rows.forEach(function(r){var tr=h("tr");r.forEach(function(c){tr.appendChild(h("td",null,c));});t.appendChild(tr);});return t;}
70
+ function fill(id,p,build){var s=document.getElementById("p-"+id);var b=s.querySelector(".body");var h2=s.querySelector("h2");
71
+ while(h2.childNodes.length>1)h2.removeChild(h2.lastChild);h2.appendChild(badge(p.status));
72
+ var nb=h("div",{"class":"body"});try{build(nb,p.data||{},p);}catch(e){nb.appendChild(h("div",{"class":"err"},"render error: "+e));}
73
+ nb.appendChild(err(p));nb.appendChild(src(p));s.replaceChild(nb,b);}
74
+
75
+ var R={};
76
+ R.pulse=function(b,d){
77
+ var v=d.violations||[];
78
+ if(v.length){var band=h("div",{"class":"band"});band.appendChild(h("div",{"class":"top"},v[0]));
79
+ v.slice(1).forEach(function(x){band.appendChild(h("div",null,x));});b.appendChild(band);}
80
+ else if(d.exit_code===0)b.appendChild(h("div",{"class":"OK"},"No violations."));
81
+ var m=d.metrics||{};b.appendChild(table(Object.keys(m).map(function(k){return [k,m[k]];})));
82
+ if((d.unknown_metrics||[]).length)b.appendChild(h("p",{"class":"mute"},"UNKNOWN metrics: "+d.unknown_metrics.join(", ")));
83
+ (d.stale||[]).forEach(function(x){b.appendChild(h("p",{"class":"STALE"},x));});};
84
+ R.agents=function(b,d){var c=h("div",{"class":"cards"});
85
+ (d.seats||[]).forEach(function(s){var card=h("div",{"class":"card"+(s.blocked?" blocked":"")});
86
+ card.appendChild(h("div",null,h("b",null,s.slug)," ",badge(s.state)));
87
+ if(s.state==="UNKNOWN"){card.appendChild(h("div",{"class":"mute"},s.error||"no status file"));}
88
+ else{card.appendChild(h("div",null,"model: "+show(s.model)));card.appendChild(h("div",null,"doing: "+show(s.doing)));
89
+ card.appendChild(h("div",null,"slice: "+show(s.slice)));card.appendChild(h("div",null,"updated: "+show(s.updated_min_ago)+" min ago"));
90
+ if(s.blocked_on)card.appendChild(h("div",{"class":"VIOLATION"},"blocked on: "+s.blocked_on));}
91
+ (s.root_causes||[]).forEach(function(r){card.appendChild(h("div",{"class":"mute"},r));});c.appendChild(card);});
92
+ b.appendChild(c);};
93
+ R.board=function(b,d){var f=h("div",{"class":"flow"});var cs=d.counts||{};
94
+ ["ready","building","review","approved","merged","released"].forEach(function(t){f.appendChild(h("div",null,h("b",null,show(cs[t])),t));});b.appendChild(f);
95
+ if(d.ready_below_30)b.appendChild(h("div",{"class":"err"},"ready queue below "+d.ready_target+" (D91)"));
96
+ b.appendChild(table((d.active||[]).map(function(r){return [r.id,r.owner,r.tier,r.token,show(r.age_min)+" / "+show(r.budget_min)+" min"+(r.over_budget?" OVER":"")];}),["ID","Owner","Tier","Status","Age / budget"]));
97
+ if((d.unparsed||[]).length)b.appendChild(h("p",{"class":"mute"},"Unparsed rows: "+d.unparsed.join(", ")));};
98
+ R.releases=function(b,d){
99
+ b.appendChild(table([["npm latest",show(d.latest)],["npm next",show(d.next)],["VERSION",show(d.version_file)],["last tag",show(d.last_tag)],
100
+ ["minutes since last publish",show(d.last_publish_minutes_ago)],["releases last hour",show(d.releases_last_hour)],["releases last 24h",show(d.releases_last_24h)]]));
101
+ b.appendChild(h("p",{"class":"mute"},d.npm_label));
102
+ (d.flags||[]).forEach(function(x){b.appendChild(h("div",{"class":"VIOLATION"},x));});
103
+ b.appendChild(table((d.runs||[]).slice(0,8).map(function(r){var a=h("a",{href:r.url||"#",rel:"noreferrer",target:"_blank"},String(r.databaseId));return [a,show(r.conclusion||r.status),(r.headSha||"").slice(0,8)];}),["Run","Result","SHA"]));
104
+ b.appendChild(table((d.versions||[]).slice(0,10).map(function(v){return [v.version,v.published];}),["Version","Published"]));};
105
+ R.okrs=function(b,d){b.appendChild(table((d.items||[]).map(function(i){
106
+ var cur=i.current?(show(i.current.value)+(i.current.target!==undefined?" / "+i.current.target:"")):"";
107
+ return [i.n,i.title,show(i.owner),h("span",{"class":i.state},i.state),cur];}),["#","Item","Owner","State","Value / target"]));};
108
+ R.roi=function(b,d){var c=d.cost||{},v=d.value||{},r=d.roi||{},m=c.metered||{},u=c.usage||{};
109
+ b.appendChild(h("h3",null,"Agent cost ("+c.label+")"));
110
+ b.appendChild(table([["plan USD per month",show(c.plan_usd_per_month)],["plan USD per day (ESTIMATE)",show(c.plan_usd_per_day)],["daily budget USD",show(c.daily_budget_usd)],
111
+ ["weekly plan-usage percent",show(u.weekly_percent)],["5h window percent",show(u.window_percent)],
112
+ ["metered run cost",m.state==="MEASURED"?("USD "+m.total_usd+" over "+m.measured_runs+" runs ("+m.unmeasured_runs+" unmeasured)"):("UNMEASURED: "+show(m.reason))]]));
113
+ b.appendChild(h("p",{"class":"mute"},c.label_note+". Usage numbers are plan percentages, not tokens or dollars."));
114
+ b.appendChild(h("h3",null,"Human-equivalent value ("+show(v.state||"standing payroll only")+")"));
115
+ var pr=v.standing_payroll_equivalent;
116
+ b.appendChild(table([["standing payroll equivalent per day",pr?("USD "+pr.per_day_usd+" for "+pr.seats+" seats, "+pr.label):"UNKNOWN"],
117
+ ["human-equivalent value",v.human_equivalent_usd===null||v.human_equivalent_usd===undefined?"UNMEASURED":"USD "+v.human_equivalent_usd],["ROI multiple",r.multiple===null?"not printed ("+r.note+")":r.multiple+"x ("+r.weaker_side_label+")"],
118
+ ["cost per verified outcome",(d.scoreboard||{}).cost_per_verified_outcome_usd===null?"UNMEASURED":"USD "+d.scoreboard.cost_per_verified_outcome_usd]]));
119
+ b.appendChild(h("p",{"class":"mute"},"Formula: "+v.formula));
120
+ var bs=d.basis||{};b.appendChild(h("p",{"class":"mute"},"Basis: "+bs.path+" at "+show(bs.git_sha)+", snapshot "+show(bs.snapshot_date)+(bs.stale?" (STALE: "+bs.stale_note+")":"")+", source "+show(bs.source_url)));
121
+ (d.notes||[]).forEach(function(x){b.appendChild(h("div",{"class":"err"},x));});};
122
+ R.queue=function(b,d){b.appendChild(h("p",null,"Rows: "+show(d.rows_total)+", last number "+show(d.max_number)+", from cockpit "+show(d.cockpit_rows)));
123
+ b.appendChild(table((d.last||[]).map(function(r){return [r.n,r.date,r.item];}),["#","Date","Item"]));};
124
+
125
+ function render(snap){var ps=snap.panels||{};Object.keys(R).forEach(function(k){if(ps[k])fill(k,ps[k],R[k]);});
126
+ document.getElementById("refreshed").textContent="last refreshed "+new Date().toLocaleTimeString()+" (generated "+snap.generated_at+")";
127
+ document.getElementById("fail").textContent="";}
128
+ function poll(){fetch("/api/snapshot",{cache:"no-store"}).then(function(r){return r.json();}).then(render)
129
+ .catch(function(e){document.getElementById("fail").textContent="refresh failed: "+e;});}
130
+ poll();setInterval(poll,15000);
131
+
132
+ var item=document.getElementById("item");
133
+ item.addEventListener("input",function(){document.getElementById("count").textContent=item.value.length+"/2000";});
134
+ document.getElementById("send").addEventListener("click",function(){
135
+ var btn=this,res=document.getElementById("qres");btn.disabled=true;res.textContent="sending";
136
+ fetch("/api/queue",{method:"POST",headers:{"Content-Type":"application/json","X-Cockpit-Token":TOKEN},
137
+ body:JSON.stringify({item:item.value,why:document.getElementById("why").value})})
138
+ .then(function(r){return r.json().then(function(j){return {ok:r.ok,j:j};});})
139
+ .then(function(x){res.textContent=x.ok?("Appended row "+x.j.row+": "+x.j.line):("Rejected: "+x.j.error);if(x.ok){item.value="";poll();}})
140
+ .catch(function(e){res.textContent="Failed: "+e;}).then(function(){btn.disabled=false;});});
141
+ })();
142
+ </script>
143
+ </body>
144
+ </html>
@@ -0,0 +1,33 @@
1
+ {
2
+ "source_url": "https://www.levels.fyi/companies/anthropic/salaries/software-engineer",
3
+ "snapshot_date": "2026-09-24",
4
+ "refetched_on": "2026-10-23",
5
+ "annual_total_comp_usd": {
6
+ "entry": 483000,
7
+ "senior": 621000,
8
+ "lead": 865000,
9
+ "staff": 1410000,
10
+ "median": 883000
11
+ },
12
+ "research_scientist_median_usd": {"value": null, "status": "UNVERIFIED"},
13
+ "research_engineer_usd": {"value": null, "status": "NOT FOUND"},
14
+ "overhead_multiplier": {"value": 1.25, "status": "ASSUMPTION"},
15
+ "work_hours_per_year": {"value": 2000, "status": "ASSUMPTION"},
16
+ "default_tier": "senior",
17
+ "seat_tier_status": "ASSUMPTION",
18
+ "seat_tier": {
19
+ "chief-of-staff": "lead",
20
+ "cto": "staff",
21
+ "developer-experience-lead": "lead",
22
+ "platform-lead": "lead",
23
+ "product-growth-lead": "lead",
24
+ "release-reliability-lead": "lead",
25
+ "research-harness-scientist": "lead",
26
+ "research-outcomes-scientist": "lead",
27
+ "security-lead": "lead",
28
+ "verification-scientist": "lead"
29
+ },
30
+ "hours_per_outcome": {"verified_slice": {"value": null, "status": "ASSUMPTION, unset"}},
31
+ "plan_usd_per_month": 200,
32
+ "daily_budget_usd": 50
33
+ }