@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
@@ -1,24 +1,32 @@
1
- // `crtr sys migrate` — the convergent-lane state-migration runner: converge
2
- // every writable memory store's documents to the current document format.
3
- // (Journaled canvas-db migrations run in the db's own ordered chain when the
4
- // daemon opens it; this leaf owns only the on-disk document stores.)
1
+ // `crtr sys migrate` — the on-disk state-migration runner: converge every
2
+ // stored profile manifest to the current manifest format, then every writable
3
+ // memory store's documents to the current document format. (Journaled
4
+ // canvas-db migrations run in the db's own ordered chain when the daemon opens
5
+ // it; this leaf owns only the on-disk state.)
6
+ //
7
+ // Manifests come FIRST and are read as raw bytes under the profile lock: the
8
+ // current reader treats a pre-migration manifest exactly like corrupt JSON, so
9
+ // `listProfiles()` sees nothing until this lane has run. Store discovery below
10
+ // therefore works from the lane's post-migration snapshots — which are equally
11
+ // coherent under `--dry-run`, where the same fold ran and nothing was written.
5
12
  import { dirname, join } from 'node:path';
6
13
  import { defineLeaf } from '../../core/command.js';
7
14
  import { pathExists, realpathOrSelf } from '../../core/fs-utils.js';
8
15
  import { createMemoryDocSnapshot } from '../../core/memory-resolver.js';
9
16
  import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/resolver.js';
10
17
  import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
11
- import { listProfiles, profileMemoryDir } from '../../core/profiles/manifest.js';
18
+ import { profileMemoryDir, profilesRoot, withProfileManifestLock } from '../../core/profiles/manifest.js';
12
19
  import { descendantStoreRootsExhaustive } from '../../core/nested-stores.js';
13
20
  import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
14
21
  import { runConvergentMigrations } from '../../migrations/convergent.js';
22
+ import { runProfileManifestMigrations } from '../../migrations/profile-manifests.js';
15
23
  function targetKey(target) {
16
24
  return `${realpathOrSelf(target.cwd)}\u0000${target.profileId ?? ''}\u0000${target.nodeId ?? ''}`;
17
25
  }
18
26
  /** Every writable memory store — the lint corpus minus builtin (package-owned,
19
27
  * regenerated on every install, so migrating it would fight the package).
20
28
  * Physical stores dedupe once while retaining every owning target corpus. */
