@north-light/crouter 0.3.221 → 0.3.223

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 (169) hide show
  1. package/dist/api/client.d.ts +21 -1
  2. package/dist/api/client.js +34 -0
  3. package/dist/api/dto/chat-inventory.d.ts +13 -0
  4. package/dist/api/dto/human-requests.d.ts +88 -0
  5. package/dist/api/dto/human-requests.js +4 -0
  6. package/dist/api/dto/human.d.ts +3 -0
  7. package/dist/api/dto/reviews.d.ts +2 -0
  8. package/dist/api/index.d.ts +1 -0
  9. package/dist/api/index.js +1 -0
  10. package/dist/api/routes.d.ts +7 -0
  11. package/dist/api/routes.js +10 -0
  12. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  13. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  14. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  15. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  16. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  17. package/dist/builtin-memory/04-base-worker.md +4 -8
  18. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  19. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  20. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  21. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  22. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  23. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  24. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  25. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  26. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  27. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  28. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  29. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  30. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  31. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  32. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  33. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  35. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  36. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  38. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  39. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  40. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  41. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  42. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  43. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  44. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  45. package/dist/builtin-memory/design/guide.md +35 -0
  46. package/dist/builtin-memory/design/roadmap.md +21 -0
  47. package/dist/builtin-memory/insights/capture.md +1 -1
  48. package/dist/builtin-memory/internal/memory-loading.md +4 -4
  49. package/dist/builtin-memory/internal/plugins.md +10 -1
  50. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  51. package/dist/builtin-memory/plan/roadmap.md +6 -28
  52. package/dist/builtin-memory/spec/guide.md +19 -8
  53. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
  54. package/dist/clients/attach/render/markdown-source.js +106 -1
  55. package/dist/clients/attach/session/file-links.d.ts +13 -4
  56. package/dist/clients/attach/session/file-links.js +54 -58
  57. package/dist/clients/attach/viewer.js +545 -543
  58. package/dist/clients/inbox/controller.js +1 -1
  59. package/dist/clients/inbox/page-adapter.js +2 -1
  60. package/dist/clients/inbox/resolve.d.ts +1 -0
  61. package/dist/clients/inbox/review/review-client.js +3 -1
  62. package/dist/clients/inbox/tui/panel.js +23 -4
  63. package/dist/clients/inbox/tui/render.js +6 -0
  64. package/dist/clients/inbox/tui/types.d.ts +8 -0
  65. package/dist/commands/__tests__/human.test.js +2 -2
  66. package/dist/commands/human/request.d.ts +2 -0
  67. package/dist/commands/human/request.js +281 -0
  68. package/dist/commands/human.js +5 -2
  69. package/dist/commands/memory/shared.d.ts +2 -2
  70. package/dist/commands/memory/shared.js +11 -6
  71. package/dist/commands/pkg/browse/doc-view.js +2 -0
  72. package/dist/commands/sys/config.js +2 -2
  73. package/dist/commands/sys/doctor.js +54 -2
  74. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  75. package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
  76. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  77. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  78. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  79. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  80. package/dist/core/__tests__/human-actions.test.js +116 -0
  81. package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
  82. package/dist/core/__tests__/profile-project-memory-delivery.test.js +70 -3
  83. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  84. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  85. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  86. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  87. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  88. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  89. package/dist/core/canvas/db.js +23 -0
  90. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  91. package/dist/core/canvas/human-deliveries.js +75 -0
  92. package/dist/core/config.d.ts +13 -1
  93. package/dist/core/config.js +51 -1
  94. package/dist/core/feed/inbox.d.ts +6 -0
  95. package/dist/core/feed/inbox.js +9 -1
  96. package/dist/core/human/action-binding.d.ts +21 -0
  97. package/dist/core/human/action-binding.js +40 -0
  98. package/dist/core/human/completion.d.ts +38 -0
  99. package/dist/core/human/completion.js +27 -0
  100. package/dist/core/human/convention.d.ts +2 -0
  101. package/dist/core/human/convention.js +2 -0
  102. package/dist/core/human/tickets.d.ts +25 -6
  103. package/dist/core/human/tickets.js +19 -13
  104. package/dist/core/human/types.d.ts +5 -0
  105. package/dist/core/human-actions.d.ts +25 -0
  106. package/dist/core/human-actions.js +101 -0
  107. package/dist/core/memory-resolver.js +1 -1
  108. package/dist/core/profiles/select.d.ts +2 -0
  109. package/dist/core/profiles/select.js +21 -4
  110. package/dist/core/runtime/broker/frame-dispatch.js +2 -5
  111. package/dist/core/runtime/broker-inventory.d.ts +1 -2
  112. package/dist/core/runtime/broker-inventory.js +2 -77
  113. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  114. package/dist/core/runtime/broker.js +4 -4
  115. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  116. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  117. package/dist/core/runtime/command-surface.d.ts +8 -3
  118. package/dist/core/runtime/command-surface.js +42 -6
  119. package/dist/core/runtime/launch-target.d.ts +25 -0
  120. package/dist/core/runtime/launch-target.js +54 -0
  121. package/dist/core/runtime/persona.js +3 -3
  122. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  123. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  124. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  125. package/dist/core/runtime/prospective-inventory.js +88 -0
  126. package/dist/core/runtime/spawn.d.ts +3 -1
  127. package/dist/core/runtime/spawn.js +5 -3
  128. package/dist/core/substrate/frontmatter-validation.d.ts +2 -5
  129. package/dist/core/substrate/frontmatter-validation.js +8 -4
  130. package/dist/core/substrate/gate.d.ts +4 -1
  131. package/dist/core/substrate/gate.js +5 -0
  132. package/dist/core/substrate/on-read.js +21 -32
  133. package/dist/core/substrate/render-node.d.ts +3 -2
  134. package/dist/core/substrate/render-node.js +3 -2
  135. package/dist/core/substrate/render.js +64 -37
  136. package/dist/core/substrate/schema.d.ts +17 -8
  137. package/dist/core/substrate/schema.js +14 -10
  138. package/dist/core/substrate/surface-match.d.ts +8 -7
  139. package/dist/core/substrate/surface-match.js +15 -14
  140. package/dist/core/user-settings.d.ts +4 -0
  141. package/dist/core/user-settings.js +1 -0
  142. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
  143. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  144. package/dist/daemon/api/handlers/human-requests.js +409 -0
  145. package/dist/daemon/api/handlers/human.js +3 -0
  146. package/dist/daemon/api/handlers/inbox.js +3 -0
  147. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  148. package/dist/daemon/api/handlers/nodes.js +11 -46
  149. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  150. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  151. package/dist/daemon/api/handlers/reviews.js +10 -2
  152. package/dist/daemon/api/server.js +4 -0
  153. package/dist/daemon/crtrd.js +6 -0
  154. package/dist/daemon/human/deliver-action.d.ts +16 -0
  155. package/dist/daemon/human/deliver-action.js +168 -0
  156. package/dist/daemon/human/finish.d.ts +8 -5
  157. package/dist/daemon/human/finish.js +45 -6
  158. package/dist/daemon/human/sweep.js +4 -1
  159. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  160. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  161. package/dist/daemon/review/finish.d.ts +8 -3
  162. package/dist/daemon/review/finish.js +19 -1
  163. package/dist/types.d.ts +8 -0
  164. package/dist/types.js +1 -0
  165. package/package.json +1 -1
  166. package/runtime.lock.json +2 -2
  167. package/dist/builtin-memory/00-runtime-base.md +0 -55
  168. package/dist/builtin-memory/design.md +0 -55
  169. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
