@kurrent/kcap 1.3.0-beta.6 → 1.3.0-beta.8

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.
@@ -4,38 +4,31 @@ description: >-
4
4
  Follow a kcap import that setup just started and the evals landing on it —
5
5
  "Follow my kcap import" (the prompt `kcap setup` hands to your agent), "watch my
6
6
  import", "how is my import going", "are my evals done yet". Reads the handoff file
7
- setup wrote, polls Capacitor analytics for exactly the sessions that import
8
- created, waits for the sessions that imported while the user watched to be
9
- evaluated, summarizes their evals, links to the results and offers the guided
10
- tour. Not for browsing evals in general — that is the analytics or guided-tour
11
- skill.
7
+ setup wrote, polls the eval state of exactly the sessions that import created,
8
+ waits for the sessions that imported while the user watched to be evaluated,
9
+ summarizes their evals, links to the results and offers the guided tour. Works on
10
+ every plan. Not for browsing evals in general — that is the guided-tour skill.
12
11
  ---
13
12
 
14
13
  # Eval watch
15
14
 
16
- You follow ONE import: the sessions listed in a handoff file `kcap setup` wrote. Every query you
17
- issue names only those sessions — never a repo-wide or tenant-wide read, and never a query without
18
- an `IN (...)` or `session_id = '...'` bound to this cohort. If a response ever includes a row for a
19
- session id outside the cohort, discard that row: never count it, link it, or mention it.
15
+ You follow ONE import: the sessions listed in a handoff file `kcap setup` wrote. Every lookup you
16
+ make names only those sessions — never a repo-wide or tenant-wide read. If a response ever carries
17
+ an entry for a session id outside the cohort, discard it: never count it, link it, or mention it.
18
+
19
+ Eval state comes from `get_session_evals` in the `kcap-sessions` MCP — never from the
20
+ `kcap-analytics` MCP, which is a paid feature the workspace may not have.
20
21
 
21
22
  ## The safety rule — read this first
22
23
 