21
- function migratableStores() {
29
+ function migratableStores(profiles) {
22
30
  const stores = new Map();
23
31
  const add = (dir, target) => {
24
32
  if (dir === null || dir === '' || !pathExists(dir))
@@ -49,18 +57,18 @@ function migratableStores() {
49
57
  };
50
58
  const selectedProfileId = process.env['CRTR_PROFILE_ID'] || null;
51
59
  addProjectRoots(projectScopeRoots(process.cwd(), null), selectedProfileId);
52
- const profiles = listProfiles();
53
60
  const profileTargets = [];
54
61
  for (const { profileId, manifest } of profiles) {
62
+ const home = manifest.home !== null && manifest.home !== '' ? manifest.home : null;
55
63
  const profileTarget = {
56
- cwd: manifest.home ?? manifest.projects[0] ?? process.cwd(),
64
+ cwd: home ?? manifest.projects[0]?.path ?? process.cwd(),
57
65
  profileId,
58
66
  nodeId: null,
59
67
  };
60
68
  profileTargets.push(profileTarget);
61
69
  add(profileMemoryDir(profileId), profileTarget);
62
70
  const roots = [
63
- ...new Set(manifest.projects.flatMap((project) => [join(project, '.crouter'), ...projectScopeRoots(project, null)])),
71
+ ...new Set(manifest.projects.flatMap((project) => [join(project.path, '.crouter'), ...projectScopeRoots(project.path, null)])),
64
72
  ];
65
73
  addProjectRoots(roots, profileId);
66
74
  }
@@ -82,23 +90,25 @@ function migratableStores() {
82
90
  }
83
91
  export const sysMigrateLeaf = defineLeaf({
84
92
  name: 'migrate',
85
- description: 'converge every writable memory store to the current document format',
86
- whenToUse: 'after a crtr update whose release notes name a document-format migration, or when old-format docs resurface (a restore, a sync, a hand-authored file). Convergent and idempotent: re-running over a current corpus changes nothing, so use --dry-run first only when you want to review the rewrite.',
93
+ description: 'converge every profile manifest and writable memory store to the current on-disk format',
94
+ whenToUse: 'after a crtr update whose release notes name a state migration, when profiles stop resolving on a home that predates the update, or when old-format docs resurface (a restore, a sync, a hand-authored file). Convergent and idempotent: re-running over current state changes nothing, so use --dry-run first only when you want to review the rewrite.',
87
95
  help: {
88
96
  name: 'sys migrate',
89
- summary: 'run the registered document-format migrations over every writable memory store',
97
+ summary: 'run the registered state migrations over every profile manifest and writable memory store',
90
98
  params: [
91
99
  { kind: 'flag', name: 'dry-run', type: 'bool', required: false, constraint: 'Plan and report every rewrite without writing any file.' },
92
100
  ],
93
101
  output: [
102
+ { name: 'profiles', type: 'number', required: true, constraint: 'Profile manifests scanned.' },
94
103
  { name: 'stores', type: 'number', required: true, constraint: 'Memory stores scanned.' },
95
- { name: 'changed', type: 'number', required: true, constraint: 'Files rewritten (or, under --dry-run, that would be).' },
96
- { name: 'applied', type: 'object[]', required: true, constraint: 'One row per migration that changed files in a store: {store, migration, files}. Empty when the corpus is already current.' },
97
- { name: 'skipped', type: 'object[]', required: true, constraint: 'Docs left untouched because their frontmatter is not valid YAML: {store, relPath, error}. Fix the doc, then re-run.' },
104
+ { name: 'changed', type: 'number', required: true, constraint: 'Files rewritten (or, under --dry-run, that would be) — manifests and documents together.' },
105
+ { name: 'applied', type: 'object[]', required: true, constraint: 'One row per migration that changed files: {store, lane, migration, files}, lane one of profile-manifest, convergent. Empty when the state is already current.' },
106
+ { name: 'skipped', type: 'object[]', required: true, constraint: 'Files left untouched because they could not be parsed — a doc whose frontmatter is not valid YAML, or a manifest that is not valid JSON: {store, relPath, error}. Fix the file, then re-run.' },
98
107
  { name: 'dryRun', type: 'boolean', required: true, constraint: 'True when nothing was written (preview only).' },
99
108
  ],
100
109
  outputKind: 'object',
101
110
  effects: [
111
+ 'Rewrites old-format `profile.json` manifests in place (atomic, under the same per-profile lock `profile project add/remove` take). A manifest whose structure cannot be converted fails the whole run — no project setting is ever guessed.',
102
112
  'Rewrites old-format memory documents in place (atomic per-file writes) across the writable stores: every profile and every profile-owned project (plus nested descendant stores and installed plugin stores), the current project, user, and this node\'s store. Builtin docs are package-owned and never touched.',
103
113
  'Under --dry-run: read-only, writes nothing.',
104
114
  ],
@@ -107,7 +117,15 @@ export const sysMigrateLeaf = defineLeaf({
107
117
  const dryRun = input.dryRun === true;
108
118
  const applied = [];
109
119
  const skipped = [];
110
- const stores = migratableStores();
120
+ const profilesDir = profilesRoot();
121
+ const profileRun = runProfileManifestMigrations(profilesDir, { dryRun, withLock: withProfileManifestLock });
122
+ for (const row of profileRun.applied) {
123
+ applied.push({ store: profilesDir, lane: 'profile-manifest', migration: row.migration, files: [row.path] });
124
+ }
125
+ for (const row of profileRun.skipped) {
126
+ skipped.push({ store: profilesDir, relPath: `${row.profileId}/profile.json`, error: row.error });
127
+ }
128
+ const stores = migratableStores(profileRun.snapshots);
111
129
  const snapshots = new Map();
112
130
  for (const store of stores) {
113
131
  const resolvers = store.targets.map((target) => (name) => {
@@ -137,21 +155,22 @@ export const sysMigrateLeaf = defineLeaf({
137
155
  },
138
156
  });
139
157
  for (const row of result.applied)
140
- applied.push({ store: store.dir, migration: row.migration, files: row.files });
158
+ applied.push({ store: store.dir, lane: 'convergent', migration: row.migration, files: row.files });
141
159
  for (const s of result.skipped)
142
160
  skipped.push({ store: store.dir, relPath: s.relPath, error: s.error });
143
161
  }
144
162
  const changed = applied.reduce((n, row) => n + row.files.length, 0);
145
163
  return {
164
+ profiles: profileRun.profiles,
146
165
  stores: stores.length,
147
166
  changed,
148
167
  applied,
149
168
  skipped,
150
169
  dryRun,
151
170
  follow_up: skipped.length > 0
152
- ? 'Some docs have invalid YAML frontmatter and were left untouched — fix each named file, then re-run `crtr sys migrate`.'
171
+ ? 'Some files could not be parsed and were left untouched — fix each named file, then re-run `crtr sys migrate`.'
153
172
  : changed === 0
154
- ? 'Corpus already current — nothing to rewrite.'
173
+ ? 'State already current — nothing to rewrite.'
155
174
  : dryRun
156
175
  ? 'Preview only — re-run without --dry-run to write these changes.'
157
176
  : undefined,
@@ -535,7 +535,7 @@ export async function executeSetupSubmission(submission) {
535
535
  const registered = ensureMarketplaceRegistered(marketplace);
536
536
  if (registered !== null)
537
537
  installedPackages.push(registered);
538
- installFromMarketplace({ marketplace: marketplace, plugin: plugin }, 'user');
538
+ await installFromMarketplace({ marketplace: marketplace, plugin: plugin }, 'user');
539
539
  installedPackages.push(entry.name);
540
540
  }
541
541
  catch (e) {
@@ -36,7 +36,7 @@ function projectDocRoots(sourceArg) {
36
36
  try {
37
37
  const { manifest } = loadProfileManifest(profileId);
38
38
  for (const p of manifest.projects) {
39
- const r = resolve(p);
39
+ const r = resolve(p.path);
40
40
  if (!roots.includes(r))
41
41
  roots.push(r);
42
42
  }
@@ -41,10 +41,13 @@ function dbReach(root) {
41
41
  }
42
42
  return null;
43
43
  }
44
- test('every shipped broker extension is transitively canvas.db-free', () => {
45
- for (const name of CANVAS_EXTENSION_NAMES) {
46
- const root = join(SOURCE_ROOT, 'pi-extensions', `${name}.ts`);
44
+ test('every shipped broker extension and the prospective preflight are transitively canvas.db-free', () => {
45
+ const roots = [
46
+ ...CANVAS_EXTENSION_NAMES.map((name) => join(SOURCE_ROOT, 'pi-extensions', `${name}.ts`)),
47
+ join(SOURCE_ROOT, 'core/runtime/prospective-inventory-cli.ts'),
48
+ ];
49
+ for (const root of roots) {
47
50
  const reach = dbReach(root);
48
- assert.equal(reach, null, `${name} reaches canvas.db through:\n${reach?.join('\n')}`);
51
+ assert.equal(reach, null, `${root} reaches canvas.db through:\n${reach?.join('\n')}`);
49
52
  }
50
53
  });
@@ -271,7 +271,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
271
271
  homes.push(userHome);
272
272
  process.env.HOME = userHome;
273
273
  try {
274
- const profile = createProfile('paused watcher', ['/tmp']);
274
+ const profile = createProfile('paused watcher', [{ path: '/tmp', memory: 'content' }]);
275
275
  pauseProfile(profile.profileId);
276
276
  process.env['CRTR_PROFILE_ID'] = profile.profileId;
277
277
  const pi = makeFakePi();
@@ -5,4 +5,28 @@ export default function (pi) {
5
5
  /* never invoked — isLeadingEngineCommand only checks registration */
6
6
  },
7
7
  });
8
+ pi.registerCommand('gatewaycmd', {
9
+ description: 'Gateway-advertised custom-message fixture',
10
+ gateway: true,
11
+ handler: async () => {
12
+ pi.sendMessage({
13
+ customType: 'fixture-output',
14
+ content: 'expanded agent input',
15
+ display: true,
16
+ details: { fixture: true },
17
+ });
18
+ },
19
+ });
20
+ pi.registerCommand('gatewaynext', {
21
+ description: 'Queued Gateway custom-message fixture',
22
+ gateway: true,
23
+ handler: async () => {
24
+ pi.sendMessage({
25
+ customType: 'fixture-next-output',
26
+ content: null,
27
+ display: true,
28
+ details: 'extension-details',
29
+ }, { deliverAs: 'nextTurn' });
30
+ },
31
+ });
8
32
  }
@@ -107,6 +107,28 @@ interface SessionTreeNode {
107
107
  entry: TreeEntry;
108
108
  children: SessionTreeNode[];
109
109
  }
110
+ /** The real SessionManager surface the broker reads and patches. A method
111
+ * missing here is a broken fixture, not a degraded one — the broker throws
112
+ * during session build and never boots. */
113
+ interface FakeSessionManager {
114
+ buildSessionContext: () => {
115
+ messages: AgentMessage[];
116
+ thinkingLevel: string;
117
+ model: null;
118
+ };
119
+ getLeafId: () => string | null;
120
+ getEntry: (id: string) => TreeEntry | undefined;
121
+ getTree: () => SessionTreeNode[];
122
+ getUserMessagesForForking: () => Array<{
123
+ entryId: string;
124
+ text: string;
125
+ timestamp?: string;
126
+ }>;
127
+ getSessionId: () => string;
128
+ getSessionFile: () => string;
129
+ getSessionDir: () => string;
130
+ appendCustomMessageEntry: (customType: string, content: unknown, display: boolean, details?: unknown) => string;
131
+ }
110
132
  declare class FakeSession {
111
133
  private readonly sm;
112
134
  private readonly loader;
@@ -142,24 +164,8 @@ declare class FakeSession {
142
164
  private readonly modelRt;
143
165
  constructor(sm: SessionManager, loader: ResourceLoader | undefined, model?: FakeModel, modelRuntime?: FakeModelRuntime);
144
166
  get messages(): AgentMessage[];
145
- get sessionManager(): {
146
- buildSessionContext: () => {
147
- messages: AgentMessage[];
148
- thinkingLevel: string;
149
- model: null;
150
- };
151
- getLeafId: () => string | null;
152
- getEntry: (id: string) => TreeEntry | undefined;
153
- getTree: () => SessionTreeNode[];
154
- getUserMessagesForForking: () => Array<{
155
- entryId: string;
156
- text: string;
157
- timestamp?: string;
158
- }>;
159
- getSessionId: () => string;
160
- getSessionFile: () => string;
161
- getSessionDir: () => string;
162
- };
167
+ private smFacade;
168
+ get sessionManager(): FakeSessionManager;
163
169
  get sessionId(): string;
164
170
  get sessionFile(): string | undefined;
165
171
  get model(): FakeModel | undefined;
@@ -341,8 +341,13 @@ class FakeSession {
341
341
  // truth, since `session.messages` can be sliced by pi's retry/overflow/
342
342
  // compaction recovery. Mirror that surface so the fake's snapshot still serves
343
343
  // the accrued history (G2/G3). The fake's persisted analog is the active branch.
344
+ // Built once and cached: the real SessionManager is a single object the
345
+ // broker's advertised-command contract monkey-patches
346
+ // (appendCustomMessageEntry), so a fresh literal per access would silently
347
+ // discard that patch.
348
+ smFacade;
344
349
  get sessionManager() {
345
- return {
350
+ this.smFacade ??= {
346
351
  buildSessionContext: () => this.tree.buildSessionContext(),
347
352
  getLeafId: () => this.tree.getLeafId(),
348
353
  getEntry: (id) => this.tree.getEntry(id),
@@ -351,7 +356,9 @@ class FakeSession {
351
356
  getSessionId: () => this.sm.getSessionId(),
352
357
  getSessionFile: () => this.sm.getSessionFile(),
353
358
  getSessionDir: () => this.sm.getSessionDir(),
359
+ appendCustomMessageEntry: (customType, content, display, details) => this.tree.append({ role: 'custom', customType, content, display, details }).id,
354
360
  };
361
+ return this.smFacade;
355
362
  }
356
363
  get sessionId() {
357
364
  return this.sm.getSessionId();
@@ -0,0 +1,71 @@
1
+ import { existsSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
4
+ import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
5
+ import { gatewayCommandRows, gatewayMemoryRows } from '../../runtime/chat-inventory-rows.js';
6
+ import { captureChatCapabilities, registrationChatSurface } from '../../runtime/command-surface.js';
7
+ import { runProspectiveInventory } from '../../runtime/prospective-inventory.js';
8
+ async function main() {
9
+ const extensionPath = process.argv[2];
10
+ const invocations = JSON.parse(process.argv[3] ?? '[]');
11
+ if (extensionPath === undefined)
12
+ throw new Error('extension path is required');
13
+ const cwd = process.cwd();
14
+ const home = process.env['CRTR_HOME'];
15
+ if (home === undefined || home === '')
16
+ throw new Error('CRTR_HOME is required');
17
+ const stateBeforeProspective = existsSync(home);
18
+ const prospective = await runProspectiveInventory({ cwd, profileId: null, extensions: [extensionPath] });
19
+ const stateAfterProspective = existsSync(home);
20
+ const loader = new DefaultResourceLoader({
21
+ cwd,
22
+ agentDir: join(cwd, '.pi'),
23
+ additionalExtensionPaths: [extensionPath],
24
+ noContextFiles: true,
25
+ });
26
+ await loader.reload();
27
+ const loaded = loader.getExtensions();
28
+ if (loaded.errors.length > 0)
29
+ throw new Error(JSON.stringify(loaded.errors));
30
+ const sent = [];
31
+ let activeName = '';
32
+ loaded.runtime.sendMessage = (message, options) => sent.push({ name: activeName, message, options });
33
+ const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, cwd, undefined, undefined);
34
+ const registered = runner.getRegisteredCommands();
35
+ captureChatCapabilities({ commands: registered, templates: [] });
36
+ const commands = gatewayCommandRows(registered.map((command) => {
37
+ const expansion = command.expansion;
38
+ return {
39
+ name: command.invocationName,
40
+ description: command.description ?? '',
41
+ source: 'command',
42
+ ...(expansion === undefined ? {} : { expansion }),
43
+ ...registrationChatSurface(command),
44
+ };
45
+ }));
46
+ const memoryRefs = gatewayMemoryRows(buildRefInventory().refs);
47
+ const notifications = [];
48
+ for (const invocation of invocations) {
49
+ const command = runner.getCommand(invocation.name);
50
+ if (command === undefined)
51
+ throw new Error(`command not registered: ${invocation.name}`);
52
+ activeName = invocation.name;
53
+ await command.handler(invocation.args, {
54
+ cwd,
55
+ ui: { notify: (...args) => notifications.push(args) },
56
+ });
57
+ }
58
+ writeFileSync(3, JSON.stringify({
59
+ prospective,
60
+ stateBeforeProspective,
61
+ stateAfterProspective,
62
+ commands,
63
+ memoryRefs,
64
+ sent,
65
+ notifications,
66
+ }));
67
+ }
68
+ main().catch((error) => {
69
+ process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
70
+ process.exitCode = 1;
71
+ });
@@ -0,0 +1,140 @@
1
+ import { after, afterEach, beforeEach, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { closeDb } from '../canvas/db.js';
7
+ import { claimActionDelivery, enqueueActionDelivery, readActionDelivery, } from '../canvas/human-deliveries.js';
8
+ import { actionDeliveryBackoffMs, actionDeliverySettlementForOutcome, deliverAction, } from '../../daemon/human/deliver-action.js';
9
+ let home;
10
+ const priorHome = process.env['CRTR_HOME'];
11
+ beforeEach(() => {
12
+ closeDb();
13
+ home = mkdtempSync(join(tmpdir(), 'crtr-human-action-delivery-'));
14
+ process.env['CRTR_HOME'] = home;
15
+ });
16
+ afterEach(() => {
17
+ closeDb();
18
+ rmSync(home, { recursive: true, force: true });
19
+ });
20
+ after(() => {
21
+ closeDb();
22
+ rmSync(home, { recursive: true, force: true });
23
+ if (priorHome === undefined)
24
+ delete process.env['CRTR_HOME'];
25
+ else
26
+ process.env['CRTR_HOME'] = priorHome;
27
+ });
28
+ test('action delivery retries follow the durable backoff and exit boundary', () => {
29
+ assert.equal(actionDeliveryBackoffMs(1), 5_000);
30
+ assert.equal(actionDeliveryBackoffMs(2), 10_000);
31
+ assert.equal(actionDeliveryBackoffMs(3), 20_000);
32
+ assert.equal(actionDeliveryBackoffMs(11), 3_600_000);
33
+ assert.equal(actionDeliveryBackoffMs(100), 3_600_000);
34
+ const now = 1_000_000;
35
+ const outcome = (exitCode, signal = null) => actionDeliverySettlementForOutcome({
36
+ exitCode,
37
+ signal,
38
+ timedOut: false,
39
+ stderr: 'diagnostic',
40
+ }, 2, now);
41
+ assert.deepEqual(outcome(0), { state: 'accepted' });
42
+ assert.deepEqual(outcome(77), {
43
+ state: 'pending',
44
+ nextAttemptAt: now + 10_000,
45
+ failure: { kind: 'exit', exitCode: 77, stderr: 'diagnostic' },
46
+ });
47
+ assert.deepEqual(outcome(78), {
48
+ state: 'permanent_failed',
49
+ failure: { kind: 'exit', exitCode: 78, stderr: 'diagnostic' },
50
+ });
51
+ assert.equal(outcome(79).state, 'pending');
52
+ assert.equal(outcome(null, 'SIGTERM').state, 'pending');
53
+ assert.equal(actionDeliverySettlementForOutcome({
54
+ exitCode: null,
55
+ signal: null,
56
+ timedOut: true,
57
+ stderr: '',
58
+ }, 1, now).state, 'pending');
59
+ // The kill escalation's signal is the only way to tell the graceful term from the SIGKILL.
60
+ assert.deepEqual(actionDeliverySettlementForOutcome({
61
+ exitCode: null,
62
+ signal: 'SIGKILL',
63
+ timedOut: true,
64
+ stderr: 'stalled',
65
+ }, 1, now), {
66
+ state: 'pending',
67
+ nextAttemptAt: now + 5_000,
68
+ failure: { kind: 'timeout', signal: 'SIGKILL', stderr: 'stalled' },
69
+ });
70
+ assert.equal(actionDeliverySettlementForOutcome({
71
+ exitCode: null,
72
+ signal: null,
73
+ timedOut: false,
74
+ spawnError: new Error('missing executable'),
75
+ stderr: '',
76
+ }, 1, now).state, 'pending');
77
+ });
78
+ test('the delivery child gets the consent boundary env, never crtrd\'s own', async () => {
79
+ const priorUserHome = process.env['HOME'];
80
+ const project = join(home, 'project');
81
+ mkdirSync(join(project, '.crouter'), { recursive: true });
82
+ writeFileSync(join(project, '.crouter', 'config.json'), JSON.stringify({ spawnEnv: { allow: ['NL_*'] } }));
83
+ const envDump = join(home, 'child-env.txt');
84
+ const stdinDump = join(home, 'child-stdin.json');
85
+ const script = join(home, 'action.sh');
86
+ writeFileSync(script, `#!/bin/sh\nenv > ${envDump}\ncat > ${stdinDump}\n`);
87
+ chmodSync(script, 0o755);
88
+ process.env['HOME'] = home; // no real user-scope config contributes to the boundary
89
+ process.env['NL_CORE_URL'] = 'http://core.test';
90
+ process.env['CRTRD_ONLY_SECRET'] = 'must-not-cross';
91
+ const requestId = 'b'.repeat(64);
92
+ try {
93
+ enqueueActionDelivery({
94
+ requestId,
95
+ argv: [script],
96
+ cwd: project,
97
+ completion: { schema: 'crtr.human-completion/v1', requestId, delivery: { attempt: 0 } },
98
+ });
99
+ const claimed = claimActionDelivery(requestId, Date.now(), 'claimant');
100
+ assert.ok(claimed !== null);
101
+ await deliverAction(claimed, 'claimant');
102
+ }
103
+ finally {
104
+ delete process.env['NL_CORE_URL'];
105
+ delete process.env['CRTRD_ONLY_SECRET'];
106
+ if (priorUserHome === undefined)
107
+ delete process.env['HOME'];
108
+ else
109
+ process.env['HOME'] = priorUserHome;
110
+ }
111
+ assert.equal(readActionDelivery(requestId)?.state, 'accepted');
112
+ const childEnv = new Map(readFileSync(envDump, 'utf8').split('\n').flatMap((line) => {
113
+ const split = line.indexOf('=');
114
+ return split === -1 ? [] : [[line.slice(0, split), line.slice(split + 1)]];
115
+ }));
116
+ assert.equal(childEnv.get('CRTRD_ONLY_SECRET'), undefined, 'crtrd host state must not cross without consent');
117
+ assert.equal(childEnv.get('NL_CORE_URL'), 'http://core.test', 'the target cwd\'s spawnEnv.allow carries its own config across');
118
+ assert.equal(childEnv.get('CRTR_HOME'), home, 'an action that calls crtr must reach the daemon that delivered to it');
119
+ assert.equal(JSON.parse(readFileSync(stdinDump, 'utf8')).delivery.attempt, 1);
120
+ });
121
+ test('the guarded claim admits only one contender', () => {
122
+ const requestId = 'a'.repeat(64);
123
+ enqueueActionDelivery({
124
+ requestId,
125
+ argv: ['/bin/true'],
126
+ cwd: home,
127
+ completion: { schema: 'crtr.human-completion/v1', delivery: { attempt: 0 } },
128
+ });
129
+ const now = Date.now();
130
+ const first = claimActionDelivery(requestId, now, 'claimant-a');
131
+ const second = claimActionDelivery(requestId, now, 'claimant-b');
132
+ assert.equal(first?.state, 'running');
133
+ assert.equal(first?.attempt, 1);
134
+ assert.equal(second, null);
135
+ assert.deepEqual(readActionDelivery(requestId) && {
136
+ state: readActionDelivery(requestId).state,
137
+ attempt: readActionDelivery(requestId).attempt,
138
+ claimOwner: readActionDelivery(requestId).claimOwner,
139
+ }, { state: 'running', attempt: 1, claimOwner: 'claimant-a' });
140
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,116 @@
1
+ // Run with: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/human-actions.test.ts
2
+ //
3
+ // This pins the silent execution boundary: a request must select the nearest
4
+ // project declaration before farther projects or user scope, and its relative
5
+ // executable/cwd must resolve from that declaration's authoring root.
6
+ import { after, afterEach, before, beforeEach, test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { resolveHumanAction } from '../human-actions.js';
12
+ import { createProfile } from '../profiles/manifest.js';
13
+ import { resetScopeCache } from '../scope.js';
14
+ let home;
15
+ let cwd;
16
+ let previousHome;
17
+ let previousProfile;
18
+ let previousCwd;
19
+ before(() => {
20
+ previousHome = process.env['HOME'];
21
+ previousProfile = process.env['CRTR_PROFILE_ID'];
22
+ previousCwd = process.cwd();
23
+ });
24
+ beforeEach(() => {
25
+ home = mkdtempSync(join(tmpdir(), 'crtr-human-actions-home-'));
26
+ cwd = join(home, 'work', 'outer', 'nested', 'src');
27
+ mkdirSync(cwd, { recursive: true });
28
+ process.env['HOME'] = home;
29
+ delete process.env['CRTR_PROFILE_ID'];
30
+ process.chdir(cwd);
31
+ resetScopeCache();
32
+ });
33
+ afterEach(() => {
34
+ process.chdir(previousCwd);
35
+ rmSync(home, { recursive: true, force: true });
36
+ resetScopeCache();
37
+ });
38
+ after(() => {
39
+ process.chdir(previousCwd);
40
+ if (previousHome === undefined)
41
+ delete process.env['HOME'];
42
+ else
43
+ process.env['HOME'] = previousHome;
44
+ if (previousProfile === undefined)
45
+ delete process.env['CRTR_PROFILE_ID'];
46
+ else
47
+ process.env['CRTR_PROFILE_ID'] = previousProfile;
48
+ resetScopeCache();
49
+ });
50
+ function writeAction(root, relativeExecutable, relativeCwd) {
51
+ const executable = join(root, relativeExecutable);
52
+ mkdirSync(join(executable, '..'), { recursive: true });
53
+ writeFileSync(executable, '');
54
+ chmodSync(executable, 0o755);
55
+ mkdirSync(join(root, relativeCwd), { recursive: true });
56
+ }
57
+ test('nearest project declaration wins and resolves paths from its authoring root', () => {
58
+ const userRoot = join(home, '.crouter');
59
+ writeAction(userRoot, 'user-bin', 'user-cwd');
60
+ mkdirSync(userRoot, { recursive: true });
61
+ writeFileSync(join(userRoot, 'config.json'), JSON.stringify({
62
+ humanActions: { release: { argv: ['user-bin'], cwd: 'user-cwd' }, 'user-only': { argv: ['user-bin'], cwd: 'user-cwd' } },
63
+ }));
64
+ const outer = join(home, 'work', 'outer');
65
+ writeAction(outer, 'outer-bin', 'outer-cwd');
66
+ mkdirSync(join(outer, '.crouter'), { recursive: true });
67
+ writeFileSync(join(outer, '.crouter', 'config.json'), JSON.stringify({
68
+ humanActions: { release: { argv: ['outer-bin'], cwd: 'outer-cwd' } },
69
+ }));
70
+ const nearest = join(outer, 'nested');
71
+ writeAction(nearest, 'scripts/finish-release', 'work');
72
+ mkdirSync(join(nearest, '.crouter'), { recursive: true });
73
+ writeFileSync(join(nearest, '.crouter', 'config.json'), JSON.stringify({
74
+ humanActions: { release: { argv: ['scripts/finish-release', '--nearest'], cwd: 'work' } },
75
+ }));
76
+ resetScopeCache();
77
+ assert.deepEqual(resolveHumanAction('release', cwd), {
78
+ name: 'release',
79
+ configOrigin: join(nearest, '.crouter', 'config.json'),
80
+ argv: [join(nearest, 'scripts/finish-release'), '--nearest'],
81
+ cwd: join(nearest, 'work'),
82
+ });
83
+ assert.deepEqual(resolveHumanAction('user-only', cwd), {
84
+ name: 'user-only',
85
+ configOrigin: join(userRoot, 'config.json'),
86
+ argv: [join(userRoot, 'user-bin')],
87
+ cwd: join(userRoot, 'user-cwd'),
88
+ });
89
+ const profileProject = join(home, 'profile-project');
90
+ writeAction(profileProject, 'profile-bin', 'profile-cwd');
91
+ mkdirSync(join(profileProject, '.crouter'), { recursive: true });
92
+ writeFileSync(join(profileProject, '.crouter', 'config.json'), JSON.stringify({
93
+ humanActions: { 'profile-only': { argv: ['profile-bin'], cwd: 'profile-cwd' } },
94
+ }));
95
+ process.env['CRTR_PROFILE_ID'] = createProfile('human-actions-test', [{ path: profileProject, memory: 'content' }]).profileId;
96
+ resetScopeCache();
97
+ assert.deepEqual(resolveHumanAction('profile-only', cwd), { kind: 'unknown_action', name: 'profile-only' });
98
+ });
99
+ test('a malformed nearer humanActions block does not shadow a farther declaration', () => {
100
+ const outer = join(home, 'work', 'outer');
101
+ writeAction(outer, 'outer-bin', 'outer-cwd');
102
+ mkdirSync(join(outer, '.crouter'), { recursive: true });
103
+ writeFileSync(join(outer, '.crouter', 'config.json'), JSON.stringify({
104
+ humanActions: { release: { argv: ['outer-bin'], cwd: 'outer-cwd' } },
105
+ }));
106
+ const nearest = join(outer, 'nested');
107
+ mkdirSync(join(nearest, '.crouter'), { recursive: true });
108
+ writeFileSync(join(nearest, '.crouter', 'config.json'), JSON.stringify({ humanActions: [] }));
109
+ resetScopeCache();
110
+ assert.deepEqual(resolveHumanAction('release', cwd), {
111
+ name: 'release',
112
+ configOrigin: join(outer, '.crouter', 'config.json'),
113
+ argv: [join(outer, 'outer-bin')],
114
+ cwd: join(outer, 'outer-cwd'),
115
+ });
116
+ });