@north-light/crouter 0.3.220 → 0.3.222

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 (271) hide show
  1. package/dist/api/client.d.ts +29 -0
  2. package/dist/api/client.js +44 -0
  3. package/dist/api/dto/chat-inventory.d.ts +56 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/human-requests.d.ts +88 -0
  6. package/dist/api/dto/human-requests.js +4 -0
  7. package/dist/api/dto/human.d.ts +3 -0
  8. package/dist/api/dto/profiles.d.ts +19 -5
  9. package/dist/api/dto/profiles.js +2 -1
  10. package/dist/api/dto/reviews.d.ts +2 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +8 -0
  14. package/dist/api/routes.js +11 -0
  15. package/dist/build-root.d.ts +2 -6
  16. package/dist/build-root.js +51 -4
  17. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  18. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  19. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  20. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  21. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  22. package/dist/builtin-memory/04-base-worker.md +4 -8
  23. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  24. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  25. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  26. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  27. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  28. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  29. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  30. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  31. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  32. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  33. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  35. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  36. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  38. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  39. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  40. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  41. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  42. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  43. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  44. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  45. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  46. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  47. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  48. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  49. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  50. package/dist/builtin-memory/design/guide.md +35 -0
  51. package/dist/builtin-memory/design/roadmap.md +21 -0
  52. package/dist/builtin-memory/insights/capture.md +1 -1
  53. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  54. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  55. package/dist/builtin-memory/internal/plugins.md +10 -1
  56. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  57. package/dist/builtin-memory/plan/roadmap.md +6 -22
  58. package/dist/builtin-memory/spec/guide.md +23 -6
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
  62. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  63. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  64. package/dist/clients/attach/overlays/file-review.js +2 -2
  65. package/dist/clients/attach/render/markdown-source.js +106 -1
  66. package/dist/clients/attach/session/file-links.d.ts +13 -4
  67. package/dist/clients/attach/session/file-links.js +54 -58
  68. package/dist/clients/attach/session/keys.d.ts +1 -1
  69. package/dist/clients/attach/session/profile-files.js +1 -1
  70. package/dist/clients/attach/viewer.js +698 -696
  71. package/dist/clients/inbox/controller.js +1 -1
  72. package/dist/clients/inbox/resolve.d.ts +1 -0
  73. package/dist/clients/inbox/review/launch.d.ts +8 -4
  74. package/dist/clients/inbox/review/launch.js +55 -5
  75. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  76. package/dist/clients/inbox/review/review-client.js +7 -1
  77. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  78. package/dist/clients/inbox/review-adapter.js +4 -52
  79. package/dist/commands/__tests__/human.test.js +2 -2
  80. package/dist/commands/human/request.d.ts +2 -0
  81. package/dist/commands/human/request.js +281 -0
  82. package/dist/commands/human.js +5 -2
  83. package/dist/commands/memory/lint.js +2 -1
  84. package/dist/commands/memory/read.js +1 -0
  85. package/dist/commands/memory.js +1 -1
  86. package/dist/commands/pkg/market-manage.js +165 -75
  87. package/dist/commands/pkg/plugin-inspect.js +19 -2
  88. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  89. package/dist/commands/pkg/plugin-manage.js +72 -24
  90. package/dist/commands/profile/default.js +6 -10
  91. package/dist/commands/profile/list.js +5 -3
  92. package/dist/commands/profile/new.js +21 -8
  93. package/dist/commands/profile/project.js +25 -19
  94. package/dist/commands/profile/show.js +3 -3
  95. package/dist/commands/surface-inbox.js +1 -0
  96. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  97. package/dist/commands/sys/config.js +2 -2
  98. package/dist/commands/sys/doctor.js +87 -5
  99. package/dist/commands/sys/migrate.js +38 -19
  100. package/dist/commands/sys/setup-core.js +1 -1
  101. package/dist/commands/sys/sync-project-guidance.js +1 -1
  102. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  103. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  104. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  105. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  106. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  107. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  108. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  109. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  110. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  111. package/dist/core/__tests__/human-actions.test.js +116 -0
  112. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  113. package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
  114. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  115. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  116. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  117. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  118. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  119. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  120. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  121. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  122. package/dist/core/bootstrap.js +6 -0
  123. package/dist/core/canvas/browse/app.js +5 -2
  124. package/dist/core/canvas/browse/model.d.ts +25 -15
  125. package/dist/core/canvas/browse/model.js +86 -65
  126. package/dist/core/canvas/db.js +23 -0
  127. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  128. package/dist/core/canvas/human-deliveries.js +75 -0
  129. package/dist/core/canvas/render-source.d.ts +6 -0
  130. package/dist/core/canvas/render-source.js +7 -1
  131. package/dist/core/canvas/render.js +10 -2
  132. package/dist/core/command-hooks/artifact.d.ts +10 -0
  133. package/dist/core/command-hooks/artifact.js +129 -0
  134. package/dist/core/command-hooks/catalog.d.ts +14 -0
  135. package/dist/core/command-hooks/catalog.js +38 -0
  136. package/dist/core/command-hooks/compose.d.ts +15 -0
  137. package/dist/core/command-hooks/compose.js +99 -0
  138. package/dist/core/command-hooks/discovery.d.ts +87 -0
  139. package/dist/core/command-hooks/discovery.js +174 -0
  140. package/dist/core/command-hooks/help.d.ts +5 -0
  141. package/dist/core/command-hooks/help.js +18 -0
  142. package/dist/core/command-hooks/index.d.ts +6 -0
  143. package/dist/core/command-hooks/index.js +6 -0
  144. package/dist/core/command-hooks/report.d.ts +23 -0
  145. package/dist/core/command-hooks/report.js +19 -0
  146. package/dist/core/command-hooks/schema.d.ts +27 -0
  147. package/dist/core/command-hooks/schema.js +68 -0
  148. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  149. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  150. package/dist/core/command-plugins/presence.d.ts +2 -0
  151. package/dist/core/command-plugins/presence.js +17 -0
  152. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  153. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  154. package/dist/core/command.d.ts +8 -1
  155. package/dist/core/command.js +12 -10
  156. package/dist/core/config.d.ts +13 -1
  157. package/dist/core/config.js +51 -1
  158. package/dist/core/feed/inbox.d.ts +6 -0
  159. package/dist/core/feed/inbox.js +9 -1
  160. package/dist/core/help.d.ts +7 -1
  161. package/dist/core/human/action-binding.d.ts +21 -0
  162. package/dist/core/human/action-binding.js +40 -0
  163. package/dist/core/human/completion.d.ts +38 -0
  164. package/dist/core/human/completion.js +27 -0
  165. package/dist/core/human/convention.d.ts +2 -0
  166. package/dist/core/human/convention.js +2 -0
  167. package/dist/core/human/tickets.d.ts +25 -6
  168. package/dist/core/human/tickets.js +19 -13
  169. package/dist/core/human/types.d.ts +5 -0
  170. package/dist/core/human-actions.d.ts +25 -0
  171. package/dist/core/human-actions.js +101 -0
  172. package/dist/core/io.d.ts +9 -1
  173. package/dist/core/io.js +44 -2
  174. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  175. package/dist/core/memory/inline-ref-inventory.js +15 -8
  176. package/dist/core/memory-resolver.d.ts +13 -1
  177. package/dist/core/memory-resolver.js +26 -20
  178. package/dist/core/profiles/manifest.d.ts +13 -2
  179. package/dist/core/profiles/manifest.js +84 -18
  180. package/dist/core/profiles/select.d.ts +2 -0
  181. package/dist/core/profiles/select.js +29 -12
  182. package/dist/core/render.js +11 -0
  183. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  184. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  185. package/dist/core/runtime/bearings.js +1 -1
  186. package/dist/core/runtime/broker/event-projection.js +7 -0
  187. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  188. package/dist/core/runtime/broker/frame-dispatch.js +13 -11
  189. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  190. package/dist/core/runtime/broker/read-ops.js +6 -2
  191. package/dist/core/runtime/broker-extension-render.js +1 -1
  192. package/dist/core/runtime/broker-inventory.d.ts +4 -0
  193. package/dist/core/runtime/broker-inventory.js +116 -0
  194. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  195. package/dist/core/runtime/broker-protocol.d.ts +9 -2
  196. package/dist/core/runtime/broker.js +10 -1
  197. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  198. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  199. package/dist/core/runtime/command-surface.d.ts +38 -0
  200. package/dist/core/runtime/command-surface.js +117 -0
  201. package/dist/core/runtime/launch-target.d.ts +25 -0
  202. package/dist/core/runtime/launch-target.js +54 -0
  203. package/dist/core/runtime/node-read.js +5 -0
  204. package/dist/core/runtime/persona.js +3 -3
  205. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  206. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  207. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  208. package/dist/core/runtime/prospective-inventory.js +88 -0
  209. package/dist/core/runtime/spawn.d.ts +3 -1
  210. package/dist/core/runtime/spawn.js +5 -3
  211. package/dist/core/scope.d.ts +26 -1
  212. package/dist/core/scope.js +52 -12
  213. package/dist/core/substrate/on-read.d.ts +7 -1
  214. package/dist/core/substrate/on-read.js +30 -32
  215. package/dist/core/substrate/render-node.d.ts +3 -2
  216. package/dist/core/substrate/render-node.js +3 -2
  217. package/dist/core/substrate/render.js +65 -24
  218. package/dist/core/substrate/schema.d.ts +16 -2
  219. package/dist/core/substrate/schema.js +14 -5
  220. package/dist/core/user-settings.d.ts +4 -0
  221. package/dist/core/user-settings.js +1 -0
  222. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
  223. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  224. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  225. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  226. package/dist/daemon/api/handlers/human-requests.js +409 -0
  227. package/dist/daemon/api/handlers/human.js +3 -0
  228. package/dist/daemon/api/handlers/inbox.js +3 -0
  229. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  230. package/dist/daemon/api/handlers/nodes.js +11 -46
  231. package/dist/daemon/api/handlers/profiles.js +7 -1
  232. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  233. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  234. package/dist/daemon/api/handlers/reviews.js +10 -2
  235. package/dist/daemon/api/map.d.ts +2 -1
  236. package/dist/daemon/api/map.js +3 -2
  237. package/dist/daemon/api/server.js +6 -0
  238. package/dist/daemon/crtrd.js +6 -0
  239. package/dist/daemon/human/deliver-action.d.ts +16 -0
  240. package/dist/daemon/human/deliver-action.js +168 -0
  241. package/dist/daemon/human/finish.d.ts +8 -5
  242. package/dist/daemon/human/finish.js +45 -6
  243. package/dist/daemon/human/sweep.js +4 -1
  244. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  245. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  246. package/dist/daemon/review/finish.d.ts +8 -3
  247. package/dist/daemon/review/finish.js +19 -1
  248. package/dist/hook-authoring.d.ts +75 -0
  249. package/dist/hook-authoring.js +358 -0
  250. package/dist/hook-process.d.ts +7 -0
  251. package/dist/hook-process.js +34 -0
  252. package/dist/index.d.ts +2 -0
  253. package/dist/index.js +2 -0
  254. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  255. package/dist/migrations/002-profile-project-memory.js +71 -0
  256. package/dist/migrations/profile-manifests.d.ts +30 -0
  257. package/dist/migrations/profile-manifests.js +70 -0
  258. package/dist/migrations/registry.js +10 -5
  259. package/dist/migrations/types.d.ts +28 -1
  260. package/dist/migrations/types.js +15 -9
  261. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  262. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  263. package/dist/types.d.ts +23 -6
  264. package/dist/types.js +1 -0
  265. package/package.json +1 -1
  266. package/runtime.lock.json +2 -2
  267. package/dist/builtin-memory/00-runtime-base.md +0 -55
  268. package/dist/builtin-memory/design.md +0 -55
  269. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  270. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
  271. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
