@north-light/crouter-api 0.3.173 → 0.3.175

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.
@@ -14,8 +14,8 @@ import { createServer } from 'node:http';
14
14
  import { mkdtempSync, rmSync } from 'node:fs';
15
15
  import { tmpdir } from 'node:os';
16
16
  import { join } from 'node:path';
17
- import { coldStartTimeoutMessage, CrtrClient, safeColdStartDiagnostic } from '../client.js';
18
- import { ApiError } from '../errors.js';
17
+ import { coldStartTimeoutMessage, CrtrClient, safeColdStartDiagnostic } from '../../client.js';
18
+ import { ApiError } from '../../errors.js';
19
19
  test('coldStartTimeoutMessage appends a present diagnostic to the base message', () => {
20
20
  const msg = coldStartTimeoutMessage('crtrd.err (tail):\nError: bind EADDRINUSE');
21
21
  assert.match(msg, /crtrd did not start/);
@@ -139,136 +139,3 @@ test('safeColdStartDiagnostic treats a THROWING hook as absent, not a propagated
139
139
  throw new Error('custom hook blew up');
140
140
  }), undefined);
141
141
  });
142
- /** Spin a loopback server on a temp unix socket that records every request and
143
- * replies with `respond(req)`. `respond` returning `undefined` means "handler
144
- * not reached for this call" and is never exercised by these tests. */
145
- async function startCapturingServer(respond) {
146
- const dir = mkdtempSync(join(tmpdir(), 'crtr-client-inbox-'));
147
- const socketPath = join(dir, 'crtrd.sock');
148
- const requests = [];
149
- const server = createServer((req, res) => {
150
- const chunks = [];
151
- req.on('data', (chunk) => chunks.push(chunk));
152
- req.on('end', () => {
153
- const raw = Buffer.concat(chunks).toString('utf8');
154
- const body = raw.trim() === '' ? undefined : JSON.parse(raw);
155
- const captured = { method: req.method ?? '', path: req.url ?? '', body };
156
- requests.push(captured);
157
- const { status, body: resBody } = respond(captured);
158
- res.writeHead(status, { 'content-type': 'application/json' });
159
- res.end(JSON.stringify(resBody));
160
- });
161
- });
162
- await new Promise((resolvePromise) => server.listen(socketPath, resolvePromise));
163
- const client = new CrtrClient({ socketPath, autostart: false });
164
- return {
165
- client,
166
- requests,
167
- close: () => new Promise((resolvePromise, rejectPromise) => {
168
- server.close((err) => (err ? rejectPromise(err) : resolvePromise()));
169
- }).finally(() => rmSync(dir, { recursive: true, force: true })),
170
- };
171
- }
172
- const SAMPLE_TICKET_ID = 'a'.repeat(64);
173
- test('listHumanInbox sends GET /v1/human/inbox with no body and returns the parsed list', async () => {
174
- const inboxList = { tickets: [{ ticket_id: SAMPLE_TICKET_ID, kind: 'deck', title: 'Approve deploy', blocked_since: '2026-01-01T00:00:00Z', source: {} }] };
175
- const srv = await startCapturingServer(() => ({ status: 200, body: inboxList }));
176
- try {
177
- const result = await srv.client.listHumanInbox();
178
- assert.deepEqual(result, inboxList);
179
- assert.equal(srv.requests.length, 1);
180
- assert.equal(srv.requests[0]?.method, 'GET');
181
- assert.equal(srv.requests[0]?.path, '/v1/human/inbox');
182
- assert.equal(srv.requests[0]?.body, undefined);
183
- }
184
- finally {
185
- await srv.close();
186
- }
187
- });
188
- test('getHumanInboxDeck sends GET /v1/human/inbox/:ticket_id with no body and returns the parsed deck', async () => {
189
- const deck = { ticket_id: SAMPLE_TICKET_ID, kind: 'deck', deck: { title: 'Approve deploy', interactions: [] } };
190
- const srv = await startCapturingServer(() => ({ status: 200, body: deck }));
191
- try {
192
- const result = await srv.client.getHumanInboxDeck(SAMPLE_TICKET_ID);
193
- assert.deepEqual(result, deck);
194
- assert.equal(srv.requests[0]?.method, 'GET');
195
- assert.equal(srv.requests[0]?.path, `/v1/human/inbox/${SAMPLE_TICKET_ID}`);
196
- assert.equal(srv.requests[0]?.body, undefined);
197
- }
198
- finally {
199
- await srv.close();
200
- }
201
- });
202
- test('respondHumanInboxDeck sends POST with the exact responses body and returns the canonical result unchanged', async () => {
203
- const request = { responses: [{ id: 'notify', selectedOptionId: 'ok' }] };
204
- const result = { schema: 'humanloop.response/v2', kind: 'deck', responses: request.responses, summary: 'Acknowledged', completedAt: '2026-01-01T00:00:00Z' };
205
- const srv = await startCapturingServer(() => ({ status: 200, body: result }));
206
- try {
207
- const got = await srv.client.respondHumanInboxDeck(SAMPLE_TICKET_ID, request);
208
- assert.deepEqual(got, result);
209
- assert.equal(srv.requests[0]?.method, 'POST');
210
- assert.equal(srv.requests[0]?.path, `/v1/human/inbox/${SAMPLE_TICKET_ID}/respond`);
211
- assert.deepEqual(srv.requests[0]?.body, request);
212
- }
213
- finally {
214
- await srv.close();
215
- }
216
- });
217
- test('cancelHumanInboxTicket sends POST with the reason body when a request is passed', async () => {
218
- const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: '2026-01-01T00:00:00Z', reason: 'no longer needed', actor: 'human' };
219
- const srv = await startCapturingServer(() => ({ status: 200, body: result }));
220
- try {
221
- const got = await srv.client.cancelHumanInboxTicket(SAMPLE_TICKET_ID, { reason: 'no longer needed' });
222
- assert.deepEqual(got, result);
223
- assert.equal(srv.requests[0]?.method, 'POST');
224
- assert.equal(srv.requests[0]?.path, `/v1/human/inbox/${SAMPLE_TICKET_ID}/cancel`);
225
- assert.deepEqual(srv.requests[0]?.body, { reason: 'no longer needed' });
226
- }
227
- finally {
228
- await srv.close();
229
- }
230
- });
231
- test('cancelHumanInboxTicket sends an empty object body when no request is passed', async () => {
232
- const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: '2026-01-01T00:00:00Z' };
233
- const srv = await startCapturingServer(() => ({ status: 200, body: result }));
234
- try {
235
- const got = await srv.client.cancelHumanInboxTicket(SAMPLE_TICKET_ID);
236
- assert.deepEqual(got, result);
237
- assert.deepEqual(srv.requests[0]?.body, {});
238
- }
239
- finally {
240
- await srv.close();
241
- }
242
- });
243
- test('a non-2xx inbox error body decodes to a typed ApiError (status/code/message)', async () => {
244
- const srv = await startCapturingServer(() => ({
245
- status: 409,
246
- body: { error: { code: 'ticket_kind_unsupported', message: 'review tickets have no v1 response operation' } },
247
- }));
248
- try {
249
- await assert.rejects(() => srv.client.respondHumanInboxDeck(SAMPLE_TICKET_ID, { responses: [] }), (err) => {
250
- assert.ok(err instanceof ApiError);
251
- assert.equal(err.status, 409);
252
- assert.equal(err.code, 'ticket_kind_unsupported');
253
- assert.match(err.message, /review tickets have no v1 response operation/);
254
- return true;
255
- });
256
- }
257
- finally {
258
- await srv.close();
259
- }
260
- });
261
- test('a malformed local ticket id throws a synchronous TypeError and never reaches the transport', async () => {
262
- const srv = await startCapturingServer(() => {
263
- throw new Error('handler must never be invoked for a locally-rejected ticket id');
264
- });
265
- try {
266
- assert.throws(() => srv.client.getHumanInboxDeck('not-a-valid-hash'), TypeError);
267
- assert.throws(() => srv.client.respondHumanInboxDeck('short', { responses: [] }), TypeError);
268
- assert.throws(() => srv.client.cancelHumanInboxTicket('UPPERCASE'.repeat(7)), TypeError);
269
- assert.equal(srv.requests.length, 0);
270
- }
271
- finally {
272
- await srv.close();
273
- }
274
- });
package/dist/client.d.ts CHANGED
@@ -5,18 +5,19 @@ import type { PushReportRequest, PushReportResultDTO, ReportDTO, ReportsQuery }
5
5
  import type { CloseRequest, CloseResultDTO, PromoteRequest, RelaunchRootResultDTO, ReviveRequest, ReviveResultDTO, WaitRequest, YieldRequest } from './dto/lifecycle.js';