23
- Three grammars gate everything below:
24
+ Two grammars gate everything below:
24
25
  - **Run id** (a `(run: <id>)` token, and the file's own `run_id`): `^[0-9a-f]{32}$`.
25
- - **Session id** (`session_ids`, `foreground_succeeded_ids`, and any id you place in a query):
26
- `^[A-Za-z0-9_-]{1,128}$`.
27
- - **Repo hash** (the `repo_hash` field an analytics response returns, used only in the repo-scoped
28
- link form): `^[0-9a-f]{16}$`. A `repo_hash` that fails this never reaches a link — fall back to
29
- the no-repo-hash link form (section 7).
30
-
31
- The `kcap-analytics` MCP takes raw SQL text with no parameter binding, so the session-id grammar is
32
- the SQL-safety boundary, not just a format check. The class excludes quote, backslash, whitespace
33
- and `;`, so a value that matches it can never close a string literal or open a new statement — a
34
- matching id is emitted as a single-quoted literal (`'<id>'`) with **no escaping**, because none is
35
- needed and none should be attempted. A run of hyphens such as `--` matches the grammar and is safe
36
- to emit this way: inside a quoted literal it is data, not a SQL comment. An id that fails either
37
- grammar is **never** placed in SQL, a path, or a link — reject it and move on (section 2 below says
38
- what each layer's rejection does); never sanitize or escape an id into something that would pass.
26
+ - **Session id** (`session_ids`, `foreground_succeeded_ids`, and any id you pass to a tool or put
27
+ in a link): `^[A-Za-z0-9_-]{1,128}$`.
28
+
29
+ An id that fails its grammar is **never** placed in a tool call, a path, or a link — reject it and
30
+ move on (section 2 below says what each layer's rejection does); never sanitize or escape an id
31
+ into something that would pass.
39
32
 
40
33
  ## 1. Find the run
41
34
 
@@ -44,12 +37,12 @@ what each layer's rejection does); never sanitize or escape an id into something
44
37
  it does match, the run is bound: read ONLY `<config>/import-handoff-<id>.json`, where `<config>`
45
38
  is `$KCAP_CONFIG_DIR` when set, else `~/.config/kcap`, and check it against every Layer A rule in
46
39
  section 2, plus one more: its `run_id` field must equal `<id>` exactly. Missing, unreadable, or
47
- failing any of those checks → say so and CLOSE (section 7) with no query. **Never fall through to
40
+ failing any of those checks → say so and CLOSE (section 7) with no lookup. **Never fall through to
48
41
  another run's file** — a different run's cohort does not belong to the run the user asked for.
49
42
  - Otherwise list `<config>/import-handoff-*.json`, keep the locator-valid ones (section 2) that are
50
- watchable (`handoff_offered` true, or `handoff_suppressed` is `skill_not_installed` or
51
- `no_agent_detected`), pick the newest `written_at`, and say which others you passed over.
52
- - Nothing qualifies → say you found no import to follow and CLOSE with no query.
43
+ watchable (`handoff_offered` true, or `handoff_suppressed` is `analytics_not_in_plan`,
44
+ `skill_not_installed` or `no_agent_detected`), pick the newest `written_at`, and say which others you passed over.
45
+ - Nothing qualifies → say you found no import to follow and CLOSE with no lookup.
53
46
 
54
47
  ## 2. Validate the file — three layers
55
48
 
@@ -57,24 +50,25 @@ A failure at a lower layer never re-opens the file for a higher one.
57
50
 
58
51
  **Layer A** (may this file be selected at all): JSON parses; `schema_version` is 1; `run_id` matches
59
52
  `^[0-9a-f]{32}$`; `written_at` parses as ISO-8601 and is within the last 24 hours; `handoff_offered`
60
- is a boolean. Fail → not selectable; try the next candidate file, or close with no query if none
53
+ is a boolean. Fail → not selectable; try the next candidate file, or close with no lookup if none
61
54
  remain.
62
55
 
63
- **Layer B** (may its ids drive data): `cohort` ∈ {exact, partial_exact, unknown}; `background` ∈
64
- {not_needed, running, exited_zero, failed}; `foreground_certainty` ∈ {complete, incomplete};
56
+ **Layer B** (may its ids drive data): `cohort` ∈ {exact, partial_exact, unknown}; `scope` ∈ {all,
57
+ repos} (absent reads as `all`); `background` ∈ {not_needed, running, exited_zero, failed}; `foreground_certainty` ∈ {complete, incomplete};
65
58
  `handoff_suppressed` is null or one of import_failed, no_new_sessions, nothing_landed,
66
- analytics_not_in_plan, skill_not_installed, no_agent_detected; every entry of `session_ids` and
59
+ analytics_not_in_plan, skill_not_installed, no_agent_detected (older setups write
60
+ `analytics_not_in_plan`); every entry of `session_ids` and
67
61
  `foreground_succeeded_ids` matches `^[A-Za-z0-9_-]{1,128}$`; `unattributed_on_disk` is a
68
62
  non-negative integer. Fail → **no data**: the file stays selected, but say the record is unreadable
69
- and CLOSE with links only (section 7), no query.
63
+ and CLOSE with links only (section 7), no lookup.
70
64
 
71
65
  **Layer C** (may it supply links): `server_url` starts with `http://` or `https://`. Fail → no
72
- file-sourced links (fall back to `kcap whoami` per section 7) and no query — an unverifiable
66
+ file-sourced links (fall back to `kcap whoami` per section 7) and no lookup — an unverifiable
73
67
  `server_url` also means the server-binding check in section 4 cannot run. `background_log` and
74
68
  `profile` are shown as plain text — never as a link or a command to run — only when they contain no
75
69
  control characters and are under 512 characters; otherwise omit them.
76
70
 
77
- Never splice an unvalidated value into SQL, a path, or a link.
71
+ Never splice an unvalidated value into a tool call, a path, or a link.
78
72
 
79
73
  ## 3. No-handoff files
80
74
 
@@ -82,144 +76,122 @@ If `handoff_offered` is false, branch on `handoff_suppressed` and CLOSE:
82
76
  - `no_new_sessions` → "nothing to watch — that import found no new sessions".
83
77
  - `nothing_landed` → "the sessions that import selected were skipped at import time"; point at
84
78
  `kcap import --all` for per-session reasons; no retry.
85
- - `analytics_not_in_plan` → the plan sentence (section 6); the import ran; no retry.
86
- - `import_failed` → "that import did not get running"; name `kcap import --all --yes` and the
87
- `background_log` when displayable.
88
- - `skill_not_installed` / `no_agent_detected` → the import ran and you evidently have the skill
89
- now: continue as if offered, watching the file's cohort as usual (sections 4–5).
79
+ - `import_failed` → "that import did not get running"; name `kcap import --all --yes` when the
80
+ file's `scope` is `all`; when it is `repos`, say to run `kcap setup` again and choose the same
81
+ repositories at the same Only me / Shared levels (plain `kcap import` would not keep those levels); and the `background_log`
82
+ when displayable.
83
+ - `analytics_not_in_plan` / `skill_not_installed` / `no_agent_detected` → the import ran and you
84
+ can follow it now: continue as if offered, watching the file's cohort as usual (sections 4–5).
90
85
 
91
86
  ## 4. Bind to the server — fail closed
92
87
 
93
88
  If the selected file's `cohort` is `unknown`, skip this check and go straight to section 5 — there
94
- is no cohort to query either way, and the mismatch remediation below would be misleading when
89
+ is no cohort to look up either way, and the mismatch remediation below would be misleading when
95
90
  watching was never possible.
96
91
 
97
- Otherwise, run `kcap whoami` and compare its server URL to `server_url`: lowercase scheme and host,
98
- drop a default port (80 for http, 443 for https only), trim a trailing slash, compare the path
99
- as-is. Match → continue to section 5. Mismatch or `whoami` failure → issue **NO** analytics query.
100
- CLOSE with the file's links and this remediation: start your agent from a shell where `kcap whoami`
101
- reports `<server_url>` — set `KCAP_PROFILE=<profile>`, unset `KCAP_URL`, and set `KCAP_CONFIG_DIR`
102
- if kcap uses a custom config directory — then prompt again.
92
+ Otherwise, in this order — any CLOSE here makes **NO** eval lookup:
93
+
94
+ 1. If `get_connection` is not among your `kcap-sessions` tools, this kcap is older than the skill:
95
+ say to update kcap and restart the agent, then CLOSE. `kcap whoami` is no substitute — it runs
96
+ in another process and cannot say which server the agent's MCP server reads.
97
+ 2. Run `kcap whoami`. If it fails because kcap is not logged in, say to run `kcap login`, then CLOSE.
98
+ 3. Call `get_connection` and compare its `server_url` to the file's — lowercase scheme and host,
99
+ drop a default port (80 for http, 443 for https only), trim a trailing slash, compare the path
100
+ as-is — and its `profile` to the file's `profile`, exactly. Both match → continue to section 5.
101
+ Either differs → CLOSE with the file's links and this remediation: start your agent from a
102
+ shell with `KCAP_PROFILE=<profile>`, `KCAP_URL` unset, and `KCAP_CONFIG_DIR` set if kcap uses a
103
+ custom config directory — restarting the agent, since its kcap-sessions server only reads those
104
+ when it starts — then prompt again.
103
105
 
104
106
  ## 5. Watch the cohort
105
107
 
106
- Cohort = `session_ids` (`cohort: unknown` or empty → CLOSE with links, no query). Foreground =
107
- the entries of `foreground_succeeded_ids` that are also in `session_ids`: the sessions that
108
- imported while the user watched setup, and the ones this watch waits on. K is the foreground
109
- count. A foreground entry missing from `session_ids` is never queried; when any are missing, say
110
- "M of the sessions that imported first are outside the 500 this watch can query". Open with:
111
- "Watching N sessions from this import; waiting for the K that imported first to be evaluated";
112
- for `partial_exact` add "the sessions that imported first and the most recent others, 500 at
113
- most; older ones may land and evaluate unobserved"; if `unattributed_on_disk` > 0 add the
108
+ Cohort = `session_ids` (`cohort: unknown` or empty → CLOSE with links, no lookup); N is its size.
109
+ Foreground = the entries of `foreground_succeeded_ids` that are also in `session_ids`, in file
110
+ order: the sessions that imported while the user watched setup, and the ones this watch waits on.
111
+ K is the foreground count. A foreground entry missing from `session_ids` is never looked up; when
112
+ any are missing, say "M of the sessions that imported first are outside the 500 this import
113
+ recorded".
114
+
115
+ **Watched set** W, at most 25 sessions: the first 25 foreground sessions, then — only while fewer
116
+ than 25 — the other cohort sessions in file order. Open with: "Watching W of the N sessions from
117
+ this import; waiting for the K that imported first to be evaluated". When K > 25 add "the first 25
118
+ of them"; for `partial_exact` add "the sessions that imported first and the most recent others,
119
+ 500 at most; older ones may land and evaluate unobserved"; if `unattributed_on_disk` > 0 add the
114
120
  sentence from section 7.
115
121
 
116
- Every query uses `query_analytics` with `scope: 'global'` (the MCP defaults to the caller's cwd
117
- repo and will not widen on its own), **ONE call at a time — never two in flight** — and names only
118
- cohort ids, each already validated against the grammar above, as single-quoted literals in an
119
- `IN (...)` list or `session_id = '...'`. If a response ever includes a row whose `session_id` is
120
- not in `session_ids`, drop that row before using the response for anything.
121
-
122
- **Request budget:** at most 20 cohort queries per poll. `floor = ceil(N / 20)`. The FIRST poll uses
123
- batch size `floor` exactly — no probing at a smaller size first. Every batch passes `max_rows`
124
- equal to its own size:
125
-
126
- SELECT s.session_id, s.repo_hash, s.vendor, s.model, s.duration_min, s.event_count,
127
- e.eval_run_id, e.evaluated_at, e.overall_score, e.judge_model
128
- FROM v_an_sessions s LEFT JOIN v_an_eval_summaries e ON e.session_id = s.session_id
129
- WHERE s.session_id IN ('<id>', '<id>', ...)
130
-
131
- A row = arrived; a non-null `eval_run_id` = completed. Each response body carries `max_rows`, the
132
- server's effective cap. After the first poll: if any body reported `max_rows < floor`, fail closed
133
- at once — say the server's row cap is below what watching this many sessions needs, and CLOSE,
134
- having spent no more than the 20-query budget. Otherwise raise the batch to `min(100, max_rows)`
135
- for later polls. Any batch that comes back `truncated: true`, in the first poll or any later one,
136
- fails that poll — the cap decides, not the truncation flag alone.
137
-
138
- One poll = every batch it needs, each non-truncated. A failed or truncated batch discards the whole
139
- poll — nothing from it commits — and counts as one failure toward the two-failure stop rule.
140
-
141
- A 429 fails the poll. If its text matches `retry after (\d+)s`, wait that many seconds before the
142
- next poll; otherwise wait 60 seconds. Never poll sooner than the 30-second cadence either way. HTTP
143
- 403 with `analytics_not_in_plan` → CLOSE immediately with the plan sentence (section 6); no
144
- polling, no retry, no wait.
145
-
146
- Progress = cohort ids that arrived / cohort size, and foreground ids evaluated / K, both computed
147
- from the batch rows only — never a separate count, never repo- or tenant-wide. Cadence: first
148
- snapshot immediately, then every 30 seconds. Say the progress after each poll.
149
-
150
- **Baseline**: the first snapshot's rows. An eval present there is *pre-existing*; one whose
151
- `eval_run_id` was null or absent there and non-null in a later poll *completed during this watch*.
152
- Every summary labels each session with one of those two phrases.
122
+ **One poll** = ONE `get_session_evals` call with `session_ids` set to W — never two in flight. Each
123
+ entry's `state` is one of:
124
+ - `not_found` — not ingested yet (or not visible to you);
125
+ - `not_evaluated` — arrived, no eval yet;
126
+ - `queued` — arrived, eval waiting for a worker or a retry (`queue_position`, `next_attempt_at`);
127
+ - `running` — arrived, eval in progress (`questions_done` of `total_questions`);
128
+ - `completed` — arrived and evaluated: `eval_run_id`, `evaluated_at`, `overall_score` (out of 5),
129
+ `judge_model`, `summary`, `categories`, `weakest_questions`;
130
+ - `failed` — arrived, the eval run failed (`failure_reason`);
131
+ - `error` — this lookup failed.
132
+
133
+ Arrived = any state but `not_found` and `error`. A tool error (including a login prompt), or any
134
+ `error` entry, fails the poll: nothing from it commits, and it counts toward the two-failure stop
135
+ rule. If the tool is missing — no `kcap-sessions` MCP, or one without `get_session_evals` — this
136
+ kcap is older than the skill: say to update kcap and restart the agent, then CLOSE.
137
+
138
+ Progress = watched sessions arrived / W and foreground sessions evaluated / min(K, 25), from the
139
+ poll's entries only. Cadence: first snapshot immediately, then every 30 seconds. Say the progress
140
+ after each poll.
141
+
142
+ **Baseline**: the first snapshot. A session `completed` there is *pre-existing*; one that was not
143
+ `completed` there, or completed with a different `eval_run_id`, and is `completed` in a later poll
144
+ *completed during this watch*. Every summary labels each session with one of those two phrases.
153
145
 
154
146
  **Stop**, evaluated in this order after each poll whose results have already committed:
155
- 1. K > 0 and every foreground session has a completed eval → summarize all of them, ordered by
147
+ 1. K > 0 and every watched foreground session is `completed` → summarize all of them, ordered by
156
148
  `evaluated_at` then `session_id`. A pre-existing eval counts: the wait is for these sessions to
157
149
  be evaluated, whenever that happened.
158
- 2. K = 0 and three distinct cohort sessions completed during this watch → summarize those three,
159
- ordered by `evaluated_at` then `session_id`. Pre-existing evals never satisfy this rule: on a
160
- re-import they belong to an earlier run, and stopping on them would end the watch before this
161
- import produced anything.
162
- 3. `cohort: exact`, non-empty, and every id in `session_ids` has a completed eval → summarize the
163
- foreground sessions, or with K = 0 the three with the newest `evaluated_at` (ties by
164
- `session_id`). This rule may fire on the first snapshot with nothing but pre-existing evals:
165
- a cohort that is already fully evaluated has nothing left to wait for, and the labels say so.
166
- Never fires for `partial_exact` or an empty cohort.
150
+ 2. K = 0 and three watched sessions completed during this watch → summarize those three, ordered by
151
+ `evaluated_at` then `session_id`. Pre-existing evals never satisfy this rule: on a re-import they
152
+ belong to an earlier run, and stopping on them would end the watch before this import produced
153
+ anything.
154
+ 3. `cohort: exact`, N ≤ 25, and every cohort session is `completed` → summarize the foreground
155
+ sessions, or with K = 0 the three with the newest `evaluated_at` (ties by `session_id`). This
156
+ rule may fire on the first snapshot with nothing but pre-existing evals: a cohort that is
157
+ already fully evaluated has nothing left to wait for, and the labels say so.
167
158
  4. Two consecutive failed polls → stop, reporting the failures.
168
159
  5. 10 minutes since the first snapshot → stop. No new poll starts after the deadline, but a poll
169
160
  already dispatched before it finishes normally, and its outcome is still checked against rules
170
161
  1–4 first. On a deadline stop, summarize the foreground sessions that did complete and name the
171
- ones still pending, with whether each has arrived; if none of them completed, summarize instead
172
- up to three cohort sessions with the newest `evaluated_at` (ties by `session_id`).
162
+ ones still pending, with their state; if none of them completed, summarize instead up to three
163
+ watched sessions with the newest `evaluated_at` (ties by `session_id`).
173
164
 
174
165
  No idle early-stop — quiet polls are normal and are not a reason to stop early. Evals take minutes
175
166
  per session, and the foreground sessions are the newest, so they are usually the last to complete.
176
167
 
177
- **Per-session detail** (one session per query, 2 queries per detailed session, at most 10 across
178
- the whole run). A foreground pass takes whole session chains, so K can exceed five: the first five
179
- sessions in summary order get detail; every later one gets a one-line entry — link, label and
180
- `overall_score` — and no detail query. Both queries pass `max_rows` explicitly — a query left at
181
- the agent's guess truncates on the category list and throws the detail away:
182
-
183
- -- max_rows: 25
184
- SELECT category, ROUND(AVG(score), 2) AS mean FROM v_an_eval_scores
185
- WHERE session_id = '<id>' AND outcome = 'assessed' GROUP BY category
186
- -- max_rows: 2
187
- SELECT question_id, score FROM v_an_eval_scores
188
- WHERE session_id = '<id>' AND outcome = 'assessed' ORDER BY score ASC, question_id ASC LIMIT 2
189
-
190
- Strongest = highest mean, weakest = lowest, ties alphabetical by category. A truncated or failed
191
- detail query, or a session with no assessed rows, → summarize that session by `overall_score` alone
192
- and say so; this never affects poll success or the stop rules above.
193
-
194
- **What a detailed session summary says**, one short paragraph each: the link (section 7), the
195
- baseline label, then vendor and model, length (`duration_min`, rounded, and `event_count`),
196
- `overall_score` out of 5 and the judge model, strongest and weakest category with their means, and
197
- the two weakest questions with their scores. When the `kcap-sessions` MCP is available, call `get_session_summary` once per
198
- summarized session and lead with its title or first sentence when it returns one; an empty summary
199
- is left out silently, never called "no summary".
200
-
201
- **Cohort facts**, one line after the summaries, from the last committed poll's rows only: sessions
202
- arrived out of N, sessions evaluated, and the mean `overall_score` across the evaluated ones.
168
+ **What a session summary says.** The first five sessions in summary order get one short paragraph
169
+ each: the link (section 7), the baseline label, `overall_score` out of 5 and the judge model, the
170
+ strongest and weakest of `categories` by `score` (ties alphabetical by name), the two
171
+ `weakest_questions` with their scores, and the gist of the judge's `summary`. When
172
+ `get_session_summary` returns a title or a first sentence for the session, lead with it; an empty
173
+ summary is left out silently, never called "no summary". Every later session gets a one-line entry
174
+ — link, label and `overall_score`. A completed entry with no categories is summarized by
175
+ `overall_score` alone; say so.
176
+
177
+ **Cohort facts**, one line after the summaries, from the last committed poll only: watched
178
+ sessions arrived out of W, sessions evaluated, and the mean `overall_score` across the evaluated
179
+ ones.
203
180
 
204
181
  ## 6. When little or nothing completes
205
182
 
206
183
  Explain the real gates plainly: auto-eval may be off for a repository; the server needs an eval
207
184
  agent configured; sessions under the minimum event count are skipped; already-evaluated sessions
208
- are not re-run; a recovery sweep runs hourly. If the analytics MCP is absent entirely, close
209
- immediately with the block below.
210
-
211
- Plan sentence (used here and from sections 3 and 5): "Insights isn't in this workspace's plan, so
212
- evals can't be followed from here; your import continues and evals appear in the Capacitor UI."
185
+ are not re-run; a recovery sweep runs hourly. A `failed` session names its `failure_reason`.
213
186
 
214
187
  ## 7. CLOSE — always
215
188
 
216
189
  1. How to keep watching: prompt `Follow my kcap import` again (with the same `(run: <id>)` line
217
190
  when you were bound to one).
218
- 2. Links: with a `repo_hash` matching `^[0-9a-f]{16}$`, `<server_url>/repo/<repo_hash>/sessions/<session_id>?tab=evaluation`;
219
- otherwise (no `repo_hash`, or one that fails the grammar) `<server_url>/sessions/<session_id>?tab=evaluation`;
220
- all results `<server_url>/sessions`. No valid file → `server_url` from `kcap whoami`; if that
221
- fails too, say the results live in the Capacitor server UI and that `kcap whoami` prints the URL
222
- once logged in.
191
+ 2. Links: `<server_url>/sessions/<session_id>?tab=evaluation` per session; all results
192
+ `<server_url>/sessions`. No valid file → `server_url` from `kcap whoami`; if that fails too, say
193
+ the results live in the Capacitor server UI and that `kcap whoami` prints the URL once logged
194
+ in.
223
195
  3. When `unattributed_on_disk` > 0: "N sessions on disk had no repository match; any of them that
224
196
  imported show without one — `kcap remap` places them."
225
197
  4. Offer the guided tour with the exact prompt `Start kcap guided tour`.