@@ -0,0 +1,71 @@
1
+ // 002 — profile projects gain an explicit memory value. Historical manifests
2
+ // stored `projects` as bare directory strings; the current contract is
3
+ // `{ path, memory }`, where memory caps automatic boot/workspace-open
4
+ // disclosure from that project's stores.
5
+ //
6
+ // Mapping: a string entry on the root profile becomes `preview` (root is an
7
+ // aggregate identity — it points at everything and should not absorb every
8
+ // project's full corpus), every other profile's string entries become
9
+ // `content` (they were authored as that profile's own working context).
10
+ // Root is classified ONLY by the fixed profile directory id: a manifest
11
+ // `name` is mutable, so `name === 'root'` would misclassify a renamed profile.
12
+ //
13
+ // Mixed manifests converge: already-valid objects pass through byte-identical
14
+ // and only string entries convert. Anything else — a non-array `projects`, a
15
+ // non-string/non-object entry, an object without a path, an unknown memory
16
+ // value — throws naming the file, because a guessed cap silently widens or
17
+ // narrows what every node under that profile is shown.
18
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../api/dto/profiles.js';
19
+ import { usage } from '../core/errors.js';
20
+ /** Frozen copy of `ROOT_PROFILE_ID` (core/profiles/manifest.ts). Migrations
21
+ * stay out of that module's import graph — its readers reject the very
22
+ * manifests this lane exists to convert. */
23
+ const ROOT_PROFILE_ID = 'root-00000000';
24
+ function malformed(input, detail) {
25
+ return usage(`profile manifest cannot be migrated: ${detail}`, {
26
+ received: input.path,
27
+ field: 'projects',
28
+ next: `Fix ${input.path} by hand — every project entry is a string (pre-migration) or {"path": "/abs/dir", "memory": one of ${PROFILE_PROJECT_MEMORY_VALUES.join(', ')}} — then re-run \`crtr sys migrate\`.`,
29
+ });
30
+ }
31
+ function isMemory(value) {
32
+ return typeof value === 'string' && PROFILE_PROJECT_MEMORY_VALUES.includes(value);
33
+ }
34
+ export const profileProjectMemoryMigration = {
35
+ lane: 'profile-manifest',
36
+ description: 'profile projects carry an explicit memory value',
37
+ apply(input) {
38
+ const stored = input.raw['projects'];
39
+ if (!Array.isArray(stored)) {
40
+ throw malformed(input, `projects is ${stored === undefined ? 'missing' : typeof stored}, not an array`);
41
+ }
42
+ const legacyMemory = input.profileId === ROOT_PROFILE_ID ? 'preview' : 'content';
43
+ let converted = false;
44
+ const projects = stored.map((entry) => {
45
+ if (typeof entry === 'string') {
46
+ if (entry.trim() === '')
47
+ throw malformed(input, 'a project entry is an empty path');
48
+ converted = true;
49
+ return { path: entry, memory: legacyMemory };
50
+ }
51
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
52
+ throw malformed(input, `a project entry is ${entry === null ? 'null' : typeof entry}`);
53
+ }
54
+ const { path, memory } = entry;
55
+ if (typeof path !== 'string' || path.trim() === '') {
56
+ throw malformed(input, `a project entry has no path: ${JSON.stringify(entry)}`);
57
+ }
58
+ if (!isMemory(memory)) {
59
+ throw malformed(input, `project ${path} has an unknown memory value: ${JSON.stringify(memory)}`);
60
+ }
61
+ // Byte-identical pass-through: an already-valid entry keeps whatever
62
+ // else it carries, so re-running plans no change.
63
+ return entry;
64
+ });
65
+ if (!converted)
66
+ return null;
67
+ // Spread preserves the manifest's stored key order, so the rewrite's diff
68
+ // is the projects array and nothing else.
69
+ return { ...input.raw, projects };
70
+ },
71
+ };
@@ -0,0 +1,30 @@
1
+ import type { ProfileManifestSnapshot, StateMigration } from './types.js';
2
+ export interface ProfileManifestRunResult {
3
+ /** Profile manifests found under the profiles root. */
4
+ profiles: number;
5
+ /** One row per migration that rewrote a manifest, in registry order. */
6
+ applied: {
7
+ profileId: string;
8
+ path: string;
9
+ migration: string;
10
+ }[];
11
+ /** Post-migration manifest per readable profile, in scan order — coherent
12
+ * under `dryRun`, where the same fold ran but nothing was written. */
13
+ snapshots: ProfileManifestSnapshot[];
14
+ /** Manifests left untouched because they are not valid JSON. Unreadable
15
+ * bytes are not a structure this lane can convert, and failing the run over
16
+ * one would block every other profile's convergence. */
17
+ skipped: {
18
+ profileId: string;
19
+ path: string;
20
+ error: string;
21
+ }[];
22
+ }
23
+ export declare function runProfileManifestMigrations(profilesRoot: string, opts: {
24
+ /** Held for the whole re-read/fold/write of one manifest — the same lock
25
+ * add/remove take, so a concurrent mutation either lands before the
26
+ * re-read or serializes after the write, never under it. */
27
+ withLock: <T>(profileId: string, fn: () => T) => T;
28
+ dryRun?: boolean;
29
+ migrations?: readonly StateMigration[];
30
+ }): ProfileManifestRunResult;
@@ -0,0 +1,70 @@
1
+ // The profile-manifest lane runner: fold every registered profile-manifest
2
+ // migration over each stored `profile.json`, one profile at a time, under the
3
+ // caller-supplied per-profile lock.
4
+ //
5
+ // This lane reads RAW BYTES. The current manifest reader treats a manifest
6
+ // whose projects are not the current shape exactly like corrupt JSON, so a
7
+ // pre-migration home has no readable profiles at all — every profile surface,
8
+ // `listProfiles()` included, is blind until this lane has run. That is why the
9
+ // runner enumerates the profiles root itself, why `crtr sys migrate` runs it
10
+ // before any other profile read, and why its snapshots (not `listProfiles()`)
11
+ // are what the rest of that command's discovery works from.
12
+ //
13
+ // The lock arrives as a callback rather than an import: a migration file may
14
+ // not reach into the profile module, and a lane runner that did would be one
15
+ // import away from the same blind reader.
16
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+ import { atomicWriteJson } from '../core/fs-utils.js';
19
+ import { STATE_MIGRATIONS } from './registry.js';
20
+ /** Frozen copy of the generated-profile-id shape (core/profiles/manifest.ts),
21
+ * so enumeration matches `listProfiles()`: a hand-made directory crtr can
22
+ * never resolve is skipped here too, rather than failing the whole run. */
23
+ const ID_SHAPE = /^[a-z0-9]+(?:-[a-z0-9]+)*-[0-9a-f]{8}$/;
24
+ export function runProfileManifestMigrations(profilesRoot, opts) {
25
+ const migrations = (opts.migrations ?? STATE_MIGRATIONS).filter((m) => m.lane === 'profile-manifest');
26
+ const result = { profiles: 0, applied: [], snapshots: [], skipped: [] };
27
+ if (!existsSync(profilesRoot))
28
+ return result;
29
+ for (const dirent of readdirSync(profilesRoot, { withFileTypes: true })) {
30
+ if (!dirent.isDirectory())
31
+ continue;
32
+ const profileId = dirent.name;
33
+ if (!ID_SHAPE.test(profileId))
34
+ continue;
35
+ const path = join(profilesRoot, profileId, 'profile.json');
36
+ if (!existsSync(path))
37
+ continue;
38
+ result.profiles += 1;
39
+ opts.withLock(profileId, () => {
40
+ let raw;
41
+ try {
42
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
43
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
44
+ throw new Error('manifest is not a JSON object');
45
+ }
46
+ raw = parsed;
47
+ }
48
+ catch (e) {
49
+ result.skipped.push({ profileId, path, error: (e instanceof Error ? e.message : String(e)).split('\n')[0] });
50
+ return;
51
+ }
52
+ let current = raw;
53
+ let changed = false;
54
+ for (const migration of migrations) {
55
+ const next = migration.apply({ profileId, path, raw: current });
56
+ if (next === null)
57
+ continue;
58
+ current = next;
59
+ changed = true;
60
+ result.applied.push({ profileId, path, migration: migration.description });
61
+ }
62
+ if (changed && opts.dryRun !== true)
63
+ atomicWriteJson(path, current);
64
+ // Every migration guarantees a current manifest or throws, so the folded
65
+ // record is the post-migration contract by construction.
66
+ result.snapshots.push({ profileId, path, manifest: current });
67
+ });
68
+ }
69
+ return result;
70
+ }
@@ -3,10 +3,11 @@
3
3
  //
