@north-light/crouter 0.3.197 → 0.3.198

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.
Files changed (144) hide show
  1. package/dist/api/client.d.ts +2 -2
  2. package/dist/api/dto/inbox.d.ts +46 -0
  3. package/dist/builtin-memory/00-runtime-base.md +3 -3
  4. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  5. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  6. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +1 -1
  7. package/dist/clients/attach/render/chat-view.d.ts +3 -8
  8. package/dist/clients/attach/render/chat-view.js +3 -24
  9. package/dist/clients/attach/render/markdown-source.js +0 -6
  10. package/dist/clients/attach/render/tool-calls.js +16 -2
  11. package/dist/clients/attach/viewer.js +613 -602
  12. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +2 -2
  13. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +11 -4
  14. package/dist/clients/inbox/controller.js +4 -4
  15. package/dist/clients/inbox/page-adapter.d.ts +2 -1
  16. package/dist/clients/inbox/tui/render.js +46 -25
  17. package/dist/clients/inbox/tui/slots.d.ts +1 -3
  18. package/dist/clients/inbox/tui/slots.js +8 -37
  19. package/dist/clients/inbox/tui/types.d.ts +4 -3
  20. package/dist/commands/__tests__/human.test.js +160 -23
  21. package/dist/commands/canvas-browse.js +1 -1
  22. package/dist/commands/human/doc.js +21 -74
  23. package/dist/commands/human/html.d.ts +2 -0
  24. package/dist/commands/human/html.js +81 -0
  25. package/dist/commands/human/prompts.d.ts +5 -4
  26. package/dist/commands/human/prompts.js +81 -153
  27. package/dist/commands/human/shared.d.ts +5 -6
  28. package/dist/commands/human/shared.js +13 -61
  29. package/dist/commands/human.js +7 -2
  30. package/dist/core/__tests__/daemon-boot.test.js +7 -4
  31. package/dist/core/__tests__/dead-node-policy-table.test.js +20 -36
  32. package/dist/core/__tests__/human-deliver.test.js +28 -37
  33. package/dist/core/__tests__/seam/broker-crash-teardown.test.js +13 -11
  34. package/dist/core/__tests__/seam/dormancy-release.test.js +63 -4
  35. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +3 -13
  36. package/dist/core/canvas/__tests__/attention.test.js +2 -2
  37. package/dist/core/canvas/__tests__/render-remote.test.js +2 -2
  38. package/dist/core/canvas/browse/app.d.ts +1 -1
  39. package/dist/core/canvas/browse/app.js +21 -3
  40. package/dist/core/canvas/browse/render.d.ts +4 -0
  41. package/dist/core/canvas/browse/render.js +5 -1
  42. package/dist/core/canvas/canvas.d.ts +26 -0
  43. package/dist/core/canvas/canvas.js +55 -0
  44. package/dist/core/command.js +6 -11
  45. package/dist/core/config.js +3 -2
  46. package/dist/core/help.d.ts +3 -0
  47. package/dist/core/human/__tests__/html-markdown.test.d.ts +1 -0
  48. package/dist/core/human/__tests__/html-markdown.test.js +52 -0
  49. package/dist/core/human/__tests__/page-catalog.test.d.ts +1 -0
  50. package/dist/core/human/__tests__/page-catalog.test.js +16 -0
  51. package/dist/core/human/__tests__/page-render.test.js +40 -23
  52. package/dist/core/human/__tests__/page-tickets.test.js +48 -27
  53. package/dist/core/human/__tests__/page.test.js +48 -65
  54. package/dist/core/human/__tests__/serial/inbox-core.test.js +5 -15
  55. package/dist/core/human/answer-text.d.ts +3 -0
  56. package/dist/core/human/answer-text.js +98 -0
  57. package/dist/core/human/answer.d.ts +51 -0
  58. package/dist/core/human/answer.js +125 -0
  59. package/dist/core/human/component-docs.d.ts +14 -0
  60. package/dist/core/human/component-docs.js +161 -0
  61. package/dist/core/human/html-markdown.d.ts +3 -0
  62. package/dist/core/human/html-markdown.js +288 -0
  63. package/dist/core/human/page-catalog.d.ts +19 -3
  64. package/dist/core/human/page-catalog.js +73 -16
  65. package/dist/core/human/page-render.d.ts +3 -2
  66. package/dist/core/human/page-render.js +19 -45
  67. package/dist/core/human/page-schema.d.ts +108 -8
  68. package/dist/core/human/page-schema.js +88 -31
  69. package/dist/core/human/page-synth.d.ts +2 -4
  70. package/dist/core/human/page-synth.js +23 -7
  71. package/dist/core/human/page.d.ts +14 -7
  72. package/dist/core/human/page.js +178 -90
  73. package/dist/core/human/review-schema.d.ts +1 -0
  74. package/dist/core/human/review-schema.js +1 -1
  75. package/dist/core/human/scan.d.ts +3 -2
  76. package/dist/core/human/scan.js +7 -8
  77. package/dist/core/human/summary.d.ts +4 -7
  78. package/dist/core/human/summary.js +4 -60
  79. package/dist/core/human/tickets.d.ts +10 -9
  80. package/dist/core/human/tickets.js +5 -3
  81. package/dist/core/human/types.d.ts +4 -0
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +1 -0
  84. package/dist/core/review/realize.js +9 -1
  85. package/dist/core/runtime/boot-root.js +6 -1
  86. package/dist/core/runtime/broker.js +8 -6
  87. package/dist/core/runtime/busy.d.ts +4 -3
  88. package/dist/core/runtime/busy.js +4 -3
  89. package/dist/core/runtime/spawn.js +4 -4
  90. package/dist/core/user-settings.d.ts +17 -2
  91. package/dist/core/user-settings.js +32 -11
  92. package/dist/daemon/api/handlers/human.js +1 -1
  93. package/dist/daemon/api/handlers/inbox.js +22 -7
  94. package/dist/daemon/cron-run.js +16 -2
  95. package/dist/daemon/crtrd.js +1 -1
  96. package/dist/daemon/fleet.d.ts +22 -10
  97. package/dist/daemon/fleet.js +43 -24
  98. package/dist/daemon/human/finish.d.ts +2 -1
  99. package/dist/daemon/human/finish.js +45 -28
  100. package/dist/daemon/human/sweep.js +4 -2
  101. package/dist/daemon/reconcilers/broker-supervision.js +11 -8
  102. package/dist/daemon/reconcilers/dormant-inbox.js +11 -6
  103. package/dist/daemon/reconcilers/live-obligation.d.ts +21 -1
  104. package/dist/daemon/reconcilers/live-obligation.js +30 -2
  105. package/dist/daemon/reconcilers/storage-maintenance.d.ts +5 -0
  106. package/dist/daemon/reconcilers/storage-maintenance.js +28 -1
  107. package/dist/daemon/review/sweep.js +12 -1
  108. package/dist/pages/bundle.css +1 -1
  109. package/dist/pages/bundle.js +1008 -577
  110. package/dist/pages/comments.d.ts +29 -0
  111. package/dist/pages/comments.js +51 -0
  112. package/dist/pages/controls.d.ts +98 -0
  113. package/dist/pages/controls.js +261 -0
  114. package/dist/pages/elements/cards.d.ts +15 -4
  115. package/dist/pages/elements/cards.js +299 -186
  116. package/dist/pages/elements/chart.d.ts +22 -0
  117. package/dist/pages/elements/chart.js +107 -91
  118. package/dist/pages/elements/options.d.ts +9 -0
  119. package/dist/pages/elements/options.js +268 -155
  120. package/dist/pages/elements/pages.d.ts +6 -0
  121. package/dist/pages/elements/pages.js +60 -38
  122. package/dist/pages/elements/slot.d.ts +5 -0
  123. package/dist/pages/elements/slot.js +17 -10
  124. package/dist/pages/elements/table.d.ts +17 -0
  125. package/dist/pages/elements/table.js +336 -228
  126. package/dist/pages/elements/text.d.ts +9 -3
  127. package/dist/pages/elements/text.js +350 -143
  128. package/dist/pages/host.d.ts +0 -24
  129. package/dist/pages/host.js +0 -17
  130. package/dist/pages/readonly.d.ts +24 -0
  131. package/dist/pages/readonly.js +31 -0
  132. package/dist/pages/responses.d.ts +37 -0
  133. package/dist/pages/responses.js +56 -0
  134. package/dist/pi-extensions/canvas-bash-valve.d.ts +9 -1
  135. package/dist/pi-extensions/canvas-bash-valve.js +12 -8
  136. package/dist/types.d.ts +19 -5
  137. package/dist/types.js +2 -1
  138. package/package.json +1 -1
  139. package/runtime.lock.json +2 -2
  140. package/scripts/install-runtime.mjs +19 -24
  141. package/dist/clients/attach/render/html-markdown.d.ts +0 -13
  142. package/dist/clients/attach/render/html-markdown.js +0 -206
  143. package/dist/core/human/markdown-html.d.ts +0 -8
  144. package/dist/core/human/markdown-html.js +0 -581