@@ -1,16 +1,17 @@
1
1
  import { after, afterEach, beforeEach, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { createNode, getNode, listNodes } from '../../../core/canvas/canvas.js';
7
7
  import { closeDb } from '../../../core/canvas/db.js';
8
8
  import { CrtrError } from '../../../core/errors.js';
9
- import { createProfile, loadProfileManifest, pauseProfile, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
9
+ import { createProfile, loadProfileManifest, pauseProfile, profileManifestPath, ROOT_PROFILE_ID, setProfileDefaultKind } from '../../../core/profiles/manifest.js';
10
10
  import { resetScopeCache } from '../../../core/scope.js';
11
11
  import { reviveNode } from '../../../core/runtime/revive.js';
12
+ import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
12
13
  import { spawnNode } from '../../../core/runtime/nodes.js';
13
- import { handleCreate, resolveCreateKind } from '../handlers/nodes.js';
14
+ import { handleConfig, handleCreate, resolveCreateKind } from '../handlers/nodes.js';
14
15
  const previousCrtrHome = process.env['CRTR_HOME'];
15
16
  const previousHome = process.env.HOME;
16
17
  let home = '';
@@ -101,6 +102,54 @@ test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
101
102
  });
102
103
  assert.equal(listNodes().length, beforeRows);
103
104
  });