4
4
  // Authoring contract (each migration is one file, `NNN-<desc>.ts`, appended
5
5
  // here in order):
6
- // • Idempotent. A convergent migration re-runs on every `crtr sys migrate`,
7
- // so applying it to an already-migrated store must plan zero changes. A
8
- // journaled migration runs once per database via user_version, but its
9
- // body must still tolerate a crash-resume re-run of any filesystem work.
6
+ // • Idempotent. A convergent or profile-manifest migration re-runs on every
7
+ // `crtr sys migrate`, so applying it to already-migrated state must plan
8
+ // zero changes. A journaled migration runs once per database via
9
+ // user_version, but its body must still tolerate a crash-resume re-run of
10
+ // any filesystem work.
10
11
  // • Journaled filesystem steps are crash-resumable: fs writes happen outside
11
12
  // the db transaction, so a step interrupted mid-fs-work must complete
12
13
  // (never corrupt) when the chain re-runs it.
@@ -16,4 +17,8 @@
16
17
  // This keeps the registry legal in every CLI leaf's import graph (the
17
18
  // build's V-1 gate).
18
19
  import { surfacesFrontmatterMigration } from './001-surfaces-frontmatter.js';
19
- export const STATE_MIGRATIONS = [surfacesFrontmatterMigration];
20
+ import { profileProjectMemoryMigration } from './002-profile-project-memory.js';
21
+ export const STATE_MIGRATIONS = [
22
+ surfacesFrontmatterMigration,
23
+ profileProjectMemoryMigration,
24
+ ];
@@ -1,4 +1,5 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
+ import type { ProfileManifest } from '../types.js';
2
3
  export interface JournaledMigration {
3
4
  lane: 'journaled';
4
5
  description: string;
@@ -19,7 +20,33 @@ export interface ConvergentMigration {
19
20
  * runner filters no-op changes and performs the writes. */
20
21
  apply(store: StoreSnapshot, context?: ConvergentMigrationContext): DocChange[];
21
22
  }
22
- export type StateMigration = JournaledMigration | ConvergentMigration;
23
+ /** One stored `profile.json` handed to the profile-manifest lane. */
24
+ export interface ProfileManifestInput {
25
+ /** The immediate directory name under the profiles root. The only root
26
+ * classifier a migration may key on — a manifest `name` is mutable. */
27
+ profileId: string;
28
+ /** Absolute path to the `profile.json`. */
29
+ path: string;
30
+ /** Parsed JSON exactly as stored, historical shapes included. */
31
+ raw: Record<string, unknown>;
32
+ }
33
+ export interface ProfileManifestMigration {
34
+ lane: 'profile-manifest';
35
+ description: string;
36
+ /** Pure planning: the full replacement record, or null when this manifest is
37
+ * already current. A structure the migration cannot convert THROWS — the
38
+ * run fails rather than persisting a guessed value. */
39
+ apply(input: ProfileManifestInput): Record<string, unknown> | null;
40
+ }
41
+ export type StateMigration = JournaledMigration | ConvergentMigration | ProfileManifestMigration;
42
+ /** One profile's manifest after every profile-manifest migration folded —
43
+ * the bytes on disk after a write run, and the bytes a write run WOULD have
44
+ * produced under `--dry-run`. */
45
+ export interface ProfileManifestSnapshot {
46
+ profileId: string;
47
+ path: string;
48
+ manifest: ProfileManifest;
49
+ }
23
50
  /** One parsed markdown doc in a store snapshot. */
24
51
  export interface StoreDoc {
25
52
  /** Absolute file path. */
@@ -1,11 +1,17 @@
1
- // State-migration shapes. Two lanes, split by which store owns the state:
1
+ // State-migration shapes. Three lanes, split by which store owns the state:
2
2
  //
3
- // journaled — canvas-db state. Runs inside the db's ordered migration chain
4
- // (core/canvas/db.ts appends these to MIGRATIONS), so each step
5
- // gets the chain's BEGIN IMMEDIATE/COMMIT + user_version gating
6
- // and runs exactly once per database.
7
- // convergent — on-disk memory documents. No journal exists over a user's
8
- // stores (files appear, sync in, get hand-edited), so these
9
- // CONVERGE instead: every run re-scans and rewrites whatever
10
- // still matches the old shape. `crtr sys migrate` is the runner.
3
+ // journaled — canvas-db state. Runs inside the db's ordered migration
4
+ // chain (core/canvas/db.ts appends these to MIGRATIONS), so
5
+ // each step gets the chain's BEGIN IMMEDIATE/COMMIT +
6
+ // user_version gating and runs exactly once per database.
7
+ // convergent — on-disk memory documents. No journal exists over a user's
8
+ // stores (files appear, sync in, get hand-edited), so these
9
+ // CONVERGE instead: every run re-scans and rewrites whatever
10
+ // still matches the old shape.
11
+ // profile-manifest — `~/.crouter/profiles/<id>/profile.json`. Converges like
12
+ // the document lane, but each manifest folds under the same
13
+ // per-profile lock add/remove take, and a structure the
14
+ // migration cannot convert throws instead of being guessed.
15
+ //
16
+ // `crtr sys migrate` runs both on-disk lanes, profile manifests first.
11
17
  export {};
@@ -26,7 +26,7 @@ after(() => {
26
26
  else
27
27
  process.env['CRTR_NODE_ID'] = previous.nodeId;
28
28
  });
29
- test('optional submit fields remain optional through a strict provider tool schema', () => {
29
+ test('strict provider schema stays portable while original constraints validate submit', async () => {
30
30
  home = mkdtempSync(join(tmpdir(), 'crtr-structured-output-'));
31
31
  process.env['CRTR_HOME'] = home;
32
32
  process.env['CRTR_NODE_ID'] = 'structured-output-test';
@@ -44,8 +44,14 @@ test('optional submit fields remain optional through a strict provider tool sche
44
44
  },
45
45
  required: ['summary'],
46
46
  },
47
+ tags: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ minItems: 2,
51
+ maxItems: 5,
52
+ },
47
53
  },
48
- required: ['status', 'verification', 'details'],
54
+ required: ['status', 'verification', 'details', 'tags'],
49
55
  };
50
56
  writeOutputSchema('structured-output-test', 'oneoff', schema);
51
57
  let definition;
@@ -57,15 +63,26 @@ test('optional submit fields remain optional through a strict provider tool sche
57
63
  });
58
64
  assert.ok(definition, 'the pending request registers submit');
59
65
  const parameters = definition['parameters'];
60
- assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details']);
66
+ assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details', 'tags']);
61
67
  assert.deepEqual(parameters.properties.commit, { anyOf: [{ type: 'string' }, { type: 'null' }] });
62
68
  assert.deepEqual(parameters.properties.blocker, { anyOf: [{ type: 'string' }, { type: 'null' }] });
63
69
  assert.equal(parameters.additionalProperties, false);
64
70
  assert.equal(parameters.properties.details.additionalProperties, false);
65
- assert.deepEqual(schema.required, ['status', 'verification', 'details'], 'the stored request keeps its original optionality');
71
+ assert.equal(parameters.properties.tags['minItems'], undefined);
72
+ assert.equal(parameters.properties.tags['maxItems'], undefined);
73
+ assert.match(parameters.properties.tags['description'], /minItems: 2/);
74
+ assert.match(parameters.properties.tags['description'], /maxItems: 5/);
75
+ assert.deepEqual(schema.required, ['status', 'verification', 'details', 'tags'], 'the stored request keeps its original constraints');
66
76
  const [providerTool] = convertResponsesTools([definition], {
67
77
  strict: false,
68
78
  supportsStrictMode: true,
69
79
  });
70
80
  assert.equal(providerTool['strict'], true);
81
+ const execute = definition['execute'];
82
+ await assert.rejects(execute('call', {
83
+ status: 'done',
84
+ verification: 'verified',
85
+ details: { summary: 'ok' },
86
+ tags: ['only-one'],
87
+ }, new AbortController().signal), /must not have fewer than 2 items/);
71
88
  });
