@crouton-kit/crouter 0.3.18 → 0.3.19

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 (257) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,308 @@
1
+ // Time grammar + recurrence evaluation for scheduled wakeups.
2
+ //
3
+ // This is the ONE place that owns wakeup time logic. Both the CLI surface
4
+ // (`node wake at|until|spawn`) and the daemon's firing pass import
5
+ // from here, so there is no duplicated cron usage and no surface<->daemon
6
+ // inverted dependency:
7
+ // - parseWhen / parseCadence — arm-time grammar (surface, T7)
8
+ // - nextSlotAfter — per-tick advance / coalescing (daemon, T4)
9
+ //
10
+ // Design refs: surface-design §4 (grammar), §5 (timezone/DST ruling),
11
+ // design §5.3 + D7 (engine evaluates UTC; the surface bakes the IANA zone
12
+ // into a calendar `recur`). Tick-scale only — no sub-second precision.
13
+ import { CronExpressionParser } from 'cron-parser';
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ /** Cadence floor: reject any interval / cron min-spacing below this (AC-N4). */
18
+ export const CADENCE_FLOOR_MS = 60_000;
19
+ const DURATION_UNIT_MS = {
20
+ s: 1_000,
21
+ m: 60_000,
22
+ h: 3_600_000,
23
+ d: 86_400_000,
24
+ };
25
+ // `<int><unit>` repeated; units s/m/h/d (surface-design §4.1).
26
+ const DURATION_RE = /^\+?(?:\d+[smhd])+$/;
27
+ const DURATION_PART_RE = /(\d+)([smhd])/g;
28
+ // ISO-8601 date-time core (no zone). Captures Y M D h m [s] [ms].
29
+ const ISO_CORE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?/;
30
+ // Trailing zone designator: Z | ±HH[:MM] | ±HHMM.
31
+ const ZONE_SUFFIX_RE = /^(Z|[+-]\d{2}(?::?\d{2})?)$/;
32
+ // Cron `@alias` -> 5-field expansion (the engine never stores a bare alias).
33
+ const CRON_ALIASES = {
34
+ '@yearly': '0 0 1 1 *',
35
+ '@annually': '0 0 1 1 *',
36
+ '@monthly': '0 0 1 * *',
37
+ '@weekly': '0 0 * * 0',
38
+ '@daily': '0 0 * * *',
39
+ '@midnight': '0 0 * * *',
40
+ '@hourly': '0 * * * *',
41
+ };
42
+ // ---------------------------------------------------------------------------
43
+ // Internal helpers
44
+ // ---------------------------------------------------------------------------
45
+ function whenError(code, message, received) {
46
+ return { error: { code, message, received } };
47
+ }
48
+ function cadenceError(code, message, received) {
49
+ return { error: { code, message, received } };
50
+ }
51
+ /** Total ms for a relative-duration string, or null if it is not one. */
52
+ function parseDurationMs(s) {
53
+ if (!DURATION_RE.test(s))
54
+ return null;
55
+ let total = 0;
56
+ DURATION_PART_RE.lastIndex = 0;
57
+ let m;
58
+ while ((m = DURATION_PART_RE.exec(s)) !== null) {
59
+ total += Number(m[1]) * DURATION_UNIT_MS[m[2]];
60
+ }
61
+ return total;
62
+ }
63
+ /** Host-configured IANA zone (crtr is a single-machine runtime). */
64
+ function hostZone() {
65
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
66
+ }
67
+ /** True iff `tz` is a resolvable IANA zone name. */
68
+ function isValidZone(tz) {
69
+ try {
70
+ new Intl.DateTimeFormat('en-US', { timeZone: tz });
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** Offset (ms) of `tz` at the given UTC instant: wall-clock-as-UTC minus instant. */
78
+ function zoneOffsetMs(tz, utcMs) {
79
+ const dtf = new Intl.DateTimeFormat('en-US', {
80
+ timeZone: tz,
81
+ hourCycle: 'h23',
82
+ year: 'numeric',
83
+ month: '2-digit',
84
+ day: '2-digit',
85
+ hour: '2-digit',
86
+ minute: '2-digit',
87
+ second: '2-digit',
88
+ });
89
+ const parts = {};
90
+ for (const p of dtf.formatToParts(new Date(utcMs))) {
91
+ if (p.type !== 'literal')
92
+ parts[p.type] = p.value;
93
+ }
94
+ const asUTC = Date.UTC(Number(parts['year']), Number(parts['month']) - 1, Number(parts['day']), Number(parts['hour']), Number(parts['minute']), Number(parts['second']));
95
+ return asUTC - utcMs;
96
+ }
97
+ /** Interpret a wall-clock instant in `tz` and return the UTC ms instant. */
98
+ function wallClockToUtc(y, mo, d, h, mi, s, ms, tz) {
99
+ const guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
100
+ const off1 = zoneOffsetMs(tz, guess);
101
+ let utc = guess - off1;
102
+ // Second pass settles DST-boundary cases where the offset at the candidate
103
+ // instant differs from the offset at the naive guess.
104
+ const off2 = zoneOffsetMs(tz, utc);
105
+ if (off2 !== off1)
106
+ utc = guess - off2;
107
+ return utc;
108
+ }
109
+ /** Classify a string as a zoned/bare ISO date-time, or null if it is neither. */
110
+ function classifyIso(s) {
111
+ const m = ISO_CORE_RE.exec(s);
112
+ if (!m || m.index !== 0)
113
+ return null;
114
+ const rest = s.slice(m[0].length);
115
+ if (rest === '') {
116
+ return {
117
+ zoned: false,
118
+ y: Number(m[1]),
119
+ mo: Number(m[2]),
120
+ d: Number(m[3]),
121
+ h: Number(m[4]),
122
+ mi: Number(m[5]),
123
+ s: m[6] ? Number(m[6]) : 0,
124
+ ms: m[7] ? Number(m[7].padEnd(3, '0')) : 0,
125
+ };
126
+ }
127
+ if (ZONE_SUFFIX_RE.test(rest))
128
+ return { zoned: true };
129
+ return null; // trailing garbage -> not a valid ISO instant
130
+ }
131
+ // ---------------------------------------------------------------------------
132
+ // parseWhen — resolve a one-shot <when> to an absolute UTC instant
133
+ // ---------------------------------------------------------------------------
134
+ /**
135
+ * Resolve a `<when>` to a single UTC `fireAt`:
136
+ * - relative duration (`90s`, `1h30m`) -> now + Σ
137
+ * - absolute zoned ISO (`…Z` / `±HH:MM`) -> that exact instant
138
+ * - absolute bare ISO (`2026-06-07T09:00`) -> wall clock in `tz`
139
+ * (default host-local) -> UTC
140
+ *
141
+ * A bare wall-clock time is interpreted in `opts.tz` when given, else the host
142
+ * zone, and frozen to a UTC instant (DST never re-enters a one-shot). A
143
+ * resolved instant not strictly in the future is rejected (`wake_in_past`).
144
+ */
145
+ export function parseWhen(when, opts) {
146
+ const received = when;
147
+ const raw = when.trim();
148
+ const nowMs = opts.now.getTime();
149
+ // 1. relative duration
150
+ const durMs = parseDurationMs(raw);
151
+ if (durMs !== null) {
152
+ const fireMs = nowMs + durMs;
153
+ if (fireMs <= nowMs) {
154
+ return whenError('wake_in_past', `That resolves to now or the past. Use a positive future duration (e.g. "5m", "2h").`, received);
155
+ }
156
+ return { fireAt: new Date(fireMs).toISOString() };
157
+ }
158
+ // 2/3. absolute ISO (zoned or bare)
159
+ const iso = classifyIso(raw);
160
+ if (iso) {
161
+ let fireMs;
162
+ if (iso.zoned) {
163
+ const d = new Date(raw);
164
+ if (Number.isNaN(d.getTime())) {
165
+ return whenError('bad_when', `Not a valid ISO-8601 instant: "${received}".`, received);
166
+ }
167
+ fireMs = d.getTime();
168
+ }
169
+ else {
170
+ const tz = opts.tz ?? hostZone();
171
+ if (!isValidZone(tz)) {
172
+ return whenError('unknown_zone', `Unknown time zone: "${tz}". Use an IANA name (e.g. "America/New_York").`, opts.tz ?? tz);
173
+ }
174
+ fireMs = wallClockToUtc(iso.y, iso.mo, iso.d, iso.h, iso.mi, iso.s, iso.ms, tz);
175
+ if (Number.isNaN(fireMs)) {
176
+ return whenError('bad_when', `Not a valid date-time: "${received}".`, received);
177
+ }
178
+ }
179
+ if (fireMs <= nowMs) {
180
+ return whenError('wake_in_past', `That time is now or in the past. Pick a future instant.`, received);
181
+ }
182
+ return { fireAt: new Date(fireMs).toISOString() };
183
+ }
184
+ return whenError('bad_when', `Could not parse "${received}". Use a duration ("5m", "1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00").`, received);
185
+ }
186
+ // ---------------------------------------------------------------------------
187
+ // parseCadence — resolve --every to a stored recur + first fire
188
+ // ---------------------------------------------------------------------------
189
+ /**
190
+ * Resolve a `<cadence>` (the `--every` value) to a stored `recur` JSON string
191
+ * plus the first `fireAt`:
192
+ * - fixed interval (a duration) -> {"every":"<dur>"}, first = now + interval
193
+ * - calendar cron / @alias -> {"cron":"<5-field>","tz":"<iana>"},
194
+ * first = next match strictly after now
195
+ *
196
+ * Aliases (`@daily`) are expanded to a 5-field cron before baking, and the
197
+ * IANA zone (from `opts.tz`, else host-local) is baked in so the engine stays a
198
+ * pure UTC evaluator (design D7). Rejects sub-floor spacing (`cadence_too_fast`,
199
+ * < 60s — for both intervals and seconds-granular crons), ungrammatical input
200
+ * (`bad_cadence`), and unknown zones (`unknown_zone`).
201
+ */
202
+ export function parseCadence(every, opts) {
203
+ const received = every;
204
+ const raw = every.trim();
205
+ const nowMs = opts.now.getTime();
206
+ // 1. fixed interval
207
+ const durMs = parseDurationMs(raw);
208
+ if (durMs !== null) {
209
+ if (durMs < CADENCE_FLOOR_MS) {
210
+ return cadenceError('cadence_too_fast', `Interval too fast: minimum cadence is 60s. Use a longer --every (e.g. "1m", "5m", "1h").`, received);
211
+ }
212
+ return {
213
+ recur: JSON.stringify({ every: raw }),
214
+ firstFireAt: new Date(nowMs + durMs).toISOString(),
215
+ };
216
+ }
217
+ // 2. calendar cron / alias
218
+ const expanded = CRON_ALIASES[raw] ?? raw;
219
+ const tz = opts.tz ?? hostZone();
220
+ if (opts.tz !== undefined && !isValidZone(opts.tz)) {
221
+ return cadenceError('unknown_zone', `Unknown time zone: "${opts.tz}". Use an IANA name (e.g. "America/New_York").`, opts.tz);
222
+ }
223
+ let interval;
224
+ let firstFireAt;
225
+ try {
226
+ interval = CronExpressionParser.parse(expanded, { currentDate: opts.now, tz });
227
+ firstFireAt = interval.next().toDate().toISOString();
228
+ }
229
+ catch {
230
+ return cadenceError('bad_cadence', `Could not parse cadence "${received}". Use a duration ("6h"), a 5-field cron ("0 9 * * *"), or an @alias ("@daily").`, received);
231
+ }
232
+ // Cadence floor: a 5-field cron is always >=60s; only a seconds-granular cron
233
+ // (multiple values in the seconds field) can resolve below the floor.
234
+ const secField = interval.fields?.second;
235
+ const secValues = secField && Array.isArray(secField.values) ? secField.values : [0];
236
+ if (secValues.length > 1) {
237
+ return cadenceError('cadence_too_fast', `Cron resolves below the 60s floor (sub-minute spacing). Use a cadence of at least one minute.`, received);
238
+ }
239
+ return {
240
+ recur: JSON.stringify({ cron: expanded, tz }),
241
+ firstFireAt,
242
+ };
243
+ }
244
+ // ---------------------------------------------------------------------------
245
+ // cadenceDisplay — a recur JSON rendered for humans + agents
246
+ // ---------------------------------------------------------------------------
247
+ /**
248
+ * Render a stored `recur` JSON as a compact, human-readable cadence:
249
+ * - {"every":"6h"} -> `every 6h`
250
+ * - {"cron":"0 9 * * *","tz":"America/…"} -> ``cron `0 9 * * *` (America/…)``
251
+ * - null / undefined / unparseable -> `none`
252
+ *
253
+ * The ONE cadence-display helper, shared by the `node wake` CLI surface
254
+ * (list/guidance) and the <crtr-wake> wake-provenance block (bearings.ts), so
255
+ * the cadence an agent reads in its wake block matches `crtr node wake list`
256
+ * exactly. Cadence only — never an instance count.
257
+ */
258
+ export function cadenceDisplay(recur) {
259
+ if (recur === null || recur === undefined)
260
+ return 'none';
261
+ try {
262
+ const r = JSON.parse(recur);
263
+ if (typeof r.every === 'string')
264
+ return `every ${r.every}`;
265
+ if (typeof r.cron === 'string') {
266
+ return `cron \`${r.cron}\`${typeof r.tz === 'string' ? ` (${r.tz})` : ''}`;
267
+ }
268
+ }
269
+ catch {
270
+ /* fall through */
271
+ }
272
+ return 'none';
273
+ }
274
+ // ---------------------------------------------------------------------------
275
+ // nextSlotAfter — the coalescing primitive (daemon advance)
276
+ // ---------------------------------------------------------------------------
277
+ /**
278
+ * Earliest occurrence of `recur` strictly greater than `now`, as a UTC ISO
279
+ * string. Anchors on `now` (not the stored `fire_at`), so it structurally
280
+ * coalesces every slot missed while the daemon was down (design §5.3, AC-E2).
281
+ * DST-correct because the IANA zone is baked into a calendar `recur`.
282
+ *
283
+ * parseCadence validates the cadence at arm time, so a throw here is a rare
284
+ * backstop (a corrupted/foreign row) — T4 must quarantine such a row, never
285
+ * re-query it each tick.
286
+ */
287
+ export function nextSlotAfter(recur, now) {
288
+ let parsed;
289
+ try {
290
+ parsed = JSON.parse(recur);
291
+ }
292
+ catch {
293
+ throw new Error(`nextSlotAfter: malformed recur JSON: ${recur}`);
294
+ }
295
+ if (typeof parsed.every === 'string') {
296
+ const ms = parseDurationMs(parsed.every);
297
+ if (ms === null || ms <= 0) {
298
+ throw new Error(`nextSlotAfter: invalid interval recur: ${recur}`);
299
+ }
300
+ return new Date(now.getTime() + ms).toISOString();
301
+ }
302
+ if (typeof parsed.cron === 'string') {
303
+ const tz = typeof parsed.tz === 'string' ? parsed.tz : 'UTC';
304
+ const interval = CronExpressionParser.parse(parsed.cron, { currentDate: now, tz });
305
+ return interval.next().toDate().toISOString(); // strictly after `now`
306
+ }
307
+ throw new Error(`nextSlotAfter: recur has neither 'every' nor 'cron': ${recur}`);
308
+ }
@@ -1,3 +1,30 @@
1
+ import { type NodeRow } from '../core/canvas/index.js';
2
+ import { isPidAlive } from '../core/canvas/pid.js';
3
+ export declare const YIELD_STALL_GRACE_MS: number;
4
+ export declare const NEVER_LAUNCHED_GRACE_MS: number;
5
+ /** Never-launched decision (audit 2026-06-09, Bug 4 — node mq45b6ch-9ecc2f03):
6
+ * a spawn that opened its window but whose pi never came up leaves a row with
7
+ * ZERO cycles, NO session, NO pid — and, when the window lingers (e.g. a
8
+ * remain-on-exit corpse), the live-pane pass read "no pid to judge" and left it
9
+ * forever; the parent waited on a node that was never going to run. Pure core:
10
+ * a node that has ever booted (session recorded) or ever been revived (cycles
11
+ * bumped) is NOT never-launched — the cycles gate also bounds this to ONE
12
+ * automatic retry (the relaunch bumps cycles), so a persistently failing
13
+ * launch can't loop. Age below the grace is a normal in-flight boot. */
14
+ export declare function neverLaunchedVerdict(piSessionId: string | null, cycles: number, ageMs: number): 'leave' | 'relaunch';
15
+ /** Yield-stall decision (audit 2026-06-09, Bug 1 — node mq5v9hfa-74493d45 and
16
+ * three sibling orchestrators): a `node yield` records intent='refresh' and
17
+ * agent_end dispatches the in-place respawn, but at very large contexts the pi
18
+ * process can HANG instead of exiting — leaving pi alive, the turn over, and
19
+ * the intent pending forever. The daemon only revives DEAD pis, so without
20
+ * this verdict the node is permanently stuck (no revive, no wake). Pure core,
21
+ * mirroring livenessVerdict; the kill side effects live in handleYieldStall.
22
+ *
23
+ * 'kill' ONLY when ALL of: pi alive, intent='refresh', the turn is over (busy
24
+ * marker absent — a node may legitimately keep working for many minutes after
25
+ * running `node yield` mid-turn, and a working pi must never be killed), and
26
+ * the state has persisted past YIELD_STALL_GRACE_MS. */
27
+ export declare function yieldStallVerdict(piPidAlive: boolean | null, intent: NodeRow['intent'], busy: boolean, stalledFor: number | null): 'leave' | 'pending' | 'kill';
1
28
  export type LivenessVerdict = 'leave' | 'pending' | 'revive';
2
29
  /** Decide what to do with a node whose tmux pane is alive, from its pi
3
30
  * liveness and how long it's been dead. Pure — the time-and-tmux side effects
@@ -6,12 +33,10 @@ export type LivenessVerdict = 'leave' | 'pending' | 'revive';
6
33
  * relaunch in flight) — leave those to the pane-gone pass.
7
34
  * deadFor: ms since first observed dead, or null on the first observation. */
8
35
  export declare function livenessVerdict(piPidAlive: boolean | null, deadFor: number | null): LivenessVerdict;
36
+ export declare const DEAD_REAP_GRACE_MS: number;
9
37
  /** Read the pid stored in the pidfile, or null if absent / malformed. */
10
38
  export declare function readPidfile(): number | null;
11
- /** True if a process with `pid` is currently alive (signal-0 probe). `kill(pid,
12
- * 0)` throws ESRCH when the process is gone; EPERM means it exists but isn't
13
- * ours — still alive. */
14
- export declare function isPidAlive(pid: number): boolean;
39
+ export { isPidAlive };
15
40
  /** True when a crtrd process is already running (pidfile exists + pid alive). */
16
41
  export declare function isDaemonRunning(): boolean;
17
42
  export declare function superviseTick(now?: number): Promise<void>;