@north-light/crouter 0.3.221 → 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 (155) 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/plugins.md +10 -1
  49. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  50. package/dist/builtin-memory/plan/roadmap.md +6 -28
  51. package/dist/builtin-memory/spec/guide.md +19 -8
  52. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
  53. package/dist/clients/attach/render/markdown-source.js +106 -1
  54. package/dist/clients/attach/session/file-links.d.ts +13 -4
  55. package/dist/clients/attach/session/file-links.js +54 -58
  56. package/dist/clients/attach/viewer.js +525 -523
  57. package/dist/clients/inbox/controller.js +1 -1
  58. package/dist/clients/inbox/resolve.d.ts +1 -0
  59. package/dist/clients/inbox/review/review-client.js +3 -1
  60. package/dist/commands/__tests__/human.test.js +2 -2
  61. package/dist/commands/human/request.d.ts +2 -0
  62. package/dist/commands/human/request.js +281 -0
  63. package/dist/commands/human.js +5 -2
  64. package/dist/commands/sys/config.js +2 -2
  65. package/dist/commands/sys/doctor.js +54 -2
  66. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  67. package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
  68. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  69. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  70. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  71. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  72. package/dist/core/__tests__/human-actions.test.js +116 -0
  73. package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
  74. package/dist/core/__tests__/profile-project-memory-delivery.test.js +1 -1
  75. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  76. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  77. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  78. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  79. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  80. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  81. package/dist/core/canvas/db.js +23 -0
  82. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  83. package/dist/core/canvas/human-deliveries.js +75 -0
  84. package/dist/core/config.d.ts +13 -1
  85. package/dist/core/config.js +51 -1
  86. package/dist/core/feed/inbox.d.ts +6 -0
  87. package/dist/core/feed/inbox.js +9 -1
  88. package/dist/core/human/action-binding.d.ts +21 -0
  89. package/dist/core/human/action-binding.js +40 -0
  90. package/dist/core/human/completion.d.ts +38 -0
  91. package/dist/core/human/completion.js +27 -0
  92. package/dist/core/human/convention.d.ts +2 -0
  93. package/dist/core/human/convention.js +2 -0
  94. package/dist/core/human/tickets.d.ts +25 -6
  95. package/dist/core/human/tickets.js +19 -13
  96. package/dist/core/human/types.d.ts +5 -0
  97. package/dist/core/human-actions.d.ts +25 -0
  98. package/dist/core/human-actions.js +101 -0
  99. package/dist/core/memory-resolver.js +1 -1
  100. package/dist/core/profiles/select.d.ts +2 -0
  101. package/dist/core/profiles/select.js +21 -4
  102. package/dist/core/runtime/broker/frame-dispatch.js +2 -5
  103. package/dist/core/runtime/broker-inventory.d.ts +1 -2
  104. package/dist/core/runtime/broker-inventory.js +2 -77
  105. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  106. package/dist/core/runtime/broker.js +4 -4
  107. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  108. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  109. package/dist/core/runtime/command-surface.d.ts +8 -3
  110. package/dist/core/runtime/command-surface.js +42 -6
  111. package/dist/core/runtime/launch-target.d.ts +25 -0
  112. package/dist/core/runtime/launch-target.js +54 -0
  113. package/dist/core/runtime/persona.js +3 -3
  114. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  115. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  116. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  117. package/dist/core/runtime/prospective-inventory.js +88 -0
  118. package/dist/core/runtime/spawn.d.ts +3 -1
  119. package/dist/core/runtime/spawn.js +5 -3
  120. package/dist/core/substrate/on-read.js +17 -28
  121. package/dist/core/substrate/render-node.d.ts +3 -2
  122. package/dist/core/substrate/render-node.js +3 -2
  123. package/dist/core/substrate/render.js +51 -19
  124. package/dist/core/substrate/schema.d.ts +5 -1
  125. package/dist/core/substrate/schema.js +4 -4
  126. package/dist/core/user-settings.d.ts +4 -0
  127. package/dist/core/user-settings.js +1 -0
  128. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
  129. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  130. package/dist/daemon/api/handlers/human-requests.js +409 -0
  131. package/dist/daemon/api/handlers/human.js +3 -0
  132. package/dist/daemon/api/handlers/inbox.js +3 -0
  133. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  134. package/dist/daemon/api/handlers/nodes.js +11 -46
  135. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  136. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  137. package/dist/daemon/api/handlers/reviews.js +10 -2
  138. package/dist/daemon/api/server.js +4 -0
  139. package/dist/daemon/crtrd.js +6 -0
  140. package/dist/daemon/human/deliver-action.d.ts +16 -0
  141. package/dist/daemon/human/deliver-action.js +168 -0
  142. package/dist/daemon/human/finish.d.ts +8 -5
  143. package/dist/daemon/human/finish.js +45 -6
  144. package/dist/daemon/human/sweep.js +4 -1
  145. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  146. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  147. package/dist/daemon/review/finish.d.ts +8 -3
  148. package/dist/daemon/review/finish.js +19 -1
  149. package/dist/types.d.ts +8 -0
  150. package/dist/types.js +1 -0
  151. package/package.json +1 -1
  152. package/runtime.lock.json +2 -2
  153. package/dist/builtin-memory/00-runtime-base.md +0 -55
  154. package/dist/builtin-memory/design.md +0 -55
  155. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