@@ -5,6 +5,7 @@
5
5
  // stop-guard will reprompt the node with an explicit error about the invalid file.
6
6
  import { mkdirSync, writeFileSync } from 'node:fs';
7
7
  import { dirname } from 'node:path';
8
+ import { validateToolArguments } from '@earendil-works/pi-ai';
8
9
  import { emitEvent } from '../core/events/emit.js';
9
10
  import { publishBrokerReport } from '../core/runtime/broker/daemon-ops.js';
10
11
  import { outputResultPath, readOutputRequest, removeOutputSchema, } from '../core/runtime/structured-output.js';
@@ -30,6 +31,85 @@ function retireTool(pi) {
30
31
  /* best-effort; the execute handler is inert when no schema file exists */
31
32
  }
32
33
  }
34
+ const STRICT_STRING_FORMATS = new Set([
35
+ 'date-time', 'time', 'date', 'duration', 'email', 'hostname', 'uri', 'ipv4', 'ipv6', 'uuid',
36
+ ]);
37
+ // Provider strict modes accept a structural subset; omitted constraints stay visible in descriptions.
38
+ function providerStrictSchema(schema) {
39
+ const transform = (value) => {
40
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
41
+ return value;
42
+ const source = { ...value };
43
+ const take = (key) => {
44
+ const child = source[key];
45
+ delete source[key];
46
+ return child;
47
+ };
48
+ const transformed = {};
49
+ const ref = take('$ref');
50
+ if (ref !== undefined)
51
+ return { $ref: ref };
52
+ const defs = take('$defs');
53
+ if (defs !== null && typeof defs === 'object' && !Array.isArray(defs)) {
54
+ transformed['$defs'] = Object.fromEntries(Object.entries(defs).map(([name, definition]) => [name, transform(definition)]));
55
+ }
56
+ const type = take('type');
57
+ const anyOf = take('anyOf');
58
+ const oneOf = take('oneOf');
59
+ const allOf = take('allOf');
60
+ if (Array.isArray(anyOf))
61
+ transformed['anyOf'] = anyOf.map(transform);
62
+ else if (Array.isArray(oneOf))
63
+ transformed['anyOf'] = oneOf.map(transform);
64
+ else if (Array.isArray(allOf))
65
+ transformed['allOf'] = allOf.map(transform);
66
+ else if (type !== undefined)
67
+ transformed['type'] = type;
68
+ else
69
+ return value;
70
+ const description = take('description');
71
+ if (description !== undefined)
72
+ transformed['description'] = description;
73
+ const title = take('title');
74
+ if (title !== undefined)
75
+ transformed['title'] = title;
76
+ if (type === 'object') {
77
+ const properties = take('properties');
78
+ transformed['properties'] = properties !== null && typeof properties === 'object' && !Array.isArray(properties)
79
+ ? Object.fromEntries(Object.entries(properties).map(([name, property]) => [name, transform(property)]))
80
+ : {};
81
+ take('additionalProperties');
82
+ transformed['additionalProperties'] = false;
83
+ const required = take('required');
84
+ if (required !== undefined)
85
+ transformed['required'] = required;
86
+ }
87
+ else if (type === 'string') {
88
+ const format = take('format');
89
+ if (typeof format === 'string' && STRICT_STRING_FORMATS.has(format))
90
+ transformed['format'] = format;
91
+ else if (format !== undefined)
92
+ source['format'] = format;
93
+ }
94
+ else if (type === 'array') {
95
+ const items = take('items');
96
+ if (items !== undefined)
97
+ transformed['items'] = transform(items);
98
+ const minItems = take('minItems');
99
+ if (minItems === 0 || minItems === 1)
100
+ transformed['minItems'] = minItems;
101
+ else if (minItems !== undefined)
102
+ source['minItems'] = minItems;
103
+ }
104
+ if (Object.keys(source).length > 0) {
105
+ const existing = typeof transformed['description'] === 'string' ? transformed['description'] : '';
106
+ const constraints = `{${Object.entries(source).map(([key, child]) => `${key}: ${JSON.stringify(child)}`).join(', ')}}`;
107
+ transformed['description'] = existing === '' ? constraints : `${existing}\n\n${constraints}`;
108
+ }
109
+ return transformed;
110
+ };
111
+ return transform(schema);
112
+ }
33
113
  function strictToolSchema(schema) {
34
114
  const transform = (value) => {
35
115
  if (Array.isArray(value))
@@ -61,7 +141,10 @@ function strictToolSchema(schema) {
61
141
  transformed['required'] = Object.keys(properties);
62
142
  return transformed;
63
143
  };
64
- return transform(schema);
144
+ return providerStrictSchema(transform(schema));
145
+ }
146
+ function validateResult(value, schema) {
147
+ return validateToolArguments({ name: 'submit', description: '', parameters: schema }, { type: 'toolCall', id: 'structured-output', name: 'submit', arguments: value });
65
148
  }
66
149
  function omitStrictNulls(value, schema) {
67
150
  if (value === null || typeof value !== 'object' || Array.isArray(value) || schema === null || typeof schema !== 'object' || Array.isArray(schema))
@@ -156,7 +239,7 @@ export function registerCanvasStructuredOutput(pi) {
156
239
  details: { submitted: false, reason: 'schema_replaced' },
157
240
  };
158
241
  }
159
- const result = omitStrictNulls(params, current.request.schema);
242
+ const result = validateResult(omitStrictNulls(params, current.request.schema), current.request.schema);
160
243
  const resultPath = outputResultPath(nodeId);
161
244
  const json = JSON.stringify(result, null, 2);
162
245
  mkdirSync(dirname(resultPath), { recursive: true });
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BindingId } from './core/keybindings/catalog.js';
2
+ import type { ProfileProject } from './api/dto/profiles.js';
2
3
  export type Scope = 'user' | 'project' | 'builtin';