105
+ test('prospective root selection stays read-only before the root profile is scaffolded', async () => {
106
+ const manifestPath = profileManifestPath(ROOT_PROFILE_ID);
107
+ assert.equal(existsSync(manifestPath), false);
108
+ const target = await resolveLaunchTarget({ root: true, cwd }, { readOnlyProfileSelection: true });
109
+ assert.equal(target.profileId, ROOT_PROFILE_ID);
110
+ assert.equal(target.profile, null);
111
+ assert.equal(target.kind, 'general');
112
+ assert.equal(target.cwd, cwd);
113
+ assert.equal(existsSync(manifestPath), false);
114
+ });
115
+ test('target-scoped prospective validation does not rescope node writes', async () => {
116
+ mkdirSync(join(cwd, '.crouter'), { recursive: true });
117
+ writeFileSync(join(cwd, '.crouter', 'config.json'), JSON.stringify({
118
+ kinds: { 'target-only': { whenToUse: 'Only in this project.' } },
119
+ }));
120
+ const target = await resolveLaunchTarget({ root: true, cwd, kind: 'target-only' }, { readOnlyProfileSelection: true });
121
+ assert.equal(target.kind, 'target-only');
122
+ const create = {
123
+ method: 'POST',
124
+ path: '/v1/nodes',
125
+ params: {},
126
+ query: new URLSearchParams(),
127
+ body: { root: true, profile: null, cwd, kind: 'target-only' },
128
+ };
129
+ await assert.rejects(() => handleCreate(create), /unknown kind: target-only/);
130
+ const id = 'ambient-kind-write';
131
+ createNode({
132
+ node_id: id,
133
+ name: id,
134
+ created: new Date().toISOString(),
135
+ cwd,
136
+ kind: 'general',
137
+ mode: 'base',
138
+ lifecycle: 'terminal',
139
+ status: 'active',
140
+ parent: null,
141
+ profile_id: null,
142
+ });
143
+ const patch = {
144
+ method: 'PATCH',
145
+ path: `/v1/nodes/${id}/config`,
146
+ params: { id },
147
+ query: new URLSearchParams(),
148
+ body: { kind: 'target-only' },
149
+ };
150
+ await assert.rejects(() => handleConfig(patch), /unknown kind: target-only/);
151
+ assert.equal(getNode(id)?.kind, 'general');
152
+ });
104
153
  test('create kind resolves from the profile default when omitted', () => {
105
154
  const profile = createProfile('default kind', [{ path: cwd, memory: 'content' }]);
106
155
  setProfileDefaultKind(profile.profileId, 'developer');
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const humanRequestRoutes: RouteTable;
@@ -0,0 +1,409 @@
1
+ // Durable programmatic human requests — crtrd `/v1/human/requests`. A request
2
+ // is an ordinary page ticket with an optional frozen completion action, so it
3
+ // is the same record the inbox lists, answers, and dismisses. Creation is the
4
+ // only surface that writes the action binding; every settlement path funnels
5
+ // through the shared first-writer-wins store.
6
+ import { randomBytes } from 'node:crypto';
7
+ import { statSync } from 'node:fs';
8
+ import { basename, isAbsolute } from 'node:path';
9
+ import { ApiError } from '../../../api/index.js';
10
+ import { readActionDelivery } from '../../../core/canvas/human-deliveries.js';
11
+ import { resolvePageComponents } from '../../../core/config.js';
12
+ import { resolveHumanAction } from '../../../core/human-actions.js';
13
+ import { HUMAN_ACTION_NAME_PATTERN, publishTicketActionBinding, readTicketActionBinding, } from '../../../core/human/action-binding.js';
14
+ import { completionEventFor } from '../../../core/human/completion.js';
15
+ import { parsePage } from '../../../core/human/page.js';
16
+ import { opaqueInboxTicketId, ticketDir } from '../../../core/human/root.js';
17
+ import { preparePageSource, publishPage, readPageTicket, readTicketResult, validatePageTicketResponses, } from '../../../core/human/tickets.js';
18
+ import { cancelHumanTicket, resolvePageTicket } from '../../human/finish.js';
19
+ import { resolveTicket, requireTicketId } from './inbox.js';
20
+ const REASON_MAX = 1000;
21
+ const ACTOR_MAX = 200;
22
+ const DEFAULT_DELIVERY = { placement: 'inline', inbox: true, reply: false };
23
+ const SOURCE_FIELDS = ['sessionName', 'askedBy', 'emittedAt', 'profileName', 'nodeId'];
24
+ // ===========================================================================
25
+ // Body validation
26
+ // ===========================================================================
27
+ function isPlainObject(value) {
28
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
29
+ }
30
+ function requireNoExtraKeys(obj, allowed, where) {
31
+ for (const key of Object.keys(obj)) {
32
+ if (!allowed.includes(key))
33
+ throw new ApiError(400, 'invalid_request', `unexpected field in ${where}: ${key}`);
34
+ }
35
+ }
36
+ function invalid(message) {
37
+ return new ApiError(400, 'invalid_request', message);
38
+ }
39
+ function validatePage(raw) {
40
+ if (!isPlainObject(raw))
41
+ throw invalid('`page` must be an object');
42
+ requireNoExtraKeys(raw, ['dialect', 'source'], '`page`');
43
+ const dialect = raw['dialect'];
44
+ if (dialect !== 'jsx' && dialect !== 'html')
45
+ throw invalid('`page.dialect` must be "jsx" or "html"');
46
+ const source = raw['source'];
47
+ if (typeof source !== 'string' || source.trim() === '')
48
+ throw invalid('`page.source` must be the authored page text');
49
+ return { dialect, source };
50
+ }
51
+ /** Creation pins a programmatic request to the inbox with no bridge to reply to. */
52
+ function validateCreateDelivery(raw) {
53
+ if (raw === undefined)
54
+ return DEFAULT_DELIVERY;
55
+ if (!isPlainObject(raw))
56
+ throw invalid('`delivery` must be an object');
57
+ requireNoExtraKeys(raw, ['placement', 'inbox', 'reply'], '`delivery`');
58
+ const placement = validatePlacement(raw['placement'], DEFAULT_DELIVERY.placement);
59
+ const inbox = raw['inbox'] ?? true;
60
+ const reply = raw['reply'] ?? false;
61
+ if (inbox !== true)
62
+ throw invalid('`delivery.inbox` must be true: a programmatic request must stay answerable');
63
+ if (reply !== false)
64
+ throw invalid('`delivery.reply` must be false: a programmatic request has no agent bridge to reply to');
65
+ return { placement, inbox: true, reply: false };
66
+ }
67
+ /** Replace re-presents a pending page; where the ticket is routed is fixed at
68
+ * creation, so only placement is replaceable. */
69
+ function validateReplaceDelivery(raw, prior) {
70
+ if (raw === undefined)
71
+ return prior;
72
+ if (!isPlainObject(raw))
73
+ throw invalid('`delivery` must be an object');
74
+ requireNoExtraKeys(raw, ['placement'], '`delivery`');
75
+ return { ...prior, placement: validatePlacement(raw['placement'], prior.placement) };
76
+ }
77
+ function validatePlacement(raw, fallback) {
78
+ const placement = raw ?? fallback;
79
+ if (placement !== 'inline' && placement !== 'panel')
80
+ throw invalid('`delivery.placement` must be "inline" or "panel"');
81
+ return placement;
82
+ }
83
+ function validateSource(raw) {
84
+ if (raw === undefined)
85
+ return undefined;
86
+ if (!isPlainObject(raw))
87
+ throw invalid('`source` must be an object');
88
+ requireNoExtraKeys(raw, SOURCE_FIELDS, '`source`');
89
+ const source = {};
90
+ for (const field of SOURCE_FIELDS) {
91
+ const value = raw[field];
92
+ if (value === undefined)
93
+ continue;
94
+ if (typeof value !== 'string' || value === '')
95
+ throw invalid(`\`source.${field}\` must be a nonempty string`);
96
+ source[field] = value;
97
+ }
98
+ return source;
99
+ }
100
+ function validateCreatorCwd(raw) {
101
+ if (typeof raw !== 'string' || raw === '')
102
+ throw invalid('`creator_cwd` is required');
103
+ if (!isAbsolute(raw))
104
+ throw invalid('`creator_cwd` must be an absolute path');
105
+ try {
106
+ if (!statSync(raw).isDirectory())
107
+ throw invalid(`\`creator_cwd\` is not a directory: ${raw}`);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof ApiError)
111
+ throw error;
112
+ throw invalid(`\`creator_cwd\` does not exist: ${raw}`);
113
+ }
114
+ return raw;
115
+ }
116
+ function validateActionSelection(raw) {
117
+ if (raw === undefined)
118
+ return undefined;
119
+ if (!isPlainObject(raw))
120
+ throw invalid('`action` must be an object');
121
+ requireNoExtraKeys(raw, ['name', 'payload'], '`action`');
122
+ const name = raw['name'];
123
+ if (typeof name !== 'string' || !HUMAN_ACTION_NAME_PATTERN.test(name)) {
124
+ throw invalid('`action.name` must be a declared action name');
125
+ }
126
+ // The completion document always carries `action.payload`, so an omitted one
127
+ // is normalized here rather than serialized away downstream.
128
+ return { name, payload: raw['payload'] ?? null };
129
+ }
130
+ function validateCreateRequest(raw) {
131
+ if (!isPlainObject(raw))
132
+ throw invalid('request body is required');
133
+ requireNoExtraKeys(raw, ['page', 'delivery', 'source', 'creator_cwd', 'action'], 'the request body');
134
+ const source = validateSource(raw['source']);
135
+ const action = validateActionSelection(raw['action']);
136
+ return {
137
+ page: validatePage(raw['page']),
138
+ delivery: validateCreateDelivery(raw['delivery']),
139
+ ...(source === undefined ? {} : { source }),
140
+ creatorCwd: validateCreatorCwd(raw['creator_cwd']),
141
+ ...(action === undefined ? {} : { action }),
142
+ };
143
+ }
144
+ function validateReplaceRequest(raw, priorDelivery) {
145
+ if (!isPlainObject(raw))
146
+ throw invalid('request body is required');
147
+ requireNoExtraKeys(raw, ['page', 'delivery'], 'the request body');
148
+ return { page: validatePage(raw['page']), delivery: validateReplaceDelivery(raw['delivery'], priorDelivery) };
149
+ }
150
+ function optionalBoundedString(raw, field, max) {
151
+ if (raw === undefined)
152
+ return undefined;
153
+ if (typeof raw !== 'string')
154
+ throw invalid(`\`${field}\` must be a string`);
155
+ if (raw.trim() === '' || raw.length > max)
156
+ throw invalid(`\`${field}\` must be nonempty after trim and at most ${max} characters`);
157
+ return raw;
158
+ }
159
+ function validateRespondRequest(raw) {
160
+ if (!isPlainObject(raw))
161
+ throw invalid('request body is required');
162
+ requireNoExtraKeys(raw, ['responses', 'actor'], 'the request body');
163
+ if (!isPlainObject(raw['responses']))
164
+ throw invalid('`responses` must be an object');
165
+ const actor = optionalBoundedString(raw['actor'], 'actor', ACTOR_MAX);
166
+ return { responses: raw['responses'], ...(actor === undefined ? {} : { actor }) };
167
+ }
168
+ function validateSettleRequest(raw) {
169
+ if (raw === undefined)
170
+ return {};
171
+ if (!isPlainObject(raw))
172
+ throw invalid('request body must be an object');
173
+ requireNoExtraKeys(raw, ['reason', 'actor'], 'the request body');
174
+ const reason = optionalBoundedString(raw['reason'], 'reason', REASON_MAX);
175
+ const actor = optionalBoundedString(raw['actor'], 'actor', ACTOR_MAX);
176
+ return { ...(reason === undefined ? {} : { reason }), ...(actor === undefined ? {} : { actor }) };
177
+ }
178
+ function resolveRequest(ctx) {
179
+ const requestId = requireTicketId({ ticket_id: ctx.params['request_id'] ?? '' });
180
+ const resolved = resolveTicket(requestId);
181
+ if (resolved === null || resolved.kind !== 'page')
182
+ throw new ApiError(404, 'not_found', 'no request with that id');
183
+ let manifest;
184
+ try {
185
+ manifest = parsePage(resolved.dir);
186
+ }
187
+ catch {
188
+ throw new ApiError(500, 'internal', 'the request could not be read');
189
+ }
190
+ return {
191
+ requestId,
192
+ ticketId: resolved.ticketId,
193
+ dir: resolved.dir,
194
+ manifest,
195
+ result: readTicketResult(resolved.dir),
196
+ binding: readTicketActionBinding(resolved.dir),
197
+ };
198
+ }
199
+ function requestState(result) {
200
+ if (result === null)
201
+ return 'pending';
202
+ if (result.kind === 'canceled')
203
+ return completionEventFor(result);
204
+ return 'answered';
205
+ }
206
+ function deliveryDetail(row) {
207
+ if (row === null)
208
+ return { state: 'none', attempt: 0 };
209
+ const detail = { state: row.state, attempt: row.attempt };
210
+ // A next attempt is only meaningful for a pending retry; the enqueue instant
211
+ // of an untried delivery says nothing a client can act on.
212
+ if (row.state === 'pending' && row.lastFailure !== null)
213
+ detail.next_attempt_at = new Date(row.nextAttemptAt).toISOString();
214
+ if (row.acceptedAt !== null)
215
+ detail.accepted_at = new Date(row.acceptedAt).toISOString();
216
+ if (row.permanentFailedAt !== null)
217
+ detail.permanent_failed_at = new Date(row.permanentFailedAt).toISOString();
218
+ if (row.lastFailure !== null) {
219
+ const failure = row.lastFailure;
220
+ detail.last_failure = {
221
+ kind: failure.kind,
222
+ ...(failure.exitCode === undefined ? {} : { exit_code: failure.exitCode }),
223
+ ...(failure.signal === undefined ? {} : { signal: failure.signal }),
224
+ ...(failure.message === undefined ? {} : { message: failure.message }),
225
+ ...(failure.stderr === undefined ? {} : { stderr: failure.stderr }),
226
+ };
227
+ }
228
+ return detail;
229
+ }
230
+ function requestToDTO(request) {
231
+ const { manifest, result, binding } = request;
232
+ const emittedAt = manifest.source?.emittedAt;
233
+ if (emittedAt === undefined)
234
+ throw new ApiError(500, 'internal', 'the request carries no emitted time');
235
+ const state = requestState(result);
236
+ const dto = {
237
+ request_id: request.requestId,
238
+ state,
239
+ title: manifest.title,
240
+ source: manifest.source ?? {},
241
+ emitted_at: emittedAt,
242
+ };
243
+ if (manifest.subtitle !== undefined)
244
+ dto.subtitle = manifest.subtitle;
245
+ if (result !== null) {
246
+ dto.settled_at = result.kind === 'canceled' ? result.canceledAt : result.completedAt;
247
+ // Never fabricated for a dismissal or a withdrawal: only an answer has responses.
248
+ if (state === 'answered' && result.kind === 'page')
249
+ dto.responses = result.responses;
250
+ if (result.kind === 'canceled' && result.reason !== undefined)
251
+ dto.reason = result.reason;
252
+ const actor = result.kind === 'review' ? undefined : result.actor;
253
+ if (actor !== undefined)
254
+ dto.actor = actor;
255
+ }
256
+ if (binding !== null) {
257
+ dto.action = { name: binding.name, payload: binding.payload };
258
+ dto.delivery = deliveryDetail(readActionDelivery(request.requestId));
259
+ }
260
+ return dto;
261
+ }
262
+ // ===========================================================================
263
+ // POST /v1/human/requests — create
264
+ // ===========================================================================
265
+ /** Authoring failure is the caller's malformed page, never a server fault. */
266
+ function prepareOrReject(dialect, document, source, delivery) {
267
+ try {
268
+ return preparePageSource({ dialect, document, source, delivery, productKinds: resolvePageComponents() });
269
+ }
270
+ catch (error) {
271
+ throw invalid(error instanceof Error ? error.message : 'the page could not be prepared');
272
+ }
273
+ }
274
+ function handleCreate(ctx) {
275
+ const request = validateCreateRequest(ctx.body);
276
+ // Action resolution precedes every write: a rejected action must leave no
277
+ // inbox row for a human to answer.
278
+ let resolvedAction;
279
+ if (request.action !== undefined) {
280
+ const resolution = resolveHumanAction(request.action.name, request.creatorCwd);
281
+ if ('kind' in resolution) {
282
+ const detail = resolution.kind === 'unknown_action'
283
+ ? `no action named ${resolution.name} is declared from ${request.creatorCwd}`
284
+ : `${resolution.name} is declared in ${resolution.configOrigin} but unusable: ${resolution.reason}`;
285
+ throw new ApiError(400, resolution.kind, detail);
286
+ }
287
+ resolvedAction = resolution;
288
+ }
289
+ const prepared = prepareOrReject(request.page.dialect, request.page.source, request.source, request.delivery);
290
+ const dir = ticketDir(`page-${randomBytes(8).toString('hex')}`);
291
+ // The binding is written before the page exists, so a visible request is
292
+ // never momentarily unbound and `replace` can never reach the approved effect.
293
+ if (request.action !== undefined && resolvedAction !== undefined) {
294
+ publishTicketActionBinding(dir, {
295
+ schema: 'crtr.human-action-binding/v1',
296
+ name: request.action.name,
297
+ payload: request.action.payload,
298
+ creatorCwd: request.creatorCwd,
299
+ configOrigin: resolvedAction.configOrigin,
300
+ argv: resolvedAction.argv,
301
+ cwd: resolvedAction.cwd,
302
+ });
303
+ }
304
+ const published = publishPage(dir, prepared);
305
+ return {
306
+ status: 201,
307
+ body: {
308
+ request_id: opaqueInboxTicketId(basename(published.dir)),
309
+ state: 'pending',
310
+ ...(request.action === undefined ? {} : { action: { name: request.action.name } }),
311
+ delivery_state: 'none',
312
+ },
313
+ };
314
+ }
315
+ // ===========================================================================
316
+ // GET /v1/human/requests/:request_id — read
317
+ // ===========================================================================
318
+ function handleGet(ctx) {
319
+ return { status: 200, body: requestToDTO(resolveRequest(ctx)) };
320
+ }
321
+ // ===========================================================================
322
+ // POST /v1/human/requests/:request_id/replace — revise the pending page
323
+ // ===========================================================================
324
+ function handleReplace(ctx) {
325
+ const request = resolveRequest(ctx);
326
+ if (request.result !== null)
327
+ throw new ApiError(409, 'already_settled', 'the request already has a terminal result');
328
+ const body = validateReplaceRequest(ctx.body, request.manifest.delivery);
329
+ // Identity and provenance are the creation's, verbatim — a revision changes
330
+ // only what is on screen, never who asked or when.
331
+ const prepared = prepareOrReject(body.page.dialect, body.page.source, request.manifest.source, body.delivery);
332
+ publishPage(request.dir, prepared);
333
+ return { status: 200, body: requestToDTO({ ...request, manifest: prepared.manifest }) };
334
+ }
335
+ // ===========================================================================
336
+ // Settlement — respond, dismiss, cancel
337
+ // ===========================================================================
338
+ /** The settlement funnels speak the inbox's error vocabulary; requests speak
339
+ * the wire contract's. This is the one translation. */
340
+ function settlementError(error) {
341
+ if (error instanceof ApiError) {
342
+ if (error.code === 'ticket_already_resolved')
343
+ return new ApiError(409, 'already_settled', 'the request already has a terminal result');
344
+ if (error.code === 'page_has_no_response_lifecycle')
345
+ return new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
346
+ return error;
347
+ }
348
+ if (error instanceof Error && error.message === 'page_has_no_response_lifecycle') {
349
+ return new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
350
+ }
351
+ return new ApiError(500, 'internal', 'the request could not be settled');
352
+ }
353
+ function requireSettleable(request) {
354
+ if (request.result !== null)
355
+ throw new ApiError(409, 'already_settled', 'the request already has a terminal result');
356
+ if (readPageTicket(request.dir).state === 'passive') {
357
+ throw new ApiError(409, 'not_settleable', 'this request carries no response lifecycle');
358
+ }
359
+ }
360
+ async function handleRespond(ctx) {
361
+ const request = resolveRequest(ctx);
362
+ const body = validateRespondRequest(ctx.body);
363
+ requireSettleable(request);
364
+ try {
365
+ validatePageTicketResponses(request.dir, body.responses);
366
+ }
367
+ catch (error) {
368
+ // `requireSettleable` already refused a page with no response lifecycle, so
369
+ // anything left here is the caller's response map.
370
+ throw invalid(error instanceof Error ? error.message : 'the response map is invalid');
371
+ }
372
+ let result;
373
+ try {
374
+ result = await resolvePageTicket(request.ticketId, body.responses, body.actor === undefined ? {} : { actor: body.actor });
375
+ }
376
+ catch (error) {
377
+ throw settlementError(error);
378
+ }
379
+ return { status: 200, body: requestToDTO({ ...request, result, binding: request.binding }) };
380
+ }
381
+ async function settle(ctx, disposition) {
382
+ const request = resolveRequest(ctx);
383
+ const body = validateSettleRequest(ctx.body);
384
+ requireSettleable(request);
385
+ let result;
386
+ try {
387
+ result = await cancelHumanTicket(request.ticketId, { ...body, disposition });
388
+ }
389
+ catch (error) {
390
+ throw settlementError(error);
391
+ }
392
+ return { status: 200, body: requestToDTO({ ...request, result }) };
393
+ }
394
+ /** The recipient surface deliberately closing the request without answering. */
395
+ function handleDismiss(ctx) {
396
+ return settle(ctx, 'dismissed');
397
+ }
398
+ /** The requester withdrawing its own request. */
399
+ function handleCancel(ctx) {
400
+ return settle(ctx, 'canceled');
401
+ }
402
+ export const humanRequestRoutes = [
403
+ { method: 'POST', pattern: '/v1/human/requests', handler: handleCreate },
404
+ { method: 'GET', pattern: '/v1/human/requests/:request_id', handler: handleGet },
405
+ { method: 'POST', pattern: '/v1/human/requests/:request_id/replace', handler: handleReplace },
406
+ { method: 'POST', pattern: '/v1/human/requests/:request_id/respond', handler: handleRespond },
407
+ { method: 'POST', pattern: '/v1/human/requests/:request_id/dismiss', handler: handleDismiss },
408
+ { method: 'POST', pattern: '/v1/human/requests/:request_id/cancel', handler: handleCancel },
409
+ ];
@@ -108,6 +108,9 @@ async function handleCancel(ctx) {
108
108
  if (request !== undefined && (typeof request !== 'object' || request === null)) {
109
109
  throw new ApiError(400, 'invalid_request', 'ticket cancel body must be an object');
110
110
  }
111
+ if (request?.disposition !== undefined && request.disposition !== 'canceled' && request.disposition !== 'dismissed') {
112
+ throw new ApiError(400, 'invalid_request', '`disposition` must be "canceled" or "dismissed"');
113
+ }
111
114
  const dir = ticketDir(ticketId);
112
115
  const review = getReviewByBridge(ticketId);
113
116
  try {
@@ -364,6 +364,9 @@ async function handleCancel(ctx) {
364
364
  const result = await cancelHumanTicket(resolved.ticketId, {
365
365
  reason: request.reason ?? DEFAULT_CANCEL_REASON,
366
366
  actor: 'human',
367
+ // This is the recipient surface: closing a request here is a dismissal,
368
+ // never the requester withdrawing it.
369
+ disposition: 'dismissed',
367
370
  });
368
371
  if (result.kind !== 'canceled')
369
372
  throw new ApiError(409, 'ticket_already_resolved', 'ticket was already resolved');
@@ -1,14 +1,12 @@
1
1
  import { type ModelRegistryLike } from '../../../core/model-routes.js';
2
- import { assertProfileActive } from '../../../core/profiles/manifest.js';
3
2
  import type { HandlerResult, RequestContext, RouteTable } from '../router.js';
4
3
  interface NodeHandlerDeps {
5
4
  modelRegistry?: ModelRegistryLike;
6
5
  }
7
- export declare function resolveCreateKind(kind: string | undefined, profile: ReturnType<typeof assertProfileActive>): string;
6
+ export { resolveCreateKind } from '../../../core/runtime/launch-target.js';
8
7
  export declare function handleCreate(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
9
8
  export declare function handleList(ctx: RequestContext): HandlerResult;
10
9
  export declare function handlePromote(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
11
10
  export declare function handleYield(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
12
11
  export declare function handleConfig(ctx: RequestContext, deps?: NodeHandlerDeps): Promise<HandlerResult>;
13
12
  export declare const nodeRoutes: RouteTable;
14
- export {};
@@ -7,10 +7,10 @@
7
7
  //
8
8
  // `{id}` is the target node.
9
9
  import { readdirSync } from 'node:fs';
10
- import { basename, resolve } from 'node:path';
10
+ import { basename } from 'node:path';
11
11
  import { appendSituationalContext, closeNode, getNode, listNodes, nowIso, reviveNode, } from '../../../index.js';
12
12
  import { formatSituationalProse } from '../../../core/runtime/situational-context.js';
13
- import { assertLaunchModelRegistered, forkNode, resolveProfileId, spawnChild } from '../../../core/runtime/spawn.js';
13
+ import { assertLaunchModelRegistered, forkNode, spawnChild } from '../../../core/runtime/spawn.js';
14
14
  import { childrenOf, subscribersOf, subscriptionsOf, setMessageWait, updateNode } from '../../../core/canvas/canvas.js';
15
15
  import { subtreeIds } from '../../../core/canvas/nav-model.js';
16
16
  import { ticketCountsForNodes } from '../../../core/canvas/attention.js';
@@ -32,7 +32,7 @@ import { BrokerUnreachableError } from '../../../core/runtime/broker-request.js'
32
32
  import { isBrokerLive, persistDormantModel, setModelLive } from '../../../core/runtime/model-swap.js';
33
33
  import { writeOutputSchema } from '../../../core/runtime/structured-output.js';
34
34
  import { claimWarmNode, refillWarmPool, warmPoolEnabled } from '../../../core/runtime/warm-pool.js';
35
- import { assertProfileActive, profileHome } from '../../../core/profiles/manifest.js';
35
+ import { resolveLaunchTarget } from '../../../core/runtime/launch-target.js';
36
36
  import { fullName } from '../../../core/canvas/labels.js';
37
37
  import { reportsForNode } from './reports.js';
38
38
  import { getCron } from '../../../core/canvas/crons.js';
@@ -99,28 +99,6 @@ function parseCreateBody(body) {
99
99
  }
100
100
  return b;
101
101
  }
102
- /** The directory a new node is pinned to. An explicit `pin_cwd` wins; else a
103
- * managed child inherits its spawner's cwd (which is what keeps a worktree
104
- * node's whole subtree inside that worktree); else the launch's own `cwd`, the
105
- * directory the create came from; else the profile's home.
106
- *
107
- * The launch directory beating the profile home is what lets one profile cover
108
- * many working directories — a root started inside a planted instance runs in
109
- * that instance while still taking its identity, purview, and memory from the
110
- * profile covering it. The profile supplies a home only for a create that named
111
- * no directory at all. */
112
- function resolveNodeCwd(req, parent, profileId, contextCwd) {
113
- if (req.pin_cwd !== undefined && req.pin_cwd !== '')
114
- return resolve(req.pin_cwd);
115
- if (parent != null) {
116
- const spawner = getNode(parent);
117
- if (spawner !== null)
118
- return spawner.cwd;
119
- }
120
- if (req.cwd !== undefined && req.cwd !== '')
121
- return resolve(req.cwd);
122
- return profileHome(profileId) ?? contextCwd;
123
- }
124
102
  /** Build the immediate spawn recipe. */
125
103
  function buildRecipe(req, parent, cwd) {
126
104
  const recipe = {
@@ -185,9 +163,7 @@ function warmRequestFor(req, cwd, profileId) {
185
163
  name: req.name ?? null,
186
164
  };
187
165
  }
188
- export function resolveCreateKind(kind, profile) {
189
- return kind ?? profile?.manifest.default_kind ?? 'general';
190
- }
166
+ export { resolveCreateKind } from '../../../core/runtime/launch-target.js';
191
167
  function requestFromLaunch(launch) {
192
168
  const intent = launch.modelIntent === undefined
193
169
  ? undefined
@@ -229,20 +205,8 @@ export async function handleCreate(ctx, deps = {}) {
229
205
  // Resolve and validate the exact launch recipe before the warm pool can claim
230
206
  // a spare (or the cold path can create a worktree/node). The registry belongs
231
207
  // to this daemon, never to the possibly-remote CLI caller.
232
- const contextCwd = req.cwd ?? process.cwd();
233
- const profileId = await resolveProfileId(req.profile, parent ?? null, contextCwd);
234
- const profile = assertProfileActive(profileId);
235
- const kind = resolveCreateKind(req.kind, profile);
208
+ const { profileId, kind, cwd, launch } = await resolveLaunchTarget(req, { validateKind: assertKindServer });
236
209
  const resolvedReq = { ...req, kind };
237
- const cwd = resolveNodeCwd(resolvedReq, parent, profileId, contextCwd);
238
- await assertKindServer(kind, cwd, profileId);
239
- const launch = (await buildLaunchSpecAsync(kind, (resolvedReq.mode ?? 'base'), {
240
- lifecycle: req.root === true ? (req.root_lifecycle ?? 'resident') : 'terminal',
241
- hasManager: req.root !== true,
242
- ...(req.model !== undefined ? { model: req.model } : {}),
243
- cwd,
244
- profileId,
245
- })).launch;
246
210
  await assertLaunchModelRegistered(requestFromLaunch(launch), { cwd, profileId }, deps.modelRegistry);
247
211
  // --- Warm path: hand out an already-booted spare instead of paying a full
248
212
  // engine boot. A miss is not a failure — it falls through to the cold
@@ -680,7 +644,7 @@ export async function handleConfig(ctx, deps = {}) {
680
644
  // --- kind / mode / lifecycle: reshape via canonical primitive (CLI parity). ---
681
645
  if (patch.kind !== undefined || patch.mode !== undefined || patch.lifecycle !== undefined) {
682
646
  if (patch.kind !== undefined)
683
- await assertKindServer(patch.kind, meta.cwd, meta.profile_id ?? null);
647
+ await assertKindServer(patch.kind);
684
648
  const newKind = patch.kind ?? meta.kind;
685
649
  const newMode = (patch.mode ?? meta.mode);
686
650
  const newLifecycle = patch.lifecycle ?? meta.lifecycle;
@@ -712,10 +676,11 @@ export async function handleConfig(ctx, deps = {}) {
712
676
  // to receive it, so it is a documented Stage-A no-op here.
713
677
  return { status: 200, body: detailOf(id) };
714
678
  }
715
- /** Validate a `--kind` against the installed personas server-side (crtrd reads
716
- * the merged launch config). Mirrors the CLI's assertKind. */
717
- async function assertKindServer(kind, cwd, profileId) {
718
- const kinds = Object.keys((await readMergedLaunchConfigAsync(cwd, profileId)).kinds);
679
+ /** POST /v1/nodes and PATCH /v1/nodes/:id/config validate kinds in the daemon's
680
+ * ambient configuration; prospective reads validate their resolved target. The
681
+ * read is subprocess-isolated — crtrd must never resolve config on its own loop. */
682
+ async function assertKindServer(kind) {
683
+ const kinds = Object.keys((await readMergedLaunchConfigAsync(process.cwd(), process.env['CRTR_PROFILE_ID'] || null)).kinds);
719
684
  if (!kinds.includes(kind)) {
720
685
  throw usage(`unknown kind: ${kind}`, { received: kind, next: `Valid kinds: ${kinds.sort().join(', ')}.` });
721
686
  }
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const prospectiveChatInventoryRoutes: RouteTable;