@north-light/crouter 0.3.192 → 0.3.193
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/dist/api/client.d.ts +31 -10
- package/dist/api/client.js +83 -24
- package/dist/api/dto/bash-jobs.d.ts +22 -0
- package/dist/api/dto/bash-jobs.js +9 -0
- package/dist/api/dto/human.d.ts +3 -4
- package/dist/api/dto/inbox.d.ts +106 -65
- package/dist/api/dto/inbox.js +2 -8
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +7 -0
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
- package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
- package/dist/clients/attach/chrome/canvas-panels.js +1 -1
- package/dist/clients/attach/input/overlay-owner.js +1 -1
- package/dist/clients/attach/overlays/auth.js +34 -8
- package/dist/clients/attach/overlays/pickers.d.ts +5 -8
- package/dist/clients/attach/overlays/pickers.js +37 -43
- package/dist/clients/attach/viewer.js +643 -620
- package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
- package/dist/clients/inbox/controller.d.ts +2 -3
- package/dist/clients/inbox/controller.js +41 -42
- package/dist/clients/inbox/page-adapter.d.ts +33 -0
- package/dist/clients/inbox/page-adapter.js +54 -0
- package/dist/clients/inbox/resolve.d.ts +2 -2
- package/dist/clients/inbox/resolve.js +1 -1
- package/dist/clients/inbox/review-visibility.d.ts +1 -1
- package/dist/clients/inbox/review-visibility.js +1 -1
- package/dist/clients/inbox/tui/ansi.d.ts +1 -1
- package/dist/clients/inbox/tui/ansi.js +2 -2
- package/dist/clients/inbox/tui/input.d.ts +1 -3
- package/dist/clients/inbox/tui/input.js +242 -553
- package/dist/clients/inbox/tui/panel.d.ts +3 -1
- package/dist/clients/inbox/tui/panel.js +59 -187
- package/dist/clients/inbox/tui/render.d.ts +0 -25
- package/dist/clients/inbox/tui/render.js +165 -416
- package/dist/clients/inbox/tui/slots.d.ts +32 -0
- package/dist/clients/inbox/tui/slots.js +156 -0
- package/dist/clients/inbox/tui/types.d.ts +25 -13
- package/dist/clients/inbox/tui.js +5 -3
- package/dist/commands/attention.js +2 -2
- package/dist/commands/human/doc.js +66 -20
- package/dist/commands/human/prompts.d.ts +3 -3
- package/dist/commands/human/prompts.js +158 -112
- package/dist/commands/human/queue.d.ts +0 -1
- package/dist/commands/human/queue.js +34 -94
- package/dist/commands/human/shared.d.ts +10 -4
- package/dist/commands/human/shared.js +62 -13
- package/dist/commands/human.js +8 -4
- package/dist/commands/memory/read.js +13 -5
- package/dist/commands/node/bash.js +15 -28
- package/dist/commands/surface/node/focus.js +1 -1
- package/dist/core/__tests__/human-deliver.test.js +40 -46
- package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
- package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
- package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
- package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
- package/dist/core/auth-interaction.d.ts +2 -2
- package/dist/core/bash-jobs.d.ts +32 -0
- package/dist/core/bash-jobs.js +72 -1
- package/dist/core/canvas/__tests__/attention.test.js +16 -10
- package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
- package/dist/core/canvas/attention.js +1 -1
- package/dist/core/canvas/browse/app.js +1 -1
- package/dist/core/canvas/nav-model.d.ts +1 -1
- package/dist/core/canvas/nav-model.js +1 -1
- package/dist/core/canvas/remote-canvas-source.js +1 -1
- package/dist/core/config.js +4 -1
- package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-render.test.js +49 -0
- package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
- package/dist/core/human/__tests__/page-tickets.test.js +112 -0
- package/dist/core/human/__tests__/page.test.d.ts +1 -0
- package/dist/core/human/__tests__/page.test.js +139 -0
- package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
- package/dist/core/human/claim.js +2 -2
- package/dist/core/human/convention.d.ts +4 -4
- package/dist/core/human/convention.js +6 -13
- package/dist/core/human/markdown-html.d.ts +8 -0
- package/dist/core/human/markdown-html.js +581 -0
- package/dist/core/human/page-catalog.d.ts +6 -0
- package/dist/core/human/page-catalog.js +43 -0
- package/dist/core/human/page-render.d.ts +3 -0
- package/dist/core/human/page-render.js +131 -0
- package/dist/core/human/page-schema.d.ts +312 -0
- package/dist/core/human/page-schema.js +291 -0
- package/dist/core/human/page-synth.d.ts +9 -0
- package/dist/core/human/page-synth.js +17 -0
- package/dist/core/human/page.d.ts +22 -0
- package/dist/core/human/page.js +427 -0
- package/dist/core/human/review-schema.d.ts +19 -0
- package/dist/core/human/review-schema.js +34 -0
- package/dist/core/human/scan.d.ts +12 -3
- package/dist/core/human/scan.js +69 -26
- package/dist/core/human/summary.d.ts +5 -6
- package/dist/core/human/summary.js +54 -37
- package/dist/core/human/tickets.d.ts +61 -11
- package/dist/core/human/tickets.js +100 -57
- package/dist/core/human/types.d.ts +21 -66
- package/dist/core/human/types.js +1 -5
- package/dist/core/preview-registry.js +0 -1
- package/dist/core/review/ticket-filter.d.ts +1 -1
- package/dist/core/review/ticket-filter.js +2 -8
- package/dist/core/runtime/broker/read-ops.js +3 -0
- package/dist/core/runtime/broker.js +19 -10
- package/dist/core/runtime/package-health.js +1 -1
- package/dist/core/termrender/termrender.d.ts +1 -1
- package/dist/core/termrender/termrender.js +2 -2
- package/dist/core/tui/page-host.d.ts +1 -1
- package/dist/core/tui/page-host.js +2 -2
- package/dist/core/user-settings.d.ts +19 -0
- package/dist/core/user-settings.js +22 -0
- package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
- package/dist/daemon/api/handlers/bash-jobs.js +89 -0
- package/dist/daemon/api/handlers/human.js +16 -14
- package/dist/daemon/api/handlers/inbox.js +311 -227
- package/dist/daemon/api/router.d.ts +5 -1
- package/dist/daemon/api/router.js +5 -0
- package/dist/daemon/api/server.js +2 -0
- package/dist/daemon/cron-run.js +23 -10
- package/dist/daemon/human/finish.d.ts +8 -3
- package/dist/daemon/human/finish.js +35 -15
- package/dist/pages/bundle.css +1 -0
- package/dist/pages/bundle.js +1347 -0
- package/dist/pages/comments.d.ts +34 -0
- package/dist/pages/comments.js +72 -0
- package/dist/pages/elements/cards.d.ts +17 -0
- package/dist/pages/elements/cards.js +785 -0
- package/dist/pages/elements/chart.d.ts +1 -0
- package/dist/pages/elements/chart.js +648 -0
- package/dist/pages/elements/options.d.ts +21 -0
- package/dist/pages/elements/options.js +604 -0
- package/dist/pages/elements/pages.d.ts +24 -0
- package/dist/pages/elements/pages.js +355 -0
- package/dist/pages/elements/slot.d.ts +21 -0
- package/dist/pages/elements/slot.js +105 -0
- package/dist/pages/elements/table.d.ts +19 -0
- package/dist/pages/elements/table.js +906 -0
- package/dist/pages/elements/text.d.ts +25 -0
- package/dist/pages/elements/text.js +951 -0
- package/dist/pages/entry.d.ts +8 -0
- package/dist/pages/entry.js +8 -0
- package/dist/pages/host.d.ts +132 -0
- package/dist/pages/host.js +149 -0
- package/dist/pages/register.d.ts +2 -0
- package/dist/pages/register.js +3 -0
- package/dist/pages/slot-config.d.ts +36 -0
- package/dist/pages/slot-config.js +50 -0
- package/dist/pages/types.d.ts +98 -0
- package/dist/pages/types.js +23 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +2 -0
- package/package.json +8 -8
- package/runtime.lock.json +156 -101
- package/dist/clients/inbox/deck-adapter.d.ts +0 -29
- package/dist/clients/inbox/deck-adapter.js +0 -63
- package/dist/core/human/deck-factories.d.ts +0 -8
- package/dist/core/human/deck-factories.js +0 -15
- package/dist/core/human/deck-schema.d.ts +0 -70
- package/dist/core/human/deck-schema.js +0 -92
|
@@ -1,17 +1,66 @@
|
|
|
1
1
|
import { readConfig } from '../../core/config.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
function markdownAuthoring(productKinds) {
|
|
3
|
+
const productSlots = productKinds.length === 0
|
|
4
|
+
? ''
|
|
5
|
+
: `\nProduct slots registered in this home: ${productKinds.map((kind) => `\`${kind}\``).join(', ')}. Use \`slot:<kind>\`; each is response-bearing.`;
|
|
6
|
+
return [
|
|
7
|
+
'Markdown page (`.md`; one step only)',
|
|
8
|
+
'Front matter starts at byte 0 and recognizes exactly `title` (required), `subtitle` (optional), and `placement` (`inline|panel`, optional, default `panel`); every other key is invalid. Outside front matter, write directive-flavored Markdown. A top-level `slot:<kind>` JSON fence contains one object: reserved top-level `id` identifies a response-bearing slot; every other key is that kind\'s config.',
|
|
9
|
+
'Built-ins: `options` asks for one or more choices; `text` asks for text; `table` asks for row and/or column selections when enabled, otherwise displays data; `cards` asks for card selections; `chart` displays a chart and asks nothing.' + productSlots,
|
|
10
|
+
'Format specimen:',
|
|
11
|
+
'````markdown',
|
|
12
|
+
'---',
|
|
13
|
+
'title: Rollout scope',
|
|
14
|
+
'subtitle: Choose tonight\'s rollout.',
|
|
15
|
+
'placement: inline',
|
|
16
|
+
'---',
|
|
17
|
+
'',
|
|
18
|
+
'Which rollout should ship?',
|
|
19
|
+
'',
|
|
20
|
+
'```slot:options',
|
|
21
|
+
'{"id":"rollout","mode":"single","options":[{"id":"canary","label":"Canary first"}]}',
|
|
22
|
+
'```',
|
|
23
|
+
'````',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
function htmlAuthoring(productKinds) {
|
|
27
|
+
const productSlot = productKinds.length === 0
|
|
28
|
+
? ''
|
|
29
|
+
: ' Product kinds use `<crtr-slot kind="<kind>" id="…">` with the same JSON-script child.';
|
|
30
|
+
return [
|
|
31
|
+
'HTML page (`.html` or `.htm`)',
|
|
32
|
+
'Put required title and optional subtitle in `<head>` metas named `crtr-page-title` and `crtr-page-subtitle`. Placement is declared only by `<meta name="crtr-page-placement" content="inline|panel">` in `<head>`; it defaults to `panel`.',
|
|
33
|
+
'Built-ins are `<crtr-options>`, `<crtr-text>`, `<crtr-table>`, `<crtr-cards>`, and `<crtr-chart>`; `id` is an attribute and config is the first direct `<script type="application/json">` child.' + productSlot,
|
|
34
|
+
'An interactive page (any response-bearing slot), or any multi-step page, requires one `<crtr-pages>` wrapper containing one or more `<crtr-page>` steps, with every response-bearing element inside a step. A pure display page is correctly bare HTML with no `crtr-*` markup at all.',
|
|
35
|
+
'Format specimen:',
|
|
36
|
+
'```html',
|
|
37
|
+
'<!doctype html>',
|
|
38
|
+
'<html><head>',
|
|
39
|
+
' <meta name="crtr-page-title" content="Rollout scope">',
|
|
40
|
+
' <meta name="crtr-page-placement" content="inline">',
|
|
41
|
+
'</head><body>',
|
|
42
|
+
' <crtr-pages><crtr-page>',
|
|
43
|
+
' <crtr-options id="rollout"><script type="application/json">{"mode":"single","options":[{"id":"canary","label":"Canary first"}]}</script></crtr-options>',
|
|
44
|
+
' </crtr-page></crtr-pages>',
|
|
45
|
+
'</body></html>',
|
|
46
|
+
'```',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
/** Invocation-local page capability and authoring contract for `human -h`.
|
|
50
|
+
* The CLI process rebuilds command help each time, so config changes appear on
|
|
51
|
+
* the next invocation without a parallel documentation surface. */
|
|
52
|
+
export function pageHelpText() {
|
|
53
|
+
const config = readConfig('user');
|
|
54
|
+
const hardRule = '**Hard rule:** write the page to a local file and submit it with `--page PATH`; NEVER paste page markup into a message.';
|
|
55
|
+
const shorthand = 'Zero ceremony: `ask` stdin plus `--title` and `--subtitle` synthesizes a one-text-slot page; `notify` title plus `--subtitle` and optional `--body` synthesizes a display page.';
|
|
56
|
+
const dialects = config.html_pages
|
|
57
|
+
? [markdownAuthoring(config.page_components), htmlAuthoring(config.page_components)]
|
|
58
|
+
: [markdownAuthoring(config.page_components)];
|
|
59
|
+
const pageHint = config.html_pages
|
|
60
|
+
? 'Authored page file. This home accepts .md, .html, and .htm; the extension selects the dialect.'
|
|
61
|
+
: 'Authored Markdown page file. This home has html_pages off, so only .md is accepted.';
|
|
62
|
+
return { authoring: [hardRule, shorthand, ...dialects].join('\n\n'), pageHint };
|
|
63
|
+
}
|
|
15
64
|
export function resolveMaxPanes() {
|
|
16
65
|
return readConfig('user').max_panes_per_window;
|
|
17
66
|
}
|
package/dist/commands/human.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Ask creates a bridge node and submits a ticket to the store. Review creation
|
|
4
4
|
// asks crtrd to create a ticket, daemon-owned review, and companion. notify
|
|
5
|
-
// enqueues a durable
|
|
5
|
+
// enqueues a durable page with no node; show is passive display.
|
|
6
6
|
import { defineBranch } from '../core/command.js';
|
|
7
7
|
import { humanAsk, humanNotify, humanShow } from './human/prompts.js';
|
|
8
|
+
import { humanList, humanResolve, humanCancel } from './human/queue.js';
|
|
8
9
|
import { humanReviewBranch } from './human/review.js';
|
|
9
|
-
import { humanList, humanDeck, humanResolve, humanCancel } from './human/queue.js';
|
|
10
10
|
import { humanInbox } from './human/inbox.js';
|
|
11
11
|
import { humanDoc } from './human/doc.js';
|
|
12
|
+
import { pageHelpText } from './human/shared.js';
|
|
12
13
|
export function registerHuman() {
|
|
14
|
+
const pageHelp = pageHelpText();
|
|
13
15
|
return defineBranch({
|
|
14
16
|
name: 'human',
|
|
15
17
|
rootEntry: {
|
|
@@ -20,7 +22,10 @@ export function registerHuman() {
|
|
|
20
22
|
help: {
|
|
21
23
|
name: 'human',
|
|
22
24
|
summary: 'human-in-the-loop decisions, document review, and live display',
|
|
23
|
-
model:
|
|
25
|
+
model: [
|
|
26
|
+
"Tickets queue in the human inbox; nothing opens on screen. `ask` returns immediately and its answer is pushed to the asking node, while `notify` expects no reply. `review` is for live document review with anchored comments; `show` is a passive tmux live-watch rather than a ticket. Validate authored pages with `crtr human doc check`.",
|
|
27
|
+
pageHelp.authoring,
|
|
28
|
+
].join('\n\n'),
|
|
24
29
|
},
|
|
25
30
|
children: [
|
|
26
31
|
humanAsk,
|
|
@@ -29,7 +34,6 @@ export function registerHuman() {
|
|
|
29
34
|
humanShow,
|
|
30
35
|
humanCancel,
|
|
31
36
|
humanList,
|
|
32
|
-
humanDeck,
|
|
33
37
|
humanResolve,
|
|
34
38
|
humanInbox,
|
|
35
39
|
humanDoc,
|
|
@@ -55,6 +55,12 @@ function indexRoutes(doc) {
|
|
|
55
55
|
if (!byName.has(d.name))
|
|
56
56
|
byName.set(d.name, d);
|
|
57
57
|
const prefix = dir + '/';
|
|
58
|
+
// Keyed by RENDERED identity (`displayName`), not `.name`: one physical
|
|
59
|
+
// directory INDEX answers to two names — `listAllMemoryDocs` emits the
|
|
60
|
+
// path-derived `dir/INDEX`, while `resolveMemoryDoc('dir')` returns it
|
|
61
|
+
// carrying the bare dir as its identity. Keying on `.name` let a body link
|
|
62
|
+
// to a sub-INDEX (written the normal way, by bare dir name) miss the entry
|
|
63
|
+
// the directory scan already added, and the doc rendered twice.
|
|
58
64
|
const members = new Map();
|
|
59
65
|
for (const [name, d] of byName) {
|
|
60
66
|
if (!name.startsWith(prefix) || name === canonicalName)
|
|
@@ -70,8 +76,8 @@ function indexRoutes(doc) {
|
|
|
70
76
|
break;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
|
-
if (!members.has(member.name))
|
|
74
|
-
members.set(member.name, member);
|
|
79
|
+
if (!members.has(displayName(member.name)))
|
|
80
|
+
members.set(displayName(member.name), member);
|
|
75
81
|
}
|
|
76
82
|
// No members means this INDEX gates no cluster in the corpus namespace — a
|
|
77
83
|
// store-root front door whose explicit frontmatter name dodges the root
|
|
@@ -88,10 +94,12 @@ function indexRoutes(doc) {
|
|
|
88
94
|
catch {
|
|
89
95
|
continue; // dangling link — lint's finding, not read's
|
|
90
96
|
}
|
|
91
|
-
if (displayName(linked.name) !== linkName
|
|
97
|
+
if (displayName(linked.name) !== linkName)
|
|
92
98
|
continue;
|
|
93
|
-
if (
|
|
94
|
-
|
|
99
|
+
if (linkName === displayName(canonicalName) || linkName === displayName(doc.name))
|
|
100
|
+
continue;
|
|
101
|
+
if (!members.has(linkName))
|
|
102
|
+
members.set(linkName, linked);
|
|
95
103
|
}
|
|
96
104
|
return [...members.values()]
|
|
97
105
|
.map((m) => {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// `crtr node bash` — file-backed background job controls.
|
|
2
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
3
2
|
import { defineBranch, defineLeaf } from '../../core/command.js';
|
|
4
3
|
import { InputError } from '../../core/io.js';
|
|
5
4
|
import { displayMessage } from '../../core/runtime/tmux-chrome.js';
|
|
6
5
|
import { contextDir } from '../../core/canvas/paths.js';
|
|
7
|
-
import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobPaths, formatBashElapsed, jobStartedAtMs, readJobPgid, tailJobLog } from '../../core/bash-jobs.js';
|
|
6
|
+
import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobPaths, formatBashElapsed, isSafeBashJobId, jobStartedAtMs, readJobPgid, stopBackgroundBashJob, tailJobLog } from '../../core/bash-jobs.js';
|
|
8
7
|
import { cliClient, getNodeOrNull } from '../api-client.js';
|
|
9
8
|
import { nodeInPane } from '../surface/node/placement.js';
|
|
10
9
|
function resolveBashTarget(input, missingMessage) {
|
|
@@ -111,17 +110,6 @@ const nodeBashList = defineLeaf({
|
|
|
111
110
|
return jobs.map((job) => `${job.job_id} · ${formatBashElapsed(job.elapsed_ms)} · ${job.command.split('\n')[0]}\n log: ${job.log_path}`).join('\n');
|
|
112
111
|
},
|
|
113
112
|
});
|
|
114
|
-
/** SIGTERM the job's process group, give it a moment, then SIGKILL. Returns
|
|
115
|
-
* false only when the group was already gone at the first signal. */
|
|
116
|
-
function stopJobGroup(pgid) {
|
|
117
|
-
try {
|
|
118
|
-
process.kill(-pgid, 'SIGTERM');
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
113
|
const nodeBashKill = defineLeaf({
|
|
126
114
|
name: 'kill',
|
|
127
115
|
description: 'stop one of a node’s background bash jobs',
|
|
@@ -153,35 +141,34 @@ const nodeBashKill = defineLeaf({
|
|
|
153
141
|
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
154
142
|
}
|
|
155
143
|
const jobId = input['job'];
|
|
144
|
+
if (!isSafeBashJobId(jobId)) {
|
|
145
|
+
throw new InputError({ error: 'invalid_job_id', message: `invalid background job id: ${jobId}`, next: 'Use a job id reported by `crtr node bash list`.' });
|
|
146
|
+
}
|
|
156
147
|
const dir = contextDir(node.node_id);
|
|
157
148
|
const paths = bashJobPaths(dir, jobId);
|
|
158
|
-
|
|
149
|
+
const stopped = await stopBackgroundBashJob(dir, jobId);
|
|
150
|
+
if (stopped.kind === 'missing') {
|
|
159
151
|
throw new InputError({ error: 'not_found', message: `no background job ${jobId} for ${node.node_id}`, next: 'List live jobs with `crtr node bash list`.' });
|
|
160
152
|
}
|
|
161
|
-
|
|
162
|
-
|
|
153
|
+
if (stopped.kind === 'retired') {
|
|
154
|
+
throw new InputError({
|
|
155
|
+
error: 'not_found',
|
|
156
|
+
message: `background job ${jobId} for ${node.node_id} is no longer running`,
|
|
157
|
+
next: `It finished or was already stopped, so nothing was signaled. Log: ${stopped.logPath}`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (stopped.kind === 'not-stoppable') {
|
|
163
161
|
throw new InputError({
|
|
164
162
|
error: 'not_stoppable',
|
|
165
163
|
message: `job ${jobId} recorded no process group`,
|
|
166
164
|
next: `It started before crtr persisted one. Stop it by hand, then it will disappear from the list. Log: ${paths.jobLog}`,
|
|
167
165
|
});
|
|
168
166
|
}
|
|
169
|
-
const signaled = stopJobGroup(pgid);
|
|
170
|
-
// The supervisor shares the group it leads, so it usually dies with the
|
|
171
|
-
// command and never writes job.exit. Record the cancellation ourselves —
|
|
172
|
-
// that sentinel is what retires the job from every live-jobs read.
|
|
173
|
-
await new Promise((r) => setTimeout(r, 400));
|
|
174
|
-
try {
|
|
175
|
-
process.kill(-pgid, 'SIGKILL');
|
|
176
|
-
}
|
|
177
|
-
catch { /* already gone */ }
|
|
178
|
-
if (!existsSync(paths.jobExit))
|
|
179
|
-
writeFileSync(paths.jobExit, '143\n');
|
|
180
167
|
await cliClient().sendMessage(node.node_id, {
|
|
181
168
|
body: `Background bash job ${jobId} was stopped by the human before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
|
|
182
169
|
tier: 'urgent',
|
|
183
170
|
});
|
|
184
|
-
return { node_id: node.node_id, job_id: jobId, signaled };
|
|
171
|
+
return { node_id: node.node_id, job_id: jobId, signaled: stopped.signaled };
|
|
185
172
|
},
|
|
186
173
|
render: (result) => `Canceled background bash job ${result['job_id']} for ${result['node_id']}${result['signaled'] === true ? '' : ' (its process group was already gone)'}.`,
|
|
187
174
|
});
|
|
@@ -30,7 +30,7 @@ export const surfaceNodeFocusLeaf = defineLeaf({
|
|
|
30
30
|
const node = await cliClient()
|
|
31
31
|
.getNode(id)
|
|
32
32
|
.catch((err) => rethrowAsCliError(err, 'List nodes with `crtr node inspect list`.'));
|
|
33
|
-
// A kind:'human' node is a control-plane ASK (a
|
|
33
|
+
// A kind:'human' node is a control-plane ASK (a page on the human's
|
|
34
34
|
// screen), NOT a pi conversation — it has no session. Reviving one boots a
|
|
35
35
|
// confused blank "you have been revived" pi, so refuse rather than focus it.
|
|
36
36
|
// (The nav/resume UIs already hide human nodes; this guards a hand-typed id.)
|
|
@@ -5,21 +5,18 @@ import { test, describe, before, after, beforeEach, afterEach } from 'node:test'
|
|
|
5
5
|
import assert from 'node:assert/strict';
|
|
6
6
|
import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join
|
|
8
|
+
import { join } from 'node:path';
|
|
9
9
|
import { Readable } from 'node:stream';
|
|
10
10
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
11
|
-
import { cancelHumanTicket, deliverTerminalResult,
|
|
11
|
+
import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket } from '../../daemon/human/finish.js';
|
|
12
12
|
import { humanAsk, humanNotify } from '../../commands/human/prompts.js';
|
|
13
|
-
import { runCli } from '../command.js';
|
|
14
13
|
import { getNode, listNodes, closeDb } from '../canvas/index.js';
|
|
15
14
|
import { readInboxSince } from '../feed/inbox.js';
|
|
16
15
|
import { resolveRef } from '../canvas/history.js';
|
|
17
|
-
import { apiSocketPath
|
|
16
|
+
import { apiSocketPath } from '../canvas/paths.js';
|
|
18
17
|
import { spawnNode } from '../runtime/nodes.js';
|
|
19
|
-
import { resolveRoot } from '../../build-root.js';
|
|
20
|
-
import { validateDeck } from '../human/deck-schema.js';
|
|
21
18
|
import { ticketDir } from '../human/root.js';
|
|
22
|
-
import { readTicketResult,
|
|
19
|
+
import { readTicketResult, submitPage } from '../human/tickets.js';
|
|
23
20
|
let home;
|
|
24
21
|
let priorHome;
|
|
25
22
|
let server;
|
|
@@ -58,15 +55,27 @@ function bridgeUnder() {
|
|
|
58
55
|
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-ask', lifecycle: 'terminal' }).node_id;
|
|
59
56
|
return { parent, bridge };
|
|
60
57
|
}
|
|
61
|
-
function
|
|
58
|
+
function submitBridgePage(bridge) {
|
|
62
59
|
const dir = ticketDir(bridge);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-deliver-page-'));
|
|
61
|
+
const sourceFile = join(sourceDir, 'page.md');
|
|
62
|
+
writeFileSync(sourceFile, [
|
|
63
|
+
'---',
|
|
64
|
+
'title: Ship it?',
|
|
65
|
+
'subtitle: Decide whether to ship now so the release can proceed.',
|
|
66
|
+
'---',
|
|
67
|
+
'',
|
|
68
|
+
'```slot:text',
|
|
69
|
+
'{ "id": "question", "initialText": "", "editable": true }',
|
|
70
|
+
'```',
|
|
71
|
+
'',
|
|
72
|
+
].join('\n'));
|
|
73
|
+
try {
|
|
74
|
+
submitPage({ dir, sourceFile, dialect: 'md' });
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(sourceDir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
70
79
|
return dir;
|
|
71
80
|
}
|
|
72
81
|
function reportBody(ref) {
|
|
@@ -110,47 +119,32 @@ describe('enqueue is producer-only', () => {
|
|
|
110
119
|
assert.equal(typeof result['dir'], 'string');
|
|
111
120
|
for (const forbidden of ['pane_id', 'render_pane_id', 'pid', 'worker_pid'])
|
|
112
121
|
assert.ok(!(forbidden in result), `ask result must not expose ${forbidden}`);
|
|
113
|
-
const
|
|
114
|
-
assert.equal(
|
|
115
|
-
assert.equal(
|
|
116
|
-
|
|
117
|
-
assert.
|
|
118
|
-
assert.equal(
|
|
119
|
-
const deckDirectory = mkdtempSync(join(tmpdir(), 'crtr-context-file-deck-'));
|
|
120
|
-
assert.notEqual(resolve(process.cwd()), deckDirectory, 'the CLI caller cwd must differ from the deck directory');
|
|
121
|
-
const deckPath = join(deckDirectory, 'deck.json');
|
|
122
|
-
writeFileSync(join(deckDirectory, 'body.md'), 'The body came from beside the deck JSON.');
|
|
123
|
-
writeFileSync(deckPath, JSON.stringify({ title: 'Context file body', interactions: [{ id: 'decision', title: 'Use the deck', subtitle: 'Use the deck body so the person can decide with its context.', bodyPath: 'body.md', options: [{ id: 'yes', label: 'Yes' }] }] }));
|
|
124
|
-
const nodesBefore = new Set(listNodes().map((node) => node.node_id));
|
|
125
|
-
await withMutedStdout(() => withPipedStdin('', async () => {
|
|
126
|
-
await runCli(await resolveRoot('human'), ['node', 'crtr', 'human', 'ask', '--context-file', deckPath]);
|
|
127
|
-
}));
|
|
128
|
-
const bridge = listNodes().find((node) => !nodesBefore.has(node.node_id));
|
|
129
|
-
assert.ok(bridge, 'the real CLI invocation created a human bridge');
|
|
130
|
-
const contextDeck = JSON.parse(readFileSync(join(nodeDir(bridge.node_id), 'deck.json'), 'utf8'));
|
|
131
|
-
assert.equal(contextDeck.interactions[0]?.body, 'The body came from beside the deck JSON.');
|
|
132
|
-
assert.equal(contextDeck.interactions[0]?.bodyPath, undefined);
|
|
122
|
+
const manifest = JSON.parse(readFileSync(join(result['dir'], 'page.json'), 'utf8'));
|
|
123
|
+
assert.equal(manifest.title, 'Release decision');
|
|
124
|
+
assert.equal(manifest.subtitle, 'Decide whether to release now so the launch can proceed.');
|
|
125
|
+
const document = readFileSync(join(result['dir'], 'page.md'), 'utf8');
|
|
126
|
+
assert.ok(document.includes('This first line must never become the title.'));
|
|
127
|
+
assert.equal(manifest.slots.filter((slot) => slot.kind === 'text').length, 1, 'the synthesized question page carries one response slot');
|
|
133
128
|
});
|
|
134
129
|
});
|
|
135
130
|
describe('terminal ticket delivery', () => {
|
|
136
|
-
test('
|
|
131
|
+
test('resolvePageTicket pushFinals the readable answer once and wakes the subscribed parent', async () => {
|
|
137
132
|
const { parent, bridge } = bridgeUnder();
|
|
138
|
-
const dir =
|
|
139
|
-
const result = await
|
|
140
|
-
assert.equal(result.kind, '
|
|
133
|
+
const dir = submitBridgePage(bridge);
|
|
134
|
+
const result = await resolvePageTicket(bridge, { question: { text: 'yes, ship', edited: true, comments: [] } });
|
|
135
|
+
assert.equal(result.kind, 'page');
|
|
141
136
|
closeDb();
|
|
142
137
|
assert.equal(getNode(bridge)?.status, 'done');
|
|
143
138
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
144
139
|
assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
|
|
145
140
|
const body = reportBody(finals[0].ref);
|
|
146
|
-
assert.
|
|
141
|
+
assert.ok(body.includes('"text":"yes, ship"'), 'the slot answer is inlined in the report');
|
|
147
142
|
assert.ok(body.includes(join(dir, 'response.json')));
|
|
148
|
-
assert.ok(!body.includes('humanloop.response/v2'));
|
|
149
143
|
});
|
|
150
144
|
test('a replay against a terminal bridge is a no-op', async () => {
|
|
151
145
|
const { parent, bridge } = bridgeUnder();
|
|
152
|
-
|
|
153
|
-
await
|
|
146
|
+
submitBridgePage(bridge);
|
|
147
|
+
await resolvePageTicket(bridge, { question: { text: 'yes', edited: true, comments: [] } });
|
|
154
148
|
assert.equal(await deliverTerminalResult(bridge), true);
|
|
155
149
|
closeDb();
|
|
156
150
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
@@ -162,7 +156,7 @@ describe('terminal ticket delivery', () => {
|
|
|
162
156
|
const other = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'other' }).node_id;
|
|
163
157
|
const { subscribe } = await import('../canvas/canvas.js');
|
|
164
158
|
subscribe(other, bridge, true);
|
|
165
|
-
|
|
159
|
+
submitBridgePage(bridge);
|
|
166
160
|
await cancelHumanTicket(bridge, { reason: 'stale', actor });
|
|
167
161
|
closeDb();
|
|
168
162
|
assert.equal(getNode(bridge)?.status, 'done');
|
|
@@ -173,12 +167,12 @@ describe('terminal ticket delivery', () => {
|
|
|
173
167
|
assert.match(notes[0].label, /canceled/);
|
|
174
168
|
});
|
|
175
169
|
});
|
|
176
|
-
test('notify enqueues a durable
|
|
170
|
+
test('notify enqueues a durable page and creates no canvas node', async () => {
|
|
177
171
|
const before = listNodes().length;
|
|
178
172
|
const result = (await humanNotify.run({ title: 'Build done', subtitle: 'The build passed, so the change is ready to review.' }));
|
|
179
173
|
assert.equal(result['queued'], true);
|
|
180
174
|
assert.equal(typeof result['dir'], 'string');
|
|
181
|
-
assert.ok(existsSync(join(result['dir'], '
|
|
175
|
+
assert.ok(existsSync(join(result['dir'], 'page.json')));
|
|
182
176
|
assert.ok(readTicketResult(result['dir']) === null);
|
|
183
177
|
closeDb();
|
|
184
178
|
assert.equal(listNodes().length, before);
|
|
@@ -75,7 +75,7 @@ test('G1 — controller prompt runs the engine and the streamed AgentSessionEven
|
|
|
75
75
|
// ran AND its terminal frame reached the client; the intermediate types prove
|
|
76
76
|
// the stream (not just a final blob) was fanned out.
|
|
77
77
|
await c.waitFrame((f) => f.type === 'agent_end' && frameHas(f, token), 'G1 agent_end carrying the prompt token');
|
|
78
|
-
for (const t of ['message_start', 'message_update', 'tool_execution_start', 'tool_execution_end', 'message_end', 'turn_end', 'agent_start', 'turn_start']) {
|
|
78
|
+
for (const t of ['message_start', 'message_update', 'tool_execution_start', 'tool_execution_update', 'tool_execution_end', 'message_end', 'turn_end', 'agent_start', 'turn_start']) {
|
|
79
79
|
assert.ok(c.frames.some((f) => f.type === t), `G1: client received a relayed ${t} frame`);
|
|
80
80
|
}
|
|
81
81
|
assert.ok(c.frames.some((f) => f.type === 'message_start' && frameHas(f, token)), 'G1: a relayed message carries the prompt token');
|
|
@@ -101,6 +101,12 @@ test('G1b — coalesced message_update never arrives after message_end; updates
|
|
|
101
101
|
const msgEnd = types.indexOf('message_end');
|
|
102
102
|
assert.ok(lastUpdate >= 0, 'G1b: at least one message_update relayed (coalescing must not starve updates)');
|
|
103
103
|
assert.ok(msgEnd >= 0, 'G1b: message_end relayed');
|
|
104
|
+
const updates = c.frames.filter((frame) => frame.type === 'message_update');
|
|
105
|
+
assert.ok(updates.length < 12, `G1b: ${updates.length} relayed updates proves the 12-update burst was coalesced`);
|
|
106
|
+
for (const update of updates) {
|
|
107
|
+
assert.equal(update.message.role, 'assistant', 'G1b: crouter relays the in-process cumulative message_update contract');
|
|
108
|
+
assert.ok('partial' in update.assistantMessageEvent, 'G1b: crouter does not route pi JSON/RPC delta-only events through view.sock');
|
|
109
|
+
}
|
|
104
110
|
assert.ok(lastUpdate < msgEnd, `G1b: a message_update arrived AFTER message_end (update@${lastUpdate} vs end@${msgEnd}) — the coalescer flushed out of order`);
|
|
105
111
|
// tool/turn/agent frames must also never precede a stale held update.
|
|
106
112
|
for (const t of ['tool_execution_start', 'turn_end', 'agent_end']) {
|
|
@@ -4,16 +4,15 @@
|
|
|
4
4
|
// delivery is daemon-owned; no subprocess handler or delivery receipt exists.
|
|
5
5
|
import { test, before, beforeEach, afterEach, after } from 'node:test';
|
|
6
6
|
import assert from 'node:assert/strict';
|
|
7
|
-
import { mkdtempSync, existsSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { mkdtempSync, rmSync, existsSync, writeFileSync } from 'node:fs';
|
|
8
8
|
import { tmpdir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { ApiError, CrtrClient } from '../../../api/index.js';
|
|
11
11
|
import { createApiServer } from '../../../daemon/api/server.js';
|
|
12
12
|
import { apiSocketPath } from '../../canvas/paths.js';
|
|
13
|
-
import { validateDeck } from '../../human/deck-schema.js';
|
|
14
13
|
import { claimTicket, readTicketClaim } from '../../human/claim.js';
|
|
15
14
|
import { ticketDir } from '../../human/root.js';
|
|
16
|
-
import {
|
|
15
|
+
import { submitPage, submitReview } from '../../human/tickets.js';
|
|
17
16
|
import { spawnNode } from '../../runtime/nodes.js';
|
|
18
17
|
import { getNode, closeDb } from '../../canvas/index.js';
|
|
19
18
|
import { readInboxSince } from '../../feed/inbox.js';
|
|
@@ -63,6 +62,30 @@ async function expectBadResolve(nodeId, body) {
|
|
|
63
62
|
async function expectReviewSubmitGuidance(nodeId, body) {
|
|
64
63
|
await assert.rejects(() => client.request('POST', `/v1/human/tickets/${nodeId}/resolve`, body), (error) => error instanceof ApiError && error.status === 400 && error.code === 'invalid_request' && error.message.includes('POST /v1/human/reviews/:review_id/submit'));
|
|
65
64
|
}
|
|
65
|
+
/** Publish a one-text-slot page ticket into the bridge's ticket directory. */
|
|
66
|
+
function submitBridgePage(bridge) {
|
|
67
|
+
const dir = ticketDir(bridge);
|
|
68
|
+
const sourceDir = mkdtempSync(join(tmpdir(), 'crtr-e2e-page-'));
|
|
69
|
+
const sourceFile = join(sourceDir, 'page.md');
|
|
70
|
+
writeFileSync(sourceFile, [
|
|
71
|
+
'---',
|
|
72
|
+
'title: Ship it?',
|
|
73
|
+
'subtitle: Decide whether to ship now so the release can proceed.',
|
|
74
|
+
'---',
|
|
75
|
+
'',
|
|
76
|
+
'```slot:text',
|
|
77
|
+
'{ "id": "question", "initialText": "", "editable": true }',
|
|
78
|
+
'```',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n'));
|
|
81
|
+
try {
|
|
82
|
+
submitPage({ dir, sourceFile, dialect: 'md' });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
rmSync(sourceDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
66
89
|
function reportBody(ref) {
|
|
67
90
|
const resolved = resolveRef(ref);
|
|
68
91
|
if (resolved === null || resolved.source !== 'report')
|
|
@@ -71,24 +94,17 @@ function reportBody(ref) {
|
|
|
71
94
|
}
|
|
72
95
|
test('malformed resolves leave the mounted ticket claim in place', async () => {
|
|
73
96
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
|
-
const deckClaim = claimTicket(deckDir);
|
|
84
|
-
assert.ok(deckClaim !== null);
|
|
85
|
-
for (const body of [{ responses: [null] }, { response: [] }, { responses: [], debug: true }]) {
|
|
86
|
-
await expectBadResolve(deckBridge, body);
|
|
87
|
-
assert.equal(readTicketClaim(deckDir)?.token, deckClaim.token, 'malformed deck input must not take over the live claim');
|
|
97
|
+
const pageBridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
|
|
98
|
+
const pageDir = submitBridgePage(pageBridge);
|
|
99
|
+
const pageClaim = claimTicket(pageDir);
|
|
100
|
+
assert.ok(pageClaim !== null);
|
|
101
|
+
for (const body of [{ responses: [] }, { response: {} }, { responses: {}, debug: true }]) {
|
|
102
|
+
await expectBadResolve(pageBridge, body);
|
|
103
|
+
assert.equal(readTicketClaim(pageDir)?.token, pageClaim.token, 'malformed page input must not take over the live claim');
|
|
88
104
|
}
|
|
89
|
-
for (const body of [{ responses:
|
|
90
|
-
await expectReviewSubmitGuidance(
|
|
91
|
-
assert.equal(readTicketClaim(
|
|
105
|
+
for (const body of [{ responses: {}, feedback: {} }, { feedback: [] }]) {
|
|
106
|
+
await expectReviewSubmitGuidance(pageBridge, body);
|
|
107
|
+
assert.equal(readTicketClaim(pageDir)?.token, pageClaim.token, 'feedback-shaped input must not take over the live claim');
|
|
92
108
|
}
|
|
93
109
|
const reviewBridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-review', lifecycle: 'terminal' }).node_id;
|
|
94
110
|
const reviewDir = ticketDir(reviewBridge);
|
|
@@ -97,22 +113,15 @@ test('malformed resolves leave the mounted ticket claim in place', async () => {
|
|
|
97
113
|
submitReview({ dir: reviewDir, review: { title: 'Review', subtitle: 'Review this file so implementation can proceed safely.', source: {}, file } });
|
|
98
114
|
const reviewClaim = claimTicket(reviewDir);
|
|
99
115
|
assert.ok(reviewClaim !== null);
|
|
100
|
-
await expectReviewSubmitGuidance(reviewBridge, { responses:
|
|
116
|
+
await expectReviewSubmitGuidance(reviewBridge, { responses: {} });
|
|
101
117
|
assert.equal(readTicketClaim(reviewDir)?.token, reviewClaim.token, 'review ticket resolve must not take over the live claim');
|
|
102
118
|
});
|
|
103
119
|
test('the ticket resolve route pushFinals the readable answer to the parent', async () => {
|
|
104
120
|
const parent = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'asker' }).node_id;
|
|
105
121
|
const bridge = spawnNode({ kind: 'human', parent, cwd: home, name: 'human-ask', lifecycle: 'terminal' }).node_id;
|
|
106
|
-
const dir =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
deck: validateDeck({
|
|
110
|
-
title: 'Ship it?',
|
|
111
|
-
interactions: [{ id: 'question', title: 'Ship it?', subtitle: 'Decide whether to ship now so the release can proceed.', options: [], allowFreetext: true }],
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
const result = await client.resolveHumanTicket(bridge, { responses: [{ id: 'question', freetext: 'yes, ship' }] });
|
|
115
|
-
assert.deepEqual(result, { delivered: true, kind: 'deck' });
|
|
122
|
+
const dir = submitBridgePage(bridge);
|
|
123
|
+
const result = await client.resolveHumanTicket(bridge, { responses: { question: { text: 'yes, ship', edited: true, comments: [] } } });
|
|
124
|
+
assert.deepEqual(result, { delivered: true, kind: 'page' });
|
|
116
125
|
assert.equal(getNode(bridge)?.status, 'done', 'bridge finalized by crtrd');
|
|
117
126
|
const finals = readInboxSince(parent).filter((entry) => entry.from === bridge && entry.kind === 'final');
|
|
118
127
|
assert.equal(finals.length, 1, 'exactly one pushFinal reached the parent');
|
|
@@ -8,6 +8,7 @@ import { tmpdir } from 'node:os';
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { createServer } from 'node:net';
|
|
10
10
|
import { spawn as spawnProcess } from 'node:child_process';
|
|
11
|
+
import { findDaemonPids, stopDaemonProcess } from '../../../daemon/manage.js';
|
|
11
12
|
import { createNode, getNode, getRow, subscribersOf } from '../../canvas/canvas.js';
|
|
12
13
|
import { closeDb } from '../../canvas/db.js';
|
|
13
14
|
import { nodeDir } from '../../canvas/paths.js';
|
|
@@ -40,8 +41,9 @@ beforeEach(() => {
|
|
|
40
41
|
closeDb();
|
|
41
42
|
rmSync(home, { recursive: true, force: true });
|
|
42
43
|
});
|
|
43
|
-
after(() => {
|
|
44
|
+
after(async () => {
|
|
44
45
|
closeDb();
|
|
46
|
+
await Promise.all(findDaemonPids().map((pid) => stopDaemonProcess(pid)));
|
|
45
47
|
rmSync(home, { recursive: true, force: true });
|
|
46
48
|
});
|
|
47
49
|
test('managed child: parent on the spine + active subscription + provenance', () => {
|
|
@@ -18,6 +18,7 @@ import { closeDb } from '../canvas/db.js';
|
|
|
18
18
|
import { spawnNode } from '../runtime/nodes.js';
|
|
19
19
|
import { spawnChild } from '../runtime/spawn.js';
|
|
20
20
|
import { managedWorktreePath } from '../worktree.js';
|
|
21
|
+
import { findDaemonPids, stopDaemonProcess } from '../../daemon/manage.js';
|
|
21
22
|
let home;
|
|
22
23
|
let repoDir;
|
|
23
24
|
function run(args, cwd) {
|
|
@@ -41,8 +42,9 @@ beforeEach(() => {
|
|
|
41
42
|
rmSync(home, { recursive: true, force: true });
|
|
42
43
|
process.env['CRTR_HOME'] = home;
|
|
43
44
|
});
|
|
44
|
-
after(() => {
|
|
45
|
+
after(async () => {
|
|
45
46
|
closeDb();
|
|
47
|
+
await Promise.all(findDaemonPids().map((pid) => stopDaemonProcess(pid)));
|
|
46
48
|
rmSync(home, { recursive: true, force: true });
|
|
47
49
|
rmSync(repoDir, { recursive: true, force: true });
|
|
48
50
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProviderAuthInteraction } from '@earendil-works/pi-ai';
|
|
2
2
|
import type { LoginDialogComponent } from '@earendil-works/pi-coding-agent';
|
|
3
3
|
export type DialogInteractionOptions = {
|
|
4
4
|
/** Open the verification URL for `auth_url` / `device_code` events. */
|
|
@@ -16,4 +16,4 @@ export type DialogInteractionOptions = {
|
|
|
16
16
|
* the dialog occupies crouter's single inline slot in both surfaces, so swapping in pi's
|
|
17
17
|
* separate selector component (what pi's own interactive mode does) has nowhere to go.
|
|
18
18
|
*/
|
|
19
|
-
export declare function dialogAuthInteraction(dialog: LoginDialogComponent, options?: DialogInteractionOptions):
|
|
19
|
+
export declare function dialogAuthInteraction(dialog: LoginDialogComponent, options?: DialogInteractionOptions): ProviderAuthInteraction;
|