3
4
  export declare const ExitCode: {
4
5
  readonly SUCCESS: 0;
@@ -94,6 +95,13 @@ export interface PluginManifest {
94
95
  * crtr owns parse/help/render/errors and direct-spawns the plugin's one
95
96
  * executable per leaf invocation. See `src/core/command-plugins/`. */
96
97
  commands?: string;
98
+ /** Plugin-root-relative path to a declarative hook manifest (`hooks.json`).
99
+ * Must be declared together with `hookExecutable`; hooks are trusted local
100
+ * code and are unavailable to endpoint archive plugins. */
101
+ hooks?: string;
102
+ /** Plugin-root-relative executable that dispatches declarations in `hooks`.
103
+ * Must be a regular file with a POSIX executable permission bit. */
104
+ hookExecutable?: string;
97
105
  /** Bare-callable executables this plugin ships, keyed by the bare command
98
106
  * name a node's bash sees. Each value is a PLUGIN-ROOT-relative path to a
99
107
  * regular executable file when the effective set resolves. These are trusted
@@ -223,6 +231,10 @@ export interface PageComponentRegistration {
223
231
  /** A display-only product slot contributes no page response. */
224
232
  display?: boolean;
225
233
  }
234
+ export interface HumanActionConfig {
235
+ argv: string[];
236
+ cwd: string;
237
+ }
226
238
  /** The normalized product component catalog used to validate page slots. */
227
239
  export type ProductPageComponents = readonly PageComponentRegistration[];
228
240
  export interface ScopeConfig {
@@ -336,6 +348,10 @@ export interface ScopeConfig {
336
348
  * the trusted local binary is materialized into the PATH-prepended shim dir
337
349
  * by `core/runtime/bin-contributions.ts`. */
338
350
  bin?: Record<string, string>;
351
+ /** Named completion commands. Project scopes nearest to a creator's cwd
352
+ * outrank farther project scopes, then user scope; plugins and profiles do
353
+ * not contribute. Paths resolve against the declaring scope's authoring root. */
354
+ humanActions: Record<string, HumanActionConfig>;
339
355
  }
340
356
  /** One remote canvas target: where to relay-attach and how to find its
341
357
  * bearer token. The token itself is NEVER stored here — only a ref into the
@@ -411,9 +427,9 @@ export interface InstalledMarketplace {
411
427
  }
412
428
  /** The on-disk manifest at `~/.crouter/profiles/<slug>-<id>/profile.json`
413
429
  * (spec §2.1). A profile is an agent identity defined at the user root: it
414
- * names N project directories and owns its own `memory/` store. `projects`
415
- * are absolute, real-path-resolved directories in manifest order — the
416
- * pointer set `findProjectScopeRoots` walks, after the node's own
430
+ * names N project directories and owns its own `memory/` store. Project
431
+ * `path`s are absolute, real-path-resolved directories in manifest order —
432
+ * the pointer set `findProjectScopeRoots` walks, after the node's own
417
433
  * cwd. `name` is the mutable display name (and the gateable `profile` subject
418
434
  * field); `<slug>-<id>` in the directory name is the stable id and
419
435
  * never changes on rename. See `src/core/profiles/manifest.ts` for every
@@ -422,10 +438,11 @@ export interface InstalledMarketplace {
422
438
  export interface ProfileManifest {
423
439
  schema_version: number;
424
440
  name: string;
425
- projects: string[];
441
+ projects: ProfileProject[];
426
442
  /** The directory nodes under this profile are pinned to (see
427
- * `profileHome`). Normalized on read to the first project when the manifest
428
- * carries none, so a profile written before homes existed still has one. */
443
+ * `profileHome`). Normalized on read to the first project's path when the
444
+ * manifest carries none, so a profile written before homes existed still
445
+ * has one. */
429
446
  home: string | null;
430
447
  /** ISO timestamp while the profile is inert; null (or absent on older manifests) means active. */
431
448
  paused_at: string | null;
package/dist/types.js CHANGED
@@ -103,6 +103,7 @@ export function defaultScopeConfig() {
103
103
  remoteCanvas: defaultRemoteCanvasConfig(),
104
104
  spawnEnv: { allow: [] },
105
105
  bin: {},
106
+ humanActions: {},
106
107
  };
107
108
  }
