@north-light/crouter 0.3.263 → 0.3.270

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 (171) hide show
  1. package/dist/api/client.d.ts +5 -1
  2. package/dist/api/client.js +6 -0
  3. package/dist/api/dto/broker.d.ts +11 -2
  4. package/dist/api/dto/broker.js +2 -2
  5. package/dist/api/dto/canvas.d.ts +53 -2
  6. package/dist/api/dto/nodes.d.ts +10 -2
  7. package/dist/api/routes.d.ts +1 -0
  8. package/dist/api/routes.js +1 -0
  9. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +2 -2
  10. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/integration/provider-rotation.test.ts +50 -0
  11. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +46 -6
  12. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +6 -2
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +7 -2
  14. package/dist/clients/attach/session/whip-achievements.d.ts +4 -0
  15. package/dist/clients/attach/session/whip-achievements.js +145 -0
  16. package/dist/clients/attach/slash/dispatch.js +5 -3
  17. package/dist/clients/attach/viewer.js +607 -606
  18. package/dist/commands/canvas-history/grep.js +3 -22
  19. package/dist/commands/canvas-history/read.js +5 -3
  20. package/dist/commands/canvas-history/search.js +3 -22
  21. package/dist/commands/canvas-history/shared.d.ts +8 -0
  22. package/dist/commands/canvas-history/shared.js +64 -1
  23. package/dist/commands/canvas-history/stats.d.ts +1 -0
  24. package/dist/commands/canvas-history/stats.js +68 -0
  25. package/dist/commands/canvas-history.js +4 -3
  26. package/dist/commands/node/create.js +2 -2
  27. package/dist/commands/pkg/plugin-inspect.js +2 -2
  28. package/dist/commands/pkg/plugin-manage.js +3 -3
  29. package/dist/commands/sys/config.js +19 -2
  30. package/dist/commands/sys/doctor.js +4 -3
  31. package/dist/commands/sys/panels/models-panel.js +3 -1
  32. package/dist/core/__tests__/human-deliver.test.js +1 -1
  33. package/dist/core/__tests__/integration/lifecycle-hooks.test.d.ts +1 -0
  34. package/dist/core/__tests__/integration/lifecycle-hooks.test.js +182 -0
  35. package/dist/core/__tests__/integration/revive.test.js +19 -17
  36. package/dist/core/__tests__/model-pin-durability.test.js +25 -1
  37. package/dist/core/__tests__/push-final-guard.test.js +1 -1
  38. package/dist/core/__tests__/relaunch-root.test.js +17 -0
  39. package/dist/core/__tests__/revive-capacity.test.js +20 -20
  40. package/dist/core/__tests__/revive-parked-fresh.test.js +14 -14
  41. package/dist/core/__tests__/seam/broker-cap-freeze.test.js +2 -2
  42. package/dist/core/__tests__/seam/broker-provider-retry.test.js +19 -4
  43. package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
  44. package/dist/core/__tests__/seam/held-deferred-human-prompt.test.js +1 -1
  45. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +19 -2
  46. package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +2 -2
  47. package/dist/core/broker-client/__tests__/transport-relay.test.js +3 -1
  48. package/dist/core/broker-client/transport-relay.js +5 -1
  49. package/dist/core/canvas/__tests__/history-transcript.test.d.ts +1 -0
  50. package/dist/core/canvas/__tests__/history-transcript.test.js +102 -0
  51. package/dist/core/canvas/browse/app.d.ts +1 -1
  52. package/dist/core/canvas/browse/app.js +13 -10
  53. package/dist/core/canvas/browse/model.js +16 -1
  54. package/dist/core/canvas/browse/render.d.ts +23 -3
  55. package/dist/core/canvas/browse/render.js +429 -64
  56. package/dist/core/canvas/canvas.js +12 -8
  57. package/dist/core/canvas/extensions.d.ts +1 -1
  58. package/dist/core/canvas/extensions.js +7 -1
  59. package/dist/core/canvas/history.d.ts +36 -1
  60. package/dist/core/canvas/history.js +328 -7
  61. package/dist/core/canvas/labels.d.ts +2 -2
  62. package/dist/core/canvas/migrations.js +32 -0
  63. package/dist/core/canvas/node-recap.d.ts +23 -0
  64. package/dist/core/canvas/node-recap.js +67 -0
  65. package/dist/core/canvas/render-source.d.ts +33 -1
  66. package/dist/core/canvas/render-source.js +80 -22
  67. package/dist/core/canvas/types.d.ts +9 -0
  68. package/dist/core/command-hooks/discovery.d.ts +27 -1
  69. package/dist/core/command-hooks/discovery.js +52 -0
  70. package/dist/core/command-hooks/index.d.ts +4 -3
  71. package/dist/core/command-hooks/index.js +2 -1
  72. package/dist/core/command-hooks/lifecycle-catalog.d.ts +3 -0
  73. package/dist/core/command-hooks/lifecycle-catalog.js +4 -0
  74. package/dist/core/command-hooks/report.d.ts +11 -2
  75. package/dist/core/command-hooks/report.js +10 -1
  76. package/dist/core/command-hooks/schema.d.ts +16 -1
  77. package/dist/core/command-hooks/schema.js +128 -40
  78. package/dist/core/command-hooks/transport/exec-lifecycle.d.ts +12 -0
  79. package/dist/core/command-hooks/transport/exec-lifecycle.js +152 -0
  80. package/dist/core/config.js +13 -5
  81. package/dist/core/human/feedback-companion.js +1 -1
  82. package/dist/core/keybindings/catalog.d.ts +2 -2
  83. package/dist/core/keybindings/catalog.js +2 -1
  84. package/dist/core/model-routes.js +1 -1
  85. package/dist/core/review/realize.js +1 -1
  86. package/dist/core/runtime/broker/auth-reload.js +4 -1
  87. package/dist/core/runtime/broker/engine-drive.d.ts +14 -3
  88. package/dist/core/runtime/broker/engine-drive.js +29 -1
  89. package/dist/core/runtime/broker/fault-retry.js +7 -2
  90. package/dist/core/runtime/broker/frame-client.js +3 -0
  91. package/dist/core/runtime/broker/frame-dispatch.js +7 -0
  92. package/dist/core/runtime/broker/inbox.d.ts +12 -2
  93. package/dist/core/runtime/broker/inbox.js +68 -38
  94. package/dist/core/runtime/broker/rebind.js +7 -0
  95. package/dist/core/runtime/broker-protocol.d.ts +16 -1
  96. package/dist/core/runtime/broker.d.ts +1 -1
  97. package/dist/core/runtime/broker.js +27 -3
  98. package/dist/core/runtime/canvas-extensions.d.ts +1 -0
  99. package/dist/core/runtime/canvas-extensions.js +2 -0
  100. package/dist/core/runtime/interactive-deliver.d.ts +8 -2
  101. package/dist/core/runtime/interactive-deliver.js +12 -3
  102. package/dist/core/runtime/launch-target.js +1 -1
  103. package/dist/core/runtime/launch.d.ts +12 -6
  104. package/dist/core/runtime/launch.js +19 -17
  105. package/dist/core/runtime/model-swap.d.ts +5 -4
  106. package/dist/core/runtime/model-swap.js +7 -5
  107. package/dist/core/runtime/node-read.d.ts +2 -0
  108. package/dist/core/runtime/node-read.js +18 -9
  109. package/dist/core/runtime/promote.js +2 -1
  110. package/dist/core/runtime/reset.js +4 -2
  111. package/dist/core/runtime/revive-all.d.ts +1 -1
  112. package/dist/core/runtime/revive-all.js +2 -2
  113. package/dist/core/runtime/revive.d.ts +2 -2
  114. package/dist/core/runtime/revive.js +71 -13
  115. package/dist/core/runtime/session-visibility.d.ts +10 -14
  116. package/dist/core/runtime/session-visibility.js +43 -30
  117. package/dist/core/runtime/spawn.d.ts +3 -3
  118. package/dist/core/runtime/spawn.js +20 -3
  119. package/dist/core/runtime/stamp/channel.d.ts +22 -0
  120. package/dist/core/runtime/stamp/channel.js +83 -0
  121. package/dist/core/runtime/stamp/protocol.d.ts +90 -0
  122. package/dist/core/runtime/stamp/protocol.js +113 -0
  123. package/dist/core/runtime/tmux-driver.d.ts +11 -1
  124. package/dist/core/runtime/tmux-driver.js +27 -11
  125. package/dist/core/runtime/turn-visibility.d.ts +10 -0
  126. package/dist/core/runtime/turn-visibility.js +31 -0
  127. package/dist/core/self-update.js +2 -5
  128. package/dist/core/tui/draw.d.ts +1 -0
  129. package/dist/core/tui/draw.js +5 -1
  130. package/dist/core/tui/markdown.d.ts +17 -0
  131. package/dist/core/tui/markdown.js +407 -0
  132. package/dist/core/user-settings.d.ts +4 -0
  133. package/dist/core/user-settings.js +1 -0
  134. package/dist/daemon/api/__tests__/node-model-validation.test.js +21 -0
  135. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +2 -2
  136. package/dist/daemon/api/bridge.d.ts +3 -2
  137. package/dist/daemon/api/bridge.js +34 -9
  138. package/dist/daemon/api/handlers/attach.js +4 -4
  139. package/dist/daemon/api/handlers/bash-jobs.js +1 -1
  140. package/dist/daemon/api/handlers/canvas.js +62 -16
  141. package/dist/daemon/api/handlers/messages.js +14 -3
  142. package/dist/daemon/api/handlers/nodes.js +17 -7
  143. package/dist/daemon/api/map.js +2 -0
  144. package/dist/daemon/cron/sinks.js +3 -3
  145. package/dist/daemon/fleet.js +1 -1
  146. package/dist/daemon/messaging/node-message.js +16 -2
  147. package/dist/daemon/profile-delete.js +1 -1
  148. package/dist/daemon/reconcilers/broker-supervision.js +26 -9
  149. package/dist/daemon/reconcilers/live-obligation.d.ts +8 -1
  150. package/dist/daemon/reconcilers/live-obligation.js +17 -0
  151. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +6 -6
  152. package/dist/hook-authoring.d.ts +31 -1
  153. package/dist/hook-authoring.js +42 -5
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +3 -0
  156. package/dist/pi-extensions/canvas-inbox-watcher.js +27 -2
  157. package/dist/pi-extensions/canvas-recap.js +4 -0
  158. package/dist/pi-extensions/canvas-stamp.d.ts +14 -0
  159. package/dist/pi-extensions/canvas-stamp.js +77 -0
  160. package/dist/pi-extensions/canvas-stophook.js +3 -0
  161. package/dist/shared/__tests__/env-boundary.test.js +4 -2
  162. package/dist/shared/crtr-version.d.ts +2 -0
  163. package/dist/shared/crtr-version.js +18 -0
  164. package/dist/shared/env.d.ts +10 -0
  165. package/dist/shared/env.js +19 -4
  166. package/dist/shared/generated-context.d.ts +1 -2
  167. package/dist/shared/generated-context.js +3 -4
  168. package/dist/types.d.ts +10 -0
  169. package/dist/types.js +4 -0
  170. package/package.json +1 -1
  171. package/runtime.lock.json +2 -2
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ export type { CloseNodeResult } from './core/runtime/close.js';
5
5
  export { appendInbox } from './core/feed/inbox.js';
