@ngockhoale/ukit 2.2.9 → 2.2.11

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/CHANGELOG.md CHANGED
@@ -2,6 +2,73 @@
2
2
 
3
3
  All notable changes to UKit are documented here.
4
4
 
5
+ ## 2.2.11 - 2026-08-29
6
+
7
+ Same-day follow-up to 2.2.10. The two premises that release shipped as unverified were checked
8
+ against omp's own installed TypeScript source (`@oh-my-pi/pi-coding-agent` v18.0.10, not a live
9
+ session — omp still was not run) and both hold:
10
+
11
+ - **`steer` after a compaction is accepted, not dropped.** `session-maintenance.ts` fires
12
+ `session_compact` only after `agent.replaceMessages()`/`rebaseAfterCompaction()` already ran, so
13
+ the session is idle at that point, and `agent-session.ts`'s `sendCustomMessage()` non-streaming
14
+ branch appends the message to the live context regardless of `deliverAs`.
15
+ - **`display: true` does trigger a visible re-render.** `extension-ui-controller.ts`'s
16
+ `applyCustomMessageDisplay()` calls `rebuildChatFromMessages()` for the non-streaming case (the
17
+ streaming case renders via the `message_end` event instead).
18
+
19
+ `templates/.omp/hooks/pre/ukit-bridge.js`'s two `TODO(verify)` comments (PLAN.md §3 D3, D4) are
20
+ updated to record this — from "unverified, additive, worst-case-unchanged" to "source-confirmed,
21
+ one rung below a live repro." No runtime behaviour changes; both delivery paths (`steer` +
22
+ `nextTurn`, and the `finalNotice` fallback) stay in place as defense in depth either way. A stale
23
+ local `.omp/hooks/pre/ukit-bridge.js` install artifact in this repo — untracked, regenerated by
24
+ `ukit install`, and out of sync with `templates/.omp/` since before 2.2.10 shipped — was also found
25
+ and resynced; that file is never part of the published package.
26
+
27
+ ## 2.2.10 - 2026-08-29
28
+
29
+ User report: long omp (Oh My Pi) sessions go idle roughly 15 minutes into a task — no output, no
30
+ error, no notification, indistinguishable from a hang. Typing `continue` makes the session resume
31
+ and finish correctly (the user's observation, not a measurement — no field reproduction of the
32
+ stall exists on this machine to time it). Three real root causes were found by reading the source,
33
+ not guessed:
34
+
35
+ ### Fixed
36
+
37
+ - **`map-impact` — the mode wide implementation work actually routes to — was never gated by the
38
+ completion loop.** `templates/.claude/ukit/runtime/execution-ledger.mjs:12`'s `IMPLEMENT_MODES`
39
+ set omitted it, while `src/index/taskRouting.js:414` routes exactly this kind of task to
40
+ `map-impact` and `src/index/taskRouting.js:610` gives it the strictest contract
41
+ (`impact-evidence`, `write-evidence`, `verification-evidence`). So the longest tasks declared the
42
+ most evidence and got no gate at all — the session could stop with nothing written and nothing
43
+ verified, silently. `map-impact` is now in `IMPLEMENT_MODES`.
44
+ - **Reaching the continuation cap ended the session with a log line, not a message.** At
45
+ `MAX_CONTINUATIONS` (6, unchanged), `evaluateCompletion` returned `{ capped: true }` and the only
46
+ reaction was `pi.logger.warn` — a log sink, not the transcript. This is also the mechanical reason
47
+ a new prompt (`continue`) appeared to "fix" the stall: a new prompt writes a fresh `requestKey`,
48
+ which resets the per-request continuation count back to zero. The gate now spends exactly one
49
+ final continuation (`finalNotice`) instructing the model to state what is unfinished and stop, and
50
+ persists a one-shot `notified` flag on the ledger so the notice cannot refire.
51
+ - **The post-compact resume was delivered only to the next user turn, and every UKit→omp message was
52
+ `display: false`.** After an omp auto-compaction, the `docs/AI_HANDOFF/RUN.md` resume cursor was
53
+ sent with `deliverAs: 'nextTurn'` only — if compaction ended the agent loop, the cursor waited for
54
+ a human to type something. Separately, every message the bridge built hardcoded `display: false`,
55
+ so UKit had no user-visible channel in omp at all, for context or for stop notices.
56
+ `runSessionCompact` now sends the resume as `steer` (reaches a still-running turn) *and*
57
+ `nextTurn` (unchanged fallback), and `runSessionStop` now sends one `display: true` notice
58
+ whenever it stops with evidence still missing — including the previously-silent
59
+ `review-release` / ungated-mode case.
60
+
61
+ **Two premises behind the third fix are unverified — omp is not installed in this development
62
+ environment.** Whether omp still accepts a `steer` message after a compaction, and whether omp
63
+ honours `display: true` on a bridge message, were not and could not be reproduced or measured here.
64
+ Both changes are additive: if either premise is false, the `nextTurn` delivery and the model's own
65
+ `finalNotice` message (from the second fix) still reach the user, so the worst case is unchanged
66
+ from today's behaviour, never worse. See `docs/STATUS.md` for the open thread and the evidence that
67
+ would confirm the continuation-cap path directly.
68
+
69
+ - **`templates/.omp/RULES.md` gains one sticky rule**: ending a turn with no output is a defect, not
70
+ a pause — a model-side backstop for whatever a code gate cannot reach.
71
+
5
72
  ## 2.2.9 - 2026-08-28
