loki-mode 9.22.4 → 9.22.6

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 (85) hide show
  1. package/README.md +18 -4
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/issue-parser.sh +112 -0
  5. package/autonomy/lib/proof-generator.py +88 -0
  6. package/autonomy/lib/proof-pr.sh +52 -0
  7. package/autonomy/loki +147 -3
  8. package/autonomy/quickstart.sh +342 -5
  9. package/autonomy/run.sh +26 -0
  10. package/dashboard/__init__.py +1 -1
  11. package/docs/EXECUTION-COCKPIT-PLAN.md +289 -0
  12. package/docs/HARNESS-INTELLIGENCE-PLAN.md +194 -0
  13. package/docs/ISSUE-TO-PR-GOLDEN-PATH-PLAN.md +257 -0
  14. package/loki-ts/dist/loki.js +299 -297
  15. package/mcp/__init__.py +1 -1
  16. package/package.json +1 -1
  17. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  18. package/web-app/dist/assets/{AdminPage-DNGNuAge.js → AdminPage-DLhWX6si.js} +1 -1
  19. package/web-app/dist/assets/{Avatar-CqAveJaT.js → Avatar-CqyDcUxl.js} +1 -1
  20. package/web-app/dist/assets/{Badge-BtMejA0Z.js → Badge-BKGSt1n2.js} +1 -1
  21. package/web-app/dist/assets/{Button-CikxmqSs.js → Button-DuuKHXre.js} +1 -1
  22. package/web-app/dist/assets/CockpitPage-DdClYERC.js +47 -0
  23. package/web-app/dist/assets/{ComparePage-BB8DJBWR.js → ComparePage-CUWlqBxv.js} +1 -1
  24. package/web-app/dist/assets/ErrorBoundary-RwfGKyRJ.js +16 -0
  25. package/web-app/dist/assets/{EvidenceReceiptPanel-6oquKEjd.js → EvidenceReceiptPanel-CzIgk9sg.js} +1 -1
  26. package/web-app/dist/assets/{GitHubIssuesPanel-5XLYWzWH.js → GitHubIssuesPanel-CsXVKuyJ.js} +3 -3
  27. package/web-app/dist/assets/{GitHubPRsPanel-DUfmFYeu.js → GitHubPRsPanel-CYRp1eKS.js} +3 -3
  28. package/web-app/dist/assets/{HomePage-DZ4MmOdH.js → HomePage-CVJcI5pQ.js} +3 -3
  29. package/web-app/dist/assets/LoginPage-BBJNV9Qb.js +1 -0
  30. package/web-app/dist/assets/{MagicPage-CFbX5gWg.js → MagicPage-CIi9P8jK.js} +1 -1
  31. package/web-app/dist/assets/{MetricsPage-DbL6xmRQ.js → MetricsPage-Bm34ipiy.js} +2 -2
  32. package/web-app/dist/assets/{NotFoundPage-BUrjXPAR.js → NotFoundPage-DpTbcC2P.js} +1 -1
  33. package/web-app/dist/assets/{ProjectPage-iP6dJihg.js → ProjectPage-CY2SMGx9.js} +65 -90
  34. package/web-app/dist/assets/{ProjectsPage-BibN9bHl.js → ProjectsPage-DSrXUUKJ.js} +2 -2
  35. package/web-app/dist/assets/{SettingsPage-C0WJp1hW.js → SettingsPage-jvDyOqYi.js} +3 -3
  36. package/web-app/dist/assets/{ShowcasePage-DUv0Njp8.js → ShowcasePage-C8tFWKau.js} +1 -1
  37. package/web-app/dist/assets/{SystemSettingsPage-BIaCB0Vn.js → SystemSettingsPage-DH4FTH3K.js} +2 -2
  38. package/web-app/dist/assets/{TeamsPage-BE0szv9r.js → TeamsPage-C1fQYauF.js} +2 -2
  39. package/web-app/dist/assets/{TemplatesPage-BzkA9kat.js → TemplatesPage-CSTFt-4P.js} +1 -1
  40. package/web-app/dist/assets/TerminalOutput-Cu4NsyN6.js +16 -0
  41. package/web-app/dist/assets/{activity-AbfFz38r.js → activity-CtjsDzmh.js} +1 -1
  42. package/web-app/dist/assets/{bell-DF6Z6y5U.js → bell-C5es1xAs.js} +1 -1
  43. package/web-app/dist/assets/{bot-DQ4np_3n.js → bot-DE04U5Lc.js} +1 -1
  44. package/web-app/dist/assets/{check-RWuguCwE.js → check-DSKKA9EU.js} +1 -1
  45. package/web-app/dist/assets/{chevron-left-VpbQPOkC.js → chevron-left-DJ13W5AR.js} +1 -1
  46. package/web-app/dist/assets/{circle-alert-DwT82lJx.js → circle-alert-CS0BfqAd.js} +1 -1
  47. package/web-app/dist/assets/{clock-BBlUa93G.js → clock-D1Y5nruk.js} +1 -1
  48. package/web-app/dist/assets/{cloud-Dgbz3VCA.js → cloud-nU6LBoZL.js} +1 -1
  49. package/web-app/dist/assets/{code-xml-r6qqION5.js → code-xml-BZfvIdFo.js} +1 -1
  50. package/web-app/dist/assets/{copy-DMHj52B8.js → copy-CK_2O1Xv.js} +1 -1
  51. package/web-app/dist/assets/{database-uJqHSHR1.js → database-DJiwznAm.js} +1 -1
  52. package/web-app/dist/assets/{dollar-sign-BZagljek.js → dollar-sign-pO20A3QY.js} +1 -1
  53. package/web-app/dist/assets/{file-code-corner-DMopmhu0.js → file-code-corner-Besy6vek.js} +1 -1
  54. package/web-app/dist/assets/{file-plus-DU_hT_3w.js → file-plus-DeaswxYY.js} +1 -1
  55. package/web-app/dist/assets/{folder-open-BP5TtQAW.js → folder-open-cr1UvQLI.js} +1 -1
  56. package/web-app/dist/assets/{git-commit-horizontal-zFk_IobS.js → git-commit-horizontal-BLxnfbSn.js} +1 -1
  57. package/web-app/dist/assets/{globe-mthCRtfp.js → globe-DrhG1NfX.js} +1 -1
  58. package/web-app/dist/assets/{hammer-BWaLBvSA.js → hammer-CeZtPBfE.js} +1 -1
  59. package/web-app/dist/assets/index-84JUN_aA.css +1 -0
  60. package/web-app/dist/assets/{index-BXA3UT6P.js → index-CLvjO22Z.js} +24 -24
  61. package/web-app/dist/assets/{layers-Q1LXV5fy.js → layers-fldADzyE.js} +1 -1
  62. package/web-app/dist/assets/{lightbulb-vRRNJ3dB.js → lightbulb-CznOfsPb.js} +1 -1
  63. package/web-app/dist/assets/{loader-circle-KgjeHEcV.js → loader-circle---lPB1Fs.js} +1 -1
  64. package/web-app/dist/assets/{lock-BerKqno7.js → lock-D5jVF-tO.js} +1 -1
  65. package/web-app/dist/assets/{mail-XAxTbprX.js → mail-D7HkLnlW.js} +1 -1
  66. package/web-app/dist/assets/minus-C-dT19Ct.js +6 -0
  67. package/web-app/dist/assets/{package-DLPKWAvW.js → package-Cm9B-cKE.js} +1 -1
  68. package/web-app/dist/assets/{plus-NMxxwHMn.js → plus-CQwOffLD.js} +1 -1
  69. package/web-app/dist/assets/{refresh-cw-Dt42kYtM.js → refresh-cw-CvWJZLbN.js} +1 -1
  70. package/web-app/dist/assets/{rotate-ccw-BCR4Tp9u.js → rotate-ccw-C2Q1LYgM.js} +1 -1
  71. package/web-app/dist/assets/{save-sR4MJd42.js → save-BP0MvpoQ.js} +1 -1
  72. package/web-app/dist/assets/{server-SXkchokx.js → server-4i4VZXix.js} +1 -1
  73. package/web-app/dist/assets/{shield-alert-Bvgp6_Dz.js → shield-alert-D2_bgb7j.js} +1 -1
  74. package/web-app/dist/assets/thumbs-up-vACbzeXj.js +26 -0
  75. package/web-app/dist/assets/{trash-2-D0IGudH7.js → trash-2-EYjdESLs.js} +1 -1
  76. package/web-app/dist/assets/trending-down-BWBdcvE3.js +6 -0
  77. package/web-app/dist/assets/{trending-up-DDrJKL5i.js → trending-up-Bix6V_-Y.js} +1 -1
  78. package/web-app/dist/assets/{upload-C54rjN8K.js → upload-B7hh6ERF.js} +1 -1
  79. package/web-app/dist/assets/{usePolling-D7fw4YSM.js → usePolling-b_e-k-IN.js} +1 -1
  80. package/web-app/dist/assets/{user-Cg7fbzdh.js → user-pmjZKWEg.js} +1 -1
  81. package/web-app/dist/index.html +2 -2
  82. package/web-app/dist/assets/LoginPage-VMzI6ROD.js +0 -1
  83. package/web-app/dist/assets/TerminalOutput-DBrdT_Fp.js +0 -31
  84. package/web-app/dist/assets/index-DgpUuqwR.css +0 -1
  85. package/web-app/dist/assets/trending-down-BbFFBq-v.js +0 -11
