@loicngr/kobo 2.1.23 → 2.1.25
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 +40 -1
- package/CHANGELOG.md +8 -0
- package/CONFIGURATION.md +104 -3
- package/dist/mcp-server/kobo-tasks-server.js +3 -2
- package/dist/server/routes/mcp.js +13 -7
- package/dist/server/routes/workspaces.js +41 -7
- package/dist/server/services/agent/engines/claude-code/engine.js +2 -0
- package/dist/server/services/agent/engines/claude-code/progress-reporting-hook.js +99 -0
- package/dist/server/services/dev-server-service.js +3 -59
- package/dist/server/services/forge/github/provider.js +4 -2
- package/dist/server/services/forge/types.js +0 -1
- package/dist/server/services/lifecycle-hook-service.js +0 -8
- package/dist/server/services/mcp-message-delivery-service.js +1 -1
- package/dist/server/services/mcp-message-request-service.js +11 -5
- package/dist/server/services/settings-service.js +15 -0
- package/dist/server/services/workspace-dialogue-service.js +2 -0
- package/dist/server/services/workspace-message-service.js +61 -5
- package/dist/server/utils/dev-server-instance.js +58 -0
- package/dist/server/utils/git-ops.js +29 -20
- package/dist/server/utils/script-runner.js +5 -2
- package/dist/shared/forge-merge.js +5 -0
- package/dist/shared/mcp-attachments.js +58 -0
- package/dist/shared/progress-bar-prompts.js +1 -1
- package/dist/shared/workspace-creation-tools.js +4 -28
- package/dist/shared/workspace-dialogue-tools.js +18 -6
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/{ActivityFeed-1OKX_g3B.js → ActivityFeed-xJMPnXKA.js} +1 -1
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-VoA9uBw0.js → AutoLoopFinalReviewSettings-Dafi7Tue.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-DHl3hD3t.js → ChangelogPage-DH-zUtlf.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-D6EKTtni.js → ComparisonPanel-DwaQyEgb.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-CZzEOVVq.js → CreatePage-DyF_efbs.js} +1 -1
- package/src/client/dist/pwa/assets/{DashboardPage-DjLoUvjA.js → DashboardPage-B8gTooRi.js} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-BivWNITj.js +8 -0
- package/src/client/dist/pwa/assets/{DiffViewer-DEPOa3D7.css → DiffViewer-XzCvEKLJ.css} +1 -1
- package/src/client/dist/pwa/assets/{DocumentsPanel-yrQviqCd.js → DocumentsPanel-DXjjhGUg.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-D5qNeXnn.js → DrawerToggleButton-CiVNMTbq.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-CNFtj6k0.css → GitPanel-CRaLJzjc.css} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-DQjgGtoN.js +3 -0
- package/src/client/dist/pwa/assets/{HealthPage-COicuNQn.js → HealthPage-lc2Cuuks.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-ZcExZTqC.js → MainLayout-D3pGcTx8.js} +2 -2
- package/src/client/dist/pwa/assets/{MemoryPanel-D73Qz9f7.js → MemoryPanel-SrR94UVW.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-BGlq2DkD.js → MemoryProposalList-B5uVf3KU.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-CykBCmwa.js → QBtnDropdown-Cxr3lBPB.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-CAW9J1Uv.js → QExpansionItem-xIXDevKz.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-C_d-8eO7.js → QScrollArea-DeczMHVs.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-C1AN93yA.js → QSelect-GQAsf7pB.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-C9qaLW_h.js → QTooltip-be9mqhmB.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-CQpzDhq7.js → SchedulePanel-R6H-D-m5.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-fZlSSEu_.js → SearchPage-CaeYoIDw.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-BCWdQHS5.js +9 -0
- package/src/client/dist/pwa/assets/SettingsPage-Dcq1Jeoo.css +1 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-CfeO6TZb.js → SplitWorkspacePage-Dw1qUIFU.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CfDZOteT.js → TerminalPanel-Dqw88QqH.js} +1 -1
- package/src/client/dist/pwa/assets/{ToolCallItem-CHE6tylq.js → ToolCallItem-BMn7eela.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-Csev_qU6.js → TouchPan-BwCpe_Oh.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-Crr7tF1b.js → TourReplayButton-yeGYf6xT.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-DpUZOiJv.js → WorkspaceAttentionLabels-BJPw65vU.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-BHO1Q8PG.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-Bk-WWOYM.css → WorkspacePage-BnklZAYZ.css} +1 -1
- package/src/client/dist/pwa/assets/{browser-o7ftkZex.js → browser-C9U-mSDm.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-D6dZQpIb.js → build-path-tree-DYdRW2T4.js} +1 -1
- package/src/client/dist/pwa/assets/{custom-sounds-DVX9zCET.js → custom-sounds-DPsfYErA.js} +1 -1
- package/src/client/dist/pwa/assets/{de-DOE4H_Jf.js → de-Bqil7RJE.js} +3 -3
- package/src/client/dist/pwa/assets/{editor.api-C8TYnUXm.js → editor.api-iu5YkEl3.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-DfsG0_o0.js → efforts-ja6uJadn.js} +1 -1
- package/src/client/dist/pwa/assets/{es-BW57JPlm.js → es-CNcMm1Zz.js} +3 -3
- package/src/client/dist/pwa/assets/{expand-template-CJZlWMfI.js → expand-template-BbgnJ8YH.js} +1 -1
- package/src/client/dist/pwa/assets/{format-DOCqk2Dn.js → format-CLXLy6Ov.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BFocC6LJ.js → formatters-DeKgxYJe.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-DnmIKHaQ.js → fr-BObHEohg.js} +3 -3
- package/src/client/dist/pwa/assets/{freemarker2-CfME4WOK.js → freemarker2-2IrnDDvu.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-uvKdDnJl.js → handlebars-CeLN7FCP.js} +1 -1
- package/src/client/dist/pwa/assets/{html-BrEPgLuD.js → html-Bn3rhcp3.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-Df_y96N2.js +1 -0
- package/src/client/dist/pwa/assets/{index-z1Oh2-I_.js → index-CH4JpI2a.js} +4 -4
- package/src/client/dist/pwa/assets/{it-DSrEQgZQ.js → it-DDMC7Emk.js} +3 -3
- package/src/client/dist/pwa/assets/{javascript-KH2xP4lQ.js → javascript-BMvON8oT.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-D08TlsVs.js → liquid-D_AQLvQl.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-y74A51cS.js → mdx-DDJzMB8Q.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-Cx8iaAhd.js → monaco-contributions-DFKwR-_X.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-CFq0guCl.js → monaco.contribution-4AwFLnjV.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-DnGOOzCV.js → network-auth-CMERg4Qk.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-B9lUAgQl.js → notify-theme-CMzdjL91.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-TgENPFLi.js → project-color-C5mEKdlH.js} +1 -1
- package/src/client/dist/pwa/assets/{python-Cy3bAVPq.js → python-BOqcp9xc.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-gD9QOuiV.js → razor-BrGwBzd7.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-CEItY0bH.js → typescript-mOGf86uY.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-BtJ0pOy9.js → use-search-index-status-P7O7EaV_.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-COLDth2h.js → use-tours-CMFPIW6f.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-CShbwpGy.js → use-worktree-restore-DF6P7h8g.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-BOMNQgBl.js → workspace-templates-C_bgIQvs.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BJvL5qcE.js → xml-YcMBzYAm.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DtkVRZaX.js → yaml-BP_67Sds.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 +43 -4
- package/src/mcp-server/kobo-tasks-server.ts +3 -6
- package/src/client/dist/pwa/assets/DiffViewer-BQ6OEmoi.js +0 -8
- package/src/client/dist/pwa/assets/GitPanel-DCXlhcAD.js +0 -3
- package/src/client/dist/pwa/assets/SettingsPage-BCOF0ssp.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-eyJZxeCJ.js +0 -9
- package/src/client/dist/pwa/assets/WorkspacePage-_QSKLO42.js +0 -4
- package/src/client/dist/pwa/assets/i18n-BmTCjikk.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -223,6 +223,13 @@ reopen a closed database. Startup reconciles lost internal reporters while
|
|
|
223
223
|
preserving external ones. An interrupted reporter is not proof of cancelled
|
|
224
224
|
background work. Operational reporting never satisfies auto-loop verification.
|
|
225
225
|
|
|
226
|
+
Progress without measured counts is labelled explicitly, and the panel shows the
|
|
227
|
+
age of a report after two minutes without a newer one. This local clock must not
|
|
228
|
+
poll or mutate progress. Claude's per-launch PostToolUse advisory tracks only
|
|
229
|
+
observed managed progress responses, separates parent/subagent producers, and
|
|
230
|
+
limits reminders by action count and elapsed time. Do not infer percentages,
|
|
231
|
+
automatically complete bars, or block agent shutdown for missing reports.
|
|
232
|
+
|
|
226
233
|
Retain scoped activity-key fingerprints, producer sequences and dismissed rows
|
|
227
234
|
for retries; subsequent updates cannot resurrect a dismissed indicator or undo a
|
|
228
235
|
terminal state. Bound labels, steps, page size, active/retained records and update
|
|
@@ -243,7 +250,23 @@ Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `tas
|
|
|
243
250
|
|
|
244
251
|
### External MCP dialogue and update checks
|
|
245
252
|
|
|
246
|
-
Workspace-bound stdio, global stdio and HTTP expose shared `create_workspace` / `diagnose_workspace_pr` definitions in `src/shared/workspace-creation-tools.ts`. Stdio forwards them to the backend. `workspace-creation-mcp-service.ts` validates inputs and dispatches through the existing workspace and PR routes in process; preserve UI validation, checkout fingerprints, lifecycle guards and rollback. Inputs use snake_case except the existing camelCase PR checkout decisions. Attachments are inline base64, bounded to the shared 10-file/50 MiB decoded limit before checkout mutations. Keep SDK HTTP/stdio limits aligned with the shared creation envelope budget.
|
|
253
|
+
Workspace-bound stdio, global stdio and HTTP expose shared `create_workspace` / `diagnose_workspace_pr` definitions in `src/shared/workspace-creation-tools.ts`. Stdio forwards them to the backend. `workspace-creation-mcp-service.ts` validates inputs and dispatches through the existing workspace and PR routes in process; preserve UI validation, checkout fingerprints, lifecycle guards and rollback. Inputs use snake_case except the existing camelCase PR checkout decisions. Attachments are inline base64, bounded to the shared 10-file/50 MiB decoded limit before checkout mutations. Keep SDK HTTP/stdio limits aligned with the shared creation envelope budget. Creation and send_workspace_message envelopes with attachments get the larger base64 HTTP body budget; ordinary MCP requests remain 1 MiB. After a known PR creation failure, rollback only a checkout proven newly created, unchanged, clean (including ignored files) and unowned, under the Git repository lock; retain other checkouts and expose recovery details. Do not retry uncertain creation automatically or treat `creation_id` as an idempotency key. Restricted review/handoff launches cannot create workspaces or invoke lifecycle mutations. Shared lifecycle tools (`workspace-lifecycle-tools.ts` / `workspace-lifecycle-mcp-service.ts`) also expose archive, purge, delete, unarchive and restore through the same transports. Reject reserved/bulk/path-like IDs, require explicit purge/delete flags, and preserve warnings. Single-workspace DELETE accepts optional `confirmationBranch`, rechecked under its lifecycle guard before stopping processes; MCP always supplies it. Branch rename/resync share this guard so the confirmed branch and checkout path cannot change during teardown. Recovery uses `restore-worktree` before unarchive and never starts an agent.
|
|
254
|
+
|
|
255
|
+
MCP creation and single-workspace messages share `shared/mcp-attachments.ts`
|
|
256
|
+
validation and inline `{name, mime_type, data_base64}` schemas. Validate before
|
|
257
|
+
writing, reserve keyed-message identity before uploading, and fingerprint the
|
|
258
|
+
original content/session plus attachment names, types and bytes (preserving old
|
|
259
|
+
text-only fingerprints). Delivery saves files under the target worktree using
|
|
260
|
+
the existing attachment service, then passes references to the engine and chat.
|
|
261
|
+
Clean up definitively rejected files; retain them if dispatch is uncertain.
|
|
262
|
+
Never expose raw server filesystem paths as attachment inputs. Group messages
|
|
263
|
+
remain text-only.
|
|
264
|
+
|
|
265
|
+
The global JSON setting `forceImmediateMessages` (v67, default false) bypasses
|
|
266
|
+
client chat queueing for new messages using confirmed immediate delivery. It
|
|
267
|
+
also bypasses the auto-loop next-iteration UI path; normal backend immediate
|
|
268
|
+
message delivery pauses a ready loop. Do not flush existing queues on toggle,
|
|
269
|
+
remove compaction/permission/handoff guards, or queue rejected forced sends.
|
|
247
270
|
|
|
248
271
|
`POST /api/mcp` exposes stateless Streamable HTTP discovery/dialogue tools, behind the existing Host/Origin and network-token gates. The global stdio server forwards dialogue calls through this backend so agent runtime ownership remains centralized. `workspace-message-service.ts` shares delivery with WebSocket chat.
|
|
249
272
|
|
|
@@ -654,6 +677,14 @@ reuse outside PR import still forces setup off. Worktree ownership is unchanged.
|
|
|
654
677
|
|
|
655
678
|
Each event maps to one effective-settings key (`sessionEndedScript` / `prMergedScript` / `autoLoopDisabledScript`, settings migration v55), cascading project-over-global with empty meaning inherit. Execution reuses `runScript` with `eventPrefix: 'hook:<event>'`; `runScript` gained an `extraEnv` option, merged **under** the identity variables so a hook payload can never claim a different `WORKSPACE_ID`. `runLifecycleHook` never rejects and returns `null` when there is nothing to run (no script, unknown workspace, worktree gone). The pr-watcher awaits the `pr-merged` hook before auto-purge so a deploy script does not lose its worktree mid-run. See [CONFIGURATION.md → Lifecycle hooks](CONFIGURATION.md#lifecycle-hooks).
|
|
656
679
|
|
|
680
|
+
The shared `utils/script-runner.ts` supplies `INSTANCE` and `PROJECT_NAME` to
|
|
681
|
+
setup, cleanup, archive and every lifecycle hook, including shutdown. Resolve
|
|
682
|
+
these at execution time through `utils/dev-server-instance.ts`, shared with the
|
|
683
|
+
dev-server service. Never inherit ambient Docker identity or invent a Compose
|
|
684
|
+
project name: `PROJECT_NAME` is empty when no matching instance metadata exists.
|
|
685
|
+
`ScriptVariablesHelp.vue` documents common and event-specific variables in both
|
|
686
|
+
global and project settings; keep this help and CONFIGURATION.md in sync.
|
|
687
|
+
|
|
657
688
|
### Engine comparison
|
|
658
689
|
|
|
659
690
|
Successful paired creation opens the existing `split` route with `left` and `right` workspace ids in creation order. A single successful creation keeps the normal workspace route, including when the second creation fails.
|
|
@@ -723,6 +754,14 @@ Snapshot each target and its delivery mode before scheduling: auto-loop uses `ne
|
|
|
723
754
|
|
|
724
755
|
### File editing in the diff viewer
|
|
725
756
|
|
|
757
|
+
The diff defaults to inline view. Busy workspace status or known live controller disables
|
|
758
|
+
Edit with an explanatory tooltip, and updates Monaco to read-only without discarding
|
|
759
|
+
the current buffer. Historical commit comparisons remain read-only. Unpushed mode
|
|
760
|
+
lists local commits plus working-tree changes (including non-ignored untracked files)
|
|
761
|
+
against the branch remote ref, falling back to the source branch before the first push.
|
|
762
|
+
The list and file-content routes must use the same baseline semantics; failed Git
|
|
763
|
+
reads must not be reported as an empty, clean diff.
|
|
764
|
+
|
|
726
765
|
The right panel of `DiffViewer.vue` is editable when the workspace agent is stopped and the file is not in `deleted` status. `Ctrl/Cmd+S` or the explicit Save button persists the file via `POST /api/workspaces/:id/save-file`, sending `{ path, content, baseSha }` where `baseSha` is the sha256 captured at `GET /diff-file` time. The route refuses with **412 Precondition Failed** + `{ currentSha }` if the on-disk content has changed; the client shows a Reload / Keep mine dialog. Worktree-traversal guards (including parent-symlink escapes) and a 1 MB size cap live in `file-editor-service.ts`. Changing files in the tree while dirty pops an "Unsaved changes" prompt.
|
|
727
766
|
|
|
728
767
|
## Code conventions
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ 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.25
|
|
8
|
+
|
|
9
|
+
- feat: improve merge options, diff controls and progress reporting
|
|
10
|
+
|
|
11
|
+
## 2.1.24
|
|
12
|
+
|
|
13
|
+
- feat: add MCP attachments and immediate chat delivery
|
|
14
|
+
|
|
7
15
|
## 2.1.23
|
|
8
16
|
|
|
9
17
|
- feat: configure shutdown hooks and improve agent guidance
|
package/CONFIGURATION.md
CHANGED
|
@@ -397,14 +397,43 @@ silently.
|
|
|
397
397
|
|
|
398
398
|
### Hook environment variables
|
|
399
399
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
the
|
|
400
|
+
Settings → Scripts and each project's script fields include expandable
|
|
401
|
+
**Available variables** help. Setup, cleanup, archive and lifecycle scripts all
|
|
402
|
+
run in the workspace worktree (`PWD`) and receive:
|
|
403
|
+
|
|
404
|
+
| Variable | Value |
|
|
405
|
+
|---|---|
|
|
406
|
+
| `WORKSPACE_ID`, `WORKSPACE_NAME` | Workspace identifier and display name |
|
|
407
|
+
| `BRANCH_NAME`, `SOURCE_BRANCH` | Working branch and source branch |
|
|
408
|
+
| `PROJECT_PATH` | Main repository path, not the worktree |
|
|
409
|
+
| `INSTANCE` | Matching Docker instance name, or the lowercased branch with `/` and `_` replaced by `-` |
|
|
410
|
+
| `PROJECT_NAME` | Exact Compose project name from the matching `PROJECT_PATH/.container/instances/*.env` file; empty without a known matching instance |
|
|
411
|
+
|
|
412
|
+
Docker identity is resolved at each script execution using the same instance
|
|
413
|
+
metadata as the dev-server panel. It never inherits a Docker project from the
|
|
414
|
+
server's environment or guesses one from a folder name. In particular, setup
|
|
415
|
+
before the first dev-server launch usually has an empty `PROJECT_NAME`.
|
|
416
|
+
Keep the metadata file if later archive/PR hooks need that identity. These
|
|
417
|
+
variables are specific to the shared workspace script runner; dev start/stop
|
|
418
|
+
and change-source-branch commands have their own contracts documented below.
|
|
419
|
+
|
|
420
|
+
Quote variables and guard destructive Docker commands against a missing project:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
# Archive or PR merged — stop this stack, preserving volumes and images.
|
|
424
|
+
docker compose -p "${PROJECT_NAME:?Missing Docker project}" down --remove-orphans
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Add `--volumes --rmi local` only to also remove its Compose-managed volumes and
|
|
428
|
+
images without custom tags. This does not remove every image on the Docker host.
|
|
429
|
+
|
|
430
|
+
Lifecycle hooks also receive `KOBO_HOOK_EVENT` naming the event. On top of that:
|
|
403
431
|
|
|
404
432
|
| Hook | Extra variables |
|
|
405
433
|
|---|---|
|
|
406
434
|
| Session ended | `KOBO_SESSION_ID`, `KOBO_SESSION_END_REASON` (`completed` / `error` / `killed` / `watchdog`), `KOBO_SESSION_EXIT_CODE` (empty when the engine reports none), `KOBO_SESSION_STOP_CAUSE` (`user`, `replacement`, `setup`, `archive`, or `shutdown`; empty for a natural end; superseded sessions and delete/purge stops skip the hook), `KOBO_AUTOLOOP_ACTIVE` (`1` when auto-loop is enabled, so a test run may overlap with the next agent's edits) |
|
|
407
435
|
| PR merged | `KOBO_PR_NUMBER`, `KOBO_PR_URL` |
|
|
436
|
+
| Kōbō shutdown | `KOBO_SHUTDOWN_SIGNAL` |
|
|
408
437
|
| Auto-loop stopped | `KOBO_AUTOLOOP_REASON` (`completed` / `user-action`; blocked missions retain their intent and do not fire this hook), `KOBO_TASKS_PENDING` |
|
|
409
438
|
|
|
410
439
|
Only `KOBO_`-prefixed variables are passed through from the event; a hook
|
|
@@ -851,6 +880,13 @@ The setting lives in `settings.json` under `global` and has no UI control yet:
|
|
|
851
880
|
|
|
852
881
|
An external MCP client can connect to `http://127.0.0.1:3000/api/mcp` using Streamable HTTP (use your backend's configured port). It can create workspaces with the same options as the form, diagnose and check out existing PRs, discover workspaces, read their conversations, send messages and answer pending questions. Workspace-bound stdio, global stdio and HTTP share the creation contract, including brainstorm settings, final auto-loop review, workflow policy, imports, existing worktrees, comparison grouping and inline attachments. All three transports also expose archive, disk-space purge, permanent deletion with explicit branch confirmation, unarchive and recovery of purged worktrees. See the [creation and PR guide](src/mcp-server/README.md#create_workspace) for fields, explicit checkout decisions and timeout/retry behavior.
|
|
853
882
|
|
|
883
|
+
Both `create_workspace` and `send_workspace_message` accept inline `attachments`
|
|
884
|
+
with `{name, mime_type, data_base64}`: 10 files, 50 MiB decoded total, same formats
|
|
885
|
+
as the interface. Messages may contain only attachments. Kōbō saves them in the
|
|
886
|
+
destination worktree and sends file/image references to the agent and chat.
|
|
887
|
+
Keyed retries include attachment identity and never upload or send twice. See
|
|
888
|
+
[Sending files and images](src/mcp-server/README.md#sending-files-and-images).
|
|
889
|
+
|
|
854
890
|
Remote HTTP clients follow the [network access](#network-access) settings and supply the Kōbō token as `Authorization: Bearer <token>` or `X-Kobo-Token`. The endpoint uses the same Host/Origin and reverse-proxy policy as the rest of the backend. No network setting is changed automatically.
|
|
855
891
|
|
|
856
892
|
See [Connect an external LLM](src/mcp-server/README.md#connect-an-external-llm) for connection examples, tool arguments, cursor polling and authentication details.
|
|
@@ -913,6 +949,20 @@ resume, not retroactively to an already running agent.
|
|
|
913
949
|
|
|
914
950
|
The service retains at most 1,000 indicators per session, with at most 32 running
|
|
915
951
|
at once. Hidden records are retained so retries cannot recreate dismissed bars.
|
|
952
|
+
When a bar has no numeric total, the UI explicitly says **Progress not quantified**.
|
|
953
|
+
After two minutes without a newer report it shows **No update for N min**. This
|
|
954
|
+
is the age of the last received report, not proof that work stopped. The display
|
|
955
|
+
clock does not poll the server or manufacture progress.
|
|
956
|
+
|
|
957
|
+
Claude launches with progress tools also receive a bounded `PostToolUse` reminder:
|
|
958
|
+
once on creation, then after at least eight subsequent tool completions and one
|
|
959
|
+
minute without a report. Reminders are scoped to the producing agent/subagent,
|
|
960
|
+
use the last observed sequence, and follow an explicit reporting handoff. They
|
|
961
|
+
are advisory: no extra tool calls, automatic percentages or blocking stop hooks.
|
|
962
|
+
Codex and external clients retain the shared MCP reporting guidance. Agents must
|
|
963
|
+
supply measured counts when a real test total or fixed checklist is available;
|
|
964
|
+
step-only updates correctly change the description without filling the gauge.
|
|
965
|
+
|
|
916
966
|
The [MCP progress guide](src/mcp-server/README.md#session-progress-tools) documents
|
|
917
967
|
arguments, ordering, retry behavior and update limits.
|
|
918
968
|
|
|
@@ -1425,6 +1475,23 @@ Kōbō talks to the embedded [`@anthropic-ai/claude-agent-sdk`](https://github.c
|
|
|
1425
1475
|
|
|
1426
1476
|
The model selector is populated from [`src/shared/models.ts`](./src/shared/models.ts), also used by backend validation. This checkout includes Fable 5.1, Mythos 5, Opus 5, Opus 4.8, Sonnet 5 and older variants. Pick the default in **Settings → Agents**. Catalogue entries describe what Kōbō offers; provider access still depends on your account. `auto` leaves model selection to the runtime.
|
|
1427
1477
|
|
|
1478
|
+
#### Immediate chat messages
|
|
1479
|
+
|
|
1480
|
+
**Settings → Agents → Force immediate message delivery** controls
|
|
1481
|
+
`forceImmediateMessages`, **false by default**, including existing installations.
|
|
1482
|
+
When enabled, new chat messages reach a busy Claude/Codex agent directly, like
|
|
1483
|
+
**Ctrl+Enter**, instead of waiting in Kōbō's message queue. Messages sent during
|
|
1484
|
+
an auto-loop go through immediate delivery and pause the active, ready loop.
|
|
1485
|
+
Previously queued instructions stay queued; changing the setting never sends
|
|
1486
|
+
old drafts automatically. The setting affects subsequent sends without restarting
|
|
1487
|
+
the server or agent.
|
|
1488
|
+
|
|
1489
|
+
Compaction, pending questions/permissions, archive and session handoff guards
|
|
1490
|
+
still apply. Failed or rejected sends restore the draft; they do not silently
|
|
1491
|
+
queue it. MCP messages already use direct delivery and do not depend on this
|
|
1492
|
+
interface setting. Explicit next-iteration scheduling remains available through
|
|
1493
|
+
its dedicated auto-loop API.
|
|
1494
|
+
|
|
1428
1495
|
#### Session watchdogs
|
|
1429
1496
|
|
|
1430
1497
|
**Settings → Agents → Enable agent inactivity watchdogs**, at the top of the tab,
|
|
@@ -1735,6 +1802,40 @@ Adapt the port stride, the `APP_NAME`, and any extra `*_PORT` keys to your stack
|
|
|
1735
1802
|
|
|
1736
1803
|
Kōbō can open pull requests (GitHub or Bitbucket) or merge requests (GitLab) directly from the Git panel. The **forge** setting controls which service is used for a given project.
|
|
1737
1804
|
|
|
1805
|
+
### Review and edit a diff
|
|
1806
|
+
|
|
1807
|
+
The diff viewer opens in **Inline** view. Use **Side by side** to change the layout.
|
|
1808
|
+
While an agent is working, **Edit** is disabled and its tooltip explains why;
|
|
1809
|
+
the diff remains readable and review comments remain available. The editor also
|
|
1810
|
+
becomes read-only if an agent starts while the diff is open. Stop the agent to
|
|
1811
|
+
resume editing. Server-side save checks remain in force.
|
|
1812
|
+
|
|
1813
|
+
**Unpushed** compares the current working tree with `origin/<workingBranch>`:
|
|
1814
|
+
it includes local commits, staged and unstaged changes, deletions, and non-ignored
|
|
1815
|
+
untracked files. Before the first push, it uses the source branch instead. The
|
|
1816
|
+
header shows the comparison base. **Branch** retains its separate opt-in for
|
|
1817
|
+
untracked files. Comparing historical commits remains read-only.
|
|
1818
|
+
|
|
1819
|
+
### Merge a pull request
|
|
1820
|
+
|
|
1821
|
+
In the Git panel, **Merge PR** opens a confirmation. For GitHub, choose:
|
|
1822
|
+
|
|
1823
|
+
- **Merge commit** (default): preserve the commits and create a merge commit.
|
|
1824
|
+
- **Squash and merge**: combine the changes into one commit on the target branch.
|
|
1825
|
+
- **Rebase and merge**: replay the commits on the target branch without a merge commit.
|
|
1826
|
+
|
|
1827
|
+
The selection applies to this operation only. Kōbō rechecks PR readiness before
|
|
1828
|
+
calling the forge. Repository policies still apply: a disallowed method is
|
|
1829
|
+
reported as an error, with no automatic fallback or protection bypass. The
|
|
1830
|
+
source branch is kept; after a confirmed merge, remote deletion remains a
|
|
1831
|
+
separate confirmation. GitLab and Bitbucket retain their existing merge behavior.
|
|
1832
|
+
|
|
1833
|
+
`POST /api/workspaces/:id/merge-pr` accepts optional JSON
|
|
1834
|
+
`{ "method": "merge" | "squash" | "rebase" }` for integrations advertising the
|
|
1835
|
+
corresponding `forge.capabilities.mergeMethods`. An omitted body keeps the
|
|
1836
|
+
existing provider default. Invalid values return 400; unsupported methods return
|
|
1837
|
+
409 before any merge command runs.
|
|
1838
|
+
|
|
1738
1839
|
### Modes
|
|
1739
1840
|
|
|
1740
1841
|
| Value | Behaviour |
|
|
@@ -3,9 +3,10 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { getDb } from '../server/db/index.js';
|
|
6
|
+
import { MAX_MCP_ATTACHMENT_REQUEST_BYTES } from '../shared/mcp-attachments.js';
|
|
6
7
|
import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName, MEMORY_TOOL_DEFINITIONS } from '../shared/memory-tools.js';
|
|
7
8
|
import { INTERNAL_PROGRESS_BAR_TOOLS, isProgressBarTool, PROGRESS_BAR_TOOLS } from '../shared/progress-bar-tools.js';
|
|
8
|
-
import { isWorkspaceCreationTool,
|
|
9
|
+
import { isWorkspaceCreationTool, WORKSPACE_CREATION_TOOLS } from '../shared/workspace-creation-tools.js';
|
|
9
10
|
import { WORKSPACE_DIALOGUE_TOOLS } from '../shared/workspace-dialogue-tools.js';
|
|
10
11
|
import { isWorkspaceGroupMessageTool, WORKSPACE_GROUP_MESSAGE_TOOLS } from '../shared/workspace-group-message-tools.js';
|
|
11
12
|
import { isWorkspaceLifecycleTool, WORKSPACE_LIFECYCLE_TOOLS } from '../shared/workspace-lifecycle-tools.js';
|
|
@@ -1023,7 +1024,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1023
1024
|
});
|
|
1024
1025
|
const transport = new StdioServerTransport(process.stdin, process.stdout, {
|
|
1025
1026
|
// Include the JSON-lines delimiter in addition to the HTTP envelope budget.
|
|
1026
|
-
maxBufferSize:
|
|
1027
|
+
maxBufferSize: MAX_MCP_ATTACHMENT_REQUEST_BYTES + 1,
|
|
1027
1028
|
});
|
|
1028
1029
|
server.connect(transport).catch((err) => {
|
|
1029
1030
|
console.error('[kobo-tasks-server] Fatal:', err);
|
|
@@ -3,9 +3,10 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { bodyLimit } from 'hono/body-limit';
|
|
6
|
+
import { MAX_MCP_ATTACHMENT_REQUEST_BYTES } from '../../shared/mcp-attachments.js';
|
|
6
7
|
import { EXTERNAL_MEMORY_TOOL_DEFINITIONS, isMemoryToolName } from '../../shared/memory-tools.js';
|
|
7
8
|
import { isProgressBarTool, PROGRESS_BAR_TOOLS } from '../../shared/progress-bar-tools.js';
|
|
8
|
-
import { isWorkspaceCreationTool,
|
|
9
|
+
import { isWorkspaceCreationTool, WORKSPACE_CREATION_TOOLS } from '../../shared/workspace-creation-tools.js';
|
|
9
10
|
import { EXTERNAL_MCP_TOOLS } from '../../shared/workspace-dialogue-tools.js';
|
|
10
11
|
import { isWorkspaceGroupMessageTool, WORKSPACE_GROUP_MESSAGE_TOOLS, } from '../../shared/workspace-group-message-tools.js';
|
|
11
12
|
import { isWorkspaceLifecycleTool, WORKSPACE_LIFECYCLE_TOOLS } from '../../shared/workspace-lifecycle-tools.js';
|
|
@@ -19,22 +20,27 @@ import { executeWorkspaceGroupMessageTool, WorkspaceGroupMessageMcpError, } from
|
|
|
19
20
|
import { executeWorkspaceLifecycleTool, WorkspaceLifecycleMcpError, } from '../services/workspace-lifecycle-mcp-service.js';
|
|
20
21
|
import { prepareMemoryToolEnvelope } from '../utils/memory-token-budget.js';
|
|
21
22
|
const app = new Hono();
|
|
22
|
-
// Only creation envelopes may carry the larger, base64 attachment payload.
|
|
23
|
+
// Only creation and message envelopes may carry the larger, base64 attachment payload.
|
|
23
24
|
// Other tools retain their existing 1 MiB limit, including malformed envelopes.
|
|
24
|
-
app.use('*', bodyLimit({ maxSize:
|
|
25
|
+
app.use('*', bodyLimit({ maxSize: MAX_MCP_ATTACHMENT_REQUEST_BYTES }));
|
|
25
26
|
app.use('*', async (c, next) => {
|
|
26
27
|
if (c.req.raw.body) {
|
|
27
28
|
const body = await c.req.raw.clone().arrayBuffer();
|
|
28
29
|
if (body.byteLength > 1024 * 1024) {
|
|
29
|
-
let
|
|
30
|
+
let attachmentTool = false;
|
|
30
31
|
try {
|
|
31
32
|
const envelope = JSON.parse(new TextDecoder().decode(body));
|
|
32
|
-
|
|
33
|
+
attachmentTool =
|
|
34
|
+
envelope?.method === 'tools/call' &&
|
|
35
|
+
(envelope?.params?.name === 'create_workspace' ||
|
|
36
|
+
(envelope?.params?.name === 'send_workspace_message' &&
|
|
37
|
+
Array.isArray(envelope?.params?.arguments?.attachments) &&
|
|
38
|
+
envelope.params.arguments.attachments.length > 0));
|
|
33
39
|
}
|
|
34
40
|
catch {
|
|
35
41
|
/* Preserve the normal body-size rejection for malformed input. */
|
|
36
42
|
}
|
|
37
|
-
if (!
|
|
43
|
+
if (!attachmentTool)
|
|
38
44
|
return c.json({ error: 'MCP request is too large' }, 413);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
@@ -64,7 +70,7 @@ app.all('/', async (c) => {
|
|
|
64
70
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
65
71
|
sessionIdGenerator: undefined,
|
|
66
72
|
enableJsonResponse: true,
|
|
67
|
-
maxRequestBodySize:
|
|
73
|
+
maxRequestBodySize: MAX_MCP_ATTACHMENT_REQUEST_BYTES,
|
|
68
74
|
});
|
|
69
75
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
70
76
|
tools: [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { nanoid } from 'nanoid';
|
|
4
|
+
import { isMergeMethod } from '../../shared/forge-merge.js';
|
|
4
5
|
import { isWorkflowPolicy, resolveWorkflowPolicy, WORKFLOW_ACTIONS, } from '../../shared/workflow-policy.js';
|
|
5
6
|
import * as finalReviewService from '../services/auto-loop-final-review-service.js';
|
|
6
7
|
import { getReviewReturnStatus } from '../services/review-return-service.js';
|
|
@@ -3483,15 +3484,14 @@ app.get('/:id/git-stats', async (c) => {
|
|
|
3483
3484
|
// `origin/<sourceBranch>` (which can be ahead of the local ref when
|
|
3484
3485
|
// upstream landed merges Kōbō hasn't pulled). The handler refreshes
|
|
3485
3486
|
// `origin/<sourceBranch>` synchronously so the diff is never stale.
|
|
3486
|
-
// - `unpushed`:
|
|
3487
|
-
//
|
|
3487
|
+
// - `unpushed`: local commits + current worktree changes (including untracked)
|
|
3488
|
+
// vs `origin/<workingBranch>`, falling back to the source branch before the first push.
|
|
3488
3489
|
app.get('/:id/diff', async (c) => {
|
|
3489
3490
|
try {
|
|
3490
3491
|
const id = c.req.param('id');
|
|
3491
3492
|
const rawMode = c.req.query('mode');
|
|
3492
3493
|
const mode = rawMode === 'unpushed' ? 'unpushed' : rawMode === 'commits' ? 'commits' : 'branch';
|
|
3493
|
-
//
|
|
3494
|
-
// mode — `unpushed` is committed-only by definition.
|
|
3494
|
+
// Branch mode keeps its untracked opt-in; unpushed always includes local untracked files.
|
|
3495
3495
|
const includeUntracked = c.req.query('includeUntracked') === '1';
|
|
3496
3496
|
const workspace = workspaceService.getWorkspace(id);
|
|
3497
3497
|
if (!workspace) {
|
|
@@ -3525,7 +3525,7 @@ app.get('/:id/diff', async (c) => {
|
|
|
3525
3525
|
await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
|
|
3526
3526
|
}
|
|
3527
3527
|
const files = mode === 'unpushed'
|
|
3528
|
-
? await gitOps.getUnpushedChangedFilesAsync(worktreePath, workspace.workingBranch)
|
|
3528
|
+
? await gitOps.getUnpushedChangedFilesAsync(worktreePath, workspace.workingBranch, 'origin', workspace.sourceBranch)
|
|
3529
3529
|
: await gitOps.getChangedFilesAsync(worktreePath, workspace.sourceBranch, includeUntracked);
|
|
3530
3530
|
c.header('Cache-Control', 'no-store');
|
|
3531
3531
|
return c.json({
|
|
@@ -3533,6 +3533,11 @@ app.get('/:id/diff', async (c) => {
|
|
|
3533
3533
|
mode,
|
|
3534
3534
|
sourceBranch: workspace.sourceBranch,
|
|
3535
3535
|
workingBranch: workspace.workingBranch,
|
|
3536
|
+
...(mode === 'unpushed'
|
|
3537
|
+
? {
|
|
3538
|
+
baseRef: await gitOps.resolveUnpushedBaseAsync(worktreePath, workspace.workingBranch, workspace.sourceBranch),
|
|
3539
|
+
}
|
|
3540
|
+
: {}),
|
|
3536
3541
|
});
|
|
3537
3542
|
}
|
|
3538
3543
|
catch (err) {
|
|
@@ -3591,7 +3596,9 @@ app.get('/:id/diff-file', async (c) => {
|
|
|
3591
3596
|
to,
|
|
3592
3597
|
});
|
|
3593
3598
|
}
|
|
3594
|
-
const baseRef = mode === 'unpushed'
|
|
3599
|
+
const baseRef = mode === 'unpushed'
|
|
3600
|
+
? await gitOps.resolveUnpushedBaseAsync(worktreePath, workspace.workingBranch, workspace.sourceBranch)
|
|
3601
|
+
: workspace.sourceBranch;
|
|
3595
3602
|
const original = await gitOps.getFileAtRefAsync(worktreePath, baseRef, filePath);
|
|
3596
3603
|
const modified = gitOps.getFileContent(worktreePath, filePath);
|
|
3597
3604
|
c.header('Cache-Control', 'no-store');
|
|
@@ -4211,10 +4218,32 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
4211
4218
|
const workspace = workspaceService.getWorkspace(id);
|
|
4212
4219
|
if (!workspace)
|
|
4213
4220
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
4221
|
+
const rawBody = await c.req.text();
|
|
4222
|
+
let body = {};
|
|
4223
|
+
if (rawBody.length > 0) {
|
|
4224
|
+
let parsed;
|
|
4225
|
+
try {
|
|
4226
|
+
parsed = JSON.parse(rawBody);
|
|
4227
|
+
}
|
|
4228
|
+
catch {
|
|
4229
|
+
return c.json({ error: 'Request body must be valid JSON' }, 400);
|
|
4230
|
+
}
|
|
4231
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
4232
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
4233
|
+
}
|
|
4234
|
+
body = parsed;
|
|
4235
|
+
}
|
|
4236
|
+
const method = body.method;
|
|
4237
|
+
if (method !== undefined && !isMergeMethod(method)) {
|
|
4238
|
+
return c.json({ error: 'method must be merge, squash or rebase' }, 400);
|
|
4239
|
+
}
|
|
4214
4240
|
const provider = getForgeProvider(resolveForge(workspace.projectPath));
|
|
4215
4241
|
if (!provider.capabilities.canMergeRequest) {
|
|
4216
4242
|
return c.json({ error: 'This project has no forge that can merge the PR', code: 'forge_unsupported' }, 409);
|
|
4217
4243
|
}
|
|
4244
|
+
if (method !== undefined && !provider.capabilities.mergeMethods?.includes(method)) {
|
|
4245
|
+
return c.json({ error: 'This forge does not support the requested merge method', code: 'merge_method_unsupported' }, 409);
|
|
4246
|
+
}
|
|
4218
4247
|
const availability = await provider.isAvailable(workspace.worktreePath);
|
|
4219
4248
|
if (!availability.available) {
|
|
4220
4249
|
return c.json({
|
|
@@ -4229,7 +4258,12 @@ app.post('/:id/merge-pr', async (c) => {
|
|
|
4229
4258
|
if (!snapshot.readyToMerge) {
|
|
4230
4259
|
return c.json({ error: 'This PR is not ready to merge', code: 'pr_not_ready' }, 409);
|
|
4231
4260
|
}
|
|
4232
|
-
|
|
4261
|
+
if (method === undefined) {
|
|
4262
|
+
await provider.mergeRequest(workspace.worktreePath, snapshot.number);
|
|
4263
|
+
}
|
|
4264
|
+
else {
|
|
4265
|
+
await provider.mergeRequest(workspace.worktreePath, snapshot.number, method);
|
|
4266
|
+
}
|
|
4233
4267
|
const merged = await provider.getPrStatus(workspace.worktreePath, workspace.workingBranch);
|
|
4234
4268
|
return c.json({ success: true, merged: merged?.state === 'MERGED', branch: workspace.workingBranch });
|
|
4235
4269
|
}
|
|
@@ -14,6 +14,7 @@ import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
|
|
|
14
14
|
import { createMapperState, mapSdkMessage, QUOTA_PATTERN, tryEmitQuota } from './event-mapper.js';
|
|
15
15
|
import { buildClaudeOptions } from './options-builder.js';
|
|
16
16
|
import { buildCompactionSessionStartOutput } from './precompact-hook.js';
|
|
17
|
+
import { createProgressReportingHook } from './progress-reporting-hook.js';
|
|
17
18
|
import { resolveClaudeBinaryPath } from './resolve-binary.js';
|
|
18
19
|
import { buildStopHookOutput } from './stop-hook.js';
|
|
19
20
|
/**
|
|
@@ -341,6 +342,7 @@ export function createClaudeCodeEngine() {
|
|
|
341
342
|
const hooks = {
|
|
342
343
|
...(options.mcpServers?.some((server) => server.name === 'kobo-tasks' && server.env.KOBO_PROGRESS_SESSION_TOKEN)
|
|
343
344
|
? {
|
|
345
|
+
PostToolUse: [{ hooks: [createProgressReportingHook()] }],
|
|
344
346
|
SubagentStart: [
|
|
345
347
|
{
|
|
346
348
|
hooks: [
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { PROGRESS_MAX_RETAINED, PROGRESS_MAX_RUNNING } from '../../../../../shared/progress-bars.js';
|
|
2
|
+
function object(value) {
|
|
3
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
4
|
+
}
|
|
5
|
+
function progressResponse(value) {
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
if (value.length > 16_384)
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
value = JSON.parse(value);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const result = object(value);
|
|
17
|
+
if (result?.isError === true)
|
|
18
|
+
return null;
|
|
19
|
+
if (result && object(result.structuredContent))
|
|
20
|
+
return object(result.structuredContent);
|
|
21
|
+
const blocks = Array.isArray(value) ? value : result?.content;
|
|
22
|
+
if (Array.isArray(blocks)) {
|
|
23
|
+
for (const entry of blocks.slice(0, 4)) {
|
|
24
|
+
const item = object(entry);
|
|
25
|
+
if (item?.type !== 'text' || typeof item.text !== 'string' || item.text.length > 16_384)
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
return object(JSON.parse(item.text));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* Not a JSON progress response. */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
/** Per-launch advisory only: no timers, tool calls, writes or stop-blocking loops. */
|
|
39
|
+
export function createProgressReportingHook() {
|
|
40
|
+
const bars = new Map();
|
|
41
|
+
const retired = new Set();
|
|
42
|
+
const reminder = (entries) => ({
|
|
43
|
+
hookSpecificOutput: {
|
|
44
|
+
hookEventName: 'PostToolUse',
|
|
45
|
+
additionalContext: `Kōbō progress reporting: keep these existing activities current at actual milestones. Next update arguments: ${entries.map(([id, state]) => JSON.stringify({ progress_id: id, sequence: state.sequence + 1 })).join('; ')}. Add step and measured current/total when known; otherwise report step only. Call update_progress_bar after meaningful work, and finish_progress_bar with the known outcome before your final report. Do not invent progress, poll, repeat unchanged updates, recreate dismissed bars or finish background work that is still running.`,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return async (input) => {
|
|
49
|
+
if (input.hook_event_name !== 'PostToolUse' || retired.size >= PROGRESS_MAX_RETAINED)
|
|
50
|
+
return {};
|
|
51
|
+
const producer = JSON.stringify([input.session_id, input.agent_id ?? null]);
|
|
52
|
+
const tool = input.tool_name;
|
|
53
|
+
if (/^mcp__kobo-tasks__(create|update|finish)_progress_bar$/.test(tool)) {
|
|
54
|
+
const bar = progressResponse(input.tool_response);
|
|
55
|
+
if (!bar ||
|
|
56
|
+
typeof bar.id !== 'string' ||
|
|
57
|
+
!/^[\w-]{1,200}$/.test(bar.id) ||
|
|
58
|
+
!Number.isSafeInteger(bar.sequence) ||
|
|
59
|
+
bar.sequence < 0 ||
|
|
60
|
+
!['running', 'succeeded', 'failed', 'interrupted'].includes(bar.status))
|
|
61
|
+
return {};
|
|
62
|
+
if (retired.has(bar.id))
|
|
63
|
+
return {};
|
|
64
|
+
const existing = bars.get(bar.id);
|
|
65
|
+
if (existing && bar.sequence < existing.sequence)
|
|
66
|
+
return {};
|
|
67
|
+
if (bar.dismissed === true || bar.status !== 'running') {
|
|
68
|
+
bars.delete(bar.id);
|
|
69
|
+
// Parallel hook results may arrive after completion; never resurrect a reporter.
|
|
70
|
+
retired.add(bar.id);
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
// A retry is not progress. A real update may hand reporting to a delegate.
|
|
74
|
+
if (existing && bar.sequence <= existing.sequence)
|
|
75
|
+
return {};
|
|
76
|
+
if (!existing && !tool.endsWith('create_progress_bar'))
|
|
77
|
+
return {};
|
|
78
|
+
if (!existing && bars.size >= PROGRESS_MAX_RUNNING)
|
|
79
|
+
return {};
|
|
80
|
+
const state = { producer, sequence: bar.sequence, actions: 0, remindedAt: Date.now() };
|
|
81
|
+
bars.set(bar.id, state);
|
|
82
|
+
return existing ? {} : reminder([[bar.id, state]]);
|
|
83
|
+
}
|
|
84
|
+
if (tool.startsWith('mcp__kobo-tasks__') && tool.endsWith('progress_bars'))
|
|
85
|
+
return {};
|
|
86
|
+
const due = [];
|
|
87
|
+
for (const [id, state] of bars) {
|
|
88
|
+
if (state.producer !== producer)
|
|
89
|
+
continue;
|
|
90
|
+
state.actions++;
|
|
91
|
+
if (state.actions >= 8 && Date.now() - state.remindedAt >= 60_000 && due.length < 3) {
|
|
92
|
+
state.actions = 0;
|
|
93
|
+
state.remindedAt = Date.now();
|
|
94
|
+
due.push([id, state]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return due.length ? reminder(due) : {};
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync
|
|
3
|
-
import
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolveInstance, sanitizeBranchName } from '../utils/dev-server-instance.js';
|
|
4
4
|
import { assertWorkspaceLifecycleAvailable } from '../utils/workspace-lifecycle-guard.js';
|
|
5
5
|
import { getProjectSettings } from './settings-service.js';
|
|
6
6
|
import { emitEphemeral } from './websocket-service.js';
|
|
7
7
|
import { getWorkspace, listWorkspaces, updateDevServerStatus } from './workspace-service.js';
|
|
8
|
+
export { parseEnvFile, resolveInstance, sanitizeBranchName } from '../utils/dev-server-instance.js';
|
|
8
9
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
10
|
/** Build a clean env for child processes, stripping Kobo-specific variables. */
|
|
10
11
|
function cleanEnv() {
|
|
@@ -37,63 +38,6 @@ export function _resetTrackedProcessesForTests() {
|
|
|
37
38
|
generations.clear();
|
|
38
39
|
shuttingDown = false;
|
|
39
40
|
}
|
|
40
|
-
// ── Pure helpers ───────────────────────────────────────────────────────────────
|
|
41
|
-
/**
|
|
42
|
-
* Sanitize a branch name for use as a Docker instance name.
|
|
43
|
-
* Replace `/` and `_` with `-`, lowercase.
|
|
44
|
-
*/
|
|
45
|
-
export function sanitizeBranchName(branch) {
|
|
46
|
-
return branch.toLowerCase().replace(/[/_]/g, '-');
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Parse a `.env` file content into key=value pairs.
|
|
50
|
-
* Skips empty lines and comments (#). Handles quotes.
|
|
51
|
-
*/
|
|
52
|
-
export function parseEnvFile(content) {
|
|
53
|
-
const result = {};
|
|
54
|
-
for (const line of content.split('\n')) {
|
|
55
|
-
const trimmed = line.trim();
|
|
56
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
57
|
-
continue;
|
|
58
|
-
const eqIndex = trimmed.indexOf('=');
|
|
59
|
-
if (eqIndex === -1)
|
|
60
|
-
continue;
|
|
61
|
-
const key = trimmed.slice(0, eqIndex).trim();
|
|
62
|
-
let value = trimmed.slice(eqIndex + 1).trim();
|
|
63
|
-
// Strip surrounding quotes
|
|
64
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
65
|
-
value = value.slice(1, -1);
|
|
66
|
-
}
|
|
67
|
-
result[key] = value;
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Resolve the dev-server instance config for a given project + branch.
|
|
73
|
-
* Looks in `<projectPath>/.container/instances/` for `.env` files.
|
|
74
|
-
*/
|
|
75
|
-
export function resolveInstance(projectPath, workingBranch) {
|
|
76
|
-
const instancesDir = path.join(projectPath, '.container', 'instances');
|
|
77
|
-
if (!existsSync(instancesDir))
|
|
78
|
-
return null;
|
|
79
|
-
const sanitized = sanitizeBranchName(workingBranch);
|
|
80
|
-
const files = readdirSync(instancesDir).filter((f) => f.endsWith('.env'));
|
|
81
|
-
for (const file of files) {
|
|
82
|
-
const content = readFileSync(path.join(instancesDir, file), 'utf-8');
|
|
83
|
-
const parsed = parseEnvFile(content);
|
|
84
|
-
if (parsed.INSTANCE_NAME &&
|
|
85
|
-
parsed.INSTANCE_NAME.toLowerCase() === sanitized &&
|
|
86
|
-
parsed.PROJECT_NAME &&
|
|
87
|
-
/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(parsed.PROJECT_NAME)) {
|
|
88
|
-
return {
|
|
89
|
-
instanceName: parsed.INSTANCE_NAME,
|
|
90
|
-
projectName: parsed.PROJECT_NAME,
|
|
91
|
-
httpPort: parsed.HTTP_PORT ?? '',
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
41
|
// ── Docker helpers ─────────────────────────────────────────────────────────────
|
|
98
42
|
/**
|
|
99
43
|
* List all running Docker container names.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/server/services/forge/github/provider.ts
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
|
+
import { MERGE_METHODS } from '../../../../shared/forge-merge.js';
|
|
4
5
|
import { parseCliJson } from '../parse-cli-json.js';
|
|
5
6
|
import { deriveReadyToMerge, truncatePrBody } from '../types.js';
|
|
6
7
|
const execFileAsync = promisify(execFile);
|
|
@@ -157,6 +158,7 @@ export const githubProvider = {
|
|
|
157
158
|
canCreatePr: true,
|
|
158
159
|
canChangePrBase: true,
|
|
159
160
|
canMergeRequest: true,
|
|
161
|
+
mergeMethods: MERGE_METHODS,
|
|
160
162
|
canDeleteRemoteBranch: true,
|
|
161
163
|
canListPullRequests: true,
|
|
162
164
|
requestTermShort: 'PR',
|
|
@@ -192,8 +194,8 @@ export const githubProvider = {
|
|
|
192
194
|
async changePrBase(repoPath, base) {
|
|
193
195
|
await execFileAsync('gh', ['pr', 'edit', '--base', base], cliOptions(repoPath));
|
|
194
196
|
},
|
|
195
|
-
async mergeRequest(repoPath, number) {
|
|
196
|
-
await execFileAsync('gh', ['pr', 'merge', String(number),
|
|
197
|
+
async mergeRequest(repoPath, number, method = 'merge') {
|
|
198
|
+
await execFileAsync('gh', ['pr', 'merge', String(number), `--${method}`], cliOptions(repoPath));
|
|
197
199
|
},
|
|
198
200
|
async deleteRemoteBranch(repoPath, branch) {
|
|
199
201
|
await execFileAsync('git', ['push', 'origin', '--delete', branch], cliOptions(repoPath));
|