6
73
 
7
74
  Retuning UKit's context budget used to mean editing five scattered places and hand-syncing two
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ngockhoale/ukit",
3
- "version": "2.2.9",
3
+ "version": "2.2.11",
4
4
  "description": "Install/update an index-first AI workspace for Claude Code, OpenAI Codex, OpenCode, and omp (Oh My Pi).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,6 +15,7 @@ const IMPLEMENT_MODES = new Set([
15
15
  'local-build',
16
16
  'shared-edit',
17
17
  'find-cause',
18
+ 'map-impact',
18
19
  ]);
19
20
 
20
21
  function safeSegment(value) {
@@ -159,6 +160,7 @@ function freshLedger(payload, routeState, harness) {
159
160
  receipts: [],
160
161
  blocker: null,
161
162
  continuationCount: 0,
163
+ notified: false,
162
164
  updatedAt: Date.now(),
163
165
  };
164
166
  }
@@ -252,24 +254,48 @@ export function evaluateCompletion({ state = {}, ledger = {} } = {}) {
252
254
  const routeSummary = state?.routeSummary || {};
253
255
  const mode = routeSummary.executionMode || routeSummary.approachSelector?.executionMode || null;
254
256
  const evidence = requiredEvidence(state);
255
- if (!IMPLEMENT_MODES.has(mode) || evidence.length === 0 || ledger?.blocker) {
256
- return { continue: false, missingEvidence: [] };
257
+ if (ledger?.blocker) {
258
+ return { continue: false, notify: false, missingEvidence: [] };
259
+ }
260
+ if (evidence.length === 0) {
261
+ return { continue: false, notify: false, missingEvidence: [] };
257
262
  }
258
263
 
259
264
  const sameRequest = !ledger?.requestKey || !state?.requestKey || ledger.requestKey === state.requestKey;
260
265
  const effectiveLedger = sameRequest ? ledger : {};
261
266
  const missingEvidence = evidence.filter((item) => !evidenceSatisfied(item, effectiveLedger));
262
267
  if (missingEvidence.length === 0) {
263
- return { continue: false, missingEvidence: [] };
268
+ return { continue: false, notify: false, missingEvidence: [] };
269
+ }
270
+
271
+ const gated = IMPLEMENT_MODES.has(mode);
272
+ if (!gated) {
273
+ return {
274
+ continue: false,
275
+ notify: true,
276
+ missingEvidence,
277
+ reason: `UKit completion gate: missing ${missingEvidence.join(', ')}. This mode does not auto-continue; tell the user what is unfinished.`,
278
+ };
264
279
  }
265
280
 
266
281
  const continuationCount = Number(effectiveLedger?.continuationCount || 0);
267
282
  if (continuationCount >= MAX_CONTINUATIONS) {
283
+ if (effectiveLedger?.notified === true) {
284
+ return {
285
+ continue: false,
286
+ capped: true,
287
+ notify: true,
288
+ missingEvidence,
289
+ reason: `UKit continuation cap reached with missing evidence: ${missingEvidence.join(', ')}.`,
290
+ };
291
+ }
268
292
  return {
269
- continue: false,
293
+ continue: true,
294
+ finalNotice: true,
295
+ notify: true,
270
296
  capped: true,
271
297
  missingEvidence,
272
- reason: `UKit continuation cap reached with missing evidence: ${missingEvidence.join(', ')}.`,
298
+ reason: `UKit stopping with unfinished work: ${missingEvidence.join(', ')}. Tell the user what is unfinished and stop; do not continue further.`,
273
299
  };
274
300
  }
275
301
 
@@ -298,6 +324,13 @@ export async function incrementContinuation(projectRoot, payload = {}, ledger =
298
324
  return next;
299
325
  }
300
326
 
327
+ export async function markNotified(projectRoot, payload = {}, ledger = null) {
328
+ const current = ledger || await readExecutionLedger(projectRoot, payload) || freshLedger(payload, null, 'unknown');
329
+ const next = { ...current, notified: true, updatedAt: Date.now() };
330
+ await writeJsonAtomic(ledgerPath(projectRoot, payload), next);
331
+ return next;
332
+ }
333
+
301
334
  async function readStdin() {
302
335
  if (process.stdin.isTTY) return '';
303
336
  const chunks = [];
@@ -322,7 +355,8 @@ async function main() {
322
355
  const ledger = await readExecutionLedger(projectRoot, payload) || {};
323
356
  const result = evaluateCompletion({ state, ledger });
324
357
  if (result.continue) {
325
- await incrementContinuation(projectRoot, payload, ledger);
358
+ if (result.finalNotice) await markNotified(projectRoot, payload, ledger);
359
+ else await incrementContinuation(projectRoot, payload, ledger);
326
360
  process.stdout.write(`${JSON.stringify({ decision: 'block', reason: result.reason })}\n`);
327
361
  } else if (result.capped) {
328
362
  process.stderr.write(`[ukit-completion] ${result.reason}\n`);
@@ -57,6 +57,12 @@ Prefer unique current-file anchors over line numbers or stale pasted blocks. Nev
57
57
  stale spec — re-read current source and confirm before applying. Preserve existing BOM and line
58
58
  endings.
59
59
 
60
+ ## 8. Never end a turn silently
61
+
62
+ Ending a turn with no output is a defect, not a pause. If you stop before the work is finished, the
63
+ last thing you emit is one short line naming what is unfinished and what you need. Silence is never
64
+ a status — the user cannot distinguish it from a crash.
65
+
60
66
  > Maintainer note, not a per-turn rule: `modelRoles` in `.omp/config.yml` ships UNIC gateway names.
61
67
  > On a non-UNIC provider, edit only the three cost tiers — `lite`, `code`, `smart` — never `vision`,
62
68
  > which stays `unic-vision` because it is a capability lane, not a cost tier.
@@ -12,6 +12,7 @@ import { spawnSync } from 'node:child_process';
12
12
  import {
13
13
  evaluateCompletion,
14
14
  incrementContinuation,
15
+ markNotified,
15
16
  readExecutionLedger,
16
17
  readRouteState,
17
18
  recordExecutionReceipt,
@@ -337,18 +338,18 @@ function textFromContent(content) {
337
338
  .join('\n');
338
339
  }
339
340
 
340
- function hookContextMessage(content) {
341
+ function hookContextMessage(content, { display = false } = {}) {
341
342
  return {
342
343
  customType: 'ukit-hook-context',
343
344
  content,
344
- display: false,
345
+ display,
345
346
  };
346
347
  }
347
348
 
348
- function sendContext(pi, context, deliverAs) {
349
+ function sendContext(pi, context, deliverAs, { display = false } = {}) {
349
350
  const content = context.filter(Boolean).join('\n').trim();
350
351
  if (!content || typeof pi.sendMessage !== 'function') return;
351
- pi.sendMessage(hookContextMessage(content), { deliverAs });
352
+ pi.sendMessage(hookContextMessage(content, { display }), { deliverAs });
352
353
  }
353
354
 
354
355
  export async function runToolCall(pi, event, { projectRoot, context: extensionContext = {} }) {
@@ -469,6 +470,13 @@ export async function runSessionCompact(pi, event, { projectRoot, context: exten
469
470
  const metadata = runtimeMetadata(event, extensionContext);
470
471
  const payload = buildHookPayload('SessionStart', { ...metadata, source: 'compact' });
471
472
  const result = await runScriptChain(pi, HOOK_EVENT_MAP.session_start, payload, { projectRoot });
473
+ // VERIFIED (2026-08-29, source read of omp v18.0.10 pi-coding-agent/src) PLAN.md §3 D3:
474
+ // session_compact fires only after agent.replaceMessages()/rebaseAfterCompaction() already
475
+ // ran (session-maintenance.ts), i.e. the session is idle here, and agent-session.ts's
476
+ // sendCustomMessage() non-streaming branch appends the message to the live context
477
+ // regardless of deliverAs — so 'steer' is accepted right after a compaction. Not a live
478
+ // repro, so the 'nextTurn' copy below stays as defense in depth.
479
+ sendContext(pi, result.context, 'steer');
472
480
  sendContext(pi, result.context, 'nextTurn');
473
481
  return undefined;
474
482
  }
@@ -490,12 +498,26 @@ export async function runSessionStop(
490
498
  const evaluation = evaluateCompletion({ state, ledger });
491
499
  if (!evaluation.continue) {
492
500
  if (evaluation.capped) pi.logger?.warn?.(`[UKit] ${evaluation.reason}`);
501
+ if (Array.isArray(evaluation.missingEvidence) && evaluation.missingEvidence.length > 0) {
502
+ const notice = [
503
+ `[UKit] Stopping with unfinished work: ${evaluation.missingEvidence.join(', ')}.`,
504
+ evaluation.reason,
505
+ ].filter(Boolean).join(' ');
506
+ // VERIFIED (2026-08-29, source read of omp v18.0.10 pi-coding-agent/src) PLAN.md §3 D4:
507
+ // display:true on a non-streaming sendMessage payload triggers
508
+ // extension-ui-controller.ts's applyCustomMessageDisplay() -> rebuildChatFromMessages(),
509
+ // a real immediate TUI re-render (the streaming case renders via the message_end event
510
+ // instead). Not a live repro, so the finalNotice continuation below (the model's own
511
+ // message) stays as a second guaranteed channel.
512
+ sendContext(pi, [notice], 'nextTurn', { display: true });
513
+ }
493
514
  return undefined;
494
515
  }
495
516
 
496
517
  if (suppliedLedger === undefined) {
497
518
  try {
498
- await incrementContinuation(projectRoot, payload, ledger);
519
+ if (evaluation.finalNotice) await markNotified(projectRoot, payload, ledger);
520
+ else await incrementContinuation(projectRoot, payload, ledger);
499
521
  } catch (error) {
500
522
  pi.logger?.warn?.(`[UKit] continuation bookkeeping failed open: ${error?.message || error}`);
501
523
  }