@bli-cockpit/mcp 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -25
- package/dist/agent-door.d.ts +8 -1
- package/dist/agent-door.js +7 -2
- package/dist/brief-write-tools.d.ts +37 -0
- package/dist/brief-write-tools.js +178 -0
- package/dist/docs-msg-tools.js +11 -1
- package/dist/jarvis-answer-envelope.d.ts +13 -1
- package/dist/jarvis-answer-envelope.js +2 -0
- package/dist/jarvis-door.js +16 -1
- package/dist/jarvis-tools.d.ts +18 -10
- package/dist/jarvis-tools.js +28 -17
- package/dist/notes-write-tools.d.ts +34 -0
- package/dist/notes-write-tools.js +211 -0
- package/dist/ops-tools.d.ts +3 -7
- package/dist/ops-tools.js +64 -3
- package/dist/pages-tools.d.ts +6 -2
- package/dist/pages-tools.js +69 -2
- package/dist/readme-census.js +8 -6
- package/dist/server.d.ts +1 -1
- package/dist/server.js +15 -1
- package/dist/settings-write-tools.d.ts +47 -0
- package/dist/settings-write-tools.js +183 -0
- package/dist/team-write-tools.d.ts +39 -0
- package/dist/team-write-tools.js +141 -0
- package/dist/tool-result.d.ts +16 -0
- package/dist/tool-result.js +26 -0
- package/dist/verb-census.d.ts +11 -1
- package/dist/verb-census.js +29 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ about what an agent can reach.
|
|
|
34
34
|
|
|
35
35
|
<!-- BEGIN GENERATED verb census — `npm run mcp:readme` -->
|
|
36
36
|
|
|
37
|
-
**
|
|
37
|
+
**56 of 57 Tower verbs have an MCP twin.**
|
|
38
38
|
Each tool goes through the SAME door its CLI verb calls, with the same
|
|
39
39
|
collector device token — never a second route and never a service-role
|
|
40
40
|
reader. `src/verb-census.test.ts` fails when a verb is in none of the
|
|
@@ -44,7 +44,9 @@ three tables below.
|
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| `cockpit brief history` | `brief_history` | `GET /api/jarvis/brief?history=1` |
|
|
46
46
|
| `cockpit brief read` | `brief_read` | `GET /api/jarvis/brief` |
|
|
47
|
+
| `cockpit brief rewrite` | `brief_rewrite` | `POST /api/jarvis/recompile` |
|
|
47
48
|
| `cockpit brief status` | `brief_status` | `GET /api/ops/brief-status` |
|
|
49
|
+
| `cockpit correct` | `brief_correct` | `POST /api/jarvis/corrections` |
|
|
48
50
|
| `cockpit docs create` | `docs_create` | `POST /api/docs/documents` |
|
|
49
51
|
| `cockpit docs list` | `docs_list` | `GET /api/docs/documents` |
|
|
50
52
|
| `cockpit docs read` | `docs_read` | `GET /api/docs/documents/[id]` |
|
|
@@ -59,6 +61,7 @@ three tables below.
|
|
|
59
61
|
| `cockpit issue update` | `work_update_issue` | `PATCH /api/work/issues/[id]` |
|
|
60
62
|
| `cockpit jarvis` | `jarvis_ask` | `POST /api/jarvis/cli` |
|
|
61
63
|
| `cockpit jarvis --trace` | `jarvis_trace` | `GET /api/ops/trace/[id]` |
|
|
64
|
+
| `cockpit model set` | `model_set` | `POST /api/settings/jarvis-model` |
|
|
62
65
|
| `cockpit model show` | `model_show` | `GET /api/settings/jarvis-model` |
|
|
63
66
|
| `cockpit msg channels` | `msg_channels` | `GET /api/msg/channels` |
|
|
64
67
|
| `cockpit msg create` | `msg_create_channel` | `POST /api/msg/channels` |
|
|
@@ -67,44 +70,40 @@ three tables below.
|
|
|
67
70
|
| `cockpit msg send` | `msg_send` | `POST /api/msg/channels/[id]/messages` |
|
|
68
71
|
| `cockpit msg thread` | `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` |
|
|
69
72
|
| `cockpit notes list` | `notes_list` | `GET /api/notes/library` |
|
|
73
|
+
| `cockpit notes move` | `notes_move` | `POST /api/notes/move` |
|
|
74
|
+
| `cockpit notes paste` | `notes_paste` | `POST /api/notes/upload (text)` |
|
|
75
|
+
| `cockpit notes share` | `notes_share` | `POST /api/notes/share` |
|
|
70
76
|
| `cockpit notes shelf` | `notes_shelf` | `GET /api/notes/shelf` |
|
|
71
77
|
| `cockpit notes shelves` | `notes_shelves` | `GET /api/notes/library` |
|
|
72
78
|
| `cockpit notes show` | `notes_show` | `GET /api/notes/library/[id]` |
|
|
79
|
+
| `cockpit notes unshare` | `notes_unshare` | `POST /api/notes/share (share=false)` |
|
|
80
|
+
| `cockpit notes upload` | `notes_upload` | `POST /api/notes/upload` |
|
|
81
|
+
| `cockpit ops recompile` | `ops_recompile` | `POST /api/ops/recompile` |
|
|
73
82
|
| `cockpit ops status` | `ops_status` | `GET /api/ops/status` |
|
|
74
83
|
| `cockpit project list` | `work_list_projects` | `GET /api/work/projects` |
|
|
75
84
|
| `cockpit scout board` | `scout_board` | `GET /api/cockpit/scout` |
|
|
85
|
+
| `cockpit scout dismiss` | `scout_dismiss` | `POST /api/cockpit/scout (dismiss)` |
|
|
86
|
+
| `cockpit scout start` | `scout_start` | `POST /api/cockpit/scout (start)` |
|
|
87
|
+
| `cockpit scout undo` | `scout_undo` | `POST /api/cockpit/scout (undo_dismiss)` |
|
|
76
88
|
| `cockpit search` | `tower_search` | `GET /api/search` |
|
|
89
|
+
| `cockpit settings delete` | `settings_delete` | `DELETE /api/settings/env-blobs` |
|
|
77
90
|
| `cockpit settings list` | `settings_list` | `GET /api/settings/env-blobs` |
|
|
91
|
+
| `cockpit settings set` | `settings_set` | `POST /api/settings/{jarvis-model,switches,model-routing,env-blobs} + PUT /api/settings/cli-floor` |
|
|
78
92
|
| `cockpit settings show` | `settings_show` | `GET /api/settings/* + /api/team/members` |
|
|
79
93
|
| `cockpit slack coverage` | `slack_coverage` | `GET /api/ops/slack/coverage` |
|
|
80
94
|
| `cockpit slack read` | `slack_read` | `POST /api/ops/slack/read` |
|
|
81
95
|
| `cockpit team device-list` | `team_device_list` | `GET /api/team/devices` |
|
|
96
|
+
| `cockpit team device-revoke` | `team_device_revoke` | `POST /api/ambient/devices/[deviceId]/revoke` |
|
|
97
|
+
| `cockpit team invite` | `team_invite` | `POST /api/team/invite` |
|
|
82
98
|
| `cockpit team members` | `team_members` | `GET /api/team/members` |
|
|
99
|
+
| `cockpit team role` | `team_role` | `PATCH /api/team/members/[userId]/role` |
|
|
83
100
|
| `cockpit workbook` | `workbook_read` | `GET /api/cockpit/workbook` |
|
|
84
101
|
|
|
85
102
|
### Owed a twin
|
|
86
103
|
|
|
87
104
|
This table should only ever shrink.
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
| --- | --- |
|
|
91
|
-
| `cockpit brief rewrite` | BLI-3756 batch 2 — spends a model call; wants its own gate on this surface |
|
|
92
|
-
| `cockpit correct` | BLI-3756 batch 2 — filing a correction against a claim has no tool |
|
|
93
|
-
| `cockpit model set` | BLI-3756 batch 2 — a shorthand over settings personal; follows settings |
|
|
94
|
-
| `cockpit notes move` | BLI-3756 batch 2 — needs a signed-in session server-side; reachable, just unwritten |
|
|
95
|
-
| `cockpit notes paste` | BLI-3756 batch 2 — the CLI takes the body on stdin; a tool would take it as an argument |
|
|
96
|
-
| `cockpit notes share` | BLI-3756 batch 2 — a deliberate act; wants an explicit confirmation on this surface |
|
|
97
|
-
| `cockpit notes unshare` | BLI-3756 batch 2 — same gate as notes share |
|
|
98
|
-
| `cockpit notes upload` | BLI-3756 batch 2 — takes local file paths, which an MCP server on the same machine can also read |
|
|
99
|
-
| `cockpit ops recompile` | BLI-3756 batch 2 — spends a model call; wants its own gate on this surface |
|
|
100
|
-
| `cockpit scout dismiss` | BLI-3756 batch 2 — super_admin server-side; a tool would relay, not decide |
|
|
101
|
-
| `cockpit scout start` | BLI-3756 batch 2 — super_admin server-side; a tool would relay, not decide |
|
|
102
|
-
| `cockpit scout undo` | BLI-3756 batch 2 — super_admin server-side; a tool would relay, not decide |
|
|
103
|
-
| `cockpit settings delete` | BLI-3756 batch 2 — destructive; wants an explicit confirmation on this surface |
|
|
104
|
-
| `cockpit settings set` | BLI-3756 batch 2 — mostly reachable, but env CONTENT is stdin-only by rule; a tool must keep that |
|
|
105
|
-
| `cockpit team device-revoke` | BLI-3756 batch 2 — destructive; wants an explicit confirmation on this surface |
|
|
106
|
-
| `cockpit team invite` | BLI-3756 batch 2 — super_admin; a tool would relay, not decide |
|
|
107
|
-
| `cockpit team role` | BLI-3756 batch 2 — super_admin; a tool would relay, not decide |
|
|
106
|
+
_Nothing. Every Tower verb an agent could want has a door (BLI-3756)._
|
|
108
107
|
|
|
109
108
|
### Terminal-only
|
|
110
109
|
|
|
@@ -192,10 +191,52 @@ generated table above; what is worth knowing before you call one:
|
|
|
192
191
|
standing sentences, and a truncated read saying so) and the per-project
|
|
193
192
|
document library, rendered by the same walker the page uses.
|
|
194
193
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
## The write tools (BLI-3756 batch 2)
|
|
195
|
+
|
|
196
|
+
The seventeen things a person can DO from `cockpit` and could not do from an
|
|
197
|
+
agent. Same doors, same device token, same refusal words — plus one rule the
|
|
198
|
+
reads never needed:
|
|
199
|
+
|
|
200
|
+
**An act that cannot be undone is refused without `confirm: true`.** The CLI's
|
|
201
|
+
gate is `--yes` or a person at a keyboard; a client has no keyboard, so the
|
|
202
|
+
explicit flag is the whole of it here, and the reason label is the CLI's own
|
|
203
|
+
`confirmation_required`. It guards `notes_share` (sharing lets everyone signed
|
|
204
|
+
in read the note), `settings_delete`, `team_role` and `team_device_revoke`.
|
|
205
|
+
Taking a share BACK asks for nothing — nobody needs to be talked out of
|
|
206
|
+
narrowing who can read something.
|
|
207
|
+
|
|
208
|
+
- **`brief_rewrite` / `brief_correct`** — ask for a page to be written again,
|
|
209
|
+
and say that one line on it is wrong. `brief_rewrite` QUEUES and returns the
|
|
210
|
+
request id immediately; it does not wait, because an 800-second compile
|
|
211
|
+
outlives any client deadline and "the tool timed out" would be
|
|
212
|
+
indistinguishable from "the compile failed". `brief_correct` refuses a claim
|
|
213
|
+
id the page does not carry BEFORE writing, and Tower's push-back ("the record
|
|
214
|
+
disagrees") is a normal result — the correction is filed either way.
|
|
215
|
+
- **`notes_upload` / `notes_paste` / `notes_share` / `notes_unshare` /
|
|
216
|
+
`notes_move`** — put a note in from this machine's disk or from text, share
|
|
217
|
+
it, take it back, move it to another shelf. A path whose NAME looks like a
|
|
218
|
+
key or credential file is refused without being opened; so is a missing,
|
|
219
|
+
empty or over-20-MB file. A large note is one model call server-side and can
|
|
220
|
+
take minutes — your client may give up first, and the upload keeps going.
|
|
221
|
+
- **`ops_recompile`** — write one person's page again. It spends a model call;
|
|
222
|
+
`dry_run: true` resolves the person and reads the current page without
|
|
223
|
+
compiling. If Tower's own 800-second budget runs out the effect is UNKNOWN,
|
|
224
|
+
not failed, and it says so rather than inviting you to pay twice.
|
|
225
|
+
- **`scout_start` / `scout_dismiss` / `scout_undo`** — move one card. Each
|
|
226
|
+
reads the board first, matches the id EXACTLY (no prefixes on this surface —
|
|
227
|
+
a near-miss would settle a card nobody decided on), and a 403 names the gate
|
|
228
|
+
without costing you the read.
|
|
229
|
+
- **`settings_set` / `settings_delete` / `model_set`** — write one settings
|
|
230
|
+
section, delete an env file, or just change which model answers you. Env
|
|
231
|
+
content is an argument here rather than stdin (a tool call has no argv, no
|
|
232
|
+
shell history and no process list), and the rule that survives is the one
|
|
233
|
+
that mattered: it is **never echoed back** — not in the answer, not in the
|
|
234
|
+
structured payload, not in a log.
|
|
235
|
+
- **`team_invite` / `team_role` / `team_device_revoke`** — invite somebody,
|
|
236
|
+
change a role, end a machine's pairing. All three relay a super_admin
|
|
237
|
+
decision made on the server. `team_device_revoke` looks the device up and
|
|
238
|
+
names it back to you first; without `confirm: true` that naming IS the
|
|
239
|
+
answer and nothing is revoked.
|
|
199
240
|
|
|
200
241
|
## `docs_*` / `msg_*` tools (BLI-3706)
|
|
201
242
|
|
|
@@ -205,7 +246,7 @@ reason it waited in the generated "Owed a twin" table.
|
|
|
205
246
|
| `docs_tree` | `GET /api/docs/tree` | The library as its nesting rather than a flat list. Never a body. |
|
|
206
247
|
| `docs_read` | `GET /api/docs/documents/[id]` | One document's title and full body, by id or slug (resolved against `docs_list` first). |
|
|
207
248
|
| `docs_create` | `POST /api/docs/documents` | Creates a document. Omitting `body_markdown` creates an empty one. |
|
|
208
|
-
| `docs_update` | `PATCH /api/docs/documents/[id]` | Updates title/body/visibility/parent (a parent change IS a move). |
|
|
249
|
+
| `docs_update` | `PATCH /api/docs/documents/[id]` | Updates title/body/visibility/parent (a parent change IS a move). Emptying a document that holds text needs `allow_empty: true` (BLI-3757's guard). |
|
|
209
250
|
| `msg_channels` | `GET /api/msg/channels` | Every channel you are a member of. |
|
|
210
251
|
| `msg_read` | `GET /api/msg/channels/[id]/messages` | Recent messages of one channel, newest first. `channel` is an id or a `#name`. |
|
|
211
252
|
| `msg_thread` | `GET /api/msg/channels/[id]/messages?thread_parent_id=` | One thread's replies, oldest first, so it reads as a conversation. |
|
package/dist/agent-door.d.ts
CHANGED
|
@@ -12,4 +12,11 @@ export interface AgentDoorResponse {
|
|
|
12
12
|
/** Set only when no JSON answer was produced at all — DNS, TLS, timeout, non-JSON body. */
|
|
13
13
|
transportError: string | null;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The four verbs Tower's agent doors answer to. `PUT` is the CLI floor
|
|
17
|
+
* (`PUT /api/settings/cli-floor`) and `DELETE` is an env blob — both arrived
|
|
18
|
+
* with BLI-3756 batch 2, and both are the door's own choice of verb, not this
|
|
19
|
+
* package's.
|
|
20
|
+
*/
|
|
21
|
+
export type AgentDoorMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
22
|
+
export declare function callAgentDoor(session: AgentDoorSession, fetchImpl: FetchImpl, method: AgentDoorMethod, path: string, body?: unknown, timeoutMs?: number): Promise<AgentDoorResponse>;
|
package/dist/agent-door.js
CHANGED
|
@@ -9,16 +9,21 @@ export async function callAgentDoor(session, fetchImpl, method, path, body, time
|
|
|
9
9
|
const controller = new AbortController();
|
|
10
10
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
11
|
timer.unref?.();
|
|
12
|
+
// A note goes in as multipart, exactly as `cockpit notes upload` sends it,
|
|
13
|
+
// so `content-type` must be left to fetch: it is the only thing that knows
|
|
14
|
+
// the boundary it just generated, and a hand-set JSON header would make the
|
|
15
|
+
// route read an empty form.
|
|
16
|
+
const multipart = typeof FormData !== "undefined" && body instanceof FormData;
|
|
12
17
|
let response;
|
|
13
18
|
try {
|
|
14
19
|
response = await fetchImpl(url, {
|
|
15
20
|
method,
|
|
16
21
|
headers: {
|
|
17
22
|
authorization: `Bearer ${session.deviceToken}`,
|
|
18
|
-
"content-type": "application/json",
|
|
23
|
+
...(multipart ? {} : { "content-type": "application/json" }),
|
|
19
24
|
accept: "application/json",
|
|
20
25
|
},
|
|
21
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
26
|
+
...(body === undefined ? {} : { body: multipart ? body : JSON.stringify(body) }),
|
|
22
27
|
signal: controller.signal,
|
|
23
28
|
});
|
|
24
29
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brief_rewrite` / `brief_correct` MCP tools (BLI-3756 batch 2) — the two
|
|
3
|
+
* things a person can DO to the daily page, over the same doors
|
|
4
|
+
* `cockpit brief rewrite` and `cockpit correct` call with the same device
|
|
5
|
+
* token.
|
|
6
|
+
*
|
|
7
|
+
* Reads live in `brief-tools.ts`; these two write, and each carries one
|
|
8
|
+
* judgement the read side never has to make.
|
|
9
|
+
*
|
|
10
|
+
* **`brief_rewrite` asks and does not wait.** The CLI queues the compile and
|
|
11
|
+
* then watches the page for up to fifteen minutes, because a terminal can sit
|
|
12
|
+
* there. A tool call cannot: the client's own deadline would cut the call
|
|
13
|
+
* long before an 800-second compile finished, and "the tool timed out" is
|
|
14
|
+
* indistinguishable from "the compile failed" — the exact confusion this repo
|
|
15
|
+
* keeps paying for. So this tool sends `mode: "queue"`, reports the request id
|
|
16
|
+
* the moment it exists, and says in its own answer that the page is not
|
|
17
|
+
* written yet and `brief_read` is how to find out. That is the CLI's
|
|
18
|
+
* `--no-wait` shape, chosen deliberately and named out loud rather than
|
|
19
|
+
* silently.
|
|
20
|
+
*
|
|
21
|
+
* **`brief_correct` treats push-back as the answer, not an error.** The
|
|
22
|
+
* corrections door checks the claim against the live record BEFORE writing and
|
|
23
|
+
* can answer that the record disagrees. That sentence is what the caller came
|
|
24
|
+
* for: the correction WAS filed, with its outcome recorded, so it comes back
|
|
25
|
+
* as a normal result. Only a failure to file at all is an error.
|
|
26
|
+
*
|
|
27
|
+
* A claim id the page does not carry is refused HERE, before any write, in the
|
|
28
|
+
* CLI's own words — filing it anyway would land a correction on a clause
|
|
29
|
+
* nobody can find, which is how a ledger fills with rows that mean nothing.
|
|
30
|
+
*/
|
|
31
|
+
import { type ToolDeps } from "./tool-result.js";
|
|
32
|
+
export type BriefWriteDeps = ToolDeps;
|
|
33
|
+
/** The same ceiling `cockpit correct` enforces before it sends anything. */
|
|
34
|
+
export declare const MAX_CORRECTION_LENGTH = 4000;
|
|
35
|
+
export declare function registerBriefWriteTools(server: {
|
|
36
|
+
registerTool: (...args: never[]) => unknown;
|
|
37
|
+
}, deps: BriefWriteDeps): void;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `brief_rewrite` / `brief_correct` MCP tools (BLI-3756 batch 2) — the two
|
|
3
|
+
* things a person can DO to the daily page, over the same doors
|
|
4
|
+
* `cockpit brief rewrite` and `cockpit correct` call with the same device
|
|
5
|
+
* token.
|
|
6
|
+
*
|
|
7
|
+
* Reads live in `brief-tools.ts`; these two write, and each carries one
|
|
8
|
+
* judgement the read side never has to make.
|
|
9
|
+
*
|
|
10
|
+
* **`brief_rewrite` asks and does not wait.** The CLI queues the compile and
|
|
11
|
+
* then watches the page for up to fifteen minutes, because a terminal can sit
|
|
12
|
+
* there. A tool call cannot: the client's own deadline would cut the call
|
|
13
|
+
* long before an 800-second compile finished, and "the tool timed out" is
|
|
14
|
+
* indistinguishable from "the compile failed" — the exact confusion this repo
|
|
15
|
+
* keeps paying for. So this tool sends `mode: "queue"`, reports the request id
|
|
16
|
+
* the moment it exists, and says in its own answer that the page is not
|
|
17
|
+
* written yet and `brief_read` is how to find out. That is the CLI's
|
|
18
|
+
* `--no-wait` shape, chosen deliberately and named out loud rather than
|
|
19
|
+
* silently.
|
|
20
|
+
*
|
|
21
|
+
* **`brief_correct` treats push-back as the answer, not an error.** The
|
|
22
|
+
* corrections door checks the claim against the live record BEFORE writing and
|
|
23
|
+
* can answer that the record disagrees. That sentence is what the caller came
|
|
24
|
+
* for: the correction WAS filed, with its outcome recorded, so it comes back
|
|
25
|
+
* as a normal result. Only a failure to file at all is an error.
|
|
26
|
+
*
|
|
27
|
+
* A claim id the page does not carry is refused HERE, before any write, in the
|
|
28
|
+
* CLI's own words — filing it anyway would land a correction on a clause
|
|
29
|
+
* nobody can find, which is how a ledger fills with rows that mean nothing.
|
|
30
|
+
*/
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { callAgentDoor } from "./agent-door.js";
|
|
33
|
+
import { doorFailureText, errorResult, registrarFor, textResult, withSession, } from "./tool-result.js";
|
|
34
|
+
/** The same ceiling `cockpit correct` enforces before it sends anything. */
|
|
35
|
+
export const MAX_CORRECTION_LENGTH = 4000;
|
|
36
|
+
/** A rewrite queues in seconds; the compile it queues takes minutes. */
|
|
37
|
+
const QUEUE_DEADLINE_MS = 60_000;
|
|
38
|
+
const SUBJECT = z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(200)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("Whose page — a name, email or person id. Omit for your own. Changes the SUBJECT, never who is asking.");
|
|
44
|
+
/**
|
|
45
|
+
* Which page, and whose. The same read both verbs open with, and the only way
|
|
46
|
+
* either learns the person id the write doors need.
|
|
47
|
+
*/
|
|
48
|
+
async function readPage(deps, session, params) {
|
|
49
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "GET", `/api/jarvis/brief?${params.toString()}`, undefined, QUEUE_DEADLINE_MS);
|
|
50
|
+
if (!response.ok)
|
|
51
|
+
return { ok: false, text: doorFailureText("brief", response) };
|
|
52
|
+
const body = response.body;
|
|
53
|
+
if (!body.ok || !body.page?.personId) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
text: body.reply ?? "There is no page here to work on yet. The first one is compiled by the daily job.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return { ok: true, body };
|
|
60
|
+
}
|
|
61
|
+
export function registerBriefWriteTools(server, deps) {
|
|
62
|
+
const register = registrarFor(server);
|
|
63
|
+
register("brief_rewrite", {
|
|
64
|
+
title: "Ask for a Tower daily page to be written again",
|
|
65
|
+
description: "Queues a recompile of the TODAY page — the website's button and `cockpit brief rewrite`, same door. It spends "
|
|
66
|
+
+ "a model call and takes several minutes, so this tool ASKS and returns the request id immediately; it does "
|
|
67
|
+
+ "not wait. Read the result later with brief_read. An admin may ask about anybody; everyone else about their "
|
|
68
|
+
+ "own page, decided on the server.",
|
|
69
|
+
inputSchema: { subject: SUBJECT },
|
|
70
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
71
|
+
const params = new URLSearchParams({ tldr: "1" });
|
|
72
|
+
if (args.subject)
|
|
73
|
+
params.set("p", String(args.subject));
|
|
74
|
+
const page = await readPage(deps, session, params);
|
|
75
|
+
if (!page.ok)
|
|
76
|
+
return errorResult(page.text);
|
|
77
|
+
const personId = page.body.page?.personId;
|
|
78
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/jarvis/recompile", { personIds: [personId], mode: "queue" }, QUEUE_DEADLINE_MS);
|
|
79
|
+
if (!response.ok)
|
|
80
|
+
return errorResult(doorFailureText("brief_rewrite", response));
|
|
81
|
+
const answer = response.body;
|
|
82
|
+
const request = (answer.queued ?? [])[0];
|
|
83
|
+
if (!answer.ok || !request?.requestId) {
|
|
84
|
+
return errorResult(answer.reply ?? "Tower did not accept that ask.");
|
|
85
|
+
}
|
|
86
|
+
const whose = request.name ?? page.body.page?.displayName ?? "that page";
|
|
87
|
+
const scheduled = answer.compileScheduled !== false;
|
|
88
|
+
// Queue-only, said as queue-only. The ask is a real row; what is
|
|
89
|
+
// missing is somebody to run it in the next minute, and pretending
|
|
90
|
+
// otherwise would leave a caller watching a page that was never going
|
|
91
|
+
// to change.
|
|
92
|
+
const sentence = scheduled
|
|
93
|
+
? `Asked. ${whose} is being written again — this takes a few minutes. Call brief_read then; nothing here waits for it.`
|
|
94
|
+
: `${whose} is on the list to be written again, but this Tower could not start the work now `
|
|
95
|
+
+ `(${answer.compileReason ?? "no reason given"}). The daily compile will reach it. Nothing is lost.`;
|
|
96
|
+
return textResult(sentence, {
|
|
97
|
+
ok: true,
|
|
98
|
+
outcome: scheduled ? "queued" : "queued_only",
|
|
99
|
+
requestId: request.requestId,
|
|
100
|
+
personId,
|
|
101
|
+
compileScheduled: scheduled,
|
|
102
|
+
compileReason: answer.compileReason ?? null,
|
|
103
|
+
});
|
|
104
|
+
}));
|
|
105
|
+
register("brief_correct", {
|
|
106
|
+
title: "Say that one line on a Tower page is wrong",
|
|
107
|
+
description: "Files a correction against ONE claim on the daily page — `cockpit correct`, same door and same ledger. Get "
|
|
108
|
+
+ "claim ids from brief_read with `claims: true`. Tower checks the claim against the live record before "
|
|
109
|
+
+ "writing and may answer that the record disagrees; that push-back is the RESULT, not a failure — the "
|
|
110
|
+
+ "correction is filed either way, with its outcome recorded.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
claim_id: z.string().min(1).max(200).describe("A claim id from brief_read({claims: true})."),
|
|
113
|
+
text: z
|
|
114
|
+
.string()
|
|
115
|
+
.min(1)
|
|
116
|
+
.max(MAX_CORRECTION_LENGTH)
|
|
117
|
+
.describe("What is wrong, and what is true instead."),
|
|
118
|
+
subject: SUBJECT,
|
|
119
|
+
version: z.string().min(1).max(100).optional().describe("Correct one exact stored page id instead of the newest."),
|
|
120
|
+
supersedes: z
|
|
121
|
+
.string()
|
|
122
|
+
.min(1)
|
|
123
|
+
.max(200)
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("A correction on the same clause that this one takes back."),
|
|
126
|
+
},
|
|
127
|
+
}, async (args) => withSession(deps, async (session) => {
|
|
128
|
+
const claimId = String(args.claim_id ?? "");
|
|
129
|
+
const params = new URLSearchParams({ claims: "1" });
|
|
130
|
+
if (args.subject)
|
|
131
|
+
params.set("p", String(args.subject));
|
|
132
|
+
if (args.version)
|
|
133
|
+
params.set("v", String(args.version));
|
|
134
|
+
const page = await readPage(deps, session, params);
|
|
135
|
+
if (!page.ok)
|
|
136
|
+
return errorResult(page.text);
|
|
137
|
+
const claim = (page.body.claims ?? []).find((candidate) => candidate.claimId === claimId);
|
|
138
|
+
if (!claim) {
|
|
139
|
+
return errorResult(`Refused (unknown_claim): that page has no line called “${claimId}”. `
|
|
140
|
+
+ "Call brief_read with `claims: true` to see the ids. Nothing was filed.");
|
|
141
|
+
}
|
|
142
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/jarvis/corrections", {
|
|
143
|
+
personId: page.body.page?.personId,
|
|
144
|
+
pageId: page.body.page?.pageId ?? null,
|
|
145
|
+
claimId,
|
|
146
|
+
// The line as it reads now, so the ledger records what was
|
|
147
|
+
// disputed and the live check has the sentence to work from.
|
|
148
|
+
quotedText: claim.text ?? null,
|
|
149
|
+
correctionText: String(args.text ?? ""),
|
|
150
|
+
contextLinks: claim.links ?? [],
|
|
151
|
+
clauseOnPage: true,
|
|
152
|
+
clauseIsObserved: Boolean(claim.observed),
|
|
153
|
+
...(args.supersedes ? { supersedes: String(args.supersedes) } : {}),
|
|
154
|
+
}, QUEUE_DEADLINE_MS);
|
|
155
|
+
if (!response.ok)
|
|
156
|
+
return errorResult(doorFailureText("brief_correct", response));
|
|
157
|
+
const filed = response.body;
|
|
158
|
+
if (!filed.correctionId) {
|
|
159
|
+
return errorResult(filed.error ?? "Tower did not record that correction.");
|
|
160
|
+
}
|
|
161
|
+
const said = [
|
|
162
|
+
filed.reply ?? "Filed.",
|
|
163
|
+
filed.finding ? `The record says: ${filed.finding}` : "",
|
|
164
|
+
filed.link ?? "",
|
|
165
|
+
]
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.join("\n");
|
|
168
|
+
return textResult(said, {
|
|
169
|
+
ok: true,
|
|
170
|
+
correctionId: filed.correctionId,
|
|
171
|
+
tier: filed.tier ?? null,
|
|
172
|
+
outcome: filed.outcome ?? null,
|
|
173
|
+
pushBack: filed.pushBack ?? null,
|
|
174
|
+
finding: filed.finding ?? null,
|
|
175
|
+
link: filed.link ?? null,
|
|
176
|
+
});
|
|
177
|
+
}));
|
|
178
|
+
}
|
package/dist/docs-msg-tools.js
CHANGED
|
@@ -131,13 +131,20 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
131
131
|
}));
|
|
132
132
|
register("docs_update", {
|
|
133
133
|
title: "Update a Tower document",
|
|
134
|
-
description: "Updates a document's title, body, visibility, or parent (a parent change IS a move). Only the fields you
|
|
134
|
+
description: "Updates a document's title, body, visibility, or parent (a parent change IS a move). Only the fields you "
|
|
135
|
+
+ "pass change. Emptying a document that holds text needs allow_empty: true.",
|
|
135
136
|
inputSchema: {
|
|
136
137
|
id: z.string().min(1).max(200).describe("A document id, or its slug."),
|
|
137
138
|
title: z.string().min(1).max(200).optional(),
|
|
138
139
|
body_markdown: z.string().max(200_000).optional(),
|
|
139
140
|
visibility: z.enum(["org", "private"]).optional(),
|
|
140
141
|
parent_id: z.string().uuid().nullable().optional(),
|
|
142
|
+
allow_empty: z
|
|
143
|
+
.boolean()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Only when you mean to CLEAR the page. A body_markdown that would empty a document that holds "
|
|
146
|
+
+ "text is refused with refused_empty_body unless this is true (BLI-3757 — a blank save is far more "
|
|
147
|
+
+ "often a surface that lost the content than a person who meant it)."),
|
|
141
148
|
},
|
|
142
149
|
}, async (args) => withSession(deps, async (session) => {
|
|
143
150
|
const ref = String(args.id ?? "");
|
|
@@ -151,6 +158,9 @@ export function registerDocsMsgTools(server, deps) {
|
|
|
151
158
|
...(args.body_markdown !== undefined ? { body_markdown: args.body_markdown } : {}),
|
|
152
159
|
...(args.visibility !== undefined ? { visibility: args.visibility } : {}),
|
|
153
160
|
...(args.parent_id !== undefined ? { parent_id: args.parent_id } : {}),
|
|
161
|
+
// BLI-3759: sent only when the caller asked for it; the door's own
|
|
162
|
+
// refusal comes back verbatim otherwise, never retried with the flag.
|
|
163
|
+
...(args.allow_empty ? { allow_empty: true } : {}),
|
|
154
164
|
});
|
|
155
165
|
if (!response.ok)
|
|
156
166
|
return errorResult(doorFailureText("docs_update", response));
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* Every key of the envelope, in the order it is written. The literal list IS
|
|
35
35
|
* the contract: both copies assert against it, so a drift is a red suite.
|
|
36
36
|
*/
|
|
37
|
-
export declare const JARVIS_ANSWER_ENVELOPE_KEYS: readonly ["ok", "answer", "sources", "turn_id", "thread_id", "trace_thread_id", "degraded", "degraded_reasons"];
|
|
37
|
+
export declare const JARVIS_ANSWER_ENVELOPE_KEYS: readonly ["ok", "answer", "sources", "turn_id", "thread_id", "trace_thread_id", "proposal_id", "degraded", "degraded_reasons"];
|
|
38
38
|
/**
|
|
39
39
|
* The closed set of ways an answer can be second-rate without being a failure.
|
|
40
40
|
* A degraded answer is still an answer — it is printed, returned and counted —
|
|
@@ -77,6 +77,16 @@ export interface JarvisAnswerEnvelope {
|
|
|
77
77
|
* `thread_id` above and deliberately named apart from it.
|
|
78
78
|
*/
|
|
79
79
|
trace_thread_id: string | null;
|
|
80
|
+
/**
|
|
81
|
+
* BLI-3755: the coding-arm proposal this turn made, when it made one.
|
|
82
|
+
*
|
|
83
|
+
* It is what an AGENT surface is given INSTEAD of the eight-hex approval
|
|
84
|
+
* code, which is never sent here: a code a program can read is a code it
|
|
85
|
+
* could approve with, and that is not approval. The person fetches the code
|
|
86
|
+
* on a surface a person reads and hands it back. Null on every turn that
|
|
87
|
+
* proposed no coding task, which is nearly all of them.
|
|
88
|
+
*/
|
|
89
|
+
proposal_id: string | null;
|
|
80
90
|
degraded: boolean;
|
|
81
91
|
degraded_reasons: JarvisDegradedReason[];
|
|
82
92
|
}
|
|
@@ -103,5 +113,7 @@ export interface JarvisAnswerEnvelopeInput {
|
|
|
103
113
|
trace?: ReadonlyArray<{
|
|
104
114
|
status?: string;
|
|
105
115
|
}> | null;
|
|
116
|
+
/** `body.proposalId` — BLI-3755's coding-arm proposal id, when the turn made one. */
|
|
117
|
+
proposalId?: string | null;
|
|
106
118
|
}
|
|
107
119
|
export declare function buildJarvisAnswerEnvelope(input: JarvisAnswerEnvelopeInput): JarvisAnswerEnvelope;
|
|
@@ -41,6 +41,7 @@ export const JARVIS_ANSWER_ENVELOPE_KEYS = [
|
|
|
41
41
|
"turn_id",
|
|
42
42
|
"thread_id",
|
|
43
43
|
"trace_thread_id",
|
|
44
|
+
"proposal_id",
|
|
44
45
|
"degraded",
|
|
45
46
|
"degraded_reasons",
|
|
46
47
|
];
|
|
@@ -76,6 +77,7 @@ export function buildJarvisAnswerEnvelope(input) {
|
|
|
76
77
|
turn_id: turnId,
|
|
77
78
|
thread_id: input.thread ?? null,
|
|
78
79
|
trace_thread_id: input.traceThread ?? null,
|
|
80
|
+
proposal_id: input.proposalId ?? null,
|
|
79
81
|
degraded: reasons.length > 0,
|
|
80
82
|
degraded_reasons: reasons,
|
|
81
83
|
};
|
package/dist/jarvis-door.js
CHANGED
|
@@ -72,7 +72,14 @@ export async function takeTurn(deps, session, door, body, remember) {
|
|
|
72
72
|
// `accept: application/json` (which `callAgentDoor` always sends) is what
|
|
73
73
|
// makes the dashboard answer one JSON body instead of the NDJSON stream the
|
|
74
74
|
// terminal takes — an MCP tool has nobody to show live trace lines to.
|
|
75
|
-
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/jarvis/cli",
|
|
75
|
+
const response = await callAgentDoor(session, deps.fetchImpl, "POST", "/api/jarvis/cli",
|
|
76
|
+
// BLI-3755: every turn this server takes says who is reading the answer.
|
|
77
|
+
// An MCP tool result is read by a MODEL, so the coding arm withholds its
|
|
78
|
+
// approval code and answers with a `proposal_id` instead — the one thing
|
|
79
|
+
// this server could not prove about a code it relayed was that a person
|
|
80
|
+
// had ever seen it. Sent on every tool, not just the dispatch, because the
|
|
81
|
+
// arm is on the belt of every turn.
|
|
82
|
+
{ ...body, surface: "agent" }, TURN_TIMEOUT_MS);
|
|
76
83
|
if (!response.ok) {
|
|
77
84
|
log(`${TAG} turn failed ${JSON.stringify({
|
|
78
85
|
door,
|
|
@@ -102,6 +109,7 @@ export async function takeTurn(deps, session, door, body, remember) {
|
|
|
102
109
|
modelFallback: reply.model?.fallback,
|
|
103
110
|
revised: reply.revised,
|
|
104
111
|
trace: reply.trace,
|
|
112
|
+
proposalId: reply.proposalId,
|
|
105
113
|
});
|
|
106
114
|
remember({ turnId: envelope.turn_id, traceThreadId: envelope.trace_thread_id });
|
|
107
115
|
log(`${TAG} answered ${JSON.stringify({
|
|
@@ -109,6 +117,9 @@ export async function takeTurn(deps, session, door, body, remember) {
|
|
|
109
117
|
reply_chars: envelope.answer.length,
|
|
110
118
|
sources: envelope.sources.length,
|
|
111
119
|
trace_steps: reply.trace?.length ?? 0,
|
|
120
|
+
// BLI-3755: whether this turn left a coding-arm proposal behind. The id
|
|
121
|
+
// itself is in the envelope; this line says one was made.
|
|
122
|
+
proposal_recorded: envelope.proposal_id !== null,
|
|
112
123
|
degraded: envelope.degraded,
|
|
113
124
|
degraded_reasons: envelope.degraded_reasons,
|
|
114
125
|
has_turn_id: envelope.turn_id !== null,
|
|
@@ -117,6 +128,10 @@ export async function takeTurn(deps, session, door, body, remember) {
|
|
|
117
128
|
const footer = [
|
|
118
129
|
envelope.turn_id ? `turn_id: ${envelope.turn_id} (open it with jarvis_trace)` : null,
|
|
119
130
|
envelope.thread_id ? `thread_id: ${envelope.thread_id}` : null,
|
|
131
|
+
envelope.proposal_id
|
|
132
|
+
? `proposal_id: ${envelope.proposal_id} — the person fetches the approval code for it themself, `
|
|
133
|
+
+ `in a Tower tab, their Slack DM, or their own \`cockpit jarvis\` terminal. You were not sent one.`
|
|
134
|
+
: null,
|
|
120
135
|
envelope.degraded ? `degraded: ${envelope.degraded_reasons.join(", ")}` : null,
|
|
121
136
|
]
|
|
122
137
|
.filter((line) => line !== null)
|
package/dist/jarvis-tools.d.ts
CHANGED
|
@@ -32,16 +32,24 @@
|
|
|
32
32
|
* and cannot compute a code for any plan, so an invented code is refused by
|
|
33
33
|
* the dashboard exactly as it always was.
|
|
34
34
|
*
|
|
35
|
-
* Lock 2 is weaker on THIS surface than it is in a terminal
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* to
|
|
35
|
+
* Lock 2 is weaker on THIS surface than it is in a terminal: in a terminal the
|
|
36
|
+
* person types the code themselves, while through MCP it arrives as a tool
|
|
37
|
+
* argument and nothing on the wire distinguishes a code a person handed their
|
|
38
|
+
* agent from one the agent lifted out of the previous answer. BLI-3755 closed
|
|
39
|
+
* the two halves of that gap where the gate lives, not here:
|
|
40
|
+
*
|
|
41
|
+
* * `jarvis_ask` and `jarvis_dispatch` tell the dashboard they are an AGENT
|
|
42
|
+
* surface (`jarvis-door.ts` sends it on every turn), so a proposal comes
|
|
43
|
+
* back with a `proposal_id` and NO code. There is nothing in the previous
|
|
44
|
+
* answer to lift. The person fetches the code where a person reads — a
|
|
45
|
+
* Tower tab, their Slack DM, their own `cockpit jarvis` — and hands it on.
|
|
46
|
+
* * A code is spendable ONCE. A replay is refused with `approval_code_spent`,
|
|
47
|
+
* so a code that did reach an agent buys at most the one run the person
|
|
48
|
+
* approved.
|
|
49
|
+
*
|
|
50
|
+
* What is unchanged here: this server holds no secret and cannot compute a code
|
|
51
|
+
* for any plan, it never derives, guesses or fabricates one, and every relay is
|
|
52
|
+
* logged (presence and length only, never the code).
|
|
45
53
|
*/
|
|
46
54
|
import { type JarvisDeps } from "./jarvis-door.js";
|
|
47
55
|
export type { JarvisDeps } from "./jarvis-door.js";
|