6
6
  export type { InboxEntry, InboxTier, InboxKind } from './core/feed/inbox.js';
7
7
  export { appendSituationalContext } from './core/runtime/situational-context.js';
8
+ export { isTurnInternal } from './core/runtime/turn-visibility.js';
8
9
  export { getNode, listNodes } from './core/canvas/canvas.js';
9
10
  export { nodeDir } from './core/canvas/paths.js';
10
11
  export type { NodeMeta, NodeRow, NodeStatus, Lifecycle, Mode, } from './core/canvas/types.js';
package/dist/index.js CHANGED
@@ -21,6 +21,9 @@ export { appendInbox } from './core/feed/inbox.js';
21
21
  // upsert-only, narrow surface so an external composer (e.g. Hearth) never needs to
22
22
  // learn the storage file itself.
23
23
  export { appendSituationalContext } from './core/runtime/situational-context.js';
24
+ // In-process extensions use crouter's admitted turn classification rather than
25
+ // attempting to reconstruct origin and route state independently.
26
+ export { isTurnInternal } from './core/runtime/turn-visibility.js';
24
27
  // ── Canvas reads ─────────────────────────────────────────────────────────
25
28
  export { getNode, listNodes } from './core/canvas/canvas.js';
26
29
  export { nodeDir } from './core/canvas/paths.js';