108
109
  /** No remote canvas targets are configured out of the box — every one is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.220",
3
+ "version": "0.3.222",
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.220",
3
+ "version": "0.3.222",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.220",
9
+ "version": "0.3.222",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -1,55 +0,0 @@
1
- ---
2
- kind: preference
3
- when-and-why-to-read: When any node boots, this preference should be read so the node can participate safely in the live graph without losing work, user decisions, or the ability to resume.
4
- rationale: >-
5
- The living-document paragraph ("Living documents") exists because agents default to appending — plans kept old+new versions side by side, answered Q&A sections stayed behind after the answer was folded in, findings docs grew contradicted layers (observed by Silas, 2026-07-08). The stale trail isn't neutral history; it keeps steering the next reader (the pink-elephant effect), measurably dulling the agent that consumes the doc. Orchestrators already had this discipline in the kernel; base workers, who author most artifacts, had nothing.
6
-
7
- "Say what actually happens" exists because an approval request called a root a person had created an "attended root" — an invented category with no referent in the product, which forced Silas to halt the decision and ask what the term meant (2026-07-28). Agents coin taxonomies to compress a distinction; the reader pays by decoding a word that names nothing real.
8
-
9
- "Waiting is a way to end a turn" lived in its own ungated all-node doc until 2026-07-28. Same gate, same audience, never independently readable — so the split bought no routing and cost a stub cross-reference in this file pointing at a section spliced a few hundred tokens later. Split it back out only if it ever needs a gate of its own.
10
-
11
- The Mermaid line exists because the viewer's inline diagram affordance is otherwise invisible to an agent working from ordinary Markdown defaults.
12
-
13
- An "Identity" section is deliberately absent, and the artifacts section carries no paths. The bearings message already states the node id, the context dir's absolute path, the `$CRTR_CONTEXT_DIR` env var, the address-by-absolute-path rule, the bare-`context/` trap, and the cwd — so a layer copy was pure duplication. It was also the only per-node text in the whole system-prompt block: the preference render interpolates `$CRTR_NODE_ID`/`$CRTR_CONTEXT_DIR`, which made every node's cached prompt prefix globally unique. Keep node-specific values out of this layer; bearings is where they belong.
14
-
15
- Yield lives here because every node yields regardless of mode; promotion does not, because only the mode layers own that boundary — 04-base-worker for when a base node should promote, the kernel for how an orchestrator uses promotion — so restating it here duplicated the base-worker text for an audience that includes nodes it does not apply to.
16
- lint-ignore: length
17
- surfaces:
18
- - on: boot
19
- at: content
20
- ---
21
-
22
- You are a **node** in a live agent graph (the crtr canvas). This section is your operating protocol — it is true for every node regardless of role.
23
-
24
- ## Artifacts
25
- An artifact you write to your context dir is shared by pointer: whatever carries it — a report, a reply, an ask — names its absolute path, never the full substance pasted in.
26
-
27
- ## Living documents
28
- Every doc you keep — artifact, plan, findings, memory — is a living statement of what is true *now*, never a log of how it got that way. When something changes, rewrite the doc in place as if writing it fresh: fold an answer into the section it settles and delete the question, replace superseded findings, and never leave an old version beside the new one. Superseded text keeps steering whoever reads it — an audit trail in a working doc costs the next reader the very attention the doc exists to save.
29
-
30
- ## Say what actually happens
31
- Everything you write — replies, reports, approval requests, artifacts, memory docs, comments — describes systems in concrete, existing product terms: the real command, the real event, the actual cause. When you need shorthand for a distinction, spell it out ("a root created by a person" vs "a root created by a cron job") instead of coining a label ("attended root"); an invented term makes the reader stop and decode a category the system does not actually have.
32
-
33
- ## When blocked, want feedback, or need the user
34
- Don't guess at a decision a person should make. Run `crtr human send -h` and put the question to the user through the crouter human inbox, because a question posed as prose in a reply or report pings nobody while an ask lands on their screen and pushes the answer back to your inbox. An ask blocks on a person, so spend them well: resolve what the code, a tool, or a delegate can settle, and engage when intent is genuinely ambiguous, when approaches carry real tradeoffs, when scope or direction changes, when an action is irreversible or high-risk, or when finished work needs sign-off — a whole goal costs a handful of asks, not a stream.
35
-
36
- ## When crtr itself misbehaves
37
- A `crtr` command that errors unexpectedly, hangs, churns, double-spawns, or contradicts its own `-h` is a harness bug — don't silently work around it. Run `crtr sys feedback` to report it (`-h` for how), then continue.
38
-
39
- ## Yield for a fresh window
40
- When your context is filling but the mandate isn't done, yield: you revive fresh as the same node with the same mandate, carrying a note to your future self.
41
-
42
- crtr node yield # `crtr node yield -h` — refresh into a clean window, carrying a note forward
43
-
44
- Never yield carrying an unasked question: put anything you're still wondering for the user through `crtr human send` BEFORE you yield — an in-flight ask survives the refresh, and its answer wakes your fresh window like any child's report.
45
-
46
- ## Mermaid diagrams
47
- When visual structure would land faster than prose, use a Mermaid fence; the user's terminal viewer renders it inline.
48
-
49
- ## Waiting is a way to end a turn
50
-
51
- When your goal is sound but your next step is blocked on something that has not happened yet — a child's report, the user, a CI run, tomorrow morning — you are **waiting**. Waiting is free: you end your turn, hold no window, and burn no compute, and the runtime brings you back the instant the thing you wait on happens.
52
-
53
- - **Never busy-wait.** Do not hold your window open to re-poll a URL or watch a clock. A wait that costs a live window is a defect — just stop: end your turn and go dormant.
54
- - **For waits the runtime already knows — a child's report or the reply to your own human page — just stop.** Go dormant; the runtime wakes you when it lands. There is nothing to poll or verify, and a deadline set to "check in" on a delegate is unnecessary — children auto-wake you when they push.
55
- - **Schedule a wake yourself only when nothing can push to you** — recurring or scheduled standing work, or polling an external the spine can't deliver (CI, a deploy, a clock). Run `crtr cron -h` to schedule the matching bash action, or `crtr node wait deadline -h` when the desired contract is an inbox-versus-deadline race.