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.
- package/README.md +18 -4
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/issue-parser.sh +112 -0
- package/autonomy/lib/proof-generator.py +88 -0
- package/autonomy/lib/proof-pr.sh +52 -0
- package/autonomy/loki +147 -3
- package/autonomy/quickstart.sh +342 -5
- package/autonomy/run.sh +26 -0
- package/dashboard/__init__.py +1 -1
- package/docs/EXECUTION-COCKPIT-PLAN.md +289 -0
- package/docs/HARNESS-INTELLIGENCE-PLAN.md +194 -0
- package/docs/ISSUE-TO-PR-GOLDEN-PATH-PLAN.md +257 -0
- package/loki-ts/dist/loki.js +299 -297
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-DNGNuAge.js → AdminPage-DLhWX6si.js} +1 -1
- package/web-app/dist/assets/{Avatar-CqAveJaT.js → Avatar-CqyDcUxl.js} +1 -1
- package/web-app/dist/assets/{Badge-BtMejA0Z.js → Badge-BKGSt1n2.js} +1 -1
- package/web-app/dist/assets/{Button-CikxmqSs.js → Button-DuuKHXre.js} +1 -1
- package/web-app/dist/assets/CockpitPage-DdClYERC.js +47 -0
- package/web-app/dist/assets/{ComparePage-BB8DJBWR.js → ComparePage-CUWlqBxv.js} +1 -1
- package/web-app/dist/assets/ErrorBoundary-RwfGKyRJ.js +16 -0
- package/web-app/dist/assets/{EvidenceReceiptPanel-6oquKEjd.js → EvidenceReceiptPanel-CzIgk9sg.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-5XLYWzWH.js → GitHubIssuesPanel-CsXVKuyJ.js} +3 -3
- package/web-app/dist/assets/{GitHubPRsPanel-DUfmFYeu.js → GitHubPRsPanel-CYRp1eKS.js} +3 -3
- package/web-app/dist/assets/{HomePage-DZ4MmOdH.js → HomePage-CVJcI5pQ.js} +3 -3
- package/web-app/dist/assets/LoginPage-BBJNV9Qb.js +1 -0
- package/web-app/dist/assets/{MagicPage-CFbX5gWg.js → MagicPage-CIi9P8jK.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-DbL6xmRQ.js → MetricsPage-Bm34ipiy.js} +2 -2
- package/web-app/dist/assets/{NotFoundPage-BUrjXPAR.js → NotFoundPage-DpTbcC2P.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-iP6dJihg.js → ProjectPage-CY2SMGx9.js} +65 -90
- package/web-app/dist/assets/{ProjectsPage-BibN9bHl.js → ProjectsPage-DSrXUUKJ.js} +2 -2
- package/web-app/dist/assets/{SettingsPage-C0WJp1hW.js → SettingsPage-jvDyOqYi.js} +3 -3
- package/web-app/dist/assets/{ShowcasePage-DUv0Njp8.js → ShowcasePage-C8tFWKau.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BIaCB0Vn.js → SystemSettingsPage-DH4FTH3K.js} +2 -2
- package/web-app/dist/assets/{TeamsPage-BE0szv9r.js → TeamsPage-C1fQYauF.js} +2 -2
- package/web-app/dist/assets/{TemplatesPage-BzkA9kat.js → TemplatesPage-CSTFt-4P.js} +1 -1
- package/web-app/dist/assets/TerminalOutput-Cu4NsyN6.js +16 -0
- package/web-app/dist/assets/{activity-AbfFz38r.js → activity-CtjsDzmh.js} +1 -1
- package/web-app/dist/assets/{bell-DF6Z6y5U.js → bell-C5es1xAs.js} +1 -1
- package/web-app/dist/assets/{bot-DQ4np_3n.js → bot-DE04U5Lc.js} +1 -1
- package/web-app/dist/assets/{check-RWuguCwE.js → check-DSKKA9EU.js} +1 -1
- package/web-app/dist/assets/{chevron-left-VpbQPOkC.js → chevron-left-DJ13W5AR.js} +1 -1
- package/web-app/dist/assets/{circle-alert-DwT82lJx.js → circle-alert-CS0BfqAd.js} +1 -1
- package/web-app/dist/assets/{clock-BBlUa93G.js → clock-D1Y5nruk.js} +1 -1
- package/web-app/dist/assets/{cloud-Dgbz3VCA.js → cloud-nU6LBoZL.js} +1 -1
- package/web-app/dist/assets/{code-xml-r6qqION5.js → code-xml-BZfvIdFo.js} +1 -1
- package/web-app/dist/assets/{copy-DMHj52B8.js → copy-CK_2O1Xv.js} +1 -1
- package/web-app/dist/assets/{database-uJqHSHR1.js → database-DJiwznAm.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-BZagljek.js → dollar-sign-pO20A3QY.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DMopmhu0.js → file-code-corner-Besy6vek.js} +1 -1
- package/web-app/dist/assets/{file-plus-DU_hT_3w.js → file-plus-DeaswxYY.js} +1 -1
- package/web-app/dist/assets/{folder-open-BP5TtQAW.js → folder-open-cr1UvQLI.js} +1 -1
- package/web-app/dist/assets/{git-commit-horizontal-zFk_IobS.js → git-commit-horizontal-BLxnfbSn.js} +1 -1
- package/web-app/dist/assets/{globe-mthCRtfp.js → globe-DrhG1NfX.js} +1 -1
- package/web-app/dist/assets/{hammer-BWaLBvSA.js → hammer-CeZtPBfE.js} +1 -1
- package/web-app/dist/assets/index-84JUN_aA.css +1 -0
- package/web-app/dist/assets/{index-BXA3UT6P.js → index-CLvjO22Z.js} +24 -24
- package/web-app/dist/assets/{layers-Q1LXV5fy.js → layers-fldADzyE.js} +1 -1
- package/web-app/dist/assets/{lightbulb-vRRNJ3dB.js → lightbulb-CznOfsPb.js} +1 -1
- package/web-app/dist/assets/{loader-circle-KgjeHEcV.js → loader-circle---lPB1Fs.js} +1 -1
- package/web-app/dist/assets/{lock-BerKqno7.js → lock-D5jVF-tO.js} +1 -1
- package/web-app/dist/assets/{mail-XAxTbprX.js → mail-D7HkLnlW.js} +1 -1
- package/web-app/dist/assets/minus-C-dT19Ct.js +6 -0
- package/web-app/dist/assets/{package-DLPKWAvW.js → package-Cm9B-cKE.js} +1 -1
- package/web-app/dist/assets/{plus-NMxxwHMn.js → plus-CQwOffLD.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-Dt42kYtM.js → refresh-cw-CvWJZLbN.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-BCR4Tp9u.js → rotate-ccw-C2Q1LYgM.js} +1 -1
- package/web-app/dist/assets/{save-sR4MJd42.js → save-BP0MvpoQ.js} +1 -1
- package/web-app/dist/assets/{server-SXkchokx.js → server-4i4VZXix.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Bvgp6_Dz.js → shield-alert-D2_bgb7j.js} +1 -1
- package/web-app/dist/assets/thumbs-up-vACbzeXj.js +26 -0
- package/web-app/dist/assets/{trash-2-D0IGudH7.js → trash-2-EYjdESLs.js} +1 -1
- package/web-app/dist/assets/trending-down-BWBdcvE3.js +6 -0
- package/web-app/dist/assets/{trending-up-DDrJKL5i.js → trending-up-Bix6V_-Y.js} +1 -1
- package/web-app/dist/assets/{upload-C54rjN8K.js → upload-B7hh6ERF.js} +1 -1
- package/web-app/dist/assets/{usePolling-D7fw4YSM.js → usePolling-b_e-k-IN.js} +1 -1
- package/web-app/dist/assets/{user-Cg7fbzdh.js → user-pmjZKWEg.js} +1 -1
- package/web-app/dist/index.html +2 -2
- package/web-app/dist/assets/LoginPage-VMzI6ROD.js +0 -1
- package/web-app/dist/assets/TerminalOutput-DBrdT_Fp.js +0 -31
- package/web-app/dist/assets/index-DgpUuqwR.css +0 -1
- 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.
|