@@ -17,6 +17,7 @@
17
17
  import { envNodeId, envProfileId, envWatcherDebounceMs, envWatcherTickMs } from '../shared/env.js';
18
18
  import { coalesceBrokerInbox, entryCards, readBrokerCanceledEntryIds, readBrokerCursor, readBrokerInboxSince, reportRefNodeId, } from '../core/runtime/broker/inbox.js';
19
19
  import { advanceBrokerInboxCursor, brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
20
+ import { announceMessageOrigin } from '../core/runtime/stamp/channel.js';
20
21
  import { captureGoalIfAbsent, generateAndCommitInitialName } from './broker-local.js';
21
22
  import { emitEvent } from '../core/events/emit.js';
22
23
  import { errorClassFromError } from '../core/events/errors.js';
@@ -97,6 +98,26 @@ function emitStoredBestEffort(operationId, input) {
97
98
  // Delivery state remains authoritative when its observational record cannot append.
98
99
  }
99
100
  }
101
+ /** The authorship stamped onto a delivered message. A person's words arrive
102
+ * verbatim and are stamped as theirs — no channel, because the durable inbox
103
+ * is not a transport a reader can attribute. Every other delivery is the
104
+ * runtime's inbox card, carrying each entry's attribution as the coalescer
105
+ * received it. */
106
+ function deliveryOrigin(delivery) {
107
+ if (delivery.kind === 'human')
108
+ return { type: 'human' };
109
+ return {
110
+ type: 'runtime',
111
+ kind: 'inbox',
112
+ entries: delivery.entries.map((entry) => ({
113
+ ...(entry.from === null ? {} : { from: entry.from }),
114
+ ...(entry.from_name === undefined ? {} : { fromName: entry.from_name }),
115
+ kind: entry.kind,
116
+ tier: entry.tier,
117
+ ...(entry.ref === undefined ? {} : { ref: entry.ref }),
118
+ })),
119
+ };
120
+ }
100
121
  function deliveryFacts(entries, channel) {
101
122
  if (entries.length === 0)
102
123
  throw new Error(`cannot create an empty ${channel} delivery`);
@@ -729,7 +750,7 @@ class CanvasInboxWatcher {
729
750
  * unit: a throw anywhere requeues the whole entry, so a retry may repeat a
730
751
  * card already handed off (context is restatable; the human's body is not,
731
752
  * and it is sent last). */
732
- sendVisible(entries, cards, content, route, stage) {
753
+ sendVisible(entries, cards, content, origin, route, stage) {
733
754
  return this.sendDelivery('visible', entries, () => {
734
755
  if (route !== 'idle' && route !== 'steer' && route !== 'followUp') {
735
756
  throw new Error(`visible send requested for non-send route ${route}`);
@@ -737,6 +758,10 @@ class CanvasInboxWatcher {
737
758
  for (const card of cards) {
738
759
  this.pi.sendMessage({ customType: SITUATIONAL_CONTEXT_CUSTOM_TYPE, content: card, display: true }, route === 'idle' ? {} : { deliverAs: route });
739
760
  }
761
+ // Claimed by the stamp writer against the message pi persists, so a send
762
+ // that throws here leaves an announcement nobody claims rather than one
763
+ // that mislabels a later message.
764
+ announceMessageOrigin(content, origin);
740
765
  if (route === 'idle') {
741
766
  const start = brokerIdleTurnStart();
742
767
  if (start === undefined)
@@ -803,7 +828,7 @@ class CanvasInboxWatcher {
803
828
  if (!this.canMutate())
804
829
  return { inactive: true, requeueEntryIds };
805
830
  if (delivery !== undefined) {
806
- const result = this.sendVisible(delivery.entries, delivery.cards, delivery.text, route, stage);
831
+ const result = this.sendVisible(delivery.entries, delivery.cards, delivery.text, deliveryOrigin(delivery), route, stage);
807
832
  if (result === 'inactive')
808
833
  return { inactive: true, requeueEntryIds };
809
834
  if (result === 'failed') {
@@ -41,6 +41,7 @@
41
41
  import { envNodeId, envRecapIdleMs } from '../shared/env.js';
42
42
  import { brokerExtensionState } from '../core/runtime/broker/daemon-ops.js';
43
43
  import { generateRecap } from '../core/runtime/recap.js';
44
+ import { writeNodeRecap } from '../core/canvas/node-recap.js';
44
45
  import { isReviewBoundary } from '../core/runtime/session-visibility.js';
45
46
  import { editorLabelForBrokerNode } from '../core/runtime/broker-extension-render.js';
46
47
  import { generateAndCommitRecapName, readRoadmap } from './broker-local.js';
@@ -315,6 +316,9 @@ export function registerCanvasRecap(pi, recapGenerator = generateRecap, nameRege
315
316
  // Automatic work paints only while its idle snapshot remains current.
316
317
  if ((!manual && activityVersion !== snapshotVersion) || !canPaint())
317
318
  return;
319
+ // Persist what we are about to paint. The widget dies with the broker; the
320
+ // file is what `crtr canvas browse` shows for a dormant node.
321
+ writeNodeRecap(nodeId, briefing);
318
322
  recapShown = true;
319
323
  recapStale = activityVersion !== snapshotVersion;
320
324
  showRecap(briefing);
@@ -0,0 +1,14 @@
1
+ interface StampCtxLike {
2
+ sessionManager: {
3
+ /** Whole file, not the active branch: a session stamp on an abandoned
4
+ * branch still describes this file's owner. */
5
+ getEntries: () => unknown[];
6
+ };
7
+ }
8
+ interface PiLike {
9
+ on: (event: 'session_start' | 'message_end', handler: (event: unknown, ctx: StampCtxLike) => void | Promise<void>) => void;
10
+ appendEntry: (customType: string, data?: unknown) => void;
11
+ }
12
+ /** Register the stamp writer. */
13
+ export declare function registerCanvasStamp(pi: PiLike): void;
14
+ export default registerCanvasStamp;
@@ -0,0 +1,77 @@
1
+ // canvas-stamp.ts — the sole writer of `crtr-stamp` entries.
2
+ //
3
+ // Two records land in the session jsonl. A SESSION stamp names the node and the
4
+ // crtr release that own the file, appended at session start only when the
5
+ // latest one on disk disagrees — which covers a fresh session, a fork (whose
6
+ // copied history still carries the source node's stamp), a new cycle, and a
7
+ // runtime upgrade mid-node, without a rule per case. A MESSAGE stamp records
8
+ // where one user message came from, joined to it by the message's own
9
+ // timestamp.
10
+ //
11
+ // Every user message is stamped; an origin appears only when a delivery seam
12
+ // announced one (absence means unknown, never a guess). An assistant message is
13
+ // stamped only when an `ext` contribution is pending, so an ordinary turn costs
14
+ // no line.
15
+ //
16
+ // Ordering note: pi runs message_end handlers BEFORE it persists the finalized
17
+ // message, so the stamp is written as that message's structural parent. The
18
+ // join is by `msgTs`, so tree position carries no meaning here.
19
+ import { envNodeId } from '../shared/env.js';
20
+ import { crtrVersion } from '../shared/crtr-version.js';
21
+ import { claimMessageAnnouncement, drainStampExt, hasPendingStampExt, } from '../core/runtime/stamp/channel.js';
22
+ import { CRTR_STAMP_CUSTOM_TYPE, latestSessionStamp, STAMP_SCHEMA_VERSION, } from '../core/runtime/stamp/protocol.js';
23
+ /** Plain text of a message's content (string or content blocks). */
24
+ function messageText(content) {
25
+ if (typeof content === 'string')
26
+ return content;
27
+ if (Array.isArray(content)) {
28
+ return content
29
+ .filter((block) => typeof block === 'object' && block !== null && typeof block.text === 'string')
30
+ .map((block) => block.text)
31
+ .join('\n');
32
+ }
33
+ return '';
34
+ }
35
+ function envelope(kind, payload) {
36
+ const ext = drainStampExt();
37
+ return {
38
+ v: STAMP_SCHEMA_VERSION,
39
+ kind,
40
+ payload,
41
+ ...(ext === undefined ? {} : { ext }),
42
+ };
43
+ }
44
+ /** Register the stamp writer. */
45
+ export function registerCanvasStamp(pi) {
46
+ pi.on('session_start', (_event, ctx) => {
47
+ const node = envNodeId();
48
+ if (node === undefined || node.trim() === '')
49
+ return;
50
+ const crtr = crtrVersion();
51
+ const latest = latestSessionStamp(ctx.sessionManager.getEntries());
52
+ if (latest !== null && latest.payload.node === node && latest.payload.crtr === crtr)
53
+ return;
54
+ pi.appendEntry(CRTR_STAMP_CUSTOM_TYPE, envelope('session', { node, crtr }));
55
+ });
56
+ pi.on('message_end', (event) => {
57
+ const message = event?.message;
58
+ if (message === undefined || message === null)
59
+ return;
60
+ if (typeof message.timestamp !== 'number')
61
+ return;
62
+ const msgTs = message.timestamp;
63
+ if (message.role === 'user') {
64
+ const announcement = claimMessageAnnouncement(messageText(message.content));
65
+ pi.appendEntry(CRTR_STAMP_CUSTOM_TYPE, envelope('message', {
66
+ msgTs,
67
+ ...(announcement === null ? {} : announcement),
68
+ }));
69
+ return;
70
+ }
71
+ // Any other message earns a line only for a pending contribution.
72
+ if (!hasPendingStampExt())
73
+ return;
74
+ pi.appendEntry(CRTR_STAMP_CUSTOM_TYPE, envelope('message', { msgTs }));
75
+ });
76
+ }
77
+ export default registerCanvasStamp;
@@ -33,6 +33,7 @@ import { activeBackgroundBashJobs } from '../core/bash-jobs.js';
33
33
  import { CONTEXT_NUDGE_CUSTOM_TYPE, formatCard } from '../shared/generated-context.js';
34
34
  import { isReviewBoundary } from '../core/runtime/session-visibility.js';
35
35
  import { runRefreshAbort } from '../core/runtime/broker/extension-abort.js';
36
+ import { announceMessageOrigin } from '../core/runtime/stamp/channel.js';
36
37
  /** Condense a tool call into one short line for the activity cue. Picks the most
37
38
  * identifying arg (command / path / pattern / description), collapses whitespace,
38
39
  * and hard-caps the length so a giant bash command can't bloat telemetry.json. */
@@ -271,6 +272,7 @@ export function registerCanvasStophook(pi) {
271
272
  };
272
273
  const enactSettleDirective = (directive, ctx) => {
273
274
  if (directive.action === 'reprompt') {
275
+ announceMessageOrigin(directive.prompt, { type: 'runtime', kind: 'stop-guard' });
274
276
  pi.sendUserMessage(directive.prompt, { deliverAs: 'followUp' });
275
277
  return;
276
278
  }
@@ -566,6 +568,7 @@ export function registerCanvasStophook(pi) {
566
568
  firedBands.add(at);
567
569
  const note = formatCard('context-nudge', {}, steerNote(at, node.lifecycle, node.mode));
568
570
  if (runContinues) {
571
+ announceMessageOrigin(note, { type: 'runtime', kind: 'context-nudge' });
569
572
  pi.sendUserMessage(note, { deliverAs: 'steer' });
570
573
  }
571
574
  else {
@@ -17,6 +17,7 @@ const MIGRATED_VARS = [
17
17
  'CRTR_PROFILE_ID',
18
18
  'CRTR_NODE_CWD',
19
19
  'CRTR_MODEL_INTENT',
20
+ 'CRTR_MODEL_EXACT',
20
21
  'CRTR_HOME',
21
22
  'CRTR_STREAM_WATCHDOG_MS',
22
23
  'CRTR_WATCHER_TICK_MS',
@@ -35,6 +36,7 @@ const MIGRATED_VARS = [
35
36
  'CRTR_WARN_LIVE_BROKERS',
36
37
  'CRTR_TEST_UNATTENDED_PARK_MS',
37
38
  'CRTR_TEST_PARK_SUMMARY_GRACE_MS',
39
+ 'CRTR_UNATTENDED_PARK_MS',
38
40
  ];
39
41
  // Matches `process.env['VAR']`, `process.env.VAR`, or `process.env[\`VAR\`]`
40
42
  // for exactly one of MIGRATED_VARS — not a longer name that merely starts
@@ -53,8 +55,8 @@ const EXEMPTIONS = new Map([
53
55
  ],
54
56
  [
55
57
  'core/runtime/broker.ts',
56
- "clears CRTR_MODEL_INTENT via `delete process.env[...]` — a mutation, " +
57
- 'not a read, so it stays here per shared/env.ts\'s header comment.',
58
+ "clears CRTR_MODEL_INTENT and sets CRTR_MODEL_EXACT via process.env — mutations, " +
59
+ 'not reads, so they stay here per shared/env.ts\'s header comment.',
58
60
  ],
59
61
  ]);
60
62
  function tsFilesUnder(dir) {
@@ -0,0 +1,2 @@
1
+ /** The running crouter package version (`package.json` `version`). */
2
+ export declare function crtrVersion(): string;
@@ -0,0 +1,18 @@
1
+ // The installed crtr release string, read once per process from the package
2
+ // manifest. Callers that record a version into durable state (the session
3
+ // stamp) and callers that compare against npm (self-update) read this one
4
+ // implementation.
5
+ import { readFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ let cached = null;
9
+ /** The running crouter package version (`package.json` `version`). */
10
+ export function crtrVersion() {
11
+ if (cached !== null)
12
+ return cached;
13
+ // src|dist/shared/crtr-version.ts → up to src|dist/ → up to pkg root
14
+ const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
15
+ const parsed = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
16
+ cached = parsed.version;
17
+ return cached;
18
+ }
@@ -8,6 +8,8 @@ export declare function envNodeCwd(): string | undefined;
8
8
  * undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
9
9
  * in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
10
10
  export declare function envModelIntent(): string | undefined;
11
+ /** Whether the launch recipe requires this broker to keep its exact provider/model. */
12
+ export declare function envModelExact(): boolean;
11
13
  /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
12
14
  * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
13
15
  * only when a caller needs the override itself — to check whether one is
@@ -67,6 +69,14 @@ export declare function envWarnLiveBrokers(): number | undefined;
67
69
  /** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
68
70
  * gated by `envNoDaemonAutostart()` at the call site; no production default. */
69
71
  export declare function envTestUnattendedParkMs(): number | undefined;
72
+ /** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
73
+ * milliseconds; wins over the user-scope config value
74
+ * (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
75
+ * min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
76
+ * this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
77
+ * above the configured knob a deployment sets to tune how long an eligible
78
+ * resident idles before its parking turn. */
79
+ export declare function envUnattendedParkMs(): number | undefined;
70
80
  /** Test-only park-summary grace-period override
71
81
  * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
72
82
  export declare function envTestParkSummaryGraceMs(): number | undefined;
@@ -40,6 +40,10 @@ export function envNodeCwd() {
40
40
  export function envModelIntent() {
41
41
  return process.env['CRTR_MODEL_INTENT'];
42
42
  }
43
+ /** Whether the launch recipe requires this broker to keep its exact provider/model. */
44
+ export function envModelExact() {
45
+ return process.env['CRTR_MODEL_EXACT'] === '1';
46
+ }
43
47
  /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
44
48
  * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
45
49
  * only when a caller needs the override itself — to check whether one is
@@ -140,10 +144,11 @@ export function envDebug() {
140
144
  return process.env['CRTR_DEBUG'] === '1';
141
145
  }
142
146
  // Precedence — broker-supervision.ts's threshold/interval overrides. Each
143
- // returns the raw parsed value with NO default: the caller (config for the
144
- // thresholds, a hardcoded production constant for the intervals) supplies
145
- // the default, and env wins over it when set. Undefined, unparseable, or
146
- // non-positive all mean "no override" — the caller's own default applies.
147
+ // returns the raw parsed value with NO default: the caller supplies the
148
+ // default (user config for the broker thresholds and the unattended-park
149
+ // interval, a hardcoded constant for the test-only park-summary grace), and
150
+ // env wins over it when set. Undefined, unparseable, or non-positive all mean
151
+ // "no override" — the caller's own default applies.
147
152
  function parsePositiveInteger(raw) {
148
153
  if (raw === undefined || raw === '')
149
154
  return undefined;
@@ -171,6 +176,16 @@ function parsePositiveMsNoDefault(raw) {
171
176
  export function envTestUnattendedParkMs() {
172
177
  return parsePositiveMsNoDefault(process.env['CRTR_TEST_UNATTENDED_PARK_MS']);
173
178
  }
179
+ /** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
180
+ * milliseconds; wins over the user-scope config value
181
+ * (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
182
+ * min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
183
+ * this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
184
+ * above the configured knob a deployment sets to tune how long an eligible
185
+ * resident idles before its parking turn. */
186
+ export function envUnattendedParkMs() {
187
+ return parsePositiveMsNoDefault(process.env['CRTR_UNATTENDED_PARK_MS']);
188
+ }
174
189
  /** Test-only park-summary grace-period override
175
190
  * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
176
191
  export function envTestParkSummaryGraceMs() {
@@ -11,8 +11,7 @@ export declare const STALL_REPROMPT: string;
11
11
  /** The daemon's parking mandate: the last turn of a conversation the unattended
12
12
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
13
13
  * governs both the node's durable inheritance and reader-facing output:
14
- * one update for subscribers and history, then a silent stop with no assistant
15
- * prose for an external channel to relay.
14
+ * one update for subscribers and history, then a brief completion reply.
16
15
  *
17
16
  * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
18
17
  * guide (roadmap current, short and shrinking; context dir for in-progress
@@ -16,8 +16,7 @@ export const STALL_REPROMPT = "You've stopped but you're not waiting on anyone a
16
16
  /** The daemon's parking mandate: the last turn of a conversation the unattended
17
17
  * clock is concluding. Delivered live, wrapped in a `park` runtime card. It
18
18
  * governs both the node's durable inheritance and reader-facing output:
19
- * one update for subscribers and history, then a silent stop with no assistant
20
- * prose for an external channel to relay.
19
+ * one update for subscribers and history, then a brief completion reply.
21
20
  *
22
21
  * The durable inheritance shares a CONTRACT with `node yield`'s pre-invocation
23
22
  * guide (roadmap current, short and shrinking; context dir for in-progress
@@ -28,8 +27,8 @@ export const PARK_SUMMARY_PROMPT = 'This conversation has been idle with nothing
28
27
  + '2. Rewrite `$CRTR_CONTEXT_DIR/roadmap.md` for a fresh context window. This is the only handoff document a later fresh cycle receives in full. Preserve the current goal and exit criteria when they still apply, then state the present outcome; what remains or is blocked; decisions or questions still open; exact recovery handles for in-flight state; and the first safe move on return. Keep strategy and present state, not a transcript recap. Delete stale and completed steps instead of marking them done; the roadmap should stay short and shrink. Write a minimal one now if none exists.\n\n'
29
28
  + '3. Put supporting material in your context directory only when the roadmap would become bulky without it. Rewrite existing living documents rather than leave superseded versions. Name every supporting file the next cycle must read from the roadmap and say what it is for—the revive shows filenames but does not inject their contents. Task state, identifiers, and recovery detail belong here, not in memory.\n\n'
30
29
  + '4. Use memory only for a non-obvious, reusable lesson that should survive this task and is not already recorded. Read `crtr memory write -h`, find before writing, and choose the narrowest scope that will reach the next agent who needs it. Do not put a conversation recap, task status, recovery handles, or facts already captured in code or docs into memory.\n\n'
31
- + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write it for subscribers and history, not as a second roadmap. Its first line must stand alone as the current outcome, blocker, or decision that matters; then include only unfinished work, a needed decision, and concrete handles a subscriber may need. This concludes the conversation; it does not finish the mandate.\n\n'
32
- + '6. Keep this entire turn silent in the conversation: produce no assistant prose before, between, or after tool calls, and do not narrate the work. Once every required tool call has finished, end the response immediately without emitting any text, sign-off, summary, acknowledgement, or other visible content by emitting the `stop` token with no text. The deferred update from step 5 is the only reader-facing conclusion. A later message reopens you on a fresh context window grounded in your goal and roadmap, so the inheritance you leave now is what you get back.';
30
+ + '5. Push exactly one regular update with `crtr push update --tier deferred`, never `crtr push final`. Write for someone who has not seen this conversation: name the work in plain terms, say where it stands, and say what remains or is blocked. Put the current outcome in the first line; include only needed decisions and recovery handles. This concludes the conversation, not the mandate.\n\n'
31
+ + '6. When finished, reply with exactly `done`.';
33
32
  /** Static recovery prompts shared by the broker producer and display classifier. */
34
33
  export const AUTH_FAULT_RECOVERY_BODY = 'Provider credentials were just updated (a new login landed). Your previous turn stopped on a provider authentication failure. Continue from where you left off and retry the work that failed.';
35
34
  export const CONNECTION_FAULT_RECOVERY_BODY = 'The network connection is back online. Your previous turn stopped on a connection error (the network was down). Continue from where you left off and retry the work that failed.';
package/dist/types.d.ts CHANGED
@@ -157,6 +157,14 @@ export interface BrokerThresholdsConfig {
157
157
  /** Defer daemon-initiated revives once the live broker census reaches this count. */
158
158
  automaticReviveCap: number;
159
159
  }
160
+ /** Host-wide node-lifecycle timing. Read only from user scope by crtrd, like
161
+ * `brokerThresholds`: project configuration must not change another user's
162
+ * daemon. */
163
+ export interface LifecycleConfig {
164
+ /** How long an eligible resident idles before its unattended-parking turn
165
+ * begins, in milliseconds. `CRTR_UNATTENDED_PARK_MS` overrides this. */
166
+ unattendedParkMs: number;
167
+ }
160
168
  export type KeybindingOverrides = Record<string, string[]>;
161
169
  /** Compile-time seam for runtime consumers migrating to semantic binding IDs. */
162
170
  export type CanvasAction = {
@@ -251,6 +259,8 @@ export interface ScopeConfig {
251
259
  * Independent attended roots keep their normal viewer behavior. */
252
260
  auto_open_child_viewers: boolean;
253
261
  brokerThresholds: BrokerThresholdsConfig;
262
+ /** Host-wide node-lifecycle timing (user scope only). */
263
+ lifecycle: LifecycleConfig;
254
264
  /** Ring a short completion sound in the attach viewer when the node you are
255
265
  * watching finishes a turn and hands control back (the Claude-Code Stop-hook
256
266
  * ding). Only ever fires inside a live viewer pane — a headless/background
package/dist/types.js CHANGED
@@ -82,6 +82,10 @@ export function defaultScopeConfig() {
82
82
  // sized as a backstop that actually engages before the machine does.
83
83
  // Override per host with CRTR_MAX_LIVE_BROKERS / CRTR_WARN_LIVE_BROKERS.
84
84
  brokerThresholds: { warning: 16, automaticReviveCap: 32 },
85
+ // The unattended-park idle-eligibility delay: how long an eligible resident
86
+ // idles before its parking turn begins. Override per host with
87
+ // CRTR_UNATTENDED_PARK_MS.
88
+ lifecycle: { unattendedParkMs: 15 * 60_000 },
85
89
  completion_bell: true,
86
90
  working_gerunds: [...DEFAULT_WORKING_GERUNDS],
87
91
  whip_mode: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.263",
3
+ "version": "0.3.270",
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.263",
3
+ "version": "0.3.270",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.263",
9
+ "version": "0.3.270",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {