@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) 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/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -1,6 +1,10 @@
1
- /** What a node is doing right now. UI shows active+idle; `done` is hidden but
2
- * revivable; `canceled` is a user-closed node (also hidden, also revivable
3
- * not a fault); only `dead` is a fault. */
1
+ import type { SpawnChildOpts } from '../runtime/spawn.js';
2
+ /** What a node is doing right now. `active` means the engine process is live on
3
+ * its host — a tmux pane OR a headless broker (`host_kind`) NOT that it holds
4
+ * a window or that anyone is watching it (attachment is a separate axis: the
5
+ * `focuses` table for tmux, the broker's viewer set for headless). UI shows
6
+ * active+idle; `done` is hidden but revivable; `canceled` is a user-closed node
7
+ * (also hidden, also revivable — not a fault); only `dead` is a fault. */
4
8
  export type NodeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
5
9
  /** Does stopping finalize the node? terminal = worker (finalizes on push --final);
6
10
  * resident = manager/orchestrator (stays dormant, woken by inbox). */
@@ -44,6 +48,11 @@ export interface NodeIdentity {
44
48
  created: string;
45
49
  /** The dir this node is pinned to — its cwd (where pi runs, bash executes). */
46
50
  cwd: string;
51
+ /** Which HOST launches + supervises this node: 'tmux' (a pane) or 'broker' (the
52
+ * headless in-process engine). Durable IDENTITY (mirrored to the row like
53
+ * `cwd`, re-derived on index rebuild), NOT live runtime like `pane`.
54
+ * NULL ⇒ 'tmux' (every pre-broker node). */
55
+ host_kind?: 'tmux' | 'broker' | null;
47
56
  /** Role the node was born as: explore | developer | plan | review | general… */
48
57
  kind: string;
49
58
  mode: Mode;
@@ -65,6 +74,14 @@ export interface NodeIdentity {
65
74
  * `parent` so an INDEPENDENT root (parent=null) still records its lineage.
66
75
  * Audit only; null for a user-opened root. Defaults to `parent` for a child. */
67
76
  spawned_by?: string | null;
77
+ /** Fork provenance — the `--fork-from` source this node was forked from at
78
+ * spawn (the node id / session path / pi session uuid the caller passed). pi
79
+ * COPIED that conversation into this node's first session, so the node boots
80
+ * carrying the SOURCE's first-person history. Persisted here so the boot
81
+ * intro can assert this node's OWN identity over that inherited narrative
82
+ * (without it a fork impersonates its source). Undefined for a fresh node;
83
+ * identity/audit only. */
84
+ fork_from?: string | null;
68
85
  /** New subscriptions this node opens default to passive when true. */
69
86
  passive_default?: boolean;
70
87
  /** REVIVE-HOME — the tmux session a node is (re)opened into when it must
@@ -87,6 +104,12 @@ export interface NodeIdentity {
87
104
  * PATH is opened directly — immune to any cwd discrepancy. Null for older
88
105
  * nodes booted before this field existed → revive falls back to the bare id. */
89
106
  pi_session_file?: string | null;
107
+ /** A caller-pinned model TIER (ultra/strong/medium/light) that overrides the
108
+ * persona's declared default. Durable so it survives a polymorph: every
109
+ * spec-rebuild path (promote/demote/lifecycle/reset) re-passes it to
110
+ * buildLaunchSpec, while the persona default is recomputed fresh for the
111
+ * (possibly new) kind. Null/undefined ⇒ use the persona default. */
112
+ model_override?: string | null;
90
113
  /** Full pi launch recipe; rewritten on every polymorph. */
91
114
  launch?: LaunchSpec;
92
115
  }
@@ -143,6 +166,8 @@ export interface NodeRow {
143
166
  lifecycle: Lifecycle;
144
167
  status: NodeStatus;
145
168
  cwd: string;
169
+ /** Which HOST launches + supervises this node; see NodeIdentity.host_kind. */
170
+ host_kind: 'tmux' | 'broker' | null;
146
171
  parent: string | null;
147
172
  created: string;
148
173
  /** Authoritative runtime columns (see NodeRuntime). */
@@ -188,3 +213,66 @@ export interface SubscriptionRef {
188
213
  active: boolean;
189
214
  created: string;
190
215
  }
216
+ /** The four firing actions a wakeup can carry (design §4):
217
+ * - bare → revive the target node directly (resume:false), no inbox entry.
218
+ * - noted → deliver a note to the target's inbox (normal tier).
219
+ * - deadline → deliver a timeout note to the target's inbox (urgent tier); ≤1 per node.
220
+ * - spawn → spawnChild a fresh node from a stored recipe (detached, node_id NULL). */
221
+ export type WakeKind = 'bare' | 'noted' | 'deadline' | 'spawn';
222
+ /** payload for a `bare` wake — `null`, or a label carried only for list rendering. */
223
+ export interface BareWakePayload {
224
+ label?: string;
225
+ }
226
+ /** payload for a `noted` wake — a non-empty note body + a ≤120-char first-line label. */
227
+ export interface NotedWakePayload {
228
+ body: string;
229
+ label: string;
230
+ }
231
+ /** payload for a `deadline` wake — a non-empty timeout note + the machine-readable
232
+ * `timeout` mirror. The VISIBLE timeout signal rides the RENDERED label/body, not
233
+ * this flag (inbox digests never surface arbitrary data keys). ≤1 per node. */
234
+ export interface DeadlineWakePayload {
235
+ body: string;
236
+ timeout: true;
237
+ label: string;
238
+ }
239
+ /** The parsed per-kind payload union (Maj-1): the data-access layer JSON.stringifies
240
+ * this on write and JSON.parses it back on read, so callers consume `payload.body`
241
+ * / `payload as SpawnChildOpts` directly with no re-parse. A `spawn` payload is a
242
+ * SpawnChildOpts recipe with `parent` resolved to a NON-NULL armer id at arm time. */
243
+ export type WakePayload = null | BareWakePayload | NotedWakePayload | DeadlineWakePayload | SpawnChildOpts;
244
+ /** A wakeup row as stored in the `wakeups` table (canvas.db, migration v7). One row =
245
+ * "do <kind> at fire_at [every recur]". `node_id` is the revive TARGET + node-anchored
246
+ * cancel anchor (NULL = canvas-detached); `owner_id` is the ARMER (set on every row).
247
+ * `payload` is the PARSED per-kind union, never the raw JSON string (Maj-1). */
248
+ export interface Wakeup {
249
+ /** Stable id (caller-supplied at arm time, minted as `wk-${newNodeId()}`). */
250
+ wakeup_id: string;
251
+ /** Revive target + node-anchored cancel anchor; NULL = canvas-detached (spawn). */
252
+ node_id: string | null;
253
+ /** The node that armed this wake; set on EVERY row. == node_id for a self-alarm. */
254
+ owner_id: string;
255
+ /** Next absolute fire time, ISO 8601 UTC; lexicographic <= compare. */
256
+ fire_at: string;
257
+ kind: WakeKind;
258
+ /** NULL = one-shot/adaptive; else the recur JSON the daemon advances. */
259
+ recur: string | null;
260
+ /** Parsed per-kind payload (NULL for a bare one-shot with no label). */
261
+ payload: WakePayload;
262
+ /** ISO 8601, audit/order. */
263
+ created: string;
264
+ }
265
+ /** The arm-a-wake spec (the pinned shared contract). `wakeup_id` is CALLER-SUPPLIED —
266
+ * the canvas layer never mints ids (importing newNodeId from runtime/nodes.ts would
267
+ * force a canvas → runtime → canvas cycle, Min-1); the command surface mints
268
+ * `wk-${newNodeId()}` and passes it in. `owner_id` is required on every row;
269
+ * `node_id` is NULL for a detached wake (deferred spawn / spawn-cron). */
270
+ export interface ArmWakeSpec {
271
+ wakeup_id: string;
272
+ node_id?: string | null;
273
+ owner_id: string;
274
+ fire_at: string;
275
+ kind: WakeKind;
276
+ recur?: string | null;
277
+ payload?: WakePayload;
278
+ }
@@ -0,0 +1,76 @@
1
+ import type { ArmWakeSpec, Wakeup } from './types.js';
2
+ /** A thrown integrity-backstop error from armWake. `code` is the AC-N3 code the
3
+ * command surface (the ONLY sanctioned caller of armWake) maps to a rendered
4
+ * error block — armWake carries integrity backstops only (empty body / recur-on-
5
+ * deadline / unknown kind); target-resolvability + recoverable-state + the
6
+ * per-owner cap live in the surface (Min-6), so a non-surface armer could insert
7
+ * a dangling wake — every armer MUST route through the surface. */
8
+ export declare class WakeArmError extends Error {
9
+ readonly code: string;
10
+ constructor(code: string, message: string);
11
+ }
12
+ /** Which wakes a listWakes call returns. Distinguished by which key is present
13
+ * (mirrors listNodes' filter shape):
14
+ * - { node } — wakes anchored to this node (node_id = ?).
15
+ * - { owner } — wakes this node ARMED (owner_id = ?); the per-owner cap count.
16
+ * - { subtree } — wakes anchored to any node in this id set (node_id IN (…)).
17
+ * - { due } — wakes whose fire_at <= this ISO instant.
18
+ * - { canvas } — every wake on the canvas. */
19
+ export type WakeScope = {
20
+ node: string;
21
+ } | {
22
+ owner: string;
23
+ } | {
24
+ subtree: string[];
25
+ } | {
26
+ due: string;
27
+ } | {
28
+ canvas: true;
29
+ };
30
+ /** INSERT one wakeup row from the pinned spec, returning its (caller-supplied)
31
+ * `wakeup_id`. JSON.stringifies the typed `payload` union into the TEXT column
32
+ * and stamps `created`.
33
+ *
34
+ * Deadline upsert (design Q3, "≤1 deadline per node"): for `kind === 'deadline'`
35
+ * the node's existing deadline is canceled first, then the new one inserted — the
36
+ * partial unique index `idx_wakeups_deadline` is the schema-level backstop.
37
+ *
38
+ * Integrity backstops (throw WakeArmError; the surface maps the code to AC-N3):
39
+ * - empty/whitespace note `body` for `noted`/`deadline` → `empty_note`.
40
+ * - non-NULL `recur` on a `deadline` → `deadline_cannot_recur`.
41
+ * - unknown `kind` → `bad_kind`.
42
+ * NO target-resolvability / recoverable-state / cap backstop here (Min-6). */
43
+ export declare function armWake(spec: ArmWakeSpec): string;
44
+ /** UPDATE the next fire time — recurrence advancement (the daemon settles a
45
+ * recurring row to its next slot BEFORE enacting, crash-safe; design D4). */
46
+ export declare function advanceWake(wakeup_id: string, nextFireIso: string): void;
47
+ /** DELETE one wakeup by id — one-shot consumption (the daemon settles a one-shot
48
+ * row BEFORE enacting, crash-safe; design D4). */
49
+ export declare function consumeWake(wakeup_id: string): void;
50
+ /** DELETE one wakeup by id — the explicit user/surface cancel. Idempotent:
51
+ * 0 rows ⇒ no error (AC-C2). */
52
+ export declare function cancelWake(wakeup_id: string): void;
53
+ /** DELETE every wake anchored to this node AND every detached wake it ARMED —
54
+ * deliberate close/reap (ruling A). Hooked at transition('cancel'). */
55
+ export declare function cancelWakesFor(node_id: string): void;
56
+ /** DELETE this node's one-shot self-alarms (recur IS NULL); node-anchored crons
57
+ * SURVIVE — finalize is the instance finishing, not a teardown of standing
58
+ * schedules (design Q1). Hooked at transition('finalize'). */
59
+ export declare function cancelSelfAlarms(node_id: string): void;
60
+ /** DELETE this node's pending deadline(s) — cancel-on-wake (design §6.4).
61
+ * Hooked at reviveNode, so the deadline always belongs to the dormancy left. */
62
+ export declare function cancelDeadlinesFor(node_id: string): void;
63
+ /** The daemon's per-tick due query: every row whose fire_at has arrived, in
64
+ * fire_at order, with `payload` parsed into the typed union. Returns exactly ONE
65
+ * row per recurring wake regardless of missed slots — coalescing is structural
66
+ * (AC-E2). */
67
+ export declare function dueWakes(nowIso: string): Wakeup[];
68
+ /** List wakes for a scope (node / owner / subtree / due / canvas), `payload`
69
+ * parsed as in dueWakes. The subtree variant binds the id set with `?`
70
+ * placeholders (mirroring listNodes — the codebase's one safe dynamic-IN spot);
71
+ * ids are NEVER string-concatenated (Min-10). */
72
+ export declare function listWakes(scope: WakeScope): Wakeup[];
73
+ /** True if the node has ANY pending self-anchored wake (of any kind). Consumed by
74
+ * the stop-guard self-wake seam so a no-child poll node releases dormant after
75
+ * arming instead of being nagged to finish (AC-X3/AC-R1). */
76
+ export declare function hasPendingSelfWake(node_id: string): boolean;
@@ -0,0 +1,185 @@
1
+ // wakeups.ts — the WAKEUPS table data-access layer (canvas.db, migration v7).
2
+ //
3
+ // One-module-per-table (the focuses.ts precedent): ALL SQL that touches the
4
+ // `wakeups` table lives HERE and nowhere else — never appended to canvas.ts,
5
+ // which keeps the node/edge model. A WAKEUP is one durable row = "do <kind> at
6
+ // fire_at [every recur]", carrying a fire time, a kind, an optional payload, an
7
+ // optional recurrence cadence, a nullable TARGET node_id, and the owner_id of
8
+ // the node that ARMED it.
9
+ //
10
+ // Two anchors, two reap rules (design D1/D2):
11
+ // - node_id → the revive target AND the node-anchored cancel anchor; it
12
+ // carries the `ON DELETE CASCADE` FK, so a pruned node's node-anchored wakes
13
+ // are reaped FOR FREE in pruneNodes' same `DELETE FROM nodes` (canvas.ts is
14
+ // UNTOUCHED — the cascade does it; there is no pruneNodes wakeups code).
15
+ // NULL = canvas-detached (a deferred spawn / spawn-cron).
16
+ // - owner_id → the armer; a PLAIN indexed column with NO FK, so a crashed-
17
+ // then-pruned armer's detached cron is NOT cascade-dropped (Invariant E).
18
+ // Reaped only by the explicit cancelWakesFor DELETE on deliberate close.
19
+ //
20
+ // Each function is a single-statement query mirroring the canvas.ts atomic-setter
21
+ // pattern (setStatus/setIntent/setPresence) — no read-modify-write, serialized by
22
+ // WAL. The data-access layer OWNS payload serialization: armWake JSON.stringifies
23
+ // the typed per-kind union on write; dueWakes/listWakes JSON.parse it back on read
24
+ // (Maj-1), so callers consume `payload.body` / `payload as SpawnChildOpts` directly.
25
+ //
26
+ // `wakeup_id` is CALLER-SUPPLIED (the command surface mints `wk-${newNodeId()}`):
27
+ // the canvas layer never imports newNodeId from runtime/nodes.ts — that would
28
+ // force a canvas → runtime → canvas import cycle (Min-1).
29
+ import { openDb } from './db.js';
30
+ /** A thrown integrity-backstop error from armWake. `code` is the AC-N3 code the
31
+ * command surface (the ONLY sanctioned caller of armWake) maps to a rendered
32
+ * error block — armWake carries integrity backstops only (empty body / recur-on-
33
+ * deadline / unknown kind); target-resolvability + recoverable-state + the
34
+ * per-owner cap live in the surface (Min-6), so a non-surface armer could insert
35
+ * a dangling wake — every armer MUST route through the surface. */
36
+ export class WakeArmError extends Error {
37
+ code;
38
+ constructor(code, message) {
39
+ super(message);
40
+ this.code = code;
41
+ this.name = 'WakeArmError';
42
+ }
43
+ }
44
+ /** Hydrate a raw row into a Wakeup, JSON.parsing `payload` back into the typed
45
+ * per-kind union (NULL column → null). */
46
+ function wakeupFrom(r) {
47
+ const raw = r['payload'];
48
+ return {
49
+ wakeup_id: r['wakeup_id'],
50
+ node_id: r['node_id'] ?? null,
51
+ owner_id: r['owner_id'],
52
+ fire_at: r['fire_at'],
53
+ kind: r['kind'],
54
+ recur: r['recur'] ?? null,
55
+ payload: raw == null ? null : JSON.parse(raw),
56
+ created: r['created'],
57
+ };
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Writes — each a single-statement INSERT/UPDATE/DELETE.
61
+ // ---------------------------------------------------------------------------
62
+ /** INSERT one wakeup row from the pinned spec, returning its (caller-supplied)
63
+ * `wakeup_id`. JSON.stringifies the typed `payload` union into the TEXT column
64
+ * and stamps `created`.
65
+ *
66
+ * Deadline upsert (design Q3, "≤1 deadline per node"): for `kind === 'deadline'`
67
+ * the node's existing deadline is canceled first, then the new one inserted — the
68
+ * partial unique index `idx_wakeups_deadline` is the schema-level backstop.
69
+ *
70
+ * Integrity backstops (throw WakeArmError; the surface maps the code to AC-N3):
71
+ * - empty/whitespace note `body` for `noted`/`deadline` → `empty_note`.
72
+ * - non-NULL `recur` on a `deadline` → `deadline_cannot_recur`.
73
+ * - unknown `kind` → `bad_kind`.
74
+ * NO target-resolvability / recoverable-state / cap backstop here (Min-6). */
75
+ export function armWake(spec) {
76
+ const { kind } = spec;
77
+ if (kind !== 'bare' && kind !== 'noted' && kind !== 'deadline' && kind !== 'spawn') {
78
+ throw new WakeArmError('bad_kind', `unknown wake kind: ${String(kind)}`);
79
+ }
80
+ if (kind === 'noted' || kind === 'deadline') {
81
+ const body = spec.payload?.body;
82
+ if (typeof body !== 'string' || body.trim() === '') {
83
+ throw new WakeArmError('empty_note', `a ${kind} wake requires a non-empty note body.`);
84
+ }
85
+ }
86
+ if (kind === 'deadline' && spec.recur != null) {
87
+ throw new WakeArmError('deadline_cannot_recur', 'a deadline wake cannot recur.');
88
+ }
89
+ // Deadline upsert: clear the node's existing deadline before inserting the new one.
90
+ if (kind === 'deadline' && spec.node_id != null) {
91
+ cancelDeadlinesFor(spec.node_id);
92
+ }
93
+ const payloadText = spec.payload == null ? null : JSON.stringify(spec.payload);
94
+ openDb()
95
+ .prepare(`INSERT INTO wakeups (wakeup_id, node_id, owner_id, fire_at, kind, recur, payload, created)
96
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
97
+ .run(spec.wakeup_id, spec.node_id ?? null, spec.owner_id, spec.fire_at, kind, spec.recur ?? null, payloadText, new Date().toISOString());
98
+ return spec.wakeup_id;
99
+ }
100
+ /** UPDATE the next fire time — recurrence advancement (the daemon settles a
101
+ * recurring row to its next slot BEFORE enacting, crash-safe; design D4). */
102
+ export function advanceWake(wakeup_id, nextFireIso) {
103
+ openDb().prepare('UPDATE wakeups SET fire_at = ? WHERE wakeup_id = ?').run(nextFireIso, wakeup_id);
104
+ }
105
+ /** DELETE one wakeup by id — one-shot consumption (the daemon settles a one-shot
106
+ * row BEFORE enacting, crash-safe; design D4). */
107
+ export function consumeWake(wakeup_id) {
108
+ openDb().prepare('DELETE FROM wakeups WHERE wakeup_id = ?').run(wakeup_id);
109
+ }
110
+ /** DELETE one wakeup by id — the explicit user/surface cancel. Idempotent:
111
+ * 0 rows ⇒ no error (AC-C2). */
112
+ export function cancelWake(wakeup_id) {
113
+ openDb().prepare('DELETE FROM wakeups WHERE wakeup_id = ?').run(wakeup_id);
114
+ }
115
+ /** DELETE every wake anchored to this node AND every detached wake it ARMED —
116
+ * deliberate close/reap (ruling A). Hooked at transition('cancel'). */
117
+ export function cancelWakesFor(node_id) {
118
+ openDb()
119
+ .prepare('DELETE FROM wakeups WHERE node_id = ? OR owner_id = ?')
120
+ .run(node_id, node_id);
121
+ }
122
+ /** DELETE this node's one-shot self-alarms (recur IS NULL); node-anchored crons
123
+ * SURVIVE — finalize is the instance finishing, not a teardown of standing
124
+ * schedules (design Q1). Hooked at transition('finalize'). */
125
+ export function cancelSelfAlarms(node_id) {
126
+ openDb().prepare('DELETE FROM wakeups WHERE node_id = ? AND recur IS NULL').run(node_id);
127
+ }
128
+ /** DELETE this node's pending deadline(s) — cancel-on-wake (design §6.4).
129
+ * Hooked at reviveNode, so the deadline always belongs to the dormancy left. */
130
+ export function cancelDeadlinesFor(node_id) {
131
+ openDb().prepare("DELETE FROM wakeups WHERE node_id = ? AND kind = 'deadline'").run(node_id);
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Reads.
135
+ // ---------------------------------------------------------------------------
136
+ /** The daemon's per-tick due query: every row whose fire_at has arrived, in
137
+ * fire_at order, with `payload` parsed into the typed union. Returns exactly ONE
138
+ * row per recurring wake regardless of missed slots — coalescing is structural
139
+ * (AC-E2). */
140
+ export function dueWakes(nowIso) {
141
+ return openDb()
142
+ .prepare('SELECT * FROM wakeups WHERE fire_at <= ? ORDER BY fire_at')
143
+ .all(nowIso).map(wakeupFrom);
144
+ }
145
+ /** List wakes for a scope (node / owner / subtree / due / canvas), `payload`
146
+ * parsed as in dueWakes. The subtree variant binds the id set with `?`
147
+ * placeholders (mirroring listNodes — the codebase's one safe dynamic-IN spot);
148
+ * ids are NEVER string-concatenated (Min-10). */
149
+ export function listWakes(scope) {
150
+ const db = openDb();
151
+ let rows;
152
+ if ('node' in scope) {
153
+ rows = db
154
+ .prepare('SELECT * FROM wakeups WHERE node_id = ? ORDER BY fire_at')
155
+ .all(scope.node);
156
+ }
157
+ else if ('owner' in scope) {
158
+ rows = db
159
+ .prepare('SELECT * FROM wakeups WHERE owner_id = ? ORDER BY fire_at')
160
+ .all(scope.owner);
161
+ }
162
+ else if ('subtree' in scope) {
163
+ if (scope.subtree.length === 0)
164
+ return [];
165
+ const placeholders = scope.subtree.map(() => '?').join(',');
166
+ rows = db
167
+ .prepare(`SELECT * FROM wakeups WHERE node_id IN (${placeholders}) ORDER BY fire_at`)
168
+ .all(...scope.subtree);
169
+ }
170
+ else if ('due' in scope) {
171
+ rows = db
172
+ .prepare('SELECT * FROM wakeups WHERE fire_at <= ? ORDER BY fire_at')
173
+ .all(scope.due);
174
+ }
175
+ else {
176
+ rows = db.prepare('SELECT * FROM wakeups ORDER BY fire_at').all();
177
+ }
178
+ return rows.map(wakeupFrom);
179
+ }
180
+ /** True if the node has ANY pending self-anchored wake (of any kind). Consumed by
181
+ * the stop-guard self-wake seam so a no-child poll node releases dormant after
182
+ * arming instead of being nagged to finish (AC-X3/AC-R1). */
183
+ export function hasPendingSelfWake(node_id) {
184
+ return (openDb().prepare('SELECT 1 FROM wakeups WHERE node_id = ? LIMIT 1').get(node_id) !== undefined);
185
+ }
@@ -137,7 +137,10 @@ function mergeConfig(partial) {
137
137
  ? Math.floor(rawMaxPanes)
138
138
  : defaults.max_panes_per_window;
139
139
  const canvasNav = mergeCanvasNav(partial.canvasNav);
140
- return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav };
140
+ // The merge drops unknown keys, so `headless` must be carried explicitly or it
141
+ // is lost on every read-modify-write of config.json.
142
+ const headless = typeof partial.headless === 'boolean' ? partial.headless : defaults.headless;
143
+ return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav, headless };
141
144
  }
142
145
  export function updateConfig(scope, mutate) {
143
146
  const cfg = readConfig(scope);
@@ -1,3 +1,4 @@
1
+ import { parse as parseYaml } from 'yaml';
1
2
  import { isSkillType } from '../types.js';
2
3
  const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
3
4
  export function parseFrontmatter(source) {
@@ -7,7 +8,7 @@ export function parseFrontmatter(source) {
7
8
  }
8
9
  const raw = match[1];
9
10
  const body = source.slice(match[0].length);
10
- return { data: toSkillFrontmatter(parseYamlRecord(raw)), body, raw };
11
+ return { data: toSkillFrontmatter(parseYamlBlock(raw)), body, raw };
11
12
  }
12
13
  /** Like parseFrontmatter but returns the raw key/value record instead of
13
14
  * coercing to SkillFrontmatter. Used by consumers (e.g. subagents) that read
@@ -19,138 +20,50 @@ export function parseFrontmatterGeneric(source) {
19
20
  }
20
21
  const raw = match[1];
21
22
  const body = source.slice(match[0].length);
22
- return { data: parseYamlRecord(raw), body, raw };
23
+ return { data: parseYamlBlock(raw), body, raw };
23
24
  }
24
25
  function toSkillFrontmatter(out) {
25
26
  const fm = {
26
- name: typeof out.name === 'string' ? out.name : '',
27
- description: typeof out.description === 'string' ? out.description : undefined,
28
- keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
27
+ name: scalarToString(out.name) ?? '',
28
+ description: scalarToString(out.description),
29
+ keywords: Array.isArray(out.keywords) ? out.keywords.map((k) => String(k)) : undefined,
29
30
  type: isSkillType(out.type) ? out.type : undefined,
30
31
  };
31
32
  return fm;
32
33
  }
33
- function parseYamlRecord(yaml) {
34
- const lines = yaml.split(/\r?\n/);
35
- const out = {};
36
- let i = 0;
37
- while (i < lines.length) {
38
- const raw = lines[i];
39
- if (!raw.trim()) {
40
- i++;
41
- continue;
42
- }
43
- const idx = raw.indexOf(':');
44
- if (idx === -1) {
45
- i++;
46
- continue;
47
- }
48
- const key = raw.slice(0, idx).trim();
49
- const rest = raw.slice(idx + 1).trim();
50
- // Block scalar: `key: |` or `key: >` with optional chomp indicator (-/+)
51
- const blockMatch = rest.match(/^([|>])([-+]?)\s*$/);
52
- if (blockMatch) {
53
- const style = blockMatch[1];
54
- const chomp = blockMatch[2];
55
- const collected = [];
56
- let blockIndent = null;
57
- let j = i + 1;
58
- while (j < lines.length) {
59
- const r = lines[j];
60
- if (r.trim() === '') {
61
- collected.push('');
62
- j++;
63
- continue;
64
- }
65
- const ind = r.match(/^(\s*)/)?.[1].length ?? 0;
66
- if (blockIndent === null) {
67
- if (ind === 0)
68
- break;
69
- blockIndent = ind;
70
- }
71
- if (ind < blockIndent)
72
- break;
73
- collected.push(r.slice(blockIndent));
74
- j++;
75
- }
76
- while (collected.length > 0 && collected[collected.length - 1] === '')
77
- collected.pop();
78
- let value;
79
- if (style === '|') {
80
- value = collected.join('\n');
81
- }
82
- else {
83
- const parts = [];
84
- let para = [];
85
- for (const ln of collected) {
86
- if (ln === '') {
87
- if (para.length > 0) {
88
- parts.push(para.join(' '));
89
- para = [];
90
- }
91
- parts.push('');
92
- }
93
- else {
94
- para.push(ln);
95
- }
96
- }
97
- if (para.length > 0)
98
- parts.push(para.join(' '));
99
- const folded = [];
100
- for (let k = 0; k < parts.length; k++) {
101
- if (parts[k] === '' && (k === 0 || parts[k - 1] === ''))
102
- continue;
103
- folded.push(parts[k]);
104
- }
105
- value = folded.join('\n').replace(/\n+$/, '');
106
- }
107
- if (chomp !== '+')
108
- value = value.replace(/\n+$/, '');
109
- out[key] = value;
110
- i = j;
111
- continue;
112
- }
113
- // Empty value: could be a list on subsequent lines
114
- if (rest === '') {
115
- const buf = [];
116
- let j = i + 1;
117
- while (j < lines.length) {
118
- const r = lines[j];
119
- if (r.trim() === '') {
120
- j++;
121
- continue;
122
- }
123
- if (/^\s*-\s+/.test(r)) {
124
- buf.push(stripQuotes(r.replace(/^\s*-\s+/, '').trim()));
125
- j++;
126
- continue;
127
- }
128
- break;
129
- }
130
- if (buf.length > 0)
131
- out[key] = buf;
132
- i = j;
133
- continue;
134
- }
135
- if (rest.startsWith('[') && rest.endsWith(']')) {
136
- out[key] = rest
137
- .slice(1, -1)
138
- .split(',')
139
- .map((s) => stripQuotes(s.trim()))
140
- .filter(Boolean);
141
- i++;
142
- continue;
143
- }
144
- out[key] = stripQuotes(rest);
145
- i++;
146
- }
147
- return out;
34
+ /** Coerce a YAML scalar (string/number/boolean) to its string form. A skill's
35
+ * `name`/`description` are always strings to consumers; native non-string
36
+ * scalars (a numeric/boolean value) render as their string form, and
37
+ * non-scalars (null/undefined/array/object) yield undefined. */
38
+ function scalarToString(v) {
39
+ if (typeof v === 'string')
40
+ return v;
41
+ if (typeof v === 'number' || typeof v === 'boolean')
42
+ return String(v);
43
+ return undefined;
44
+ }
45
+ /** Parse the inner YAML of a frontmatter block into a key/value record.
46
+ *
47
+ * ONE strict parser, no fallback. The frontmatter contract IS "valid YAML":
48
+ * the single `yaml`-package path handles the substrate's nested `gate:` maps,
49
+ * inline `{…}` objects, and native scalar typing — and a doc whose frontmatter
50
+ * is NOT valid YAML THROWS, by design. There is deliberately no second lenient
51
+ * parser; a dual parsing strategy is the hedge we reject. Callers that iterate
52
+ * MANY docs isolate a per-doc throw at the COLLECTION layer (a clear scoped
53
+ * notice naming the bad file, then continue); single-doc callers let the error
54
+ * surface so the user learns their one requested doc is malformed.
55
+ *
56
+ * A VALID non-object document (a bare scalar, a top-level list, or an empty
57
+ * block) normalizes to an empty record, preserving the contract that callers
58
+ * always receive a Record for well-formed-but-non-mapping frontmatter. */
59
+ function parseYamlBlock(raw) {
60
+ return normalizeToRecord(parseYaml(raw));
148
61
  }
149
- function stripQuotes(s) {
150
- if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
151
- return s.slice(1, -1);
62
+ function normalizeToRecord(parsed) {
63
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
64
+ return parsed;
152
65
  }
153
- return s;
66
+ return {};
154
67
  }
155
68
  export function serializeFrontmatter(data) {
156
69
  const lines = ['---'];
@@ -0,0 +1,49 @@
1
+ import type { Scope } from '../types.js';
2
+ /**
3
+ * Thin memory-document resolver for the document substrate. Mirrors the SHAPE
4
+ * of the skill resolver (qualifier parse → scope precedence → direct path
5
+ * lookup → leaf-name fallback) but drops ALL plugin machinery: memory
6
+ * resolution is scope + leaf/path ONLY. The three memory scopes resolve in
7
+ * precedence order project > user > builtin (the same precedence concept as
8
+ * orderPluginsByResolution, minus plugins). It returns the raw parsed
9
+ * frontmatter + body; it does NOT interpret the schema, kind, gate, or rungs —
10
+ * that is the schema/gate layer's job (callers filter by `frontmatter.kind`).
11
+ */
12
+ export interface MemoryDoc {
13
+ /** Path-derived identity: the doc's path under the scope's memory/ root, no
14
+ * extension, slash-separated — e.g. memory/taste/foo.md → "taste/foo". */
15
+ name: string;
16
+ scope: Scope;
17
+ /** Absolute path to the resolved .md file. */
18
+ path: string;
19
+ /** Raw, uncoerced frontmatter record (null when the doc has no frontmatter). */
20
+ frontmatter: Record<string, unknown> | null;
21
+ /** Document body, with the frontmatter block stripped. */
22
+ body: string;
23
+ }
24
+ export interface MemoryResolutionOpts {
25
+ /** Restrict resolution to a single scope. Conflicts with a scope prefix on
26
+ * the identifier (e.g. `user/foo` with scope=project) throw. */
27
+ scope?: Scope;
28
+ }
29
+ /** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
30
+ export declare function memoryDocId(doc: MemoryDoc): string;
31
+ /** All memory docs in one scope's memory/ dir, scanned recursively for *.md
32
+ * (topical subdirs supported), sorted by path-derived name. */
33
+ export declare function listMemoryDocs(scope: Scope): MemoryDoc[];
34
+ /** All memory docs across the resolved scopes, in precedence order
35
+ * (project, then user, then builtin), each scope's docs name-sorted within. */
36
+ export declare function listAllMemoryDocs(scope?: Scope): MemoryDoc[];
37
+ /**
38
+ * Resolve a path-derived name to a single memory document.
39
+ *
40
+ * Accepted identifier forms (mirroring parseSkillQualifier, no plugins):
41
+ * <name> — bare name; resolved by scope precedence project>user>builtin
42
+ * <scope>/<name> — pinned to one scope (user|project)
43
+ * `<name>` may carry topical subdirs (`taste/foo`); a bare leaf (`foo`) falls
44
+ * back to a last-segment match across the resolved scopes.
45
+ *
46
+ * Resolution order: parse qualifier → direct memory/<name>.md lookup
47
+ * (precedence-first) → leaf-name fallback (ambiguity error listing candidates).
48
+ */
49
+ export declare function resolveMemoryDoc(rawName: string, opts?: MemoryResolutionOpts): MemoryDoc;