@@ -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
+ });
@@ -14,7 +14,7 @@ import { resetScopeCache } from '../scope.js';
14
14
  import { clearSessionCache } from '../substrate/session-cache.js';
15
15
  import { memoryDir as nodeMemoryDir } from '../runtime/memory.js';
16
16
  import { buildRefInventory } from '../memory/inline-ref-inventory.js';
17
- import { gatewayMemoryRows } from '../runtime/broker-inventory.js';
17
+ import { gatewayMemoryRows } from '../runtime/chat-inventory-rows.js';
18
18
  // ---------------------------------------------------------------------------
19
19
  // Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
20
20
  // backed (node/project/user scope docs on disk), since precedence dedup,
@@ -191,7 +191,7 @@ test('workspace-open clamps each project store the same way boot does', () => {
191
191
  const opened = renderWorkspaceOpenDocs(newNode(), new Map());
192
192
  assert.ok(opened.includes(B.pcontent), `the content-capped project delivers its body; got:\n${opened}`);
193
193
  assert.ok(!opened.includes(B.ppreview), `the cwd project must not deliver its body; got:\n${opened}`);
194
- assert.match(opened, /name="t9-ppreview-doc"[^>]*>\nWhen the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
194
+ assert.match(opened, /name="t9-ppreview-doc" readWhen="When the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
195
195
  assert.match(opened, /name="t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
196
196
  assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
197
197
  assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
@@ -0,0 +1,91 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
7
+ import { captureChatCapabilities, registrationChatSurface } from '../runtime/command-surface.js';
8
+ test('startup capability capture preserves same-name lifecycle replacements and excludes new lifecycle registrations', async () => {
9
+ const root = mkdtempSync(join(tmpdir(), 'crtr-prospective-capability-'));
10
+ const agentDir = join(root, '.pi');
11
+ const invoked = join(root, 'late-invoked');
12
+ mkdirSync(agentDir);
13
+ const first = join(root, 'first.js');
14
+ const second = join(root, 'second.js');
15
+ writeFileSync(first, `
16
+ import { writeFileSync } from 'node:fs';
17
+ export default function (pi) {
18
+ pi.registerCommand('collision', {
19
+ description: 'first factory command',
20
+ gateway: true,
21
+ argumentHint: '<first>',
22
+ handler: async () => {},
23
+ });
24
+ pi.on('session_start', () => {
25
+ pi.registerCommand('collision', {
26
+ description: 'lifecycle replacement',
27
+ argumentHint: '<replacement>',
28
+ handler: async () => {},
29
+ });
30
+ pi.registerCommand('late', {
31
+ description: 'lifecycle command',
32
+ gateway: true,
33
+ handler: async () => writeFileSync(${JSON.stringify(invoked)}, 'yes'),
34
+ });
35
+ });
36
+ }
37
+ `);
38
+ writeFileSync(second, `
39
+ export default function (pi) {
40
+ pi.registerCommand('collision', {
41
+ description: 'second factory command',
42
+ gateway: true,
43
+ argumentHint: '<second>',
44
+ handler: async () => {},
45
+ });
46
+ }
47
+ `);
48
+ try {
49
+ const loader = new DefaultResourceLoader({
50
+ cwd: root,
51
+ agentDir,
52
+ additionalExtensionPaths: [first, second],
53
+ noContextFiles: true,
54
+ });
55
+ await loader.reload();
56
+ const loaded = loader.getExtensions();
57
+ assert.deepEqual(loaded.errors, []);
58
+ const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, root, undefined, undefined);
59
+ const startupCommands = runner.getRegisteredCommands();
60
+ captureChatCapabilities({ commands: startupCommands, templates: [] });
61
+ const startupRows = startupCommands.map((command) => ({
62
+ name: command.invocationName,
63
+ ...registrationChatSurface(command),
64
+ }));
65
+ assert.deepEqual(startupRows, [
66
+ { name: 'collision:1', gatewayCapable: true, argumentHint: '<first>' },
67
+ { name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
68
+ ]);
69
+ for (const extension of loaded.extensions) {
70
+ for (const handler of extension.handlers.get('session_start') ?? []) {
71
+ await handler({ type: 'session_start' }, {});
72
+ }
73
+ }
74
+ const reloadedRows = runner.getRegisteredCommands().map((command) => ({
75
+ name: command.invocationName,
76
+ ...registrationChatSurface(command),
77
+ }));
78
+ assert.deepEqual(reloadedRows, [
79
+ { name: 'collision:1', gatewayCapable: true, argumentHint: '<replacement>' },
80
+ { name: 'late', gatewayCapable: false },
81
+ { name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
82
+ ]);
83
+ const late = runner.getCommand('late');
84
+ assert.ok(late, 'the lifecycle command remains registered and addressable');
85
+ await late.handler('', {});
86
+ assert.equal(existsSync(invoked), true, 'the lifecycle command remains invokable');
87
+ }
88
+ finally {
89
+ rmSync(root, { recursive: true, force: true });
90
+ }
91
+ });
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawn } from 'node:child_process';
7
+ import { test } from 'node:test';
8
+ import { loadedCommandPreview } from '../../../clients/attach/slash/prompt-preview.js';
9
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
10
+ const EXTENSION = join(ROOT, 'dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts');
11
+ const LIVE_PROBE = join(ROOT, 'dist/core/__tests__/fixtures/memory-slash-live-probe.js');
12
+ const COMMAND_NAMES = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
13
+ function writeSlashDoc(memoryDir, name, description, body) {
14
+ writeFileSync(join(memoryDir, `${name}.md`), `---\nkind: knowledge\nwhen-and-why-to-read: Acceptance fixture.\nshort-form: ${description}\nslash: true\n---\n${body}`);
15
+ }
16
+ function liveProbe(input) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn(process.execPath, [LIVE_PROBE, EXTENSION, JSON.stringify(input.invocations)], {
19
+ cwd: input.cwd,
20
+ env: {
21
+ ...process.env,
22
+ CRTR_HOME: input.home,
23
+ CRTR_NODE_ID: input.nodeId,
24
+ CRTR_PROFILE_ID: '',
25
+ PI_CODING_AGENT_DIR: join(input.cwd, '.pi'),
26
+ },
27
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
28
+ });
29
+ let stdout = '';
30
+ let stderr = '';
31
+ let response = '';
32
+ const stdoutStream = child.stdout;
33
+ const stderrStream = child.stderr;
34
+ const responseStream = child.stdio[3];
35
+ if (stdoutStream === null || stderrStream === null || responseStream === null) {
36
+ child.kill('SIGKILL');
37
+ reject(new Error('live probe response channel unavailable'));
38
+ return;
39
+ }
40
+ stdoutStream.setEncoding('utf8');
41
+ stderrStream.setEncoding('utf8');
42
+ responseStream.setEncoding('utf8');
43
+ stdoutStream.on('data', (chunk) => { stdout += chunk; });
44
+ stderrStream.on('data', (chunk) => { stderr += chunk; });
45
+ responseStream.on('data', (chunk) => { response += chunk; });
46
+ child.on('error', reject);
47
+ child.on('close', (code, signal) => {
48
+ if (code !== 0) {
49
+ reject(new Error(`live probe exited ${code ?? signal ?? 'unknown'}: ${stderr}${stdout}`));
50
+ return;
51
+ }
52
+ resolve(JSON.parse(response));
53
+ });
54
+ });
55
+ }
56
+ function fixtureRows(rows) {
57
+ return COMMAND_NAMES.map((name) => {
58
+ const row = rows.find((candidate) => candidate.name === name);
59
+ assert.ok(row, `Gateway advertises /${name}`);
60
+ return row;
61
+ });
62
+ }
63
+ test('node-relative memory slash commands omit preview metadata without changing live sends', async () => {
64
+ const cwd = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
65
+ const home = join(cwd, 'canvas');
66
+ const memoryDir = join(cwd, '.crouter', 'memory');
67
+ const nodeId = 'acceptance-live-node';
68
+ mkdirSync(memoryDir, { recursive: true });
69
+ writeSlashDoc(memoryDir, COMMAND_NAMES[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
70
+ writeSlashDoc(memoryDir, COMMAND_NAMES[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
71
+ writeSlashDoc(memoryDir, COMMAND_NAMES[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
72
+ try {
73
+ const live = await liveProbe({
74
+ cwd,
75
+ home,
76
+ nodeId,
77
+ invocations: COMMAND_NAMES.map((name) => ({ name, args: 'evidence' })),
78
+ });
79
+ assert.equal(live.stateBeforeProspective, false);
80
+ assert.equal(live.stateAfterProspective, false);
81
+ assert.deepEqual(live.commands, live.prospective.commands);
82
+ assert.deepEqual(live.memoryRefs, live.prospective.memory_refs);
83
+ const prospectiveRows = fixtureRows(live.prospective.commands);
84
+ const liveRows = fixtureRows(live.commands);
85
+ assert.deepEqual(liveRows, prospectiveRows);
86
+ assert.deepEqual(prospectiveRows, [
87
+ { name: COMMAND_NAMES[0], description: 'Context-relative fixture.', source: 'command' },
88
+ { name: COMMAND_NAMES[1], description: 'Node-relative fixture.', source: 'command' },
89
+ {
90
+ name: COMMAND_NAMES[2],
91
+ description: 'Node-independent fixture.',
92
+ source: 'command',
93
+ expansion: {
94
+ kind: 'memory-slash',
95
+ commandName: COMMAND_NAMES[2],
96
+ body: 'Independent payload: $ARGUMENTS.\n',
97
+ },
98
+ },
99
+ ]);
100
+ const insightsInit = live.commands.find((row) => row.name === 'insights:init');
101
+ assert.ok(insightsInit, 'Gateway completion includes /insights:init');
102
+ assert.equal(insightsInit.expansion, undefined);
103
+ assert.equal(loadedCommandPreview('/insights:init agent-facing CLI', live.commands), undefined);
104
+ assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands)?.content, 'Independent payload: first evidence.\n');
105
+ assert.equal(loadedCommandPreview('/acceptance-independent changed evidence', live.commands)?.content, 'Independent payload: changed evidence.\n');
106
+ assert.deepEqual(live.notifications, []);
107
+ const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
108
+ assert.deepEqual(sentContent, {
109
+ [COMMAND_NAMES[0]]: `Store evidence in ${join(home, 'nodes', nodeId, 'context', 'result.md')}.\n`,
110
+ [COMMAND_NAMES[1]]: `Node ${nodeId} received evidence.\n`,
111
+ [COMMAND_NAMES[2]]: 'Independent payload: evidence.\n',
112
+ });
113
+ for (const send of live.sent) {
114
+ assert.deepEqual(send.message, {
115
+ customType: 'memory-slash-command',
116
+ content: sentContent[send.name],
117
+ display: true,
118
+ });
119
+ assert.deepEqual(send.options, { triggerTurn: true });
120
+ }
121
+ assert.doesNotMatch(sentContent[COMMAND_NAMES[0]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
122
+ assert.doesNotMatch(sentContent[COMMAND_NAMES[1]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
123
+ }
124
+ finally {
125
+ rmSync(cwd, { recursive: true, force: true });
126
+ }
127
+ });
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { test } from 'node:test';
6
+ import { runProspectiveInventory } from '../../runtime/prospective-inventory.js';
7
+ test('prospective preflight keeps package stdout out of its response channel', async () => {
8
+ const cwd = mkdtempSync(join(tmpdir(), 'crtr-prospective-stdout-'));
9
+ const extension = join(cwd, 'loud-extension.mjs');
10
+ writeFileSync(extension, `
11
+ export default function (pi) {
12
+ console.log('harmless package diagnostic');
13
+ pi.registerCommand('stdout-probe', {
14
+ description: 'survives package stdout',
15
+ gateway: true,
16
+ handler: async () => {},
17
+ });
18
+ }
19
+ `);
20
+ try {
21
+ const rows = await runProspectiveInventory({ cwd, profileId: null, extensions: [extension] });
22
+ assert.deepEqual(rows.commands.find((row) => row.name === 'stdout-probe'), {
23
+ name: 'stdout-probe',
24
+ description: 'survives package stdout',
25
+ source: 'command',
26
+ });
27
+ }
28
+ finally {
29
+ rmSync(cwd, { recursive: true, force: true });
30
+ }
31
+ });
@@ -1153,6 +1153,28 @@ ALTER TABLE crons ADD COLUMN held INTEGER NOT NULL DEFAULT 0;
1153
1153
  ALTER TABLE crons ADD COLUMN last_poke_at TEXT;
1154
1154
  `);
1155
1155
  }
1156
+ /** v33 — durable, daemon-owned attempts to deliver settled human completions. */
1157
+ function addHumanActionDeliveries(db) {
1158
+ db.exec(`
1159
+ CREATE TABLE human_action_deliveries (
1160
+ request_id TEXT PRIMARY KEY,
1161
+ state TEXT NOT NULL,
1162
+ attempt INTEGER NOT NULL DEFAULT 0,
1163
+ argv_json TEXT NOT NULL,
1164
+ cwd TEXT NOT NULL,
1165
+ completion_json TEXT NOT NULL,
1166
+ next_attempt_at INTEGER NOT NULL,
1167
+ claimed_at INTEGER,
1168
+ claim_owner TEXT,
1169
+ accepted_at INTEGER,
1170
+ permanent_failed_at INTEGER,
1171
+ last_failure_json TEXT,
1172
+ created_at INTEGER NOT NULL,
1173
+ updated_at INTEGER NOT NULL
1174
+ );
1175
+ CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
1176
+ `);
1177
+ }
1156
1178
  /** The journaled lane of the state-migration registry, adapted to this chain's
1157
1179
  * step shape. Registry order is preserved, so these versions stay stable as
1158
1180
  * long as the registry is append-only (same contract as the local list). */
@@ -1198,6 +1220,7 @@ export const MIGRATIONS = [
1198
1220
  /* v30 */ dropConsultOutbox,
1199
1221
  /* v31 */ addReviewSubmitRequested,
1200
1222
  /* v32 */ addCronHeldColumns,
1223
+ /* v33 */ addHumanActionDeliveries,
1201
1224
  ...JOURNALED_STATE_MIGRATIONS,
1202
1225
  ];
1203
1226
  /** Migration indexes that manage their OWN transaction and therefore must not
@@ -0,0 +1,53 @@
1
+ export type ActionDeliveryState = 'pending' | 'running' | 'accepted' | 'permanent_failed';
2
+ export interface ActionDeliveryFailure {
3
+ kind: 'exit' | 'signal' | 'timeout' | 'spawn_error';
4
+ exitCode?: number;
5
+ signal?: string;
6
+ message?: string;
7
+ stderr?: string;
8
+ }
9
+ export interface ActionDeliveryRecord {
10
+ requestId: string;
11
+ state: ActionDeliveryState;
12
+ attempt: number;
13
+ argv: string[];
14
+ cwd: string;
15
+ completion: unknown;
16
+ nextAttemptAt: number;
17
+ claimedAt: number | null;
18
+ claimOwner: string | null;
19
+ acceptedAt: number | null;
20
+ permanentFailedAt: number | null;
21
+ lastFailure: ActionDeliveryFailure | null;
22
+ createdAt: number;
23
+ updatedAt: number;
24
+ }
25
+ /** Insert the one delivery row for a settled action-bound request. Idempotent
26
+ * on requestId: a replayed settlement must not create a second row. */
27
+ export declare function enqueueActionDelivery(row: {
28
+ requestId: string;
29
+ argv: string[];
30
+ cwd: string;
31
+ /** Complete HumanCompletionV1 with delivery.attempt set to 0. */
32
+ completion: unknown;
33
+ }): void;
34
+ /** Delivery state for `GET /v1/human/requests/:id`; null when no row exists. */
35
+ export declare function readActionDelivery(requestId: string): ActionDeliveryRecord | null;
36
+ /** Due pending records in deterministic order for the daemon's serial tick. */
37
+ export declare function dueActionDeliveries(now: number): ActionDeliveryRecord[];
38
+ /** Atomically start one due delivery attempt. A null result lost the claim. */
39
+ export declare function claimActionDelivery(requestId: string, now: number, claimOwner: string): ActionDeliveryRecord | null;
40
+ export type ActionDeliverySettlement = {
41
+ state: 'accepted';
42
+ } | {
43
+ state: 'permanent_failed';
44
+ failure: ActionDeliveryFailure;
45
+ } | {
46
+ state: 'pending';
47
+ nextAttemptAt: number;
48
+ failure: ActionDeliveryFailure;
49
+ };
50
+ /** Settle only the still-owned running attempt, so a stale process cannot overwrite a newer claim. */
51
+ export declare function settleActionDelivery(requestId: string, attempt: number, claimOwner: string, settlement: ActionDeliverySettlement, now: number): boolean;
52
+ /** A new daemon deliberately redelivers interrupted completions at least once. */
53
+ export declare function recoverRunningActionDeliveries(now: number): number;