@@ -34,11 +34,6 @@ export interface CrtrThemeBridge {
34
34
  /** Registers `listener` for later theme changes; returns its unsubscribe. */
35
35
  subscribe(listener: (tokens: Record<string, string>) => void): () => void;
36
36
  }
37
- /** Data behind a `source`-bearing config (table, cards, chart). */
38
- export interface CrtrArtifactBridge {
39
- /** Resolves the already-parsed data for `source`, or rejects when it is unreachable. */
40
- data(source: string): Promise<unknown>;
41
- }
42
37
  /** What `mountWorkspaceComponent` can answer: the host rendered it, or it has no renderer. */
43
38
  export type WorkspaceMountOutcome = 'mounted' | 'unavailable';
44
39
  /** The bridge a host installs at `window.crtr`. */
@@ -59,7 +54,6 @@ export interface CrtrHost {
59
54
  /** Flushes the pending autosave of the partial map. */
60
55
  progress(): Promise<void>;
61
56
  readonly theme: CrtrThemeBridge;
62
- readonly artifact: CrtrArtifactBridge;
63
57
  /**
64
58
  * Asks the host to render a product-registered (`unvalidated`) slot into `element`.
65
59
  * A host with no renderer for `kind` answers `'unavailable'`.
@@ -90,17 +84,6 @@ export type HostCall = {
90
84
  status: 'failed';
91
85
  reason: string;
92
86
  };
93
- /** The outcome of an artifact read, with the same three-way split as `HostCall`. */
94
- export type ArtifactData = {
95
- status: 'ok';
96
- data: unknown;
97
- } | {
98
- status: 'unavailable';
99
- reason: string;
100
- } | {
101
- status: 'failed';
102
- reason: string;
103
- };
104
87
  /** The installed bridge, or `undefined` on a bare page. */
105
88
  export declare function host(): CrtrHost | undefined;
106
89
  /**
@@ -143,13 +126,6 @@ export declare function themeTokens(): Record<string, string>;
143
126
  * to `disconnectedCallback` without checking whether a host was there.
144
127
  */
145
128
  export declare function subscribeTheme(listener: (tokens: Record<string, string>) => void): () => void;
146
- /**
147
- * Reads the data behind a `source`-bearing config. The host resolves the reference and hands
148
- * back parsed data; an element never resolves a reference itself and never fetches a URL.
149
- * Anything other than `'ok'` is a named unavailable state to render, not a reason to retry
150
- * some other way.
151
- */
152
- export declare function artifactData(source: string): Promise<ArtifactData>;
153
129
  /**
154
130
  * Asks the host to render a product-registered slot into `element`. A bare page, a host with
155
131
  * no registry, and a host whose mount threw all answer `'unavailable'` — the escape element's
@@ -147,23 +147,6 @@ export function subscribeTheme(listener) {
147
147
  return () => { };
148
148
  }
149
149
  }
150
- /**
151
- * Reads the data behind a `source`-bearing config. The host resolves the reference and hands
152
- * back parsed data; an element never resolves a reference itself and never fetches a URL.
153
- * Anything other than `'ok'` is a named unavailable state to render, not a reason to retry
154
- * some other way.
155
- */
156
- export async function artifactData(source) {
157
- const bridge = host();
158
- if (typeof bridge?.artifact?.data !== 'function')
159
- return missing('artifact.data');
160
- try {
161
- return { status: 'ok', data: await bridge.artifact.data(source) };
162
- }
163
- catch (error) {
164
- return { status: 'failed', reason: reasonOf(error) };
165
- }
166
- }
167
150
  /**
168
151
  * Asks the host to render a product-registered slot into `element`. A bare page, a host with
169
152
  * no registry, and a host whose mount threw all answer `'unavailable'` — the escape element's
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Read-only presentation for a page that can no longer take an answer.
3
+ *
4
+ * A resolved or canceled page is still opened, read, and saved — the host renders it exactly
5
+ * as before and marks it out of reach: `inert` on each response-bearing element (Northlight's
6
+ * srcdoc host does this on every `[slot-id]`), and `disabled` on the pager so submit is gone.
7
+ * `inert` blocks pointer and keyboard interaction, but it is only a hit-testing rule: the
8
+ * controls underneath still LOOK live, and a scripted click still flips a checkbox. So the
9
+ * elements take the same signal and put it where a person can see it — every input renders
10
+ * disabled and every writing box read-only, which is also what makes the state unreachable
11
+ * rather than merely unclickable.
12
+ *
13
+ * The signal is read from the DOM rather than from a new attribute, because both halves of it
14
+ * already exist: `inert` is a standard reflected attribute the host sets, and `disabled` on
15
+ * `<crtr-pages>` is the attribute the pager itself observes.
16
+ */
17
+ /** Whether this element sits on a page that can no longer take an answer. */
18
+ export declare function isReadOnly(element: HTMLElement): boolean;
19
+ /**
20
+ * Paints `root`'s controls read-only (or live again). Inputs go `disabled` — greyed, skipped
21
+ * by the tab order, and deaf even to a scripted `click()` — and writing boxes go `readOnly`,
22
+ * which keeps their text selectable and copyable while refusing every edit.
23
+ */
24
+ export declare function applyReadOnly(root: ParentNode, readOnly: boolean): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Read-only presentation for a page that can no longer take an answer.
3
+ *
4
+ * A resolved or canceled page is still opened, read, and saved — the host renders it exactly
5
+ * as before and marks it out of reach: `inert` on each response-bearing element (Northlight's
6
+ * srcdoc host does this on every `[slot-id]`), and `disabled` on the pager so submit is gone.
7
+ * `inert` blocks pointer and keyboard interaction, but it is only a hit-testing rule: the
8
+ * controls underneath still LOOK live, and a scripted click still flips a checkbox. So the
9
+ * elements take the same signal and put it where a person can see it — every input renders
10
+ * disabled and every writing box read-only, which is also what makes the state unreachable
11
+ * rather than merely unclickable.
12
+ *
13
+ * The signal is read from the DOM rather than from a new attribute, because both halves of it
14
+ * already exist: `inert` is a standard reflected attribute the host sets, and `disabled` on
15
+ * `<crtr-pages>` is the attribute the pager itself observes.
16
+ */
17
+ /** Whether this element sits on a page that can no longer take an answer. */
18
+ export function isReadOnly(element) {
19
+ return element.closest('[inert], crtr-pages[disabled]') !== null;
20
+ }
21
+ /**
22
+ * Paints `root`'s controls read-only (or live again). Inputs go `disabled` — greyed, skipped
23
+ * by the tab order, and deaf even to a scripted `click()` — and writing boxes go `readOnly`,
24
+ * which keeps their text selectable and copyable while refusing every edit.
25
+ */
26
+ export function applyReadOnly(root, readOnly) {
27
+ for (const input of root.querySelectorAll('input'))
28
+ input.disabled = readOnly;
29
+ for (const area of root.querySelectorAll('textarea'))
30
+ area.readOnly = readOnly;
31
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * What the page would submit — every response-bearing slot, not only the touched ones.
3
+ *
4
+ * `respond()` is a user-change channel: an element calls it when the person changes
5
+ * something, so a slot nobody touched has never crossed the bridge and the host's map does
6
+ * not name it. A final submit must name every response-bearing slot, and crtrd rejects a
7
+ * map that does not — correctly, because "this slot has no answer" and "the page forgot to
8
+ * mention it" would otherwise be the same thing on the wire.
9
+ *
10
+ * The answer for an untouched slot is not a guess and it is not a per-kind default anyone
11
+ * has to re-derive: it is exactly what that slot is showing right now — a text slot's
12
+ * authored text, unedited; a selection nobody made. Each element already builds that object
13
+ * for `respond()`. So each one registers its reader here at mount, and the pager reports all
14
+ * of them through `respond()` immediately before it submits.
15
+ *
16
+ * Registration itself writes nothing and asks for no autosave: opening a page and closing it
17
+ * again saves exactly what it saved before. The reporting happens at submit, where the
18
+ * autosave the reports schedule is flushed by the submit itself.
19
+ */
20
+ import type { SlotResponse } from './types.js';
21
+ /**
22
+ * Registers this element as the answer for `slotId`. Call it once at mount, after the slot's
23
+ * config has been read and its state hydrated, and only from an element whose slot actually
24
+ * bears a response — reporting a response for a display-only slot is a submit the host will
25
+ * reject by name. `read` is called at submit time, so it must return the slot's complete
26
+ * current response, exactly what `respond()` is handed on a user change.
27
+ */
28
+ export declare function registerSlotResponse(element: HTMLElement, slotId: string, read: () => SlotResponse): void;
29
+ /**
30
+ * Hands the host every registered slot's current response, so the map it holds is the whole
31
+ * page. Only the pager calls this, and only immediately before `submit()`.
32
+ *
33
+ * A reader whose element has left the document names a slot that is no longer on screen, and
34
+ * a reader that throws has no answer to give; both are dropped rather than reported, which
35
+ * leaves the host's map exactly as complete as the page really is.
36
+ */
37
+ export declare function reportSlotResponses(): void;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What the page would submit — every response-bearing slot, not only the touched ones.
3
+ *
4
+ * `respond()` is a user-change channel: an element calls it when the person changes
5
+ * something, so a slot nobody touched has never crossed the bridge and the host's map does
6
+ * not name it. A final submit must name every response-bearing slot, and crtrd rejects a
7
+ * map that does not — correctly, because "this slot has no answer" and "the page forgot to
8
+ * mention it" would otherwise be the same thing on the wire.
9
+ *
10
+ * The answer for an untouched slot is not a guess and it is not a per-kind default anyone
11
+ * has to re-derive: it is exactly what that slot is showing right now — a text slot's
12
+ * authored text, unedited; a selection nobody made. Each element already builds that object
13
+ * for `respond()`. So each one registers its reader here at mount, and the pager reports all
14
+ * of them through `respond()` immediately before it submits.
15
+ *
16
+ * Registration itself writes nothing and asks for no autosave: opening a page and closing it
17
+ * again saves exactly what it saved before. The reporting happens at submit, where the
18
+ * autosave the reports schedule is flushed by the submit itself.
19
+ */
20
+ import { respond } from './host.js';
21
+ /** One entry per slot id; a second registration for an id replaces the first. */
22
+ const readers = new Map();
23
+ /**
24
+ * Registers this element as the answer for `slotId`. Call it once at mount, after the slot's
25
+ * config has been read and its state hydrated, and only from an element whose slot actually
26
+ * bears a response — reporting a response for a display-only slot is a submit the host will
27
+ * reject by name. `read` is called at submit time, so it must return the slot's complete
28
+ * current response, exactly what `respond()` is handed on a user change.
29
+ */
30
+ export function registerSlotResponse(element, slotId, read) {
31
+ readers.set(slotId, { element, read });
32
+ }
33
+ /**
34
+ * Hands the host every registered slot's current response, so the map it holds is the whole
35
+ * page. Only the pager calls this, and only immediately before `submit()`.
36
+ *
37
+ * A reader whose element has left the document names a slot that is no longer on screen, and
38
+ * a reader that throws has no answer to give; both are dropped rather than reported, which
39
+ * leaves the host's map exactly as complete as the page really is.
40
+ */
41
+ export function reportSlotResponses() {
42
+ for (const [slotId, reader] of [...readers]) {
43
+ if (!reader.element.isConnected) {
44
+ readers.delete(slotId);
45
+ continue;
46
+ }
47
+ let response;
48
+ try {
49
+ response = reader.read();
50
+ }
51
+ catch {
52
+ continue;
53
+ }
54
+ respond(slotId, response);
55
+ }
56
+ }
@@ -1,4 +1,4 @@
1
- import { type BashOperations } from '@earendil-works/pi-coding-agent';
1
+ import { createBashToolDefinition, type BashOperations } from '@earendil-works/pi-coding-agent';
2
2
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
3
3
  /** Seconds a leading `sleep ...` will block for, or null when the command does
4
4
  * not open with a sleep (or its duration isn't statically knowable). */
@@ -7,4 +7,12 @@ export declare function leadingSleepSeconds(command: string): number | null;
7
7
  * `takePurpose`, so concurrent calls cannot exchange labels. It is consumed even
8
8
  * when the command is invalid or refused, leaving no state to leak later. */
9
9
  export declare function createValveOperations(nodeId: string, contextDir: string, takePurpose?: () => string | null): BashOperations;
10
+ type BashToolDefinition = ReturnType<typeof createBashToolDefinition>;
11
+ /** Build the valve-backed bash tool and, when configured, add `purpose` to its
12
+ * schema. Bind each purpose directly to its own execution: Pi may preflight a
13
+ * batch before starting its calls, so a shared "next purpose" slot could give a
14
+ * concurrent call the wrong label. Disabled means absent, not merely ignored —
15
+ * the model receives pi's upstream bash schema verbatim. */
16
+ export declare function createValveToolDefinition(nodeId: string, contextDir: string, cwd: string, exposePurpose: boolean): BashToolDefinition;
10
17
  export default function (pi: ExtensionAPI): void;
18
+ export {};
@@ -28,6 +28,7 @@ import { spawn } from 'node:child_process';
28
28
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
29
29
  import { homedir } from 'node:os';
30
30
  import { backgroundBashJob, bashJobPaths, formatBashElapsed, newBashJobId, normalizeBashJobPurpose } from '../core/bash-jobs.js';
31
+ import { readConfig } from '../core/config.js';
31
32
  import { Type } from 'typebox';
32
33
  import { createBashToolDefinition } from '@earendil-works/pi-coding-agent';
33
34
  // ---------------------------------------------------------------------------
@@ -351,7 +352,7 @@ export function createValveOperations(nodeId, contextDir, takePurpose = () => nu
351
352
  // ---------------------------------------------------------------------------
352
353
  // `purpose` — the one line a person reads while a command runs
353
354
  //
354
- // A deliberate deviation from taking pi's definition verbatim, and the only one
355
+ // An optional deviation from taking pi's definition verbatim, and the only one
355
356
  // here. Everywhere crtr's output reaches a non-technical audience, the host has
356
357
  // to render SOMETHING per step, and without this field its only material is the
357
358
  // command — which yields "Running command" for an entire turn. The model is the
@@ -393,13 +394,16 @@ function withPurpose(definition) {
393
394
  }),
394
395
  };
395
396
  }
