@north-light/crouter 0.3.191 → 0.3.193

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 +31 -10
  2. package/dist/api/client.js +83 -24
  3. package/dist/api/dto/bash-jobs.d.ts +22 -0
  4. package/dist/api/dto/bash-jobs.js +9 -0
  5. package/dist/api/dto/human.d.ts +3 -4
  6. package/dist/api/dto/inbox.d.ts +106 -65
  7. package/dist/api/dto/inbox.js +2 -8
  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 +7 -0
  12. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/serial/provider-rotation.test.ts +101 -3
  14. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.d.ts +6 -2
  15. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +68 -14
  16. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +87 -13
  17. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +2 -2
  18. package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
  19. package/dist/clients/attach/chrome/canvas-panels.d.ts +1 -1
  20. package/dist/clients/attach/chrome/canvas-panels.js +1 -1
  21. package/dist/clients/attach/input/overlay-owner.js +1 -1
  22. package/dist/clients/attach/overlays/auth.js +34 -8
  23. package/dist/clients/attach/overlays/pickers.d.ts +5 -8
  24. package/dist/clients/attach/overlays/pickers.js +37 -43
  25. package/dist/clients/attach/viewer.js +643 -620
  26. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +38 -128
  27. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +72 -148
  28. package/dist/clients/inbox/controller.d.ts +2 -3
  29. package/dist/clients/inbox/controller.js +41 -42
  30. package/dist/clients/inbox/page-adapter.d.ts +33 -0
  31. package/dist/clients/inbox/page-adapter.js +54 -0
  32. package/dist/clients/inbox/resolve.d.ts +2 -2
  33. package/dist/clients/inbox/resolve.js +1 -1
  34. package/dist/clients/inbox/review-visibility.d.ts +1 -1
  35. package/dist/clients/inbox/review-visibility.js +1 -1
  36. package/dist/clients/inbox/tui/ansi.d.ts +1 -1
  37. package/dist/clients/inbox/tui/ansi.js +2 -2
  38. package/dist/clients/inbox/tui/input.d.ts +1 -3
  39. package/dist/clients/inbox/tui/input.js +242 -553
  40. package/dist/clients/inbox/tui/panel.d.ts +3 -1
  41. package/dist/clients/inbox/tui/panel.js +59 -187
  42. package/dist/clients/inbox/tui/render.d.ts +0 -25
  43. package/dist/clients/inbox/tui/render.js +165 -416
  44. package/dist/clients/inbox/tui/slots.d.ts +32 -0
  45. package/dist/clients/inbox/tui/slots.js +156 -0
  46. package/dist/clients/inbox/tui/types.d.ts +25 -13
  47. package/dist/clients/inbox/tui.js +5 -3
  48. package/dist/commands/attention.js +2 -2
  49. package/dist/commands/human/doc.js +66 -20
  50. package/dist/commands/human/prompts.d.ts +3 -3
  51. package/dist/commands/human/prompts.js +158 -112
  52. package/dist/commands/human/queue.d.ts +0 -1
  53. package/dist/commands/human/queue.js +34 -94
  54. package/dist/commands/human/shared.d.ts +10 -4
  55. package/dist/commands/human/shared.js +62 -13
  56. package/dist/commands/human.js +8 -4
  57. package/dist/commands/memory/read.js +13 -5
  58. package/dist/commands/node/bash.js +15 -28
  59. package/dist/commands/surface/node/focus.js +1 -1
  60. package/dist/core/__tests__/human-deliver.test.js +40 -46
  61. package/dist/core/__tests__/seam/broker-attach-stream.test.js +7 -1
  62. package/dist/core/__tests__/serial/human-deliver-e2e.test.js +40 -31
  63. package/dist/core/__tests__/serial/spawn-root.test.js +3 -1
  64. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +3 -1
  65. package/dist/core/auth-interaction.d.ts +2 -2
  66. package/dist/core/bash-jobs.d.ts +32 -0
  67. package/dist/core/bash-jobs.js +72 -1
  68. package/dist/core/canvas/__tests__/attention.test.js +16 -10
  69. package/dist/core/canvas/__tests__/render-remote.test.js +9 -3
  70. package/dist/core/canvas/attention.js +1 -1
  71. package/dist/core/canvas/browse/app.js +1 -1
  72. package/dist/core/canvas/nav-model.d.ts +1 -1
  73. package/dist/core/canvas/nav-model.js +1 -1
  74. package/dist/core/canvas/remote-canvas-source.js +1 -1
  75. package/dist/core/config.js +4 -1
  76. package/dist/core/human/__tests__/page-render.test.d.ts +1 -0
  77. package/dist/core/human/__tests__/page-render.test.js +49 -0
  78. package/dist/core/human/__tests__/page-tickets.test.d.ts +1 -0
  79. package/dist/core/human/__tests__/page-tickets.test.js +112 -0
  80. package/dist/core/human/__tests__/page.test.d.ts +1 -0
  81. package/dist/core/human/__tests__/page.test.js +139 -0
  82. package/dist/core/human/__tests__/serial/inbox-core.test.js +42 -32
  83. package/dist/core/human/claim.js +2 -2
  84. package/dist/core/human/convention.d.ts +4 -4
  85. package/dist/core/human/convention.js +6 -13
  86. package/dist/core/human/markdown-html.d.ts +8 -0
  87. package/dist/core/human/markdown-html.js +581 -0
  88. package/dist/core/human/page-catalog.d.ts +6 -0
  89. package/dist/core/human/page-catalog.js +43 -0
  90. package/dist/core/human/page-render.d.ts +3 -0
  91. package/dist/core/human/page-render.js +131 -0
  92. package/dist/core/human/page-schema.d.ts +312 -0
  93. package/dist/core/human/page-schema.js +291 -0
  94. package/dist/core/human/page-synth.d.ts +9 -0
  95. package/dist/core/human/page-synth.js +17 -0
  96. package/dist/core/human/page.d.ts +22 -0
  97. package/dist/core/human/page.js +427 -0
  98. package/dist/core/human/review-schema.d.ts +19 -0
  99. package/dist/core/human/review-schema.js +34 -0
  100. package/dist/core/human/scan.d.ts +12 -3
  101. package/dist/core/human/scan.js +69 -26
  102. package/dist/core/human/summary.d.ts +5 -6
  103. package/dist/core/human/summary.js +54 -37
  104. package/dist/core/human/tickets.d.ts +61 -11
  105. package/dist/core/human/tickets.js +100 -57
  106. package/dist/core/human/types.d.ts +21 -66
  107. package/dist/core/human/types.js +1 -5
  108. package/dist/core/preview-registry.js +0 -1
  109. package/dist/core/review/ticket-filter.d.ts +1 -1
  110. package/dist/core/review/ticket-filter.js +2 -8
  111. package/dist/core/runtime/broker/read-ops.js +3 -0
  112. package/dist/core/runtime/broker.js +19 -10
  113. package/dist/core/runtime/package-health.js +1 -1
  114. package/dist/core/termrender/termrender.d.ts +1 -1
  115. package/dist/core/termrender/termrender.js +2 -2
  116. package/dist/core/tui/page-host.d.ts +1 -1
  117. package/dist/core/tui/page-host.js +2 -2
  118. package/dist/core/user-settings.d.ts +19 -0
  119. package/dist/core/user-settings.js +22 -0
  120. package/dist/daemon/api/handlers/bash-jobs.d.ts +2 -0
  121. package/dist/daemon/api/handlers/bash-jobs.js +89 -0
  122. package/dist/daemon/api/handlers/human.js +16 -14
  123. package/dist/daemon/api/handlers/inbox.js +311 -227
  124. package/dist/daemon/api/router.d.ts +5 -1
  125. package/dist/daemon/api/router.js +5 -0
  126. package/dist/daemon/api/server.js +2 -0
  127. package/dist/daemon/cron-run.js +23 -10
  128. package/dist/daemon/human/finish.d.ts +8 -3
  129. package/dist/daemon/human/finish.js +35 -15
  130. package/dist/pages/bundle.css +1 -0
  131. package/dist/pages/bundle.js +1347 -0
  132. package/dist/pages/comments.d.ts +34 -0
  133. package/dist/pages/comments.js +72 -0
  134. package/dist/pages/elements/cards.d.ts +17 -0
  135. package/dist/pages/elements/cards.js +785 -0
  136. package/dist/pages/elements/chart.d.ts +1 -0
  137. package/dist/pages/elements/chart.js +648 -0
  138. package/dist/pages/elements/options.d.ts +21 -0
  139. package/dist/pages/elements/options.js +604 -0
  140. package/dist/pages/elements/pages.d.ts +24 -0
  141. package/dist/pages/elements/pages.js +355 -0
  142. package/dist/pages/elements/slot.d.ts +21 -0
  143. package/dist/pages/elements/slot.js +105 -0
  144. package/dist/pages/elements/table.d.ts +19 -0
  145. package/dist/pages/elements/table.js +906 -0
  146. package/dist/pages/elements/text.d.ts +25 -0
  147. package/dist/pages/elements/text.js +951 -0
  148. package/dist/pages/entry.d.ts +8 -0
  149. package/dist/pages/entry.js +8 -0
  150. package/dist/pages/host.d.ts +132 -0
  151. package/dist/pages/host.js +149 -0
  152. package/dist/pages/register.d.ts +2 -0
  153. package/dist/pages/register.js +3 -0
  154. package/dist/pages/slot-config.d.ts +36 -0
  155. package/dist/pages/slot-config.js +50 -0
  156. package/dist/pages/types.d.ts +98 -0
  157. package/dist/pages/types.js +23 -0
  158. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +13 -5
  159. package/dist/pi-extensions/canvas-structured-output.js +7 -3
  160. package/dist/types.d.ts +4 -0
  161. package/dist/types.js +2 -0
  162. package/package.json +8 -8
  163. package/runtime.lock.json +156 -101
  164. package/dist/clients/inbox/deck-adapter.d.ts +0 -29
  165. package/dist/clients/inbox/deck-adapter.js +0 -63
  166. package/dist/core/human/deck-factories.d.ts +0 -8
  167. package/dist/core/human/deck-factories.js +0 -15
  168. package/dist/core/human/deck-schema.d.ts +0 -70
  169. package/dist/core/human/deck-schema.js +0 -92