6
6
  import type { SubscribeRequest, SubscriptionDTO } from './dto/subscriptions.js';
7
7
  import type { FocusDTO, RegisterFocusRequest, SetFocusPaneRequest } from './dto/focus.js';
8
- import { type ArmCronRequest, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery } from './dto/crons.js';
8
+ import { type ArmCronRequest, type CancelCronQuery, type CronDTO, type CronRunDTO, type CronScopeQuery, type CronShowDTO, type ListCronsQuery } from './dto/crons.js';
9
9
  import type { NodeConfigPatch } from './dto/config.js';
10
10
  import type { AttachEnsureRequest, AttachEnsureResultDTO } from './dto/attach.js';
11
11
  import type { EnsureProfileRequest, ProfileDTO } from './dto/profiles.js';
12
12
  import type { FilePeekDTO } from './dto/files.js';
13
13
  import type { CredentialRemovalResultDTO, CredentialResultDTO, InstallCredentialRequest, ModelAuthListDTO } from './dto/modelauth.js';
14
14
  import type { CreateHumanBridgeRequest, HumanBridgeResultDTO, HumanCancelRequest, HumanCancelResultDTO, HumanResolveRequest, HumanResolveResultDTO } from './dto/human.js';
15
- import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, ReviewCancelResultDTO, ReviewDocumentBaseDTO, ReviewDTO, ReviewListDTO, ReviewOpenResultDTO, ReviewSubmitResultDTO } from './dto/reviews.js';
15
+ import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, ReviewCancelResultDTO, ReviewDocumentBaseDTO, ReviewDTO, ReviewListDTO, ReviewSubmitResultDTO } from './dto/reviews.js';
16
16
  import type { CreateReviewCommentRequest, EditReviewCommentRequest, ListReviewCommentsQuery, ReadReviewCommentEventsQuery, ReviewCommentActionRequest, ReviewCommentDetailDTO, ReviewCommentEventsDTO, ReviewCommentListDTO, ReviewCommentMutationDTO, ReviewCommentRangeBatchRequest, ReviewCommentRangeBatchResultDTO } from './dto/review-comments.js';