@@ -0,0 +1,289 @@
1
+ # Execution Cockpit - build plan
2
+
3
+ Lane: LOKI-EXECUTION-COCKPIT-70. Scope: `web-app/` only, plus the narrowest
4
+ `web-app/server.py` read adapter if strictly necessary (see "Backend changes").
5
+
6
+ ## 0. What was read, and one correction
7
+
8
+ The directive says "read AGENTS.md fully". **There is no `AGENTS.md` at the repo
9
+ root** (`find . -maxdepth 3 -name AGENTS.md` returns nothing). Read instead:
10
+
11
+ - `CLAUDE.md` (root, project instructions) - style rules, release workflow.
12
+ - `BRANDING.md` - Loki (agent/OSS) vs Autonomi (commercial). Local web app is
13
+ "Loki Mode dashboard", so cockpit copy uses Loki, never Autonomi.
14
+ - `COMPONENTS.md` - component map. **Stale claim corrected**: it labels
15
+ `web-app/` "deprecated v7.44.0", but web-app is live and shipped -
16
+ `package.json:99-109` publishes `web-app/dist/` + `server.py`,
17
+ `prepublishOnly` builds it, `autonomy/loki:7496` serves it via `loki web`, and
18
+ three feature commits landed in `web-app/src` after v7.44.0 (`9e0cfdb0`,
19
+ `4d8299c9`, `4831933b`). Building here is correct.
20
+ - `docs/COCKPIT-SPEC.md`, `docs/COCKPIT-UX-MANDATE.md` - prior cockpit work.
21
+ Both target the **terminal** surface (`loki cockpit`, in-terminal image
22
+ render). This plan is the **web** cockpit; it inherits their principles
23
+ (honest always, never fake progress, alive-not-static, identity-first) and
24
+ does not touch the terminal path.
25
+ - `web-app/server.py` (8782 lines) - the API the web app actually calls.
26
+ **Not** `dashboard/server.py`, which is a different surface on a different port.
27
+ - `web-app/src/**` - App.tsx routing, `api/client.ts`, `types/api.ts`,
28
+ `components/ui/*`, `tailwind.config.js`, `ProjectWorkspace.tsx`,
29
+ `EvidenceReceiptPanel.tsx`.
30
+
31
+ ## 1. The product
32
+
33
+ Not chat. An **issue-to-PR execution cockpit**: one screen that answers, in
34
+ order, the four questions a reviewer actually has.
35
+
36
+ 1. **What was it asked to do, and how will we know it worked?** - task header
37
+ with the goal and acceptance criteria.
38
+ 2. **Where is it now?** - calm phase timeline plus elapsed / time-to-first-signal.
39
+ 3. **What did it change, and what proves it?** - changed-file review surface and
40
+ evidence attached to the outcome.
41
+ 4. **What do I do about it?** - final actions, each either real or honestly
42
+ disabled, plus risk and rollback.
43
+
44
+ Agent chatter (logs, per-agent traces) is **secondary and collapsed by default**.
45
+
46
+ ## 2. User journey
47
+
48
+ ```
49
+ Projects list -> /project/:sessionId/cockpit
50
+ |
51
+ +-------------------+-------------------+
52
+ | |
53
+ LIVE (this session is the running one) HISTORICAL (any other session)
54
+ | |
55
+ header: goal + acceptance header: goal + acceptance
56
+ timeline: current phase pulses timeline: rendered from final status,
57
+ metrics: elapsed ticking labelled "historical", no ticking clock
58
+ evidence: gates as they land evidence: gates as recorded
59
+ actions: pause / stop (real) actions: open PR / rollback (real)
60
+ chatter: live log stream chatter: stored session log
61
+ ```
62
+
63
+ Entry: a "Cockpit" affordance on the existing project route. Exit: back to the
64
+ workspace or to the projects list. Deep-linkable; survives reload.
65
+
66
+ ## 3. State model - the load-bearing decision
67
+
68
+ Live run state on this backend is **global**, not per-session:
69
+ `/api/session/status`, `/api/session/checklist`, `/api/session/logs`,
70
+ `/api/session/agents` and `/ws` all read one module-level `session` singleton
71
+ (`web-app/server.py:140`). There is exactly one running build per server.
72
+
73
+ Review state is **per-session**: `/api/sessions/{id}/git/status`, `/git/log`,
74
+ `/git/pr`, `/checkpoints`, `/github/prs/*`.
75
+
76
+ The cockpit spans both, so it must decide which mode it is in. The predicate:
77
+
78
+ ```ts
79
+ // LiveBinding: the global run telemetry belongs to THIS session only when the
80
+ // running process is working in this session's directory.
81
+ isLive = status.running && normalize(status.projectDir) === normalize(session.path)
82
+ ```
83
+
84
+ Both sides are absolute paths: `SessionDetail.path` is `str(target)`
85
+ (`server.py:3831`) and `StatusResponse.projectDir` is `session.project_dir`,
86
+ assigned from the resolved project dir at `server.py:2704`. `normalize` strips a
87
+ trailing slash only; no other coercion, because a coincidental match would be
88
+ worse than a miss.
89
+
90
+ **When `isLive` is false the cockpit renders zero live facts** - no phase pulse,
91
+ no ticking elapsed, no "running" badge. It shows the historical view sourced from
92
+ `SessionDetail.status` and the per-session endpoints. This is the single rule
93
+ that keeps the screen from inventing a run.
94
+
95
+ ### View states
96
+
97
+ | State | Predicate | Renders |
98
+ |---|---|---|
99
+ | `loading` | detail fetch in flight | skeletons in the real layout shape |
100
+ | `empty` | detail loaded, no PRD and no changed files | "nothing recorded for this session yet" + what would populate it |
101
+ | `running` | `isLive && !paused` | live timeline, ticking elapsed, live gates |
102
+ | `paused` | `isLive && status.paused` | timeline frozen with an explicit paused marker, Resume enabled |
103
+ | `recovering` | `isLive && status.exit_code == null && phase == 'starting'` | "starting up" - the backend's own synthetic phase, labelled as such, never as progress |
104
+ | `failed` | `!isLive && detail.status == 'failed'`, or `isLive && exit_code not in (null, 0)` | failure header, exit code, last output, rollback surfaced first |
105
+ | `completed` | `!isLive && detail.status == 'completed'` | historical timeline, evidence, final actions |
106
+ | `disconnected` | WS closed while `isLive` | last-known state, dimmed, with a "reconnecting" note and the age of the data |
107
+
108
+ `disconnected` is not decoration: the WS reconnects on a 3s timer
109
+ (`client.ts:786`) and a stale screen that looks live is an invented fact.
110
+
111
+ ## 4. Phase map - the highest-risk table
112
+
113
+ The backend's phase vocabulary is **not** the directive's six display phases.
114
+ Real values, traced to source:
115
+
116
+ | Backend value | Written by | Display phase |
117
+ |---|---|---|
118
+ | `idle` | `server.py:2924` default | Not started |
119
+ | `starting` | `server.py:2996` synthetic (<15s, nothing on disk yet) | Understanding |
120
+ | `BOOTSTRAP` | `run.sh:6270` seed record | Understanding |
121
+ | `REASON` | `run.sh:3485` `get_rarv_phase_name` | Planning |
122
+ | `ACT` | `run.sh:3486` | Editing |
123
+ | `REFLECT` | `run.sh:3487` | Review |
124
+ | `VERIFY` | `run.sh:3488` | Testing |
125
+ | `BUILDING` | `run.sh:26167` `_advance_current_phase` | Editing |
126
+ | `COMPLETED` | `run.sh:26286` | PR-ready |
127
+ | `FAILED` | `run.sh:26289`, `:26294` | Failed |
128
+ | `UNKNOWN` | `run.sh:3489` | Unknown |
129
+ | anything else | - | **renders the raw string verbatim** |
130
+
131
+ Both routes persist the RARV name: bash at `run.sh:22295`, Bun via
132
+ `updateCurrentPhase` (`loki-ts/src/runner/autonomous.ts:670` ->
133
+ `state.ts:632`). The map lives in one exported constant so it is greppable and
134
+ testable. An unmapped phase is shown as itself - never silently bucketed.
135
+
136
+ Two honesty notes the UI must carry:
137
+
138
+ - RARV phases cycle (`iteration % 4`), so the timeline is **not** a monotonic
139
+ progress bar. It renders as a repeating cycle with the current step marked,
140
+ and iteration N of max shown alongside. Presenting REASON->VERIFY as a
141
+ one-way march would misrepresent the engine.
142
+ - `starting` is the server's own placeholder for "process alive, nothing written
143
+ yet". Labelled "starting up", not counted as work done.
144
+
145
+ ## 5. Metrics
146
+
147
+ | Metric | Source | Honesty rule |
148
+ |---|---|---|
149
+ | Elapsed | `status.uptime` (seconds, server-computed) | Ticks locally between 2s pushes; resets to server truth on each push. Only when `isLive`. |
150
+ | Iteration | `status.iteration` / `status.max_iterations` | `max_iterations` falls back to env default 10 (`server.py:2977`); shown as "of ~10" when it came from the fallback is over-claiming, so we show the bare number and the max without an "estimated" claim only when a real value was read. If `max_iterations` came from the env fallback we render iteration alone. |
151
+ | Cost | `status.cost` | Zero renders as "not recorded", never "$0.00 spent". |
152
+ | Time to first result | **client-observed only** | The first transition out of `idle`/`starting` seen *by this mount*. `.loki/app-runner/first-preview.json` exists in the engine but **no web-app endpoint serves it** (`grep -n first-preview web-app/server.py` -> nothing). So TTFR is labelled "first signal seen this session" and is **hidden entirely on a reload that missed the transition**, rather than shown as an authoritative number. |
153
+
154
+ ## 6. Evidence
155
+
156
+ | Tile | Source | Empty behaviour |
157
+ |---|---|---|
158
+ | Quality gates | `GET /api/session/checklist` -> `.loki/state/checklist.json` | The endpoint returns all-zeros with `items: []` when the file is absent (`server.py:3160`). **Zeros are not "all passing"** - renders "no gate results recorded". |
159
+ | Evidence receipt | `GET /api/proofs`, `/proofs/summary`, `/proofs/{run_id}` | Reuses the existing `EvidenceReceiptPanel`; no second receipt view. `unknown` stays its own bucket. |
160
+ | Changed files | `GET /api/sessions/{id}/git/status` | **Dedupe by path.** The endpoint emits two entries for one path when it is both staged and unstaged-modified (`server.py:5405-5411`), so a naive count is wrong. Staged/unstaged is folded into one row per path. |
161
+ | Tests / build / lint | `POST /sessions/{id}/test`, `/review` | These **run** things and return `{output, returncode}` - they are actions, not passive evidence. The tile shows "not run" until the user triggers it, and never implies a result that was not fetched. |
162
+
163
+ ## 7. Actions - capability table
164
+
165
+ Every affordance is checked against a real endpoint. Anything without one
166
+ renders disabled **with a visible reason**, never a dead grey button.
167
+
168
+ | Action | Endpoint | State | Guard |
169
+ |---|---|---|---|
170
+ | Pause | `POST /api/session/pause` | Real | Enabled only when `isLive && !paused` |
171
+ | Resume | `POST /api/session/resume` | Real | Enabled only when `isLive && paused` |
172
+ | Stop | `POST /api/session/stop` | Real | Enabled only when `isLive`; confirm step |
173
+ | Open PR | `POST /api/sessions/{id}/git/pr` (title, body) | Real | Needs title; disabled when no changed files, reason shown |
174
+ | Commit | `POST /api/sessions/{id}/git/commit` | Real | Disabled when working tree clean |
175
+ | Push | `POST /api/sessions/{id}/git/push` | Real | Disabled when nothing ahead |
176
+ | Rollback | `POST /api/sessions/{id}/checkpoints/{cp_id}/restore` | Real, **destructive** | Two-step confirm naming the checkpoint. Disabled with "no checkpoints recorded" when the list is empty. |
177
+ | Request change | `POST /api/sessions/{id}/chat/preview` (dry-run diff) | Real, read-only | Shows what *would* change. Deliberately **not** wired to `chatStart` - that is the chat surface this product is not. |
178
+ | Approve | none | **Disabled / planned** | No endpoint approves a local run. `reviewGitHubPR` approves an *existing GitHub PR*; offered only when a PR number is known, otherwise the button reads "requires an open PR". |
179
+
180
+ ## 8. Responsive behaviour
181
+
182
+ - **Mobile (<768px) - monitoring.** Single column, sticky task header, timeline
183
+ as a vertical stepper, metrics as a two-up row, evidence and diff collapsed
184
+ into disclosures. Actions in a bottom bar (the repo already has
185
+ `MobileBottomNav.tsx` as precedent). Diff bodies are horizontally scrollable,
186
+ never squeezed.
187
+ - **Tablet (768-1279px).** Two columns: timeline + metrics left, evidence and
188
+ diff right.
189
+ - **Desktop (>=1280px) - review.** Three regions: task header full-width;
190
+ timeline + metrics + risk in a left rail; diff review as the main surface;
191
+ evidence docked right. Chatter is a collapsed drawer at the bottom.
192
+
193
+ Existing `react-resizable-panels` is available, but the cockpit uses plain CSS
194
+ grid - fixed regions, no resize handles to fiddle with. (Noted because
195
+ `resizable-panels` v4 reads a bare numeric `size` as pixels, a known trap here.)
196
+
197
+ ## 9. Accessibility
198
+
199
+ - **Landmarks**: `header` / `nav` / `main` / `aside` / `footer`, each labelled.
200
+ - **Keyboard**: every action reachable by Tab in visual order; the diff file list
201
+ is a roving-tabindex listbox (Up/Down to move, Enter to open); `g` then a digit
202
+ jumps to a region; `?` opens a shortcut sheet; Esc closes any disclosure. No
203
+ keyboard trap - the chatter drawer uses the existing `useFocusTrap` only while
204
+ it is modal on mobile.
205
+ - **Live regions**: phase changes and gate results announce via a polite
206
+ `aria-live` region, throttled so a 2s push does not spam a screen reader.
207
+ Elapsed does **not** announce.
208
+ - **Reduced motion**: follow the existing `motion-reduce:animate-none` idiom
209
+ (`ui/Badge.tsx:32`). The phase pulse, progress transitions, and any
210
+ celebration become static under `prefers-reduced-motion`.
211
+ - **Contrast**: body text on `#FFFEFB` uses `muted-accessible` (`#6B6960`), not
212
+ `muted` (`#939084`), which fails AA at small sizes.
213
+ - **Status is never colour-only**: every state carries an icon and a text label.
214
+ - **Dark mode**: `darkMode: 'class'` with a full `dark-*` token set - every
215
+ surface carries a dark variant or half the app looks broken.
216
+
217
+ ## 10. Files
218
+
219
+ New, all under `web-app/src`:
220
+
221
+ | File | Role |
222
+ |---|---|
223
+ | `cockpit/phases.ts` | The phase map (section 4), display metadata, `mapPhase()` with verbatim fallback |
224
+ | `cockpit/useCockpitState.ts` | Fetches session detail + status + checklist + git status + checkpoints; owns `isLive`, view state, WS subscription, TTFR observation |
225
+ | `cockpit/TaskHeader.tsx` | Goal + acceptance criteria + provider/branch identity |
226
+ | `cockpit/PhaseTimeline.tsx` | The calm timeline; cycle-aware, reduced-motion aware |
227
+ | `cockpit/RunMetrics.tsx` | Elapsed, iteration, cost, TTFR - each with its empty rule |
228
+ | `cockpit/ChangeReview.tsx` | Deduped changed-file list + diff pane |
229
+ | `cockpit/EvidencePanel.tsx` | Gates, receipt link, test/build/lint "not run" states |
230
+ | `cockpit/RiskPanel.tsx` | Uncertainty + rollback affordance |
231
+ | `cockpit/FinalActions.tsx` | Section 7 table, rendered |
232
+ | `cockpit/AgentChatter.tsx` | Collapsed-by-default log drawer |
233
+ | `cockpit/ExecutionCockpit.tsx` | Composition + layout + keyboard map |
234
+ | `pages/CockpitPage.tsx` | Route shell, error boundary |
235
+
236
+ Modified:
237
+
238
+ | File | Change |
239
+ |---|---|
240
+ | `src/App.tsx` | One route: `/project/:sessionId/cockpit` |
241
+ | `src/api/client.ts` | Only if a needed call is missing a wrapper (additive) |
242
+ | `src/types/api.ts` | Add `exit_code` / `last_output` to `StatusResponse` - the server already returns them (`server.py:3025-3026`); the type just never declared them |
243
+
244
+ ### Backend changes
245
+
246
+ **None planned.** Everything the cockpit needs is already on `web-app/server.py`.
247
+ If a genuine gap appears, the adapter goes in `web-app/server.py` (the server the
248
+ web app talks to), not `dashboard/server.py`. Any such addition is read-only and
249
+ called out in the final report.
250
+
251
+ ## 11. Verification
252
+
253
+ `web-app` has **no unit-test harness** - only Playwright e2e (`tests/e2e/`,
254
+ baseURL `127.0.0.1:57375`) and pytest for the server. No new dependencies. So:
255
+
256
+ 1. `npm run build` (`tsc -b && vite build`) - typecheck + build.
257
+ 2. `npm run lint` - eslint.
258
+ 3. A deterministic Playwright spec, `tests/e2e/cockpit.spec.ts`, driving every
259
+ view state via `page.route()` interception of the API - no live build needed.
260
+ Fixtures cover: loading, empty, running, paused, recovering, failed,
261
+ completed, disconnected, and the **non-live** case (a session that is not the
262
+ running one must show zero live facts).
263
+ 4. A pure-function check on `mapPhase()` over every row of the section-4 table
264
+ plus an unmapped value - the one place a silent bug would be invisible.
265
+
266
+ Screenshots: captured with the installed Playwright only if a browser binary is
267
+ already present. If `npx playwright install` would be required, screenshots are
268
+ skipped and that is reported, not silently omitted.
269
+
270
+ ## 12. Acceptance criteria
271
+
272
+ - [ ] No live phase, elapsed, or running badge renders when `isLive` is false.
273
+ - [ ] Every phase in the section-4 table maps to its stated display phase; an
274
+ unmapped phase renders verbatim.
275
+ - [ ] An absent checklist renders "no gate results recorded", never a green 0/0.
276
+ - [ ] Changed-file count matches distinct paths when a file is both staged and
277
+ unstaged-modified.
278
+ - [ ] Every disabled action states its reason in visible text.
279
+ - [ ] Rollback requires a confirm naming the checkpoint.
280
+ - [ ] Full keyboard traversal of header, timeline, diff list, and actions.
281
+ - [ ] Every animation is inert under `prefers-reduced-motion`.
282
+ - [ ] Mobile layout is single-column with no horizontal page scroll.
283
+ - [ ] Dark mode renders every cockpit surface.
284
+ - [ ] `npm run build` and `npm run lint` pass; the Playwright cockpit spec passes.
285
+
286
+ ## 13. Out of scope
287
+
288
+ Version bump, release, commit, push. Any external or public mutation. Changes to
289
+ `dashboard/`, the terminal cockpit, or the chat surface.
@@ -0,0 +1,194 @@
1
+ # Harness Intelligence Plan (features 5-8)
2
+
3
+ Lane: LOKI-HARNESS-INTELLIGENCE-69. Status: plan + MVP implemented, unreleased.
4
+
5
+ ## Preconditions, stated honestly
6
+
7
+ **AGENTS.md is not present in this repository.** Verified with
8
+ `find . -name AGENTS.md -not -path "*/node_modules/*"` -> no results. The binding
9
+ instruction set used for this work is `CLAUDE.md` (repo root) plus the global
10
+ `~/.claude/CLAUDE.md`. If an AGENTS.md is expected to exist, this plan needs
11
+ re-review against it.
12
+
13
+ **Feature numbering is taken from the directive, not from an in-repo backlog.**
14
+ No numbered backlog entry for features 5-8 was found; `docs/V8-MAJOR-RELEASE-PLAN.md`
15
+ numbers its harness-intelligence items `3a`-`3d` (all shipped in v8.0.0). The
16
+ mapping used here comes from the approved decomposition in the directive:
17
+
18
+ | # | Feature | Module |
19
+ |---|---------|--------|
20
+ | 5 | Deterministic task-class -> capability-tier router | `loki-ts/src/runner/capability_router.ts` |
21
+ | 6 | Local evidence-backed repository profile | `loki-ts/src/runner/repo_profile.ts` |
22
+ | 7 | Parallel execution manifest | `loki-ts/src/runner/exec_manifest.ts` |
23
+ | 8 | Deterministic recovery policy | `loki-ts/src/runner/recovery_policy.ts` |
24
+
25
+ ## The binding architectural constraint: parity is fixture-locked
26
+
27
+ `loki-ts/tests/parity/build_prompt.test.ts` asserts **60 SHA-256 byte-exact
28
+ fixtures** over `buildPrompt()`. Each fixture supplies its own env object
29
+ (`const env = { ...fx.env }`), so any new env var is *absent* during parity runs.
30
+
31
+ This makes default-off gating an architectural requirement, not a preference:
32
+
33
+ > **Every one of the four modules reads its flag first and returns the
34
+ > identity/passthrough value when the flag is unset.**
35
+
36
+ This is the existing codebase idiom, already used by `tierRouteModel`
37
+ (`loki-ts/src/providers/claude_flags.ts:84`):
38
+
39
+ ```ts
40
+ if (process.env["LOKI_TIER_ROUTING"] !== "1") return model;
41
+ ```
42
+
43
+ Consequence: with all four flags unset, `buildPrompt()` output is byte-identical,
44
+ model resolution is byte-identical, and the bash route is untouched. No bash
45
+ mirror obligation is incurred, because no default-path behavior changes.
46
+
47
+ ## Ownership boundaries (exact)
48
+
49
+ Deliberately narrow, to keep `autonomy/run.sh` collision-free. Each module is a
50
+ new file with a small pure-function interface; none of them edit `run.sh`.
51
+
52
+ ### Feature 5 - capability router owns model identity
53
+
54
+ `capability_router.ts` is **the only module that may emit a provider/model ID.**
55
+
56
+ - Consumes `getRarvTier()` (`rarv.ts:81`) for the session/RARV tier.
57
+ - Reuses `envPinned()` semantics from `claude_flags.ts` for explicit overrides.
58
+ - Precedence, highest first: **explicit model override** (`LOKI_CLAUDE_MODEL_*` /
59
+ `LOKI_MODEL_*`) > **session ceiling** (`LOKI_SESSION_MODEL` pin) > task-class
60
+ routing > passthrough.
61
+ - The session ceiling is a *ceiling*: routing may move down from it, never up.
62
+ - Recovery (feature 8) may request only a **tier**; it never names a model.
63
+
64
+ ### Feature 6 - repository profile owns learned/profile state
65
+
66
+ - Writes `.loki/profile/repo-profile.json` via `lokiDir()` (`util/paths.ts:47`)
67
+ and `atomicWriteFileSync` (`state.ts:208`).
68
+ - Record carries a **content hash** (over the evidence inputs) and a
69
+ **freshness** timestamp. A profile whose hash no longer matches, or that is
70
+ older than the TTL, is stale and is not injected.
71
+ - **Namespace/privacy:** the profile is namespaced per repository, and only
72
+ evidence-backed *facts derived from files in the repo* are recorded. No
73
+ absolute host paths, no environment values, no secrets.
74
+ - **Bounded injection:** the prompt fragment is hard-capped in characters. An
75
+ over-cap profile is truncated, never injected whole.
76
+
77
+ ### Feature 7 - execution manifest owns worktree/result validation
78
+
79
+ - Declares streams with **acceptance criteria**, **non-overlapping path scopes**,
80
+ a **base SHA**, and exactly **one integration owner**.
81
+ - Overlapping path scopes are **serialized**, not run in parallel.
82
+ - A stream result whose base SHA no longer matches HEAD is **rejected as stale**.
83
+ - **Reads** the repo-profile snapshot; **never writes** learned memory. This
84
+ matters because `create_worktree` (`autonomy/run.sh:5205`) does `cp -r .loki`
85
+ into each worktree, so a worktree-local write would fork the profile per
86
+ stream. The profile is read-only from the parent.
87
+ - Recovery does **not** own merges; the manifest's integration owner does.
88
+
89
+ ### Feature 8 - recovery policy owns failure -> action mapping
90
+
91
+ Builds on the seams that already exist:
92
+ - `classifyFailure()` (`retry_class.ts:80`) for provider-wire failure classes.
93
+ - `LAST_ERROR.json` (schema at `autonomy/run.sh:1800`) for the current failure.
94
+ - The append-only bounded archive written by `_loki_archive_last_error`
95
+ (`autonomy/run.sh:1845`, capped at 50 entries) is **the repeated-signature
96
+ circuit-breaker input.** It already exists; recovery reads it.
97
+
98
+ Mapping (deterministic, table-driven):
99
+
100
+ | Classified failure | Action |
101
+ |---|---|
102
+ | auth / unknown model / bad request / quota | `stop` (no retry) |
103
+ | transient (rate limit, network, 5xx) | `retry` under caps |
104
+ | compile / test failure | `revise` |
105
+ | corrupted working tree | `checkpoint_rollback` |
106
+ | provider unavailable | `failover` (tier request only) |
107
+ | caps exhausted / repeated signature | `escalate` then `stop` |
108
+
109
+ Caps: max attempts, max wall-clock seconds, max cost. Any cap exceeded -> escalate.
110
+
111
+ **Hazard, load-bearing.** `retry_class.ts` carries a long comment explaining that
112
+ the text it classifies is *the agent's entire 64KB captured output*, not a provider
113
+ error envelope; patterns matching ordinary application vocabulary previously fired
114
+ on benign output and killed healthy builds. Compile failures are exactly that
115
+ category (an agent that builds a compiler test emits the trigger text). Therefore:
116
+
117
+ > **Compile classification is driven by a build exit code / build-command capture,
118
+ > never by a regex over agent prose.**
119
+
120
+ ## Rollout flags (all default OFF)
121
+
122
+ | Flag | Feature | Unset behavior |
123
+ |---|---|---|
124
+ | `LOKI_CAPABILITY_ROUTER=1` | 5 | passthrough: returned model unchanged |
125
+ | `LOKI_REPO_PROFILE=1` | 6 | no profile read, no prompt injection |
126
+ | `LOKI_EXEC_MANIFEST=1` | 7 | no manifest planned or validated |
127
+ | `LOKI_RECOVERY_POLICY=1` | 8 | existing `shouldStopRetrying` behavior verbatim |
128
+
129
+ Cap overrides (only meaningful when feature 8 is on):
130
+ `LOKI_RECOVERY_MAX_ATTEMPTS` (3), `LOKI_RECOVERY_MAX_SECONDS` (1800),
131
+ `LOKI_RECOVERY_MAX_COST` (unset = uncapped), `LOKI_RECOVERY_SIGNATURE_THRESHOLD` (3).
132
+
133
+ Profile knobs: `LOKI_REPO_PROFILE_TTL_SECONDS` (86400),
134
+ `LOKI_REPO_PROFILE_MAX_CHARS` (2000).
135
+
136
+ ## Acceptance
137
+
138
+ 1. With all flags unset, `bun test tests/parity/` stays 60/60 byte-exact.
139
+ 2. Override precedence: an explicit `LOKI_CLAUDE_MODEL_PLANNING` survives routing.
140
+ 3. Session ceiling is never exceeded upward by task-class routing.
141
+ 4. A profile with a mismatched content hash or expired TTL is not injected.
142
+ 5. Profile injection is bounded to `LOKI_REPO_PROFILE_MAX_CHARS`.
143
+ 6. Overlapping path scopes serialize; a stale base SHA is rejected.
144
+ 7. An auth failure produces `stop` with zero retries.
145
+ 8. A transient failure produces `retry` while under caps.
146
+ 9. A compile failure produces `revise`.
147
+ 10. A repeated failure signature at threshold produces `stop`.
148
+ 11. State artifacts are truthful: no artifact claims an action that did not run.
149
+
150
+ ## Migration and recovery
151
+
152
+ - **Migration: none required.** All four features are additive and default-off.
153
+ An existing `.loki/` directory needs no change; a missing `.loki/profile/`
154
+ is created on first write when feature 6 is enabled.
155
+ - **Forward compatibility:** every persisted record carries a `schema_version`.
156
+ A record with an unrecognized `schema_version` is treated as absent, never as
157
+ an error, so an older binary reading a newer artifact degrades to default-off
158
+ behavior rather than crashing.
159
+ - **Recovery from corrupt state:** an unparseable profile / manifest / recovery
160
+ artifact is treated as absent. Delete `.loki/profile/` or `.loki/manifest/` to
161
+ reset; nothing else depends on them.
162
+ - **Rollback:** unset the flag. No persisted state needs cleanup, because no
163
+ default path reads these artifacts.
164
+
165
+ ## What is WIRED vs what ships as a SEAM
166
+
167
+ Stated explicitly, because an unstated seam reported as a feature is not honest.
168
+
169
+ | # | Module | Status |
170
+ |---|---|---|
171
+ | 8 | recovery policy | **WIRED.** `autonomy`'s retry branch calls `decideRecovery` (`loki-ts/src/runner/autonomous.ts:878`), replacing the direct `shouldStopRetrying` call. Default-off delegates to it verbatim. |
172
+ | 5 | capability router | **SEAM.** Exercised by feature 8's `failover` tier request; no call site yet resolves a dispatch model through it. |
173
+ | 6 | repository profile | **SEAM.** `buildProfile()` never self-triggers, so nothing builds a profile in a live run yet. Trigger belongs at run start, next to the existing memory-context assembly in `build_prompt`. |
174
+ | 7 | execution manifest | **SEAM.** Planning/validation are pure functions; the parallel path (`create_worktree`, `autonomy/run.sh:5205`) does not consult them yet. |
175
+
176
+ The vertical slice is coherent rather than four demos: feature 8 is live on the
177
+ real retry path, and its `failover` action requests a tier that feature 5 owns
178
+ resolving. The remaining call sites are deliberately deferred, not forgotten.
179
+
180
+ ## Integration order
181
+
182
+ 1. Feature 5 (router) - no dependencies.
183
+ 2. Feature 6 (profile) - independent; only feature 7 reads its snapshot.
184
+ 3. Feature 8 (recovery) - depends on 5 for tier-request resolution.
185
+ 4. Feature 7 (manifest) - reads 6's snapshot, and uses 8's classification for
186
+ stream-level failures.
187
+
188
+ ## Non-goals for this MVP
189
+
190
+ - No provider network calls anywhere in these modules.
191
+ - No version bump, no release, no commit, no push.
192
+ - No edits to `autonomy/run.sh` (collision avoidance is the point).
193
+ - No automatic merge behavior; the manifest records an integration owner and
194
+ validates results, and a human or the existing merge path performs merges.