@@ -3,19 +3,29 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync,
3
3
  import { spawn } from 'node:child_process';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
- import { submitDeck, submitReview, completeDeck, cancelTicket, readTicketResult } from '../../tickets.js';
6
+ import { submitPage, submitReview, completePage, cancelTicket, readTicketResult } from '../../tickets.js';
7
7
  import { claimTicket } from '../../claim.js';
8
8
  import { scanInbox } from '../../scan.js';
9
9
  import { atomicWriteJson, progressPath, responsePath } from '../../convention.js';
10
- import { validateDeck } from '../../deck-schema.js';
11
10
  import { ticketDir } from '../../root.js';
12
11
  const temp = mkdtempSync(join(tmpdir(), 'crtr-inbox-core-'));
13
12
  process.env['CRTR_HOME'] = temp;
14
13
  const source = join(temp, 'source.md');
15
14
  writeFileSync(source, '# Source\n');
16
- const deck = (blockedSince) => ({ title: 'A deck', source: { blockedSince, nodeId: 'node-1' }, interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [{ id: 'yes', label: 'Yes' }] }] });
17
- assert.throws(() => validateDeck({ interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', options: [] }] }), /title/, 'a deck must carry its own explicit inbox title');
18
- assert.throws(() => validateDeck({ title: 'Missing subtitle', interactions: [{ id: 'go', title: 'Go?', options: [] }] }), /subtitle/, 'a deck interaction without an authored subtitle is invalid');
15
+ const pageSource = join(temp, 'page.md');
16
+ writeFileSync(pageSource, [
17
+ '---',
18
+ 'title: A page',
19
+ 'subtitle: Go? requires your attention.',
20
+ '---',
21
+ '',
22
+ '```slot:text',
23
+ '{ "id": "go", "initialText": "", "editable": true }',
24
+ '```',
25
+ '',
26
+ ].join('\n'));
27
+ const page = (id, blockedSince) => submitPage({ dir: ticketDir(id), sourceFile: pageSource, dialect: 'md', source: { blockedSince, nodeId: 'node-1' } });
28
+ const answer = { go: { text: 'yes', edited: true, comments: [] } };
19
29
  async function waitFor(paths) {
20
30
  for (let attempts = 0; !paths.every(existsSync); attempts++) {
21
31
  if (attempts > 1_000)
@@ -23,60 +33,60 @@ async function waitFor(paths) {
23
33
  await new Promise((resolvePromise) => setTimeout(resolvePromise, 1));
24
34
  }
25
35
  }
26
- const oldDeck = submitDeck({ dir: ticketDir('z-last'), deck: deck('2025-01-01T00:00:00.000Z') });
27
- const newDeck = submitDeck({ dir: ticketDir('a-first'), deck: deck('2025-01-02T00:00:00.000Z') });
36
+ page('z-last', '2025-01-01T00:00:00.000Z');
37
+ const newPage = page('a-first', '2025-01-02T00:00:00.000Z');
28
38
  const review = submitReview({ dir: ticketDir('review'), review: { file: source, title: 'Review source', subtitle: 'Review findings require your decision.', source: { nodeId: 'node-1' }, blockedSince: '2025-01-02T00:00:00.000Z' } });
29
39
  assert.throws(() => submitReview({ dir: ticketDir('missing-review-subtitle'), review: { file: source, title: 'Missing subtitle', source: {} } }), /subtitle/, 'a review without an authored subtitle is invalid');
30
40
  assert.throws(() => submitReview({ dir: ticketDir('bad-review'), review: { file: join(temp, 'missing.md'), title: 'Bad', subtitle: 'This invalid review must be rejected.', source: {} } }), /existing absolute path/);
31
41
  assert.equal(existsSync(ticketDir('bad-review')), false, 'invalid submission does not strand an id');
32
42
  const prepared = ticketDir('prepared');
33
43
  mkdirSync(prepared);
34
- writeFileSync(join(prepared, 'body.md'), 'prepared body');
35
- const preparedTicket = submitDeck({ dir: prepared, deck: { ...deck('2025-01-02T00:00:00.000Z'), interactions: [{ id: 'go', title: 'Go?', subtitle: 'Go? requires your attention.', bodyPath: 'body.md', options: [{ id: 'yes', label: 'Yes' }] }] } });
44
+ writeFileSync(join(prepared, 'asset.png'), 'prepared asset');
45
+ const preparedTicket = submitPage({ dir: prepared, sourceFile: pageSource, dialect: 'md', source: { blockedSince: '2025-01-02T00:00:00.000Z', nodeId: 'node-1' } });
36
46
  assert.equal(preparedTicket.dir, realpathSync(prepared), 'submission accepts a crouter-precreated direct child and colocated assets');
37
47
  // Same timestamp is stable by id; progress is resumable work, never a hidden lease.
38
- atomicWriteJson(progressPath(newDeck.dir), { kind: 'deck', responses: [], savedAt: new Date().toISOString() });
48
+ atomicWriteJson(progressPath(newPage.dir), { kind: 'page', responses: {}, savedAt: new Date().toISOString() });
39
49
  const scanned = scanInbox();
40
50
  assert.deepEqual(scanned.map((item) => item.id), ['a-first', 'prepared', 'review', 'z-last']);
41
51
  assert.equal(scanned.find((item) => item.id === 'a-first')?.claim, undefined);
42
52
  assert.equal(scanned.find((item) => item.id === 'review')?.kind, 'review');
43
53
  assert.equal(scanned.find((item) => item.id === 'review')?.subtitle, 'Review findings require your decision.', 'review scanning exposes its required subtitle');
44
- const firstClaim = claimTicket(newDeck.dir);
54
+ const firstClaim = claimTicket(newPage.dir);
45
55
  assert.notEqual(firstClaim, null);
46
- assert.equal(claimTicket(newDeck.dir), null, 'a second controller cannot claim a live ticket');
56
+ assert.equal(claimTicket(newPage.dir), null, 'a second controller cannot claim a live ticket');
47
57
  // A separate process writes a genuine remote claim whose heartbeat is already
48
58
  // stale, then exits; this process recovers it — cross-process stale recovery.
49
- const staleDir = submitDeck({ dir: ticketDir('stale'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
59
+ const staleDir = page('stale', '2025-01-03T00:00:00.000Z').dir;
50
60
  const staleClaimReady = join(temp, 'stale-claim-ready');
51
61
  await new Promise((resolveWorker, rejectWorker) => spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-stale-claim-worker.ts', staleDir, staleClaimReady], { cwd: process.cwd(), stdio: ['ignore', 'ignore', 'inherit'] }).once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('stale-claim worker failed'))));
52
62
  assert.notEqual(JSON.parse(readFileSync(staleClaimReady, 'utf8')), null, 'a separate process wrote a stale remote claim on disk');
53
63
  assert.notEqual(claimTicket(staleDir), null, 'a remote claim written by another process recovers once its heartbeat has expired');
54
- const malformedDir = submitDeck({ dir: ticketDir('malformed-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
64
+ const malformedDir = page('malformed-claim', '2025-01-03T00:00:00.000Z').dir;
55
65
  writeFileSync(join(malformedDir, 'claim.json'), '{');
56
66
  assert.notEqual(claimTicket(malformedDir), null, 'partial claim crash artifact recovers');
57
- const final = completeDeck(newDeck.dir, [{ id: 'go', selectedOptionId: 'yes' }], firstClaim.token);
67
+ const final = completePage(newPage.dir, answer, firstClaim.token);
58
68
  assert.equal(final.won, true);
59
- assert.equal(readTicketResult(newDeck.dir)?.kind, 'deck');
60
- assert.equal(claimTicket(newDeck.dir), null, 'terminal tickets cannot be claimed');
69
+ assert.equal(readTicketResult(newPage.dir)?.kind, 'page');
70
+ assert.equal(claimTicket(newPage.dir), null, 'terminal tickets cannot be claimed');
61
71
  const exactResults = [
62
- { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: '2025-01-04T00:00:00.000Z' },
72
+ { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: '2025-01-04T00:00:00.000Z' },
63
73
  { schema: 'humanloop.review-response/v1', kind: 'review', result: { file: source, submitted: true, approved: true, comments: [], submittedAt: '2025-01-04T00:00:00.000Z', savedAt: '2025-01-04T00:00:00.000Z', reviewId: 'review-1', changed: false, baselineSha256: 'baseline', approvedSha256: 'approved', companionNodeId: 'companion-1', commentsTotal: 0, commentsUnresolved: 0 }, completedAt: '2025-01-04T00:00:00.000Z' },
64
74
  { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: '2025-01-04T00:00:00.000Z', reason: 'stop', actor: 'test' },
65
75
  ];
66
76
  for (const [index, result] of exactResults.entries()) {
67
- const dir = submitDeck({ dir: ticketDir(`strict-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
77
+ const dir = page(`strict-${index}`, '2025-01-03T00:00:00.000Z').dir;
68
78
  writeFileSync(responsePath(dir), JSON.stringify(result));
69
79
  assert.equal(readTicketResult(dir)?.kind, result.kind, `strict decoder accepts exact ${result.kind} result`);
70
80
  }
71
81
  const validFeedback = exactResults[1].result;
72
82
  const iso = '2025-01-04T00:00:00.000Z';
73
83
  const rejectFixtures = [
74
- { why: 'a wrong kind for a known schema', result: { schema: 'humanloop.response/v2', kind: 'review', responses: [], summary: '', completedAt: iso } },
75
- { why: 'an unknown schema id', result: { schema: 'humanloop.response/v9', kind: 'deck', responses: [], summary: '', completedAt: iso } },
76
- { why: 'a deck missing its required completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '' } },
77
- { why: 'a deck with a non-ISO completedAt', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: 'now' } },
78
- { why: 'a deck with an unrecognized nested response field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [{ id: 'go', bogus: true }], summary: '', completedAt: iso } },
79
- { why: 'a deck with an extra top-level field', result: { schema: 'humanloop.response/v2', kind: 'deck', responses: [], summary: '', completedAt: iso, extra: true } },
84
+ { why: 'a wrong kind for a known schema', result: { schema: 'crtr.page-response/v1', kind: 'review', responses: {}, summary: '', completedAt: iso } },
85
+ { why: 'an unknown schema id', result: { schema: 'crtr.page-response/v9', kind: 'page', responses: {}, summary: '', completedAt: iso } },
86
+ { why: 'a page missing its required completedAt', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '' } },
87
+ { why: 'a page with a non-ISO completedAt', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: 'now' } },
88
+ { why: 'a page whose responses are not a slot-keyed map', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: [{ id: 'go' }], summary: '', completedAt: iso } },
89
+ { why: 'a page with an extra top-level field', result: { schema: 'crtr.page-response/v1', kind: 'page', responses: {}, summary: '', completedAt: iso, extra: true } },
80
90
  { why: 'a review missing its required result', result: { schema: 'humanloop.review-response/v1', kind: 'review', completedAt: iso } },
81
91
  { why: 'a review whose nested result is not submitted', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, submitted: false }, completedAt: iso } },
82
92
  { why: 'a review with incomplete comment columns', result: { schema: 'humanloop.review-response/v1', kind: 'review', result: { ...validFeedback, approved: true, comments: [{ id: 'c1', line: 1, endLine: 1, colStart: 1, lineText: '# Source', comment: 'x', createdAt: iso }] }, completedAt: iso } },
@@ -87,29 +97,29 @@ const rejectFixtures = [
87
97
  { why: 'a canceled result with an extra top-level field', result: { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: iso, extra: true } },
88
98
  ];
89
99
  for (const [index, fixture] of rejectFixtures.entries()) {
90
- const dir = submitDeck({ dir: ticketDir(`strict-reject-${index}`), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
100
+ const dir = page(`strict-reject-${index}`, '2025-01-03T00:00:00.000Z').dir;
91
101
  writeFileSync(responsePath(dir), JSON.stringify(fixture.result));
92
102
  assert.equal(readTicketResult(dir), null, `strict decoder rejects ${fixture.why}`);
93
103
  }
94
104
  const protocolDir = ticketDir('stale-protocol');
95
105
  mkdirSync(protocolDir);
96
106
  writeFileSync(responsePath(protocolDir), '{}');
97
- assert.throws(() => submitDeck({ dir: protocolDir, deck: deck('2025-01-03T00:00:00.000Z') }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
98
- const cancelDir = submitDeck({ dir: ticketDir('cancel-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
107
+ assert.throws(() => submitPage({ dir: protocolDir, sourceFile: pageSource, dialect: 'md' }), /protocol state/, 'precreated assets may not include humanloop lifecycle state');
108
+ const cancelDir = page('cancel-race', '2025-01-03T00:00:00.000Z').dir;
99
109
  const canceled = cancelTicket(cancelDir, { reason: 'stop' });
100
110
  assert.equal(canceled.status, 'canceled');
101
111
  assert.equal(cancelTicket(cancelDir).status, 'already_resolved', 'first final writer wins');
102
- const finalRaceDir = submitDeck({ dir: ticketDir('final-race'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
112
+ const finalRaceDir = page('final-race', '2025-01-03T00:00:00.000Z').dir;
103
113
  const finalRaceClaim = claimTicket(finalRaceDir);
104
114
  const finalBarrier = join(temp, 'final-race-barrier');
105
115
  const finalReady = join(temp, 'final-race-ready');
106
116
  const cancelWorker = spawn(process.execPath, ['--conditions=crtr-src', '--import', 'tsx', 'src/core/human/__tests__/inbox-cancel-worker.ts', finalRaceDir, finalBarrier, finalReady], { cwd: process.cwd(), stdio: ['ignore', 'pipe', 'pipe'] });
107
117
  await waitFor([finalReady]);
108
118
  writeFileSync(finalBarrier, 'go');
109
- const finalRace = completeDeck(finalRaceDir, [{ id: 'go', selectedOptionId: 'yes' }], finalRaceClaim.token);
119
+ const finalRace = completePage(finalRaceDir, answer, finalRaceClaim.token);
110
120
  await new Promise((resolveWorker, rejectWorker) => cancelWorker.once('exit', (code) => code === 0 ? resolveWorker() : rejectWorker(new Error('cancel worker failed'))));
111
- assert.equal(readTicketResult(finalRaceDir)?.kind, finalRace.won ? 'deck' : 'canceled', 'submit and cancellation race has one canonical winner');
112
- const concurrentDir = submitDeck({ dir: ticketDir('concurrent-claim'), deck: deck('2025-01-03T00:00:00.000Z') }).dir;
121
+ assert.equal(readTicketResult(finalRaceDir)?.kind, finalRace.won ? 'page' : 'canceled', 'submit and cancellation race has one canonical winner');
122
+ const concurrentDir = page('concurrent-claim', '2025-01-03T00:00:00.000Z').dir;
113
123
  const claimBarrier = join(temp, 'claim-race-barrier');
114
124
  // Both workers block on this release until the parent has read both results, so
115
125
  // neither winner's pid dies during the peer's attempt (a dead same-host pid is
@@ -1,7 +1,7 @@
1
1
  import { existsSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { hostname } from 'node:os';
3
3
  import { randomUUID } from 'node:crypto';
4
- import { claimPath, deckPath, responsePath, reviewPath } from './convention.js';
4
+ import { claimPath, pageManifestPath, responsePath, reviewPath } from './convention.js';
5
5
  import { readJsonOrNull } from '../fs-utils.js';
6
6
  import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
7
7
  const REMOTE_STALE_MS = 30_000;
@@ -30,7 +30,7 @@ export function isStaleClaim(claim, now = Date.now(), localHost = hostname()) {
30
30
  /** Acquire a visible claim only for a still-pending ticket. Malformed crash artifacts are stale claims. */
31
31
  export function claimTicket(dir, opts = {}) {
32
32
  return withExclusiveDirectoryLock(`${dir}/.ticket-lock`, () => {
33
- if (existsSync(responsePath(dir)) || (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir))))
33
+ if (existsSync(responsePath(dir)) || (!existsSync(pageManifestPath(dir)) && !existsSync(reviewPath(dir))))
34
34
  return null;
35
35
  const path = claimPath(dir);
36
36
  const existing = readTicketClaim(dir);
@@ -1,5 +1,7 @@
1
- import type { Deck, InteractionResponse } from './types.js';
2
- export declare function deckPath(dir: string): string;
1
+ import type { PageManifest } from './page-schema.js';
2
+ /** The authored page source, colocated with its derived manifest in the bridge node directory. */
3
+ export declare function pagePath(dir: string, dialect: PageManifest['dialect']): string;
4
+ export declare function pageManifestPath(dir: string): string;
3
5
  export declare function reviewPath(dir: string): string;
4
6
  export declare function responsePath(dir: string): string;
5
7
  export declare function progressPath(dir: string): string;
@@ -13,6 +15,4 @@ export declare function requireTicket(dir: string): string;
13
15
  export declare function atomicWriteJson(path: string, value: unknown): void;
14
16
  /** Publish one immutable JSON record without replacing an existing winner. */
15
17
  export declare function publishJsonExclusive(path: string, value: unknown): boolean;
16
- export declare function writeResponse(dir: string, responses: InteractionResponse[], completedAt: string, deck?: Deck): string;
17
- export declare function writeProgress(dir: string, responses: InteractionResponse[]): void;
18
18
  export declare function clearProgress(dir: string): void;
@@ -1,13 +1,14 @@
1
1
  import { existsSync, linkSync, mkdirSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
- import { buildSummary } from './summary.js';
4
- export function deckPath(dir) { return `${dir}/deck.json`; }
3
+ /** The authored page source, colocated with its derived manifest in the bridge node directory. */
4
+ export function pagePath(dir, dialect) { return `${dir}/page.${dialect}`; }
5
+ export function pageManifestPath(dir) { return `${dir}/page.json`; }
5
6
  export function reviewPath(dir) { return `${dir}/review.json`; }
6
7
  export function responsePath(dir) { return `${dir}/response.json`; }
7
8
  export function progressPath(dir) { return `${dir}/progress.json`; }
8
9
  export function claimPath(dir) { return `${dir}/claim.json`; }
9
10
  export function interactionState(dir) {
10
- if (!existsSync(deckPath(dir)) && !existsSync(reviewPath(dir)))
11
+ if (!existsSync(pageManifestPath(dir)) && !existsSync(reviewPath(dir)))
11
12
  return 'missing';
12
13
  if (existsSync(responsePath(dir)))
13
14
  return 'resolved';
@@ -18,8 +19,8 @@ export function isClaimed(dir) { return existsSync(claimPath(dir)); }
18
19
  /** Resolve and verify a ticket directory without a registry. */
19
20
  export function requireTicket(dir) {
20
21
  const canonicalDir = realpathSync(dir);
21
- if (!existsSync(deckPath(canonicalDir)) && !existsSync(reviewPath(canonicalDir))) {
22
- throw new Error('ticket directory has no deck or review');
22
+ if (!existsSync(pageManifestPath(canonicalDir)) && !existsSync(reviewPath(canonicalDir))) {
23
+ throw new Error('ticket directory has no page or review');
23
24
  }
24
25
  return canonicalDir;
25
26
  }
@@ -47,14 +48,6 @@ export function publishJsonExclusive(path, value) {
47
48
  unlinkSync(tmp);
48
49
  }
49
50
  }
50
- export function writeResponse(dir, responses, completedAt, deck) {
51
- const summary = deck === undefined ? '' : buildSummary(deck, responses);
52
- atomicWriteJson(responsePath(dir), { schema: 'humanloop.response/v2', kind: 'deck', responses, summary, completedAt });
53
- return responsePath(dir);
54
- }
55
- export function writeProgress(dir, responses) {
56
- atomicWriteJson(progressPath(dir), { kind: 'deck', responses, savedAt: new Date().toISOString() });
57
- }
58
51
  export function clearProgress(dir) {
59
52
  try {
60
53
  unlinkSync(progressPath(dir));
@@ -0,0 +1,8 @@
1
+ /** Escape text for an HTML text node. */
2
+ export declare function escapeHtmlText(raw: string): string;
3
+ /** Escape a value for a double-quoted HTML attribute. */
4
+ export declare function escapeHtmlAttribute(raw: string): string;
5
+ /** Render one markdown document fragment as HTML. */
6
+ export declare function renderMarkdownToHtml(markdown: string): string;
7
+ /** Render inline markdown. Exported for callers rendering a single line. */
8
+ export declare function renderInline(source: string): string;