396
- /** Bind each purpose directly to its own tool execution. Pi may preflight a
397
- * batch before starting its calls, so a shared "next purpose" slot could give
398
- * a concurrent call the wrong label. */
399
- function createPurposeValveToolDefinition(nodeId, contextDir, cwd) {
400
- const schemaDefinition = withPurpose(createBashToolDefinition(cwd, { operations: createValveOperations(nodeId, contextDir) }));
397
+ /** Build the valve-backed bash tool and, when configured, add `purpose` to its
398
+ * schema. Bind each purpose directly to its own execution: Pi may preflight a
399
+ * batch before starting its calls, so a shared "next purpose" slot could give a
400
+ * concurrent call the wrong label. Disabled means absent, not merely ignored —
401
+ * the model receives pi's upstream bash schema verbatim. */
402
+ export function createValveToolDefinition(nodeId, contextDir, cwd, exposePurpose) {
403
+ const baseDefinition = createBashToolDefinition(cwd, { operations: createValveOperations(nodeId, contextDir) });
404
+ const schemaDefinition = exposePurpose ? withPurpose(baseDefinition) : baseDefinition;
401
405
  const execute = (toolCallId, params, signal, onUpdate, ctx) => {
402
- const purpose = normalizeBashJobPurpose(params['purpose']);
406
+ const purpose = exposePurpose ? normalizeBashJobPurpose(params['purpose']) : null;
403
407
  const definition = createBashToolDefinition(cwd, { operations: createValveOperations(nodeId, contextDir, () => purpose) });
404
408
  return definition.execute(toolCallId, params, signal, onUpdate, ctx);
405
409
  };
@@ -418,6 +422,6 @@ export default function (pi) {
418
422
  // is built against. registerTool replaces the builtin by name; re-firing on
419
423
  // every session_start is idempotent (last registration wins).
420
424
  pi.on('session_start', (_event, ctx) => {
421
- pi.registerTool(createPurposeValveToolDefinition(nodeId, contextDir, ctx.cwd));
425
+ pi.registerTool(createValveToolDefinition(nodeId, contextDir, ctx.cwd, readConfig('user').bash_tool_purpose));
422
426
  });
423
427
  }
package/dist/types.d.ts CHANGED
@@ -160,6 +160,16 @@ export interface KindConfig {
160
160
  whenToUse: string;
161
161
  availableTo?: string[];
162
162
  }
163
+ export interface PageComponentRegistration {
164
+ kind: string;
165
+ description?: string;
166
+ useWhen?: string;
167
+ doc?: string;
168
+ /** A display-only product slot contributes no page response. */
169
+ display?: boolean;
170
+ }
171
+ /** The normalized product component catalog used to validate page slots. */
172
+ export type ProductPageComponents = readonly PageComponentRegistration[];
163
173
  export interface ScopeConfig {
164
174
  schema_version: number;
165
175
  marketplaces: Record<string, ConfigMarketplaceEntry>;
@@ -183,12 +193,12 @@ export interface ScopeConfig {
183
193
  * or empty lists fall back to `['working']`. */
184
194
  working_gerunds: string[];
185
195
  /** Play the whip header animation whenever the human sends a prompt, and once
186
- * when an automatically opened managed-child viewer starts. Default false. */
196
+ * when a viewer opens for an agent with an initial prompt. Default false. */
187
197
  whip_mode: boolean;
188
- /** Teach agent help the HTML page-authoring dialect. Off keeps page authoring Markdown-only. */
189
- html_pages: boolean;
190
- /** Product-registered page slot kinds beyond crtr's built-in catalog. */
191
- page_components: string[];
198
+ /** Product-registered page components beyond crtr's built-in catalog. */
199
+ page_components: PageComponentRegistration[];
200
+ /** Prompt agents to use HTML pages by default for structured or interactive human content. Default false. */
201
+ html_surface: boolean;
192
202
  /** Playful urgency messages the attach-viewer whip action sends to an agent. Missing, malformed, or empty lists fall back to the built-in rotation. */
193
203
  whip_messages: string[];
194
204
  /** Initial mouse wheel scrolling mode for each attach viewer. `tmux` follows
@@ -211,6 +221,10 @@ export interface ScopeConfig {
211
221
  * `both` keeps both. Tool calls and results are never kept. Full detail for
212
222
  * every cycle stays in the session file. */
213
223
  condensed_history: CondensedHistoryMode;
224
+ /** Expose crouter's optional `purpose` label in the bash tool schema. Turning
225
+ * this off leaves the bash valve active but gives agents pi's ordinary bash
226
+ * definition with only its upstream parameters. Default true. */
227
+ bash_tool_purpose: boolean;
214
228
  /** Fold every SETTLED tool call in the attach viewer down to its single call
215
229
  * line (`/fold-tools`, Alt+C → z → f). A tool still running is never folded —
216
230
  * it folds itself once it settles. Toggling it in a viewer writes back here,
package/dist/types.js CHANGED
@@ -78,12 +78,13 @@ export function defaultScopeConfig() {
78
78
  completion_bell: true,
79
79
  working_gerunds: [...DEFAULT_WORKING_GERUNDS],
80
80
  whip_mode: false,
81
- html_pages: false,
82
81
  page_components: [],
82
+ html_surface: false,
83
83
  whip_messages: [...DEFAULT_WHIP_MESSAGES],
84
84
  mouse_mode_default: 'tmux',
85
85
  live_cycles: DEFAULT_LIVE_CYCLES,
86
86
  condensed_history: DEFAULT_CONDENSED_HISTORY,
87
+ bash_tool_purpose: true,
87
88
  fold_finished_tools: false,
88
89
  summarize_tool_calls: false,
89
90
  detailed_tool_recaps: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.197",
3
+ "version": "0.3.198",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.197",
3
+ "version": "0.3.198",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.197",
9
+ "version": "0.3.198",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -14,7 +14,10 @@ const GENERATIONS = join(RUNTIME_HOME, 'generations');
14
14
  const LOCK = join(RUNTIME_HOME, 'install.lock');
15
15
  const LOCK_OWNER_FILE = 'owner.json';
16
16
  const RUNTIME_LOCK = 'runtime.lock.json';
17
- const KEEP_RECENT_GENERATIONS = 3;
17
+ // The selected generation plus one prior, so a bad selection can be rolled
18
+ // back by hand. Anything older is reinstallable from npm and is not worth the
19
+ // ~500MB/generation it costs on disk.
20
+ const KEEP_RECENT_GENERATIONS = 2;
18
21
  const FS_CONCURRENCY = 64;
19
22
  // How long an install.lock with no readable owner record is given the
20
23
  // benefit of the doubt (the owner is mid-write, or on an ancient version of
@@ -174,40 +177,36 @@ function generationIdsIn(text) {
174
177
  return [...text.matchAll(/generations\/([a-f0-9]{64})(?:[/\s]|$)/g)].map((match) => match[1]);
175
178
  }
176
179
 
177
- /** Return every generation used by a running process and whether every stable
178
- * selector process exposed its generation lease. */
180
+ /** Every generation a running process is using — named in its argv, or held as
181
+ * an open entry-file lease (how a stable `crtr`/`crtrd` shim, whose argv names
182
+ * no generation, keeps the one it resolved; see `holdGenerationEntryFd` in
183
+ * bin/runtime-selector.mjs and the matching open in src/cli.ts).
184
+ *
185
+ * Detection is positive-only and that is deliberate: a generation nothing here
186
+ * names is prunable. An earlier revision also tracked whether every stable shim
187
+ * had exposed a lease and skipped the whole GC when one had not. That made one
188
+ * unidentifiable process veto all pruning permanently — a single pre-upgrade
189
+ * viewer, which could never hold a lease the mechanism predated, silently
190
+ * stopped GC forever and grew this directory by ~500MB per install. */
179
191
  async function liveGenerationIds() {
180
192
  const output = await run('ps', ['-axww', '-o', 'pid=,command='], RUNTIME_HOME, true);
181
193
  const live = new Set(generationIdsIn(output));
182
194
  const nodePids = [];
183
- const opaqueStablePids = new Set();
184
195
  for (const line of output.split('\n')) {
185
196
  const match = /^\s*(\d+)\s+(.*)$/.exec(line);
186
197
  if (!match) continue;
187
198
  const [, pid, command] = match;
188
- const stableSelector = /^(?:\S*node\s+)?\S*\/bin\/(?:crtr|crtrd|crouter)(?:\s|$)/.test(command);
189
- if (stableSelector && generationIdsIn(command).length === 0) opaqueStablePids.add(pid);
190
199
  if (/(?:^|[\s/])(?:node|crtrd?|crouter)(?:[\s]|$)/.test(command)) nodePids.push(pid);
191
200
  }
192
- if (nodePids.length === 0) return { live, uncertain: opaqueStablePids.size > 0 };
201
+ if (nodePids.length === 0) return live;
193
202
 
194
203
  // One lsof invocation resolves cwd references and the runtime entry-file
195
204
  // lease held by each stable CLI process without a per-process startup penalty.
196
205
  const open = await probe('lsof', ['-n', '-P', '-a', '-p', nodePids.join(','), '-Fn'], RUNTIME_HOME);
197
- let pid;
198
- const exposedStablePids = new Set();
199
206
  for (const line of open.split('\n')) {
200
- const processMatch = /^p(\d+)$/.exec(line);
201
- if (processMatch) {
202
- pid = processMatch[1];
203
- continue;
204
- }
205
- const ids = generationIdsIn(line);
206
- for (const id of ids) live.add(id);
207
- if (pid !== undefined && ids.length > 0 && opaqueStablePids.has(pid)) exposedStablePids.add(pid);
207
+ for (const id of generationIdsIn(line)) live.add(id);
208
208
  }
209
- const uncertain = [...opaqueStablePids].some((stablePid) => !exposedStablePids.has(stablePid));
210
- return { live, uncertain };
209
+ return live;
211
210
  }
212
211
 
213
212
  async function selectedGenerationId() {
@@ -219,11 +218,7 @@ async function selectedGenerationId() {
219
218
 
220
219
  async function pruneGenerations() {
221
220
  const selected = await selectedGenerationId();
222
- const { live, uncertain } = await liveGenerationIds();
223
- if (uncertain) {
224
- process.stderr.write('runtime generation GC skipped: a live stable crouter shim does not expose its generation lease\n');
225
- return;
226
- }
221
+ const live = await liveGenerationIds();
227
222
  const entries = await readdir(GENERATIONS, { withFileTypes: true });
228
223
  const candidates = [];
229
224
  for (const entry of entries) {
@@ -1,13 +0,0 @@
1
- export interface HtmlMarkdownStyles {
2
- kbd: (text: string) => string;
3
- mark: (text: string) => string;
4
- hint: (text: string) => string;
5
- }
6
- export interface HtmlMarkdownOptions {
7
- /** Undefined honors each block's `open` attribute. A boolean is the viewer's
8
- * global Ctrl+O override. */
9
- detailsExpanded: boolean | undefined;
10
- styles: HtmlMarkdownStyles;
11
- }
12
- /** Transform the strict supported HTML subset in one Markdown string. */
13
- export declare function transformHtmlMarkdown(source: string, options: HtmlMarkdownOptions): string;