17
- import type { CancelInboxTicketRequest, CanceledTicketResultDTO, DeckTicketResultDTO, InboxDeckDTO, InboxListDTO, InboxTicketIdDTO, RespondInboxDeckRequest } from './dto/inbox.js';
17
+ import type { InboxListDTO } from './dto/inbox.js';
18
18
  import type { AttentionCountsDTO, AttentionDTO, DashboardDTO, DashboardQuery, HistoryGrepQuery, HistoryGrepResultDTO, HistoryReadQuery, HistoryReadResultDTO, HistorySearchQuery, HistorySearchResultDTO, PruneRequest, PruneResultDTO, RebuildIndexResultDTO, RosterDTO, SnapshotDTO } from './dto/canvas.js';
19
19
  import type { CloseWorktreeResultDTO } from './dto/worktree.js';
20
+ import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerInboxCursorDirective, BrokerInboxCursorRequest, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerPersonaAckRequest, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest } from './dto/broker-ops.js';
20
21
  export interface CrtrClientOptions {
21
22
  /** Unix socket path (default local transport). Exactly one of socketPath|baseUrl. */
22
23
  socketPath?: string;
@@ -86,6 +87,13 @@ export declare class CrtrClient {
86
87
  forkNode(id: string): Promise<NodeDetailDTO>;
87
88
  reviveNode(id: string, req?: ReviveRequest): Promise<ReviveResultDTO>;
88
89
  relaunchRoot(id: string): Promise<RelaunchRootResultDTO>;
90
+ bindBrokerSession(id: string, req: BrokerSessionBoundRequest): Promise<BrokerSessionBoundResultDTO>;
91
+ settleBroker(id: string, req: BrokerSettleRequest): Promise<BrokerSettleDirective>;
92
+ advanceBrokerInboxCursor(id: string, req: BrokerInboxCursorRequest): Promise<BrokerInboxCursorDirective>;
93
+ commitBrokerModel(id: string, req: BrokerModelCommitRequest): Promise<BrokerModelCommitResultDTO>;
94
+ brokerExtensionState(id: string): Promise<BrokerExtensionStateDTO>;
95
+ commitBrokerGeneratedName(id: string, req: BrokerGeneratedNameRequest): Promise<BrokerGeneratedNameResultDTO>;
96
+ commitBrokerPersonaAck(id: string, req: BrokerPersonaAckRequest): Promise<void>;
89
97
  closeNode(id: string, req?: CloseRequest): Promise<CloseResultDTO>;
90
98
  recycleNode(id: string): Promise<NodeDetailDTO>;
91
99
  demoteNode(id: string): Promise<NodeDetailDTO>;
@@ -122,7 +130,7 @@ export declare class CrtrClient {
122
130
  * advance the schedule or consume a one-shot; never escalates. */
123
131
  runCron(cronId: string, q?: CronScopeQuery): Promise<CronRunDTO>;
124
132
  /** Cancel one cron (`DELETE /v1/crons/:cronId`, idempotent). */
125
- cancelCron(cronId: string, q?: CronScopeQuery): Promise<void>;
133
+ cancelCron(cronId: string, q?: CancelCronQuery): Promise<void>;
126
134
  ensureAttach(id: string, req?: AttachEnsureRequest): Promise<AttachEnsureResultDTO>;
127
135
  getReports(id: string, q?: ReportsQuery): Promise<ReportDTO[]>;
128
136
  getTranscript(id: string, q?: TranscriptQuery): Promise<TranscriptDTO>;
@@ -159,7 +167,6 @@ export declare class CrtrClient {
159
167
  listReviews(query?: ListReviewsQuery): Promise<ReviewListDTO>;
160
168
  getReview(reviewId: string): Promise<ReviewDTO>;
161
169
  getReviewByBridge(bridgeNodeId: string): Promise<ReviewDTO>;
162
- openReview(reviewId: string): Promise<ReviewOpenResultDTO>;
163
170
  submitReview(reviewId: string): Promise<ReviewSubmitResultDTO>;
164
171
  cancelReview(reviewId: string, req?: CancelReviewRequest): Promise<ReviewCancelResultDTO>;
165
172
  getReviewDocumentBase(reviewId: string): Promise<ReviewDocumentBaseDTO>;
@@ -172,17 +179,7 @@ export declare class CrtrClient {
172
179
  resolveReviewComment(commentId: string, req?: ReviewCommentActionRequest): Promise<ReviewCommentMutationDTO>;
173
180
  reopenReviewComment(commentId: string, req?: ReviewCommentActionRequest): Promise<ReviewCommentMutationDTO>;
174
181
  deleteReviewComment(commentId: string, req?: ReviewCommentActionRequest): Promise<ReviewCommentMutationDTO>;
175
- /** Pending deck/review tickets across every available crouter-owned
176
- * humanloop root. */
177
182
  listHumanInbox(): Promise<InboxListDTO>;
178
- /** Read one pending deck by its opaque ticket id, with Markdown bodies
179
- * resolved inline. */
180
- getHumanInboxDeck(ticketId: InboxTicketIdDTO): Promise<InboxDeckDTO>;
181
- /** Submit ordered interaction responses for a pending deck. Single-assignment
182
- * server-side: a competing resolution races to `ticket_already_resolved`. */
183
- respondHumanInboxDeck(ticketId: InboxTicketIdDTO, request: RespondInboxDeckRequest): Promise<DeckTicketResultDTO>;
184
- /** Cancel a pending deck (terminal response, never deletion). */
185
- cancelHumanInboxTicket(ticketId: InboxTicketIdDTO, request?: CancelInboxTicketRequest): Promise<CanceledTicketResultDTO>;
186
183
  /** Composed client-side from `GET /v1/nodes` + `GET /v1/status` (spec §6.3 —
187
184
  * the dashboard is absorbed into those two reads; there is no single route).
188
185
  * `generated_at` is the client-side capture instant of the composition. */
@@ -220,12 +217,6 @@ export declare class CrtrClient {
220
217
  * it raw, so a value carrying `/`, whitespace or `?` would corrupt the
221
218
  * request line rather than 404 cleanly. Mirrors `nodePath`. */
222
219
  private cronPath;
223
- /** Validate an opaque inbox ticket id before route construction. A local
224
- * shape violation is a caller bug, not a server-rejectable request — throws
225
- * `TypeError` (matching the existing safe-segment discipline of a local
226
- * precondition, distinct from `nodePath`'s `ApiError` because that one IS a
227
- * request the server could plausibly receive and reject itself). */
228
- private ticketId;
229
220
  /** Validate a review id before route construction (D-A6). Reviews are minted
230
221
  * and addressed like node ids, but arrive from agent argv, so a malformed
231
222
  * one is a plausible request the server would also reject — `ApiError`,
package/dist/client.js CHANGED
@@ -118,6 +118,27 @@ export class CrtrClient {
118
118
  relaunchRoot(id) {
119
119
  return this.request('POST', routes.nodeRelaunchRoot(this.nodePath(id)), {});
120
120
  }
121
+ bindBrokerSession(id, req) {
122
+ return this.request('POST', routes.nodeBrokerSessionBound(this.nodePath(id)), req);
123
+ }
124
+ settleBroker(id, req) {
125
+ return this.request('POST', routes.nodeBrokerSettle(this.nodePath(id)), req);
126
+ }
127
+ advanceBrokerInboxCursor(id, req) {
128
+ return this.request('POST', routes.nodeBrokerInboxCursor(this.nodePath(id)), req);
129
+ }
130
+ commitBrokerModel(id, req) {
131
+ return this.request('POST', routes.nodeBrokerModel(this.nodePath(id)), req);
132
+ }
133
+ brokerExtensionState(id) {
134
+ return this.request('GET', routes.nodeBrokerExtensionState(this.nodePath(id)));
135
+ }
136
+ commitBrokerGeneratedName(id, req) {
137
+ return this.request('POST', routes.nodeBrokerGeneratedName(this.nodePath(id)), req);
138
+ }
139
+ commitBrokerPersonaAck(id, req) {
140
+ return this.request('POST', routes.nodeBrokerPersonaAck(this.nodePath(id)), req);
141
+ }
121
142
  closeNode(id, req) {
122
143
  return this.request('POST', routes.nodeClose(this.nodePath(id)), req ?? {});
123
144
  }
@@ -292,9 +313,6 @@ export class CrtrClient {
292
313
  getReviewByBridge(bridgeNodeId) {
293
314
  return this.request('GET', routes.humanReviewByBridge(this.nodePath(bridgeNodeId)));
294
315
  }
295
- openReview(reviewId) {
296
- return this.request('POST', routes.humanReviewOpen(this.reviewPath(reviewId)), {});
297
- }
298
316
  submitReview(reviewId) {
299
317
  return this.request('POST', routes.humanReviewSubmit(this.reviewPath(reviewId)), {});
300
318
  }
@@ -333,26 +351,10 @@ export class CrtrClient {
333
351
  deleteReviewComment(commentId, req = {}) {
334
352
  return this.request('POST', routes.humanCommentDelete(this.commentPath(commentId)), req);
335
353
  }
336
- // ---- Humanloop inbox (Northlight crouter-inbox v1, inbox-contract.md §A) --
337
- /** Pending deck/review tickets across every available crouter-owned
338
- * humanloop root. */
354
+ // ---- Attached terminal viewer inbox -------------------------------------
339
355
  listHumanInbox() {
340
356
  return this.request('GET', routes.humanInbox());
341
357
  }
342
- /** Read one pending deck by its opaque ticket id, with Markdown bodies
343
- * resolved inline. */
344
- getHumanInboxDeck(ticketId) {
345
- return this.request('GET', routes.humanInboxTicket(this.ticketId(ticketId)));
346
- }
347
- /** Submit ordered interaction responses for a pending deck. Single-assignment
348
- * server-side: a competing resolution races to `ticket_already_resolved`. */
349
- respondHumanInboxDeck(ticketId, request) {
350
- return this.request('POST', routes.humanInboxRespond(this.ticketId(ticketId)), request);
351
- }
352
- /** Cancel a pending deck (terminal response, never deletion). */
353
- cancelHumanInboxTicket(ticketId, request) {
354
- return this.request('POST', routes.humanInboxCancel(this.ticketId(ticketId)), request ?? {});
355
- }
356
358
  // ---- Canvas reads / maintenance ---------------------------------------
357
359
  /** Composed client-side from `GET /v1/nodes` + `GET /v1/status` (spec §6.3 —
358
360
  * the dashboard is absorbed into those two reads; there is no single route).
@@ -444,17 +446,6 @@ export class CrtrClient {
444
446
  }
445
447
  return id;
446
448
  }
447
- /** Validate an opaque inbox ticket id before route construction. A local
448
- * shape violation is a caller bug, not a server-rejectable request — throws
449
- * `TypeError` (matching the existing safe-segment discipline of a local
450
- * precondition, distinct from `nodePath`'s `ApiError` because that one IS a
451
- * request the server could plausibly receive and reject itself). */
452
- ticketId(id) {
453
- if (!/^[a-f0-9]{64}$/.test(id)) {
454
- throw new TypeError(`invalid inbox ticket id: ${JSON.stringify(id)}`);
455
- }
456
- return id;
457
- }
458
449
  /** Validate a review id before route construction (D-A6). Reviews are minted
459
450
  * and addressed like node ids, but arrive from agent argv, so a malformed
460
451
  * one is a plausible request the server would also reject — `ApiError`,
@@ -0,0 +1,158 @@
1
+ import type { ExitIntentDTO, NodeStatusDTO } from './common.js';
2
+ /** `POST /v1/nodes/{id}/broker/session-bound` body. Pi's session-start reason
3
+ * distinguishes an ordinary boot/resume from `/new`, whose child-side session
4
+ * reset is a different durable operation. `reviewBoundaryIds` reports only the
5
+ * review markers visible in Pi's current branch; crtrd owns the node binding
6
+ * used to decide whether that branch is valid. */
7
+ export interface BrokerSessionBoundRequest {
8
+ piSessionId: string;
9
+ sessionFile: string | null;
10
+ pid: number;
11
+ reason: string | null;
12
+ reviewBoundaryIds: string[];
13
+ }
14
+ /** Pi-side consequence selected by crtrd after binding the session. The
15
+ * handler itself never calls back into the broker while servicing the request. */
16
+ export interface BrokerSessionBoundResultDTO {
17
+ action: 'none' | 'relaunch_root' | 'shutdown';
18
+ }
19
+ /** `POST /v1/nodes/{id}/broker/settle` body. These are the facts only Pi can
20
+ * know at its settlement boundary; crtrd reads all current canvas state and
21
+ * selects the durable consequence. */
22
+ export interface BrokerSettleRequest {
23
+ stopReason: string;
24
+ backgroundJobsRunning: boolean;
25
+ pushedFinal: boolean;
26
+ askedHuman: boolean;
27
+ }
28
+ /** The only consequence a settle caller may enact. crtrd has already committed
29
+ * every canvas and placement effect before returning this directive. */
30
+ export type BrokerSettleDirective = {
31
+ action: 'reprompt';
32
+ prompt: string;
33
+ } | {
34
+ action: 'stay_dormant';
35
+ } | {
36
+ action: 'shutdown';
37
+ };
38
+ /** `POST /v1/nodes/{id}/broker/inbox-cursor` body. The watcher advances this
39
+ * only after Pi has settled every handoff through the supplied physical entry.
40
+ * `brokerPid` binds the commit to the broker generation crtrd currently owns. */
41
+ export interface BrokerInboxCursorRequest {
42
+ throughEntryId: string;
43
+ brokerPid: number;
44
+ }
45
+ /** crtrd either commits the durable cursor or preserves it while a refresh
46
+ * discards the current Pi conversation, so the next broker replays the entry. */
47
+ export type BrokerInboxCursorDirective = {
48
+ action: 'advanced';
49
+ } | {
50
+ action: 'hold_refresh';
51
+ };
52
+ /** Durable model recipe selected by the live broker after Pi accepts a model or thinking change. */
53
+ export interface BrokerModelCommitRequest {
54
+ spec: string;
55
+ pinnedOverride?: boolean;
56
+ userSelected: boolean;
57
+ }
58
+ export interface BrokerModelCommitResultDTO {
59
+ modelOverride: string;
60
+ }
61
+ /** The dependency-light identity/runtime projection broker extensions need to
62
+ * render their local Pi hooks without reading canvas.db themselves. */
63
+ export interface BrokerExtensionNodeDTO {
64
+ node_id: string;
65
+ name: string;
66
+ description?: string;
67
+ icon?: string;
68
+ kind: string;
69
+ mode: 'base' | 'orchestrator';
70
+ lifecycle: 'terminal' | 'resident';
71
+ status: NodeStatusDTO;
72
+ cwd: string;
73
+ parent: string | null;
74
+ fork_from: string | null;
75
+ profile_id: string | null;
76
+ managed_worktree?: {
77
+ state: 'open' | 'closed';
78
+ path: string;
79
+ branch: string;
80
+ base_ref: string;
81
+ base_sha: string;
82
+ } | null;
83
+ review_binding?: {
84
+ review_id: string;
85
+ origin_node_id: string;
86
+ branch_file: string;
87
+ target_file: string;
88
+ } | null;
89
+ intent: ExitIntentDTO;
90
+ persona_ack?: {
91
+ mode: 'base' | 'orchestrator';
92
+ lifecycle: 'terminal' | 'resident';
93
+ };
94
+ created: string;
95
+ }
96
+ export interface BrokerExtensionSubjectDTO {
97
+ kind: string;
98
+ mode: 'base' | 'orchestrator';
99
+ lifecycle: 'terminal' | 'resident';
100
+ hasManager: boolean;
101
+ cwd: string;
102
+ scope: 'user' | 'project';
103
+ orchestration: {
104
+ depth: number;
105
+ };
106
+ profile: string | null;
107
+ }
108
+ /** One resolved report sender. Report contents stay broker-local filesystem
109
+ * data; this daemon projection supplies only existence and display metadata. */
110
+ export interface BrokerReportNodeDTO {
111
+ node_id: string;
112
+ name: string;
113
+ created: string;
114
+ }
115
+ /** `GET /v1/nodes/{id}/broker/extension-state`. This is deliberately a fixed
116
+ * extension rendering projection, not a generic node/state read API. */
117
+ export interface BrokerExtensionStateDTO {
118
+ node: BrokerExtensionNodeDTO;
119
+ warm_spare: boolean;
120
+ ancestors: BrokerExtensionNodeDTO[];
121
+ children: BrokerExtensionNodeDTO[];
122
+ fork_source: BrokerExtensionNodeDTO | null;
123
+ subject: BrokerExtensionSubjectDTO;
124
+ report_nodes: BrokerReportNodeDTO[];
125
+ }
126
+ /** Guarded generated-label update. `initial` can only fill a blank generated
127
+ * description; `recap` additionally compares the exact automatic-name snapshot
128
+ * captured before the headless naming call. */
129
+ export type BrokerGeneratedNameRequest = {
130
+ kind: 'initial';
131
+ description: string;
132
+ icon: string;
133
+ } | {
134
+ kind: 'recap';
135
+ description: string;
136
+ icon: string;
137
+ expected: {
138
+ name: string;
139
+ description: string;
140
+ icon: string;
141
+ kind: string;
142
+ };
143
+ };
144
+ /** A daemon-selected Pi editor-label directive. No handler calls a broker. */
145
+ export interface BrokerGeneratedNameResultDTO {
146
+ applied: boolean;
147
+ editorLabel?: string;
148
+ }
149
+ export interface BrokerPersonaAckRequest {
150
+ from: {
151
+ mode: 'base' | 'orchestrator';
152
+ lifecycle: 'terminal' | 'resident';
153
+ };
154
+ to: {
155
+ mode: 'base' | 'orchestrator';
156
+ lifecycle: 'terminal' | 'resident';
157
+ };
158
+ }
@@ -0,0 +1,6 @@
1
+ // Broker-to-daemon operation DTOs. These requests are emitted by a broker Pi
2
+ // extension, but are handled exclusively by crtrd so canvas state keeps one
3
+ // owner.
4
+ //
5
+ // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
6
+ export {};
@@ -74,6 +74,12 @@ export interface ListCronsQuery {
74
74
  export interface CronScopeQuery {
75
75
  profile?: string | null;
76
76
  }
77
+ /** Cancellation-only query. `run_id` is set automatically inside a cron run,
78
+ * letting crtrd distinguish a run ending itself from an external cancellation
79
+ * that must terminate an in-flight process. */
80
+ export interface CancelCronQuery extends CronScopeQuery {
81
+ run_id?: string;
82
+ }
77
83
  /** One settled run-log entry (`GET /v1/crons/:cronId`, `POST /v1/crons/:cronId/run`). */
78
84
  export interface CronRunDTO {
79
85
  run_id: string;
@@ -1,5 +1,4 @@
1
1
  import type { NodeIdDTO } from './common.js';
2
- import type { InteractionResponseDTO } from './inbox.js';
3
2
  /** One anchored review comment, mirroring crouter's ticket-store shape as a
4
3
  * plain structural wire type (no store import). */
5
4
  export interface FeedbackCommentDTO {
@@ -37,6 +36,13 @@ export interface FeedbackResultDTO {
37
36
  commentsTotal: number;
38
37
  commentsUnresolved: number;
39
38
  }
39
+ export interface InteractionResponseDTO {
40
+ id: string;
41
+ selectedOptionId?: string;
42
+ selectedOptionIds?: string[];
43
+ freetext?: string;
44
+ optionComments?: Record<string, string>;
45
+ }
40
46
  /** `POST /v1/human/tickets/{node_id}/resolve` body — a deck answer. */
41
47
  export interface HumanResolveRequest {
42
48
  responses: InteractionResponseDTO[];
@@ -1,7 +1,6 @@
1
1
  import type { IsoTime } from './common.js';
2
2
  /** Opaque, stable, URL-safe ticket id: lowercase SHA-256 hex of
3
- * `canonicalRoot + "\0" + ticketBasename`. Clients must treat it as opaque —
4
- * it discloses no home filesystem path. */
3
+ * `canonicalRoot + "\0" + ticketBasename`. */
5
4
  export type InboxTicketIdDTO = string;
6
5
  export type InteractionKindDTO = 'notify' | 'decision' | 'context' | 'error' | 'review';
7
6
  export interface DeckSourceDTO {
@@ -33,72 +32,3 @@ export type InboxTicketSummaryDTO = DeckTicketSummaryDTO | ReviewTicketSummaryDT
33
32
  export interface InboxListDTO {
34
33
  tickets: InboxTicketSummaryDTO[];
35
34
  }
36
- export interface InteractionOptionDTO {
37
- id: string;
38
- label: string;
39
- description?: string;
40
- }
41
- export interface InteractionPreAnswerDTO {
42
- selectedOptionId?: string;
43
- selectedOptionIds?: string[];
44
- freetext?: string;
45
- label?: string;
46
- }
47
- export interface InteractionDTO {
48
- id: string;
49
- title: string;
50
- subtitle: string;
51
- /** Resolved source Markdown — `bodyPath` is deliberately impossible here. */
52
- body?: string;
53
- options: InteractionOptionDTO[];
54
- multiSelect?: boolean;
55
- allowFreetext?: boolean;
56
- freetextLabel?: string;
57
- kind?: InteractionKindDTO;
58
- preAnswered?: InteractionPreAnswerDTO;
59
- }
60
- export interface DeckDTO {
61
- title: string;
62
- source?: DeckSourceDTO;
63
- interactions: InteractionDTO[];
64
- }
65
- /** `GET /v1/human/inbox/:ticket_id` result for a pending deck. */
66
- export interface InboxDeckDTO {
67
- ticket_id: InboxTicketIdDTO;
68
- kind: 'deck';
69
- deck: DeckDTO;
70
- }
71
- export interface InteractionResponseDTO {
72
- id: string;
73
- selectedOptionId?: string;
74
- selectedOptionIds?: string[];
75
- freetext?: string;
76
- optionComments?: Record<string, string>;
77
- }
78
- /** `POST /v1/human/inbox/:ticket_id/respond` body. */
79
- export interface RespondInboxDeckRequest {
80
- responses: InteractionResponseDTO[];
81
- }
82
- /** `POST /v1/human/inbox/:ticket_id/respond` result — the canonical humanloop
83
- * `humanloop.response/v2` result, unchanged. */
84
- export interface DeckTicketResultDTO {
85
- schema: 'humanloop.response/v2';
86
- kind: 'deck';
87
- responses: InteractionResponseDTO[];
88
- summary: string;
89
- completedAt: IsoTime;
90
- }
91
- /** `POST /v1/human/inbox/:ticket_id/cancel` body. `reason`, when present, must
92
- * be nonempty after trim and at most 1000 characters. */
93
- export interface CancelInboxTicketRequest {
94
- reason?: string;
95
- }
96
- /** `POST /v1/human/inbox/:ticket_id/cancel` result — the canonical humanloop
97
- * `humanloop.cancel/v1` result, unchanged. `actor` is always `"human"`. */
98
- export interface CanceledTicketResultDTO {
99
- schema: 'humanloop.cancel/v1';
100
- kind: 'canceled';
101
- canceledAt: IsoTime;
102
- reason?: string;
103
- actor?: string;
104
- }
package/dist/dto/inbox.js CHANGED
@@ -1,10 +1,2 @@
1
- // Humanloop inbox DTOs crtrd `/v1/human/inbox` (Northlight crouter-inbox v1,
2
- // inbox-contract.md §A). Crouter API envelope fields use the existing
3
- // snake_case convention; nested humanloop protocol objects retain their
4
- // canonical camelCase field names so they cross the wire without translation
5
- // or loss. Optional fields are omitted when absent, never serialized as
6
- // `null`. `bodyPath` is deliberately impossible on this wire — crtrd resolves
7
- // it server-side via humanloop's `parseDeck` and returns inline `body`.
8
- //
9
- // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
1
+ // Ticket summaries rendered by the attached terminal viewer.
10
2
  export {};
@@ -45,6 +45,12 @@ export interface NodeSummaryDTO {
45
45
  intent: ExitIntentDTO;
46
46
  waiting_for: NodeIdDTO | null;
47
47
  pi_pid: number | null;
48
+ /** Launch-time process identity paired with `pi_pid`, or null when absent. */
49
+ pi_pid_identity: string | null;
50
+ /** Current tmux placement cache. Local tmux callers re-check the pane itself before acting. */
51
+ window: string | null;
52
+ tmux_session: string | null;
53
+ pane: string | null;
48
54
  final_report: string | null;
49
55
  finalized_at: IsoTime | null;
50
56
  }
@@ -103,13 +109,6 @@ export interface NodeDetailDTO extends NodeSummaryDTO {
103
109
  * (never launched, or no token accounting yet). */
104
110
  context_tokens?: number | null;
105
111
  pi_session_id?: string | null;
106
- /** Launch-time process-identity fingerprint captured alongside `pi_pid`
107
- * (`NodeMeta.pi_pid_identity`), or null. Surfaced so `revive --now`'s
108
- * client-side SIGTERM can pass the identity baseline to `recordedPidLiveness`
109
- * and refuse to signal a stranger process that reused a dead broker's pid.
110
- * Null when the node predates the field or its launch-time capture failed
111
- * (fail-open — no baseline means no guard, not a false mismatch). */
112
- pi_pid_identity?: string | null;
113
112
  /** The node's durable model override (`NodeMeta.model_override`), or null when
114
113
  * it runs on the kind/profile default. Surfaced so `node config --model`
115
114
  * can report the resolved model after a patch. */
@@ -1,4 +1,4 @@
1
- import type { IsoTime, NodeIdDTO, NodeStatusDTO } from './common.js';
1
+ import type { IsoTime, NodeIdDTO } from './common.js';
2
2
  import type { FeedbackResultDTO } from './human.js';
3
3
  export type ReviewOriginKindDTO = 'ticket' | 'inline';
4
4
  /** `binding` is intentionally never exposed over the API. */
@@ -73,16 +73,6 @@ export interface ListReviewsQuery {
73
73
  export interface ReviewListDTO {
74
74
  reviews: ReviewDTO[];
75
75
  }
76
- /** Daemon-derived result of opening an active review or acknowledging a terminal one. */
77
- export type ReviewOpenResultDTO = {
78
- review: ReviewDTO;
79
- companion_node_id: NodeIdDTO;
80
- companion_status: NodeStatusDTO;
81
- disposition: 'open';
82
- } | {
83
- review: ReviewDTO;
84
- disposition: 'terminal';
85
- };
86
76
  /** Daemon-derived result of terminal review approval. */
87
77
  export interface ReviewSubmitResultDTO {
88
78
  review: ReviewDTO;
package/dist/routes.d.ts CHANGED
@@ -19,6 +19,13 @@ export declare const routes: {
19
19
  readonly nodeFork: (id: string) => string;
20
20
  readonly nodeRevive: (id: string) => string;
21
21
  readonly nodeRelaunchRoot: (id: string) => string;
22
+ readonly nodeBrokerSessionBound: (id: string) => string;
23
+ readonly nodeBrokerSettle: (id: string) => string;
24
+ readonly nodeBrokerInboxCursor: (id: string) => string;
25
+ readonly nodeBrokerModel: (id: string) => string;
26
+ readonly nodeBrokerExtensionState: (id: string) => string;
27
+ readonly nodeBrokerGeneratedName: (id: string) => string;
28
+ readonly nodeBrokerPersonaAck: (id: string) => string;
22
29
  readonly nodeClose: (id: string) => string;
23
30
  readonly nodeRecycle: (id: string) => string;
24
31
  readonly nodeDemote: (id: string) => string;
@@ -54,7 +61,6 @@ export declare const routes: {
54
61
  readonly humanReviews: () => string;
55
62
  readonly humanReview: (reviewId: string) => string;
56
63
  readonly humanReviewByBridge: (bridgeNodeId: string) => string;
57
- readonly humanReviewOpen: (reviewId: string) => string;
58
64
  readonly humanReviewSubmit: (reviewId: string) => string;
59
65
  readonly humanReviewCancel: (reviewId: string) => string;
60
66
  readonly humanReviewDocument: (reviewId: string) => string;
@@ -67,9 +73,6 @@ export declare const routes: {
67
73
  readonly humanCommentReopen: (commentId: string) => string;
68
74
  readonly humanCommentDelete: (commentId: string) => string;
69
75
  readonly humanInbox: () => string;
70
- readonly humanInboxTicket: (ticketId: string) => string;
71
- readonly humanInboxRespond: (ticketId: string) => string;
72
- readonly humanInboxCancel: (ticketId: string) => string;
73
76
  readonly profiles: () => string;
74
77
  readonly profile: (name: string) => string;
75
78
  readonly modelAuths: () => string;
package/dist/routes.js CHANGED
@@ -37,6 +37,13 @@ export const routes = {
37
37
  nodeFork: (id) => `${V}/nodes/${id}/fork`,
38
38
  nodeRevive: (id) => `${V}/nodes/${id}/revive`,
39
39
  nodeRelaunchRoot: (id) => `${V}/nodes/${id}/relaunch-root`,
40
+ nodeBrokerSessionBound: (id) => `${V}/nodes/${id}/broker/session-bound`,
41
+ nodeBrokerSettle: (id) => `${V}/nodes/${id}/broker/settle`,
42
+ nodeBrokerInboxCursor: (id) => `${V}/nodes/${id}/broker/inbox-cursor`,
43
+ nodeBrokerModel: (id) => `${V}/nodes/${id}/broker/model`,
44
+ nodeBrokerExtensionState: (id) => `${V}/nodes/${id}/broker/extension-state`,
45
+ nodeBrokerGeneratedName: (id) => `${V}/nodes/${id}/broker/generated-name`,
46
+ nodeBrokerPersonaAck: (id) => `${V}/nodes/${id}/broker/persona-ack`,
40
47
  nodeClose: (id) => `${V}/nodes/${id}/close`,
41
48
  nodeRecycle: (id) => `${V}/nodes/${id}/recycle`,
42
49
  nodeDemote: (id) => `${V}/nodes/${id}/demote`,
@@ -80,7 +87,6 @@ export const routes = {
80
87
  humanReviews: () => `${V}/human/reviews`,
81
88
  humanReview: (reviewId) => `${V}/human/reviews/${reviewId}`,
82
89
  humanReviewByBridge: (bridgeNodeId) => `${V}/human/reviews/by-bridge/${bridgeNodeId}`,
83
- humanReviewOpen: (reviewId) => `${V}/human/reviews/${reviewId}/open`,
84
90
  humanReviewSubmit: (reviewId) => `${V}/human/reviews/${reviewId}/submit`,
85
91
  humanReviewCancel: (reviewId) => `${V}/human/reviews/${reviewId}/cancel`,
86
92
  humanReviewDocument: (reviewId) => `${V}/human/reviews/${reviewId}/document`,
@@ -92,11 +98,8 @@ export const routes = {
92
98
  humanCommentResolve: (commentId) => `${V}/human/comments/${commentId}/resolve`,
93
99
  humanCommentReopen: (commentId) => `${V}/human/comments/${commentId}/reopen`,
94
100
  humanCommentDelete: (commentId) => `${V}/human/comments/${commentId}/delete`,
95
- // Humanloop inbox (Northlight crouter-inbox v1, inbox-contract.md §A)
101
+ // Attached terminal viewer inbox
96
102
  humanInbox: () => `${V}/human/inbox`,
97
- humanInboxTicket: (ticketId) => `${V}/human/inbox/${ticketId}`,
98
- humanInboxRespond: (ticketId) => `${V}/human/inbox/${ticketId}/respond`,
99
- humanInboxCancel: (ticketId) => `${V}/human/inbox/${ticketId}/cancel`,
100
103
  // Profiles (server-side for P2 Core; CLI profile verbs stay fs-local)
101
104
  profiles: () => `${V}/profiles`,
102
105
  profile: (name) => `${V}/profiles/${name}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter-api",
3
- "version": "0.3.173",
3
+ "version": "0.3.175",
4
4
  "description": "Typed crtrd /v1 API contract — DTOs, route builders, the error contract, and the CrtrClient. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",