@loicngr/kobo 2.1.12 → 2.1.15
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/AGENTS.md +33 -6
- package/CHANGELOG.md +12 -0
- package/CONFIGURATION.md +34 -4
- package/README.md +1 -1
- package/dist/mcp-server/kobo-tasks-server.js +43 -1
- package/dist/server/db/auto-loop-final-review-schema.js +18 -0
- package/dist/server/db/migrations.js +10 -1
- package/dist/server/db/review-return-schema.js +18 -1
- package/dist/server/db/schema.js +2 -0
- package/dist/server/index.js +2 -1
- package/dist/server/routes/workspaces.js +61 -1
- package/dist/server/services/agent/engines/claude-code/engine.js +61 -5
- package/dist/server/services/agent/orchestrator.js +158 -38
- package/dist/server/services/auto-loop-final-review-service.js +329 -0
- package/dist/server/services/auto-loop-service.js +53 -10
- package/dist/server/services/review-launch-runtime.js +17 -0
- package/dist/server/services/review-return-service.js +58 -10
- package/dist/server/services/review-service.js +59 -11
- package/dist/server/services/workspace-service.js +11 -0
- package/dist/server/services/workspace-template-service.js +10 -0
- package/dist/shared/auto-loop-review.js +1 -0
- package/dist/shared/review-return.js +1 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-D7jouiuP.js +2 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-Q-OlSQ1c.css +1 -0
- package/src/client/dist/pwa/assets/AutoLoopFinalReviewSettings-lKjfaC6V.js +1 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-DD90Nl6c.js → ChangelogPage-BplM7qEB.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-D_BQFWiT.js → ComparisonPanel-DxK5v9w_.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-D5ilP5kt.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-DxOes_cF.css +1 -0
- package/src/client/dist/pwa/assets/{DashboardPage-DyvTdmBg.js → DashboardPage-BsP-MATS.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-Cd15vFWj.js +8 -0
- package/src/client/dist/pwa/assets/{DocumentsPanel-bm639sIu.js → DocumentsPanel-CGrf8Q3H.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-KfwSzjeY.js → DrawerToggleButton-BANeLSsR.js} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-CEPV5Qw0.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-37IHOIW-.js → HealthPage-BVTNx-92.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-C6nvIQjU.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DBKtwilW.js +2 -0
- package/src/client/dist/pwa/assets/{MemoryPanel-CucZMhjy.js → MemoryPanel-C71rTpv9.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-CUQybzVR.js → MemoryProposalList-CoqhUGrv.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-x6qPyl_7.js → QBtnDropdown-U9Ey5W8h.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-D33aoRoG.js → QExpansionItem-Be_A1MT1.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-Di-D6t2u.js → QScrollArea-D4lYzAPQ.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-MdPXvwm4.js → QSelect-fS0DLOd1.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-BhwG7lp2.js → QTooltip-kStOgY0X.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-qFi9Wxzh.js → SchedulePanel-C7nTGdWE.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-DA9nRmsA.js → SearchPage-C6J72pkQ.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-SuH-aIzv.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-Brw2XsYJ.js → SplitWorkspacePage-Bqn4gdVw.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-DVP18-nk.js → TerminalPanel-zLeDCReI.js} +9 -9
- package/src/client/dist/pwa/assets/{ToolCallItem-cSHh3p_F.js → ToolCallItem-B8b_Sagj.js} +3 -3
- package/src/client/dist/pwa/assets/{TouchPan-C6LZZ3rw.js → TouchPan-Bl8VzIDK.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Czlqo18H.js → TourReplayButton-pL0HCboU.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-BDYGq0pC.js → WorkspaceAttentionLabels-9Eomx0rQ.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B248nKIU.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-B6clgURl.js +4 -0
- package/src/client/dist/pwa/assets/{browser-ifCwUGpE.js → browser-C-PJ4alw.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-BBkGq3lN.js → build-path-tree-SW9rj7B_.js} +1 -1
- package/src/client/dist/pwa/assets/custom-sounds-toz-4drX.js +1 -0
- package/src/client/dist/pwa/assets/de-Ddxe8cc6.js +17 -0
- package/src/client/dist/pwa/assets/{editor.api-CV_seGJR.js → editor.api-BJTPhnh8.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-ttw0bL63.js → efforts-BO9xGn9q.js} +1 -1
- package/src/client/dist/pwa/assets/es-DBJIQeWW.js +17 -0
- package/src/client/dist/pwa/assets/{expand-template-C8biMRXo.js → expand-template-BOZMtWau.js} +1 -1
- package/src/client/dist/pwa/assets/{format-Cu95dbxt.js → format-hEnpRyit.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C25D1RSd.js → formatters-DD_eFc4z.js} +1 -1
- package/src/client/dist/pwa/assets/fr-yBzIIPaQ.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-DJEFr74q.js → freemarker2-B6nUofvT.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-C_lzsMjX.js → handlebars-4P3gtM8p.js} +1 -1
- package/src/client/dist/pwa/assets/{html-CWmLFAmY.js → html-C9ZzWo-y.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-2-ULabk2.js +1 -0
- package/src/client/dist/pwa/assets/index-Vk99jtxD.js +20 -0
- package/src/client/dist/pwa/assets/it-D44U-PT1.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-tsuiEUe-.js → javascript-BGEC5ggd.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-BBr9BvUk.js → liquid-DNtF630O.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-gjBH1cXG.js → mdx-CICwyOd0.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CvGcBIsb.js → monaco-contributions-BkuwvgrT.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-Uw-Lfb8Z.js → monaco.contribution-GX8XA-PE.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-KnWzjYIc.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-CWuZTz4Z.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-BDQa2TgU.js → project-color-B1SMFjSe.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DlIgD1n6.js → python-n5uPdTVS.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-AraFPlpM.js → razor-AOj0nsUt.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-CB8UpXHM.js → typescript-Uhkrqihh.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CSREHQM4.js → use-search-index-status-CW0BdpBZ.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-6yxNVmjH.js → use-tours-CndFlyL-.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-BM3Giig1.js → use-worktree-restore-BZxibZS5.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BPMFvTnQ.js → workspace-templates-W557e1-m.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-uRjL1P43.js → xml-B-hnMgEs.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-C9CFR4qV.js → yaml-DqXkTvgm.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +26 -0
- package/src/mcp-server/kobo-tasks-server.ts +46 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BTRzc60o.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-BZAoCukb.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-CdCs8PRS.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-D0K2PlZD.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-C-Ejlc9_.js +0 -3
- package/src/client/dist/pwa/assets/MainLayout-BQOOup9o.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DXQ1ouYl.js +0 -2
- package/src/client/dist/pwa/assets/SettingsPage-DcV6apqE.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-BwUkijpS.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bwfv559o.css +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CeSgjIY7.js +0 -1
- package/src/client/dist/pwa/assets/de-CHY2BAfL.js +0 -17
- package/src/client/dist/pwa/assets/es-DbcSKp38.js +0 -17
- package/src/client/dist/pwa/assets/fr-CyyJ8S2c.js +0 -17
- package/src/client/dist/pwa/assets/i18n-CobVQiTM.js +0 -1
- package/src/client/dist/pwa/assets/index-ImUH7Bbt.js +0 -20
- package/src/client/dist/pwa/assets/it-XQJW2Xkr.js +0 -17
- package/src/client/dist/pwa/assets/network-auth-D0Vm6a09.js +0 -1
- package/src/client/dist/pwa/assets/notify-theme-PMSxqP6t.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -116,7 +116,8 @@ src/
|
|
|
116
116
|
| `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
|
|
117
117
|
| `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
|
|
118
118
|
| `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
|
|
119
|
-
| `pending_review_returns` |
|
|
119
|
+
| `pending_review_returns` | durable review-to-source return, original LLM configuration and dispatch phase (v44, extended v52) |
|
|
120
|
+
| `auto_loop_final_reviews` | configured final reviewer, review/correction cycle, scoped verdict and progress tracking (v52) |
|
|
120
121
|
| `pending_wakeups` | one-row-per-workspace scheduler for the `schedule_wakeup` MCP tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
|
|
121
122
|
| `pending_quota_backoffs` | persisted quota retries and their attempt counts |
|
|
122
123
|
| `pending_crons` | recurring workspace schedules and their next run |
|
|
@@ -276,6 +277,10 @@ not normal cleanup. The SDK still constructs its runtime command; Kōbō's
|
|
|
276
277
|
`spawnClaudeCodeProcess` hook tracks actual child exit and forwards stderr.
|
|
277
278
|
`EngineProcess.closed` waits for both that exit and iterator completion because
|
|
278
279
|
the SDK's internal cleanup wait is bounded and can return before child exit.
|
|
280
|
+
Liveness must also inspect non-empty SDK content deltas (text, thinking,
|
|
281
|
+
input JSON, signature), not just mapped UI events: long reasoning/tool argument
|
|
282
|
+
generation can otherwise be killed as idle. Do not count empty deltas or neutral
|
|
283
|
+
metadata, or let child deltas cancel a settled parent's drain/grace.
|
|
279
284
|
|
|
280
285
|
**OpenAI Codex** (`codex/`): uses the **`codex app-server` JSON-RPC protocol** (line-delimited JSON over stdio with a long-lived `codex` subprocess). The engine layers are:
|
|
281
286
|
- `jsonrpc/transport.ts` + `jsonrpc/peer.ts`: generic JSON-RPC 2.0 stdio peer (request correlation, notifications, server-initiated requests)
|
|
@@ -363,11 +368,33 @@ Resume selection is scoped to the target engine; never pass a reviewer's native
|
|
|
363
368
|
conversation id to the original engine, even after cancellation or restart.
|
|
364
369
|
The report reassembles text deltas/final snapshots, is bounded to 24,000 characters,
|
|
365
370
|
and links back to the complete review session through the conversation tool.
|
|
366
|
-
|
|
367
|
-
The
|
|
368
|
-
|
|
369
|
-
return. Startup
|
|
370
|
-
|
|
371
|
+
For a manual review, the receiving agent summarizes and awaits instructions before fixes.
|
|
372
|
+
The durable phase tracks `reviewing`, `ready`, `dispatching`, `unknown` and `blocked`.
|
|
373
|
+
Client disconnect and server shutdown preserve the original session, configurations,
|
|
374
|
+
review prompt and completed return prompt. Startup resumes an interrupted reviewer
|
|
375
|
+
or a known-unsent return after admission and confirmed runtime closure. Claim the
|
|
376
|
+
return before dispatch; confirm it only once the engine accepts the initial input.
|
|
377
|
+
Claude's session initialization is insufficient: wait for a foreground acknowledgement
|
|
378
|
+
correlated with the initial message UUID (`EngineProcess.initialPromptAccepted`).
|
|
379
|
+
Codex's readiness follows the accepted `turn.start` response.
|
|
380
|
+
An interrupted dispatch becomes `unknown`, with explicit retry/cancel controls;
|
|
381
|
+
never automatically resend a possibly delivered summary. Explicit user stop cancels
|
|
382
|
+
the continuation. Errors retain recoverable intent and restore original settings.
|
|
383
|
+
|
|
384
|
+
`auto_loop_final_reviews` (migration v52) adds an optional final gate configured
|
|
385
|
+
before execution. Each pass starts a separate Plan review session with its own
|
|
386
|
+
engine/model/effort/instructions. Only the active reviewer capability may submit
|
|
387
|
+
the structured `kobo__submit_final_review` verdict. Findings become work tasks,
|
|
388
|
+
invalidate finalization, and return to the exact original working session for
|
|
389
|
+
automatic corrections under its original permissions. Repeat verification and a
|
|
390
|
+
fresh review until zero findings, then summarize in the source session before
|
|
391
|
+
completion. The source must come from the actual loop controller/ending writer, including
|
|
392
|
+
adoption of an already-running session; never infer it from UI selection. A
|
|
393
|
+
reviewer resume failure invalidates only its own native conversation, preserving
|
|
394
|
+
other source sessions. Confirmed capacity release wakes manual durable returns
|
|
395
|
+
as well as auto-loops. Preserve the loop's admission, quota, stop and stagnation safeguards;
|
|
396
|
+
prose alone cannot clear the gate. Persist this gate and manual review returns
|
|
397
|
+
across restart without allowing a second writer.
|
|
371
398
|
`workspace:configuration` is ephemeral and updates all four LLM settings across tabs.
|
|
372
399
|
|
|
373
400
|
### Delete confirmation
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 2.1.15
|
|
8
|
+
|
|
9
|
+
- feat(auto-loop): add durable final reviews and correction cycles
|
|
10
|
+
|
|
11
|
+
## 2.1.14
|
|
12
|
+
|
|
13
|
+
- fix(auto-loop): correct banner visibility and allow dismissal
|
|
14
|
+
|
|
15
|
+
## 2.1.13
|
|
16
|
+
|
|
17
|
+
- fix(claude): count streaming generation as watchdog activity
|
|
18
|
+
|
|
7
19
|
## 2.1.12
|
|
8
20
|
|
|
9
21
|
- fix(claude): close settled SDK sessions without false watchdogs
|
package/CONFIGURATION.md
CHANGED
|
@@ -615,9 +615,33 @@ receiving agent summarizes the findings and waits for your instructions before
|
|
|
615
615
|
applying fixes. The complete review remains in its own session history.
|
|
616
616
|
|
|
617
617
|
With the option off, subsequent conversation continues with the review LLM.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
original
|
|
618
|
+
Closing the browser or shutting down Kōbō preserves the automatic return. After a
|
|
619
|
+
server restart, Kōbō resumes the interrupted review and returns its summary to the
|
|
620
|
+
original conversation when capacity and quotas permit. A completed review waiting
|
|
621
|
+
to return keeps its summary. Explicitly stopping the agent cancels the return.
|
|
622
|
+
|
|
623
|
+
If the server stops during summary delivery and cannot confirm whether it arrived,
|
|
624
|
+
the chat displays a pending return with **Retry** and **Cancel**. Inspect the original
|
|
625
|
+
conversation before retrying: it may already contain that summary. A failed review
|
|
626
|
+
also keeps a recoverable return instead of silently losing it.
|
|
627
|
+
|
|
628
|
+
### Final review for auto-loop
|
|
629
|
+
|
|
630
|
+
Enable **Final review** in the creation form or the workspace's Auto-loop panel.
|
|
631
|
+
Configure the reviewer engine, model, reasoning effort and additional instructions
|
|
632
|
+
in advance. Workspace templates and remembered creation preferences retain these
|
|
633
|
+
settings. Each review uses a separate session in Plan mode and returns to the exact
|
|
634
|
+
working session with its original LLM settings and permissions.
|
|
635
|
+
|
|
636
|
+
After all work and final checks pass, Kōbō launches the reviewer. Its structured
|
|
637
|
+
findings become tasks for the original agent to correct automatically. The agent
|
|
638
|
+
verifies the corrections and completes final checks, then Kōbō launches a fresh
|
|
639
|
+
review. This cycle continues until a review reports zero findings; the original
|
|
640
|
+
session receives the result before auto-loop completes. Repeated unchanged findings
|
|
641
|
+
trigger a diagnostic and bounded further attempts, then block for attention.
|
|
642
|
+
Closing the client or restarting the server preserves this cycle as well as the
|
|
643
|
+
return. A manual review outside auto-loop still summarizes and waits for your
|
|
644
|
+
instructions before applying fixes.
|
|
621
645
|
|
|
622
646
|
## Workspace templates and duplication
|
|
623
647
|
|
|
@@ -1285,6 +1309,12 @@ agents, and 30 minutes for tools (including background Bash, workflow and MCP
|
|
|
1285
1309
|
jobs). A compaction has a separate absolute 10-minute limit. Waiting for your
|
|
1286
1310
|
answer or permission pauses the inactivity watchdog.
|
|
1287
1311
|
|
|
1312
|
+
Non-empty streaming text, reasoning, tool-argument JSON and signature fragments
|
|
1313
|
+
count as activity even when they are not displayed in chat. Empty deltas and
|
|
1314
|
+
neutral metadata do not reset inactivity, and child output cannot postpone a
|
|
1315
|
+
settled parent's closure. The inactivity limit measures silence, not total time
|
|
1316
|
+
spent generating a large plan or file.
|
|
1317
|
+
|
|
1288
1318
|
Watchdog notices remain in conversation history, but do not display a red error
|
|
1289
1319
|
banner. Other failures retain their banners. Watchdog shutdown requests both
|
|
1290
1320
|
cancellation and closure of the SDK transport. Kōbō still requires confirmed
|
|
@@ -1978,7 +2008,7 @@ and across Unicode normalization boundaries.
|
|
|
1978
2008
|
|
|
1979
2009
|
## Auto-loop completion and recovery
|
|
1980
2010
|
|
|
1981
|
-
Auto-loop remains enabled while work is unfinished. Its panel distinguishes grooming, execution and final verification from waiting, blocked, completed and stopped states. All todos and acceptance criteria must be complete, followed by a successful final verification. The final check runs again after task changes or new instructions. Empty custom finalization settings use the built-in final check.
|
|
2011
|
+
Auto-loop remains enabled while work is unfinished. Its panel distinguishes grooming, execution and final verification from waiting, blocked, completed and stopped states. All todos and acceptance criteria must be complete, followed by a successful final verification. The final check runs again after task changes or new instructions. Empty custom finalization settings use the built-in final check. With [Final review](#final-review-for-auto-loop) configured, completion also requires an independent review with zero findings and the summary returned to the original session. Review findings reopen work and final verification before another review.
|
|
1982
2012
|
|
|
1983
2013
|
Completing an auto-loop task through MCP or HTTP requires structured verification: a non-empty `method`, `summary`, and `checks` array with named checks whose `status` is `passed`. A required test that could not run stays open (`not_run`); writing test source alone is insufficient. These are results reported by the agent, not independently executed by Kōbō.
|
|
1984
2014
|
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Kōbō (工房, "workshop") turns *one agent, one terminal* into a real workflow
|
|
|
19
19
|
|---|---|
|
|
20
20
|
| **🗂️ Isolated worktrees, two engines** | Every workspace is its own git worktree and branch — parallel sessions never collide. Pick Claude Code or OpenAI Codex per workspace, and [continue in a fresh session](#fresh-sessions-and-llm-handoffs) with the same LLM or a different model/engine, preserving the worktree and mission history. |
|
|
21
21
|
| **💬 Live chat, real diff review** | Streaming responses, inline Edit/Write diffs, a reasoning panel, and a Monaco diff viewer with **inline file editing**, conflict resolution, and one-click `Sync` / `Push` / `Open PR` / `Merge`. |
|
|
22
|
-
| **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
|
|
22
|
+
| **🔁 Auto-loop, cron, wakeups** | Turn an agent loose on the task list: it works through tasks, retries on rate limits, and stops itself when there's nothing left, progress stalls, or it needs you. An optional [independent final review](CONFIGURATION.md#final-review-for-auto-loop) returns findings to the original session for automatic correction and repeats until none remain, with durable recovery after restart. Cron schedules and one-shot wakeups keep workspaces moving on their own timeline. |
|
|
23
23
|
| **🔀 Create from a PR/MR, or from a ticket** | Pick an open pull/merge request from GitHub, GitLab, or Bitbucket and Kōbō resolves every local conflict for you before spinning up the workspace. Or start straight from a Notion page or a Sentry issue URL. |
|
|
24
24
|
|
|
25
25
|
## Quick start
|
|
@@ -8,6 +8,8 @@ import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js'
|
|
|
8
8
|
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
9
9
|
import { callMemoryTool, isMemoryOutputSuppressed } from './memory-client.js';
|
|
10
10
|
import { callWorkspaceDialogueTool } from './workspace-dialogue-client.js';
|
|
11
|
+
const finalReviewToken = process.env.KOBO_FINAL_REVIEW_TOKEN;
|
|
12
|
+
const finalReviewSessionId = process.env.KOBO_FINAL_REVIEW_SESSION_ID;
|
|
11
13
|
const handoffId = process.env.KOBO_HANDOFF_ID;
|
|
12
14
|
const handoffToken = process.env.KOBO_HANDOFF_TOKEN;
|
|
13
15
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
@@ -150,6 +152,33 @@ const TASK_ORDER_PROPERTIES = {
|
|
|
150
152
|
},
|
|
151
153
|
};
|
|
152
154
|
const WORKSPACE_SCOPED_TOOLS = [
|
|
155
|
+
{
|
|
156
|
+
name: 'submit_final_review',
|
|
157
|
+
description: 'Submit the final structured verdict for this backend-owned auto-loop review, then end your turn. Include all actionable findings; use an empty findings array only when the entire mission is clear.',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
summary: { type: 'string', minLength: 1, maxLength: 12000 },
|
|
162
|
+
findings: {
|
|
163
|
+
type: 'array',
|
|
164
|
+
maxItems: 100,
|
|
165
|
+
items: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {
|
|
168
|
+
severity: { type: 'string', enum: ['critical', 'important', 'minor'] },
|
|
169
|
+
file: { type: 'string', minLength: 1, maxLength: 1000 },
|
|
170
|
+
line: { type: 'integer', minimum: 1 },
|
|
171
|
+
description: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
172
|
+
recommendation: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
173
|
+
},
|
|
174
|
+
required: ['severity', 'file', 'description', 'recommendation'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ['summary', 'findings'],
|
|
179
|
+
},
|
|
180
|
+
annotations: { destructiveHint: false, openWorldHint: false },
|
|
181
|
+
},
|
|
153
182
|
{
|
|
154
183
|
name: 'submit_session_handoff',
|
|
155
184
|
description: 'Submit the final Markdown handoff requested by Kōbō for this generation turn, then end your turn. Only available during a backend-owned session transfer; does not launch or stop sessions.',
|
|
@@ -643,7 +672,9 @@ function availableTools() {
|
|
|
643
672
|
: GLOBAL_TOOLS;
|
|
644
673
|
return tools.filter((tool) => handoffId && handoffToken
|
|
645
674
|
? tool.name === 'submit_session_handoff' || tool.annotations?.readOnlyHint === true
|
|
646
|
-
:
|
|
675
|
+
: finalReviewToken && finalReviewSessionId
|
|
676
|
+
? tool.name === 'submit_final_review' || tool.annotations?.readOnlyHint === true
|
|
677
|
+
: tool.name !== 'submit_session_handoff' && tool.name !== 'submit_final_review');
|
|
647
678
|
}
|
|
648
679
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: availableTools() }));
|
|
649
680
|
/** Wrap a successful result as an MCP tool response with JSON text content. */
|
|
@@ -675,6 +706,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
675
706
|
return fail(`Tool '${name}' requires a workspace-bound MCP session (KOBO_WORKSPACE_ID not set).`);
|
|
676
707
|
}
|
|
677
708
|
try {
|
|
709
|
+
if (finalReviewToken && finalReviewSessionId && !availableTools().some((tool) => tool.name === name))
|
|
710
|
+
return fail('This is a read-only final review. Only read tools and submit_final_review are available.');
|
|
711
|
+
if (name === 'submit_final_review') {
|
|
712
|
+
if (!workspaceId || !finalReviewToken || !finalReviewSessionId)
|
|
713
|
+
return fail('No final review is active for this MCP invocation.');
|
|
714
|
+
return ok(await backendRequest('POST', `/api/workspaces/${workspaceId}/auto-loop/final-review/report`, {
|
|
715
|
+
token: finalReviewToken,
|
|
716
|
+
sessionId: finalReviewSessionId,
|
|
717
|
+
report: a,
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
678
720
|
if (handoffId && handoffToken && !availableTools().some((tool) => tool.name === name))
|
|
679
721
|
return fail('This turn is reserved for a handoff report. Only read tools and submit_session_handoff are available.');
|
|
680
722
|
if (name === 'submit_session_handoff') {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function initAutoLoopFinalReviewSchema(db) {
|
|
2
|
+
db.exec(`CREATE TABLE IF NOT EXISTS auto_loop_final_reviews (
|
|
3
|
+
workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
4
|
+
configuration TEXT NOT NULL,
|
|
5
|
+
state TEXT NOT NULL DEFAULT 'pending',
|
|
6
|
+
cycle INTEGER NOT NULL DEFAULT 0,
|
|
7
|
+
findings_count INTEGER,
|
|
8
|
+
reason TEXT,
|
|
9
|
+
review_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
|
10
|
+
original_session_id TEXT REFERENCES agent_sessions(id) ON DELETE SET NULL,
|
|
11
|
+
token TEXT,
|
|
12
|
+
verdict TEXT,
|
|
13
|
+
previous_findings TEXT,
|
|
14
|
+
stagnant_cycles INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
return_prompt TEXT,
|
|
16
|
+
updated_at TEXT NOT NULL
|
|
17
|
+
)`);
|
|
18
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { initAutoLoopFinalReviewSchema } from './auto-loop-final-review-schema.js';
|
|
1
2
|
import { initAutoLoopSchema } from './auto-loop-schema.js';
|
|
2
3
|
import { initMcpMessageSchema } from './mcp-message-schema.js';
|
|
3
4
|
import { migrateMemorySchema } from './memory-schema.js';
|
|
4
|
-
import { initReviewReturnSchema } from './review-return-schema.js';
|
|
5
|
+
import { initReviewReturnSchema, migrateDurableReviewReturns } from './review-return-schema.js';
|
|
5
6
|
import { initSchema } from './schema.js';
|
|
6
7
|
import { initSearchSchema } from './search-schema.js';
|
|
7
8
|
import { initSessionHandoffSchema } from './session-handoff-schema.js';
|
|
@@ -799,6 +800,14 @@ export const migrations = [
|
|
|
799
800
|
name: 'persistent-memory-schema',
|
|
800
801
|
migrate: migrateMemorySchema,
|
|
801
802
|
},
|
|
803
|
+
{
|
|
804
|
+
version: 52,
|
|
805
|
+
name: 'durable-final-reviews',
|
|
806
|
+
migrate(db) {
|
|
807
|
+
migrateDurableReviewReturns(db);
|
|
808
|
+
initAutoLoopFinalReviewSchema(db);
|
|
809
|
+
},
|
|
810
|
+
},
|
|
802
811
|
];
|
|
803
812
|
/** Current schema version — always equals the highest migration version. */
|
|
804
813
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
|
@@ -5,6 +5,23 @@ export function initReviewReturnSchema(db) {
|
|
|
5
5
|
original_session_id TEXT NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
|
|
6
6
|
original_configuration TEXT NOT NULL,
|
|
7
7
|
review_configuration TEXT NOT NULL,
|
|
8
|
-
created_at TEXT NOT NULL
|
|
8
|
+
created_at TEXT NOT NULL,
|
|
9
|
+
phase TEXT NOT NULL DEFAULT 'reviewing',
|
|
10
|
+
review_prompt TEXT,
|
|
11
|
+
return_prompt TEXT,
|
|
12
|
+
last_error TEXT
|
|
9
13
|
)`);
|
|
10
14
|
}
|
|
15
|
+
/** v52: retain the return intent and its dispatch boundary across server restarts. */
|
|
16
|
+
export function migrateDurableReviewReturns(db) {
|
|
17
|
+
const columns = new Set(db.prepare('PRAGMA table_info(pending_review_returns)').all().map((row) => row.name));
|
|
18
|
+
for (const [name, declaration] of Object.entries({
|
|
19
|
+
phase: "TEXT NOT NULL DEFAULT 'reviewing'",
|
|
20
|
+
review_prompt: 'TEXT',
|
|
21
|
+
return_prompt: 'TEXT',
|
|
22
|
+
last_error: 'TEXT',
|
|
23
|
+
})) {
|
|
24
|
+
if (!columns.has(name))
|
|
25
|
+
db.exec(`ALTER TABLE pending_review_returns ADD COLUMN ${name} ${declaration}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/server/db/schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initAutoLoopFinalReviewSchema } from './auto-loop-final-review-schema.js';
|
|
1
2
|
import { initAutoLoopSchema } from './auto-loop-schema.js';
|
|
2
3
|
import { initMcpMessageSchema } from './mcp-message-schema.js';
|
|
3
4
|
import { initMemorySchema } from './memory-schema.js';
|
|
@@ -236,6 +237,7 @@ export function initSchema(db) {
|
|
|
236
237
|
initMcpMessageSchema(db);
|
|
237
238
|
initReviewReturnSchema(db);
|
|
238
239
|
initAutoLoopSchema(db);
|
|
240
|
+
initAutoLoopFinalReviewSchema(db);
|
|
239
241
|
initSessionHandoffSchema(db);
|
|
240
242
|
initMemorySchema(db);
|
|
241
243
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -34,7 +34,7 @@ import usageRoutes from './routes/usage.js';
|
|
|
34
34
|
import voiceRouter from './routes/voice.js';
|
|
35
35
|
import workspaceTemplatesRouter from './routes/workspace-templates.js';
|
|
36
36
|
import workspacesRouter from './routes/workspaces.js';
|
|
37
|
-
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
37
|
+
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, resumePendingReviewReturns, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
38
38
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
39
39
|
import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
|
|
40
40
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
@@ -240,6 +240,7 @@ const server = serve({
|
|
|
240
240
|
// ephemeral or SERVER_PORT-selected port. Quota ownership comes first.
|
|
241
241
|
restoreRetryCountsFromDb();
|
|
242
242
|
quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
|
|
243
|
+
resumePendingReviewReturns();
|
|
243
244
|
autoLoopService.rehydrate();
|
|
244
245
|
wakeupService.rehydrate();
|
|
245
246
|
cronService.restoreOnBoot();
|
|
@@ -2,6 +2,8 @@ import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process'
|
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
4
|
import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
|
|
5
|
+
import * as finalReviewService from '../services/auto-loop-final-review-service.js';
|
|
6
|
+
import { getReviewReturnStatus } from '../services/review-return-service.js';
|
|
5
7
|
import { getSearchIndexStatus, searchEvents } from '../services/search-service.js';
|
|
6
8
|
import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
|
|
7
9
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
@@ -466,6 +468,14 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
466
468
|
let setupScriptConfigured = false;
|
|
467
469
|
try {
|
|
468
470
|
const { body, attachments } = await readWorkspaceCreationRequest(c.req.raw);
|
|
471
|
+
if (body.autoLoopFinalReview !== undefined) {
|
|
472
|
+
try {
|
|
473
|
+
finalReviewService.parseFinalReviewConfiguration(body.autoLoopFinalReview);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid final review configuration' }, 400);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
469
479
|
if (body.workflowPolicy !== undefined && !isWorkflowPolicy(body.workflowPolicy))
|
|
470
480
|
return c.json({ error: 'Invalid workflowPolicy' }, 400);
|
|
471
481
|
// workingBranch is derived from git when worktreePath is provided, so
|
|
@@ -821,6 +831,8 @@ app.post('/', migrationGuard, creationBodyLimit, async (c) => {
|
|
|
821
831
|
...(useReusedWorktree ? {} : { worktreesPath: globalSettings.worktreesPath }),
|
|
822
832
|
});
|
|
823
833
|
createdWorkspace = workspace;
|
|
834
|
+
if (body.autoLoopFinalReview !== undefined)
|
|
835
|
+
finalReviewService.configureFinalReview(workspace.id, body.autoLoopFinalReview);
|
|
824
836
|
// Enable auto-loop before starting the initial brainstorming session so
|
|
825
837
|
// its model override is available when selecting that session's model.
|
|
826
838
|
if (body.autoLoop === true) {
|
|
@@ -1700,6 +1712,54 @@ app.get('/auto-loop-states', (c) => {
|
|
|
1700
1712
|
}
|
|
1701
1713
|
});
|
|
1702
1714
|
app.route('/', autoLoopMessagesRoutes);
|
|
1715
|
+
app.get('/:id/auto-loop/final-review', (c) => {
|
|
1716
|
+
const id = c.req.param('id');
|
|
1717
|
+
if (!workspaceService.getWorkspace(id))
|
|
1718
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
1719
|
+
return c.json(finalReviewService.getFinalReviewStatus(id));
|
|
1720
|
+
});
|
|
1721
|
+
app.patch('/:id/auto-loop/final-review', migrationGuard, async (c) => {
|
|
1722
|
+
try {
|
|
1723
|
+
const body = await c.req.json();
|
|
1724
|
+
return c.json(finalReviewService.configureFinalReview(c.req.param('id'), body?.configuration));
|
|
1725
|
+
}
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid request' }, error instanceof ReviewRequestError ? error.status : 400);
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
app.post('/:id/auto-loop/final-review/report', migrationGuard, async (c) => {
|
|
1731
|
+
try {
|
|
1732
|
+
const body = await c.req.json();
|
|
1733
|
+
return c.json(finalReviewService.submitFinalReviewReport(c.req.param('id'), body.sessionId, body.token, body.report));
|
|
1734
|
+
}
|
|
1735
|
+
catch (error) {
|
|
1736
|
+
return c.json({ error: error instanceof Error ? error.message : 'Invalid review report' }, error instanceof ReviewRequestError ? error.status : 400);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
app.get('/:id/review-return', (c) => {
|
|
1740
|
+
const id = c.req.param('id');
|
|
1741
|
+
if (!workspaceService.getWorkspace(id))
|
|
1742
|
+
return c.json({ error: 'Workspace not found' }, 404);
|
|
1743
|
+
return c.json(getReviewReturnStatus(id));
|
|
1744
|
+
});
|
|
1745
|
+
app.post('/:id/review-return/retry', migrationGuard, (c) => {
|
|
1746
|
+
try {
|
|
1747
|
+
agentManager.retryReviewReturn(c.req.param('id'));
|
|
1748
|
+
return c.json(getReviewReturnStatus(c.req.param('id')));
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
return c.json({ error: error instanceof Error ? error.message : 'Review recovery failed' }, 409);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
app.post('/:id/review-return/cancel', migrationGuard, (c) => {
|
|
1755
|
+
try {
|
|
1756
|
+
agentManager.cancelReviewReturn(c.req.param('id'));
|
|
1757
|
+
return c.json({ ok: true });
|
|
1758
|
+
}
|
|
1759
|
+
catch (error) {
|
|
1760
|
+
return c.json({ error: error instanceof Error ? error.message : 'Review cancellation failed' }, 409);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1703
1763
|
// GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
|
|
1704
1764
|
// Admission inspection never changes deadlines, pending schedules or lifecycle state.
|
|
1705
1765
|
app.get('/:id/automatic-admission', (c) => {
|
|
@@ -1979,7 +2039,7 @@ app.delete('/:id/sessions/:sessionId', migrationGuard, (c) => {
|
|
|
1979
2039
|
}
|
|
1980
2040
|
catch (err) {
|
|
1981
2041
|
const message = err instanceof Error ? err.message : String(err);
|
|
1982
|
-
return c.json({ error: message }, message.includes('active session') ? 409 : 500);
|
|
2042
|
+
return c.json({ error: message }, message.includes('active session') || err instanceof workspaceService.ReviewSessionInUseError ? 409 : 500);
|
|
1983
2043
|
}
|
|
1984
2044
|
});
|
|
1985
2045
|
// POST /api/workspaces/:id/deferred-tool-use/answer — resume a deferred
|
|
@@ -75,8 +75,19 @@ class ClaudeInputStream {
|
|
|
75
75
|
closed = false;
|
|
76
76
|
queuedForcedMessages = 0;
|
|
77
77
|
unansweredMessages = new Set();
|
|
78
|
+
initialUuid;
|
|
79
|
+
initialYielded = false;
|
|
80
|
+
acceptInitial;
|
|
81
|
+
rejectInitial;
|
|
82
|
+
initialPromptAccepted = new Promise((resolve, reject) => {
|
|
83
|
+
this.acceptInitial = resolve;
|
|
84
|
+
this.rejectInitial = reject;
|
|
85
|
+
});
|
|
78
86
|
constructor(initialPrompt) {
|
|
79
|
-
|
|
87
|
+
const initial = this.toUserMessage(initialPrompt);
|
|
88
|
+
this.initialUuid = initial.uuid;
|
|
89
|
+
this.messages = [{ message: initial, forced: false }];
|
|
90
|
+
void this.initialPromptAccepted.catch(() => { });
|
|
80
91
|
}
|
|
81
92
|
send(text) {
|
|
82
93
|
if (this.closed)
|
|
@@ -91,6 +102,7 @@ class ClaudeInputStream {
|
|
|
91
102
|
wake?.();
|
|
92
103
|
}
|
|
93
104
|
close() {
|
|
105
|
+
this.rejectInitial(new Error('Claude ended before confirming consumption of the initial prompt'));
|
|
94
106
|
this.closed = true;
|
|
95
107
|
const wake = this.waiting;
|
|
96
108
|
this.waiting = undefined;
|
|
@@ -99,6 +111,19 @@ class ClaudeInputStream {
|
|
|
99
111
|
hasUnansweredInput() {
|
|
100
112
|
return this.queuedForcedMessages > 0 || this.unansweredMessages.size > 0;
|
|
101
113
|
}
|
|
114
|
+
/** Only provider-correlated foreground frames acknowledge this new prompt, never init/history/children. */
|
|
115
|
+
acknowledgeInitialInput(message) {
|
|
116
|
+
if (!this.initialYielded || ('parent_tool_use_id' in message && message.parent_tool_use_id != null))
|
|
117
|
+
return;
|
|
118
|
+
if (!['assistant', 'stream_event', 'result', 'user'].includes(message.type))
|
|
119
|
+
return;
|
|
120
|
+
const frame = message;
|
|
121
|
+
const ids = [...(frame.user_message_uuids ?? []), ...(frame.user_message_uuid ? [frame.user_message_uuid] : [])];
|
|
122
|
+
if (frame.type === 'user' && frame.uuid)
|
|
123
|
+
ids.push(frame.uuid);
|
|
124
|
+
if (ids.includes(this.initialUuid))
|
|
125
|
+
this.acceptInitial();
|
|
126
|
+
}
|
|
102
127
|
acknowledgeResult(result) {
|
|
103
128
|
// One Claude turn can consume several queued prompts. A result counts
|
|
104
129
|
// turns, not messages: only retire the sends explicitly covered by it.
|
|
@@ -133,6 +158,8 @@ class ClaudeInputStream {
|
|
|
133
158
|
if (next.forced)
|
|
134
159
|
this.queuedForcedMessages--;
|
|
135
160
|
this.unansweredMessages.add(next.message.uuid);
|
|
161
|
+
if (next.message.uuid === this.initialUuid)
|
|
162
|
+
this.initialYielded = true;
|
|
136
163
|
yield next.message;
|
|
137
164
|
continue;
|
|
138
165
|
}
|
|
@@ -145,6 +172,24 @@ class ClaudeInputStream {
|
|
|
145
172
|
return { type: 'user', uuid: randomUUID(), message: { role: 'user', content: text }, parent_tool_use_id: null };
|
|
146
173
|
}
|
|
147
174
|
}
|
|
175
|
+
/** Generation can be active without any user-visible mapped event. */
|
|
176
|
+
function hasStreamingContent(msg) {
|
|
177
|
+
if (msg.type !== 'stream_event' || msg.event.type !== 'content_block_delta')
|
|
178
|
+
return false;
|
|
179
|
+
const delta = msg.event.delta;
|
|
180
|
+
switch (delta.type) {
|
|
181
|
+
case 'text_delta':
|
|
182
|
+
return delta.text.length > 0;
|
|
183
|
+
case 'thinking_delta':
|
|
184
|
+
return delta.thinking.length > 0;
|
|
185
|
+
case 'input_json_delta':
|
|
186
|
+
return delta.partial_json.length > 0;
|
|
187
|
+
case 'signature_delta':
|
|
188
|
+
return delta.signature.length > 0;
|
|
189
|
+
default:
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
148
193
|
export function createClaudeCodeEngine() {
|
|
149
194
|
return {
|
|
150
195
|
id: 'claude-code',
|
|
@@ -211,6 +256,13 @@ export function createClaudeCodeEngine() {
|
|
|
211
256
|
// would approve ExitPlanMode and then every edit. Anything that needs a
|
|
212
257
|
// permission, a question included, is refused outright instead.
|
|
213
258
|
if (options.readOnly) {
|
|
259
|
+
// This capability-scoped report changes orchestration metadata only.
|
|
260
|
+
// The backend validates its token and current reviewer session again.
|
|
261
|
+
if (toolName === 'mcp__kobo-tasks__submit_final_review' &&
|
|
262
|
+
options.mcpServers?.some((server) => server.name === 'kobo-tasks' &&
|
|
263
|
+
server.env.KOBO_FINAL_REVIEW_TOKEN &&
|
|
264
|
+
server.env.KOBO_FINAL_REVIEW_SESSION_ID))
|
|
265
|
+
return Promise.resolve({ behavior: 'allow', updatedInput: input });
|
|
214
266
|
return Promise.resolve({
|
|
215
267
|
behavior: 'deny',
|
|
216
268
|
message: 'This is a read-only review session: do not modify anything or ask the user. Put the finding in your final report instead.',
|
|
@@ -659,13 +711,16 @@ export function createClaudeCodeEngine() {
|
|
|
659
711
|
turnLiveness.start();
|
|
660
712
|
try {
|
|
661
713
|
for await (const msg of q) {
|
|
714
|
+
inputStream.acknowledgeInitialInput(msg);
|
|
662
715
|
const events = mapSdkMessage(msg, mapperState);
|
|
663
716
|
const isForeground = !('parent_tool_use_id' in msg && msg.parent_tool_use_id != null);
|
|
717
|
+
const streamingContent = hasStreamingContent(msg);
|
|
664
718
|
const foregroundProgress = isForeground &&
|
|
665
|
-
|
|
666
|
-
ev.kind === 'message:
|
|
667
|
-
|
|
668
|
-
|
|
719
|
+
(streamingContent ||
|
|
720
|
+
events.some((ev) => ev.kind === 'message:text' ||
|
|
721
|
+
ev.kind === 'message:thinking' ||
|
|
722
|
+
ev.kind === 'tool:call' ||
|
|
723
|
+
ev.kind === 'tool:result'));
|
|
669
724
|
// A parent continuation owns the foreground again. Background
|
|
670
725
|
// progress notifications alone do not end the between-turn wait.
|
|
671
726
|
if (foregroundProgress) {
|
|
@@ -847,6 +902,7 @@ export function createClaudeCodeEngine() {
|
|
|
847
902
|
})();
|
|
848
903
|
const engineProcess = {
|
|
849
904
|
ready,
|
|
905
|
+
initialPromptAccepted: inputStream.initialPromptAccepted,
|
|
850
906
|
closed: iteratorPromise.then(() => runtimeClosed),
|
|
851
907
|
get pid() {
|
|
852
908
|
return undefined;
|