@agfpd/iapeer 0.2.76 → 0.2.78

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 (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +2 -2
  3. package/package.json +21 -2
  4. package/src/cli/index.ts +47 -48
  5. package/src/connect/index.ts +4 -4
  6. package/src/core/constants.ts +3 -3
  7. package/src/core/normalize.ts +2 -2
  8. package/src/create/index.ts +1 -1
  9. package/src/daemon/index.ts +5 -5
  10. package/src/daemon/main.ts +9 -9
  11. package/src/enable/index.ts +19 -18
  12. package/src/enable/provisionCommand.ts +10 -10
  13. package/src/identity/index.ts +2 -2
  14. package/src/identity/profileStandard.ts +8 -8
  15. package/src/init/index.ts +20 -13
  16. package/src/install/index.ts +2 -2
  17. package/src/install/signing.ts +3 -4
  18. package/src/launch/adapters/claude.ts +7 -7
  19. package/src/launch/adapters/codex.ts +5 -6
  20. package/src/launch/canary.ts +8 -8
  21. package/src/launch/cmdlog.ts +3 -3
  22. package/src/launch/composeSystemPrompt.ts +2 -1
  23. package/src/launch/index.ts +5 -6
  24. package/src/launch/invocation.ts +1 -1
  25. package/src/launch/launchd.ts +14 -15
  26. package/src/launch/launchdRun.ts +6 -6
  27. package/src/launch/nativeMemory.ts +9 -9
  28. package/src/launch/ptyHost.ts +1 -1
  29. package/src/launch/readyGateModel.ts +3 -3
  30. package/src/launch/types.ts +7 -7
  31. package/src/lifecycle/eventlog.ts +2 -2
  32. package/src/lifecycle/index.ts +21 -21
  33. package/src/lifecycle/queue.ts +1 -1
  34. package/src/onboard/index.ts +19 -13
  35. package/src/onboard/memory.ts +5 -5
  36. package/src/onboard/steps.ts +8 -9
  37. package/src/provision/index.ts +2 -3
  38. package/src/runtime/deploy.ts +2 -2
  39. package/src/runtime/index.ts +4 -4
  40. package/src/shadow/gate.ts +6 -6
  41. package/src/shadow/index.ts +2 -2
  42. package/src/status/index.ts +11 -11
  43. package/src/storage/index.ts +1 -1
  44. package/src/supervisor/daemon.ts +14 -8
  45. package/src/transport/index.ts +11 -11
  46. package/src/update/index.ts +4 -4
  47. package/src/cli/cli.test.ts +0 -603
  48. package/src/cli/listTui.test.ts +0 -71
  49. package/src/codec/codec.test.ts +0 -271
  50. package/src/connect/connect.test.ts +0 -163
  51. package/src/core/constants.test.ts +0 -32
  52. package/src/core/normalize.test.ts +0 -98
  53. package/src/create/create.test.ts +0 -143
  54. package/src/daemon/daemon-http.test.ts +0 -114
  55. package/src/daemon/daemon.test.ts +0 -267
  56. package/src/daemon/main.test.ts +0 -413
  57. package/src/enable/enable.test.ts +0 -95
  58. package/src/enable/provisionCommand.test.ts +0 -107
  59. package/src/identity/identity.test.ts +0 -279
  60. package/src/identity/profileStandard.test.ts +0 -204
  61. package/src/init/init.test.ts +0 -213
  62. package/src/init/runtime-resolve.test.ts +0 -49
  63. package/src/install/install.test.ts +0 -66
  64. package/src/install/signing.test.ts +0 -84
  65. package/src/launch/bootdeliver.test.ts +0 -108
  66. package/src/launch/bootstrap.test.ts +0 -56
  67. package/src/launch/canary.test.ts +0 -346
  68. package/src/launch/cmdlog.test.ts +0 -94
  69. package/src/launch/codexHooksTrust.test.ts +0 -248
  70. package/src/launch/composeSystemPrompt.layers.test.ts +0 -445
  71. package/src/launch/composeSystemPrompt.test.ts +0 -145
  72. package/src/launch/hostComposerOccupied.test.ts +0 -63
  73. package/src/launch/invocation.test.ts +0 -174
  74. package/src/launch/launch.test.ts +0 -359
  75. package/src/launch/launchd.test.ts +0 -499
  76. package/src/launch/nativeMemory.test.ts +0 -193
  77. package/src/launch/ptyHost.test.ts +0 -38
  78. package/src/launch/readyGateModel.test.ts +0 -72
  79. package/src/launch/sockdir.test.ts +0 -74
  80. package/src/lifecycle/eventlog.test.ts +0 -114
  81. package/src/lifecycle/lifecycle.test.ts +0 -1056
  82. package/src/lifecycle/queue.test.ts +0 -185
  83. package/src/onboard/memory.test.ts +0 -157
  84. package/src/onboard/onboard.test.ts +0 -39
  85. package/src/onboard/steps.test.ts +0 -201
  86. package/src/provision/provision.test.ts +0 -288
  87. package/src/registry/registry.test.ts +0 -432
  88. package/src/runtime/runtime.test.ts +0 -226
  89. package/src/runtime/update.test.ts +0 -192
  90. package/src/shadow/enumerate-skip.test.ts +0 -67
  91. package/src/shadow/gate.test.ts +0 -286
  92. package/src/shadow/install.test.ts +0 -153
  93. package/src/shadow/shadow.test.ts +0 -42
  94. package/src/status/status.test.ts +0 -208
  95. package/src/storage/peers-home.test.ts +0 -34
  96. package/src/storage/rotatelog.test.ts +0 -44
  97. package/src/storage/storage.test.ts +0 -65
  98. package/src/supervisor/attached-marker.test.ts +0 -84
  99. package/src/supervisor/boot.test.ts +0 -66
  100. package/src/supervisor/client.attach.test.ts +0 -107
  101. package/src/supervisor/daemon.test.ts +0 -213
  102. package/src/supervisor/deliver.test.ts +0 -58
  103. package/src/supervisor/panelog.test.ts +0 -95
  104. package/src/supervisor/protocol.test.ts +0 -222
  105. package/src/supervisor/selfargv.test.ts +0 -22
  106. package/src/supervisor/spinner-strip.test.ts +0 -52
  107. package/src/transport/compactDone.test.ts +0 -144
  108. package/src/transport/composerQueue.test.ts +0 -204
  109. package/src/transport/hostControl.test.ts +0 -87
  110. package/src/transport/hostDeliver.test.ts +0 -137
  111. package/src/transport/hostLive.test.ts +0 -50
  112. package/src/transport/routesend-wake.test.ts +0 -357
  113. package/src/update/update.test.ts +0 -208
@@ -1,445 +0,0 @@
1
- // Layer 3 (registry) + Layer 4 (other domains) tests for composeSystemPrompt,
2
- // plus the FS-discovery gatherPromptInput. The golden file (composeSystemPrompt
3
- // .test.ts) proves layers 1+2 stay byte-for-byte; THIS file proves the new
4
- // layers render correctly, in order (general → specific), and that empty/absent
5
- // inputs add nothing (backward-compat with the legacy bytes).
6
-
7
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
8
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { composeSystemPrompt, gatherPromptInput } from './composeSystemPrompt.ts'
12
- import type { ComposePromptInput, PromptDomainBlock, PublicPeerSummary } from './types.ts'
13
-
14
- const base: Omit<ComposePromptInput, 'peers' | 'pluginDomains'> = {
15
- personality: 'iapeer',
16
- description: 'impl',
17
- cwd: '/Users/macmini/Peers/iapeer',
18
- platform: 'darwin',
19
- osVersion: '15.5',
20
- user: 'macmini',
21
- hostname: 'macmini',
22
- today: '2026-06-06',
23
- peerDoctrine: 'Be the implementer.\n',
24
- globalDoctrine: 'GLOBAL\n',
25
- }
26
-
27
- const peerA: PublicPeerSummary = {
28
- personality: 'boris',
29
- runtime: 'claude',
30
- runtimes: ['claude'],
31
- description: 'PM partner',
32
- intelligence: 'artificial',
33
- }
34
- const peerB: PublicPeerSummary = {
35
- personality: 'arthur',
36
- runtime: 'telegram',
37
- runtimes: ['telegram', 'claude'],
38
- description: '',
39
- intelligence: 'natural',
40
- }
41
-
42
- // ─────────────────────────────────────────────────────────────────────────────
43
- // Pure renderer — Layers 3 + 4
44
- // ─────────────────────────────────────────────────────────────────────────────
45
-
46
- describe('composeSystemPrompt — layer 3 (registry)', () => {
47
- test('empty/omitted peers add NOTHING (legacy bytes preserved)', () => {
48
- const legacy = composeSystemPrompt(base)
49
- expect(composeSystemPrompt({ ...base, peers: [] })).toBe(legacy)
50
- expect(composeSystemPrompt({ ...base, peers: undefined })).toBe(legacy)
51
- expect(legacy).not.toContain('## Known peers')
52
- })
53
-
54
- test('renders WITH descriptions + all 5 normalized fields (boris refinement #1)', () => {
55
- const out = composeSystemPrompt({ ...base, peers: [peerA] })
56
- expect(out).toContain('## Known peers')
57
- expect(out).toContain('- boris — PM partner')
58
- expect(out).toContain(' runtime: claude')
59
- expect(out).toContain(' runtimes: claude')
60
- expect(out).toContain(' intelligence: artificial')
61
- })
62
-
63
- test('a peer with an empty description renders no " — " suffix', () => {
64
- const out = composeSystemPrompt({ ...base, peers: [peerB] })
65
- expect(out).toContain('- arthur\n')
66
- expect(out).not.toContain('- arthur — ')
67
- expect(out).toContain(' runtimes: telegram, claude')
68
- })
69
-
70
- test('layer 3 sits AFTER the doctrine (layer 2), separated by a blank line', () => {
71
- const out = composeSystemPrompt({ ...base, peers: [peerA] })
72
- expect(out.indexOf('Be the implementer.')).toBeLessThan(out.indexOf('## Known peers'))
73
- expect(out).toContain('Be the implementer.\n\n## Known peers')
74
- })
75
- })
76
-
77
- describe('composeSystemPrompt — layer 4 (other domains)', () => {
78
- const dom = (over: Partial<PromptDomainBlock>): PromptDomainBlock => ({ domain: 'X', ...over })
79
-
80
- test('empty/omitted domains add NOTHING (legacy bytes preserved)', () => {
81
- const legacy = composeSystemPrompt(base)
82
- expect(composeSystemPrompt({ ...base, pluginDomains: [] })).toBe(legacy)
83
- expect(composeSystemPrompt({ ...base, pluginDomains: undefined })).toBe(legacy)
84
- })
85
-
86
- test('a presence-marker domain (empty halves) emits no text and no dangling separator (boris refinement #2)', () => {
87
- const legacy = composeSystemPrompt(base)
88
- // a 0-byte marker → both halves '' → contributes nothing
89
- expect(composeSystemPrompt({ ...base, pluginDomains: [dom({ local: '' })] })).toBe(legacy)
90
- expect(composeSystemPrompt({ ...base, pluginDomains: [dom({ global: '', local: '' })] })).toBe(legacy)
91
- // a marker next to a real domain must not inject a blank block between them
92
- const out = composeSystemPrompt({
93
- ...base,
94
- pluginDomains: [dom({ domain: 'MARKER', local: '' }), dom({ domain: 'REAL', global: 'real-content\n' })],
95
- })
96
- expect(out).toContain('real-content')
97
- expect(out).not.toMatch(/\n{4,}/) // no 3+ blank lines = no empty block injected
98
- })
99
-
100
- test('within a domain, global precedes local (general → specific), each its own section', () => {
101
- const out = composeSystemPrompt({
102
- ...base,
103
- pluginDomains: [dom({ domain: 'MM', global: 'mm-global', local: 'mm-local' })],
104
- })
105
- // per-file now: global and local are SEPARATE sections (blank line between),
106
- // not a tight single-newline merge
107
- expect(out).toContain('mm-global\n\nmm-local')
108
- expect(out.indexOf('mm-global')).toBeLessThan(out.indexOf('mm-local'))
109
- })
110
-
111
- test('per-file path markers: each half carries its <!-- path --> marker', () => {
112
- const out = composeSystemPrompt({
113
- ...base,
114
- pluginDomains: [
115
- dom({ domain: 'MM', global: 'mm-global\n', globalPath: '~/.iapeer/MERGEMIND.md', local: 'mm-local\n', localPath: '~/p/.iapeer/MERGEMIND.md' }),
116
- ],
117
- })
118
- expect(out).toContain('<!-- ~/.iapeer/MERGEMIND.md -->\nmm-global')
119
- expect(out).toContain('<!-- ~/p/.iapeer/MERGEMIND.md -->\nmm-local')
120
- })
121
-
122
- test('layer 4 sits after layer 3; domains kept in the order given', () => {
123
- const out = composeSystemPrompt({
124
- ...base,
125
- peers: [peerA],
126
- pluginDomains: [dom({ domain: 'AAA', global: 'aaa' }), dom({ domain: 'ZZZ', local: 'zzz' })],
127
- })
128
- expect(out.indexOf('## Known peers')).toBeLessThan(out.indexOf('aaa'))
129
- expect(out.indexOf('aaa')).toBeLessThan(out.indexOf('zzz'))
130
- expect(out).toContain('aaa\n\nzzz')
131
- })
132
-
133
- test('full 4-layer ordering: YAML → doctrine → registry → domains', () => {
134
- const out = composeSystemPrompt({ ...base, peers: [peerA], pluginDomains: [dom({ global: 'domain-text' })] })
135
- const iYaml = out.indexOf('personality:')
136
- const iDoctrine = out.indexOf('Be the implementer.')
137
- const iReg = out.indexOf('## Known peers')
138
- const iDom = out.indexOf('domain-text')
139
- expect(iYaml).toBeGreaterThanOrEqual(0)
140
- expect(iYaml).toBeLessThan(iDoctrine)
141
- expect(iDoctrine).toBeLessThan(iReg)
142
- expect(iReg).toBeLessThan(iDom)
143
- })
144
- })
145
-
146
- describe('composeSystemPrompt — layer 5 (doctrine fragments)', () => {
147
- const frag = (over: Partial<PromptDomainBlock>): PromptDomainBlock => ({ domain: 'F', ...over })
148
-
149
- test('empty/omitted fragments add NOTHING (legacy bytes preserved)', () => {
150
- const legacy = composeSystemPrompt(base)
151
- expect(composeSystemPrompt({ ...base, promptFragments: [] })).toBe(legacy)
152
- expect(composeSystemPrompt({ ...base, promptFragments: undefined })).toBe(legacy)
153
- })
154
-
155
- test('a presence-marker fragment (empty halves) emits no text and no dangling separator', () => {
156
- const legacy = composeSystemPrompt(base)
157
- expect(composeSystemPrompt({ ...base, promptFragments: [frag({ local: '' })] })).toBe(legacy)
158
- expect(composeSystemPrompt({ ...base, promptFragments: [frag({ global: '', local: '' })] })).toBe(legacy)
159
- const out = composeSystemPrompt({
160
- ...base,
161
- promptFragments: [frag({ domain: 'EMPTY', local: '' }), frag({ domain: 'REAL', local: 'idx-content\n' })],
162
- })
163
- expect(out).toContain('idx-content')
164
- expect(out).not.toMatch(/\n{4,}/)
165
- })
166
-
167
- test('within a fragment stem, global (host-wide guide) precedes local (per-peer index)', () => {
168
- const out = composeSystemPrompt({
169
- ...base,
170
- promptFragments: [frag({ domain: 'MEM', global: 'mem-guide', local: 'mem-index' })],
171
- })
172
- expect(out).toContain('mem-guide\n\nmem-index') // per-file: separate sections
173
- expect(out.indexOf('mem-guide')).toBeLessThan(out.indexOf('mem-index'))
174
- })
175
-
176
- test('layer 5 sits AFTER layer 4 (most-volatile last), separated by a blank line', () => {
177
- const out = composeSystemPrompt({
178
- ...base,
179
- pluginDomains: [{ domain: 'D', global: 'domain-text' }],
180
- promptFragments: [{ domain: 'MEM', local: 'fragment-text' }],
181
- })
182
- expect(out.indexOf('domain-text')).toBeLessThan(out.indexOf('fragment-text'))
183
- expect(out).toContain('domain-text\n\nfragment-text')
184
- })
185
-
186
- test('full 5-layer ordering: YAML → doctrine → registry → domains → fragments', () => {
187
- const out = composeSystemPrompt({
188
- ...base,
189
- peers: [peerA],
190
- pluginDomains: [{ domain: 'D', global: 'domain-text' }],
191
- promptFragments: [{ domain: 'MEM', global: 'fragment-text' }],
192
- })
193
- const order = [
194
- out.indexOf('personality:'),
195
- out.indexOf('Be the implementer.'),
196
- out.indexOf('## Known peers'),
197
- out.indexOf('domain-text'),
198
- out.indexOf('fragment-text'),
199
- ]
200
- expect(order.every(i => i >= 0)).toBe(true)
201
- expect(order).toEqual([...order].sort((a, b) => a - b))
202
- })
203
- })
204
-
205
- // ─────────────────────────────────────────────────────────────────────────────
206
- // gatherPromptInput — FS discovery over ~/.iapeer + <cwd>/.iapeer
207
- // ─────────────────────────────────────────────────────────────────────────────
208
-
209
- describe('gatherPromptInput — FS discovery of all 4 layers', () => {
210
- let root: string
211
- let globalRoot: string
212
- let cwd: string
213
- let localRoot: string
214
-
215
- beforeAll(() => {
216
- root = mkdtempSync(join(tmpdir(), 'iapeer-gather-'))
217
- globalRoot = join(root, 'home', '.iapeer')
218
- cwd = join(root, 'peer')
219
- localRoot = join(cwd, '.iapeer')
220
- mkdirSync(globalRoot, { recursive: true })
221
- mkdirSync(localRoot, { recursive: true })
222
-
223
- // Layer 2 — IAPEER.md, global + local
224
- writeFileSync(join(globalRoot, 'IAPEER.md'), 'GLOBAL DOCTRINE\n')
225
- writeFileSync(join(localRoot, 'IAPEER.md'), 'LOCAL ROLE\n')
226
-
227
- // Layer 4 — domains: both halves / global-only / local-only / 0-byte marker
228
- writeFileSync(join(globalRoot, 'MERGEMIND.md'), 'mm-global\n')
229
- writeFileSync(join(localRoot, 'MERGEMIND.md'), 'mm-local\n')
230
- writeFileSync(join(globalRoot, 'AAA.md'), 'aaa-global-only\n')
231
- writeFileSync(join(localRoot, 'ZZZ.md'), 'zzz-local-only\n')
232
- writeFileSync(join(localRoot, 'MARKER.md'), '') // presence marker, must not merge
233
- // a non-.md file must be ignored
234
- writeFileSync(join(localRoot, 'peer-profile.json'), '{}')
235
-
236
- // Layer 5 — fragments/ subdir of both roots: a paired stem (host-wide guide +
237
- // per-peer index) and a global-only fragment; a non-.md file must be ignored.
238
- const globalFrags = join(globalRoot, 'fragments')
239
- const localFrags = join(localRoot, 'fragments')
240
- mkdirSync(globalFrags, { recursive: true })
241
- mkdirSync(localFrags, { recursive: true })
242
- writeFileSync(join(globalFrags, 'iapeer-memory.md'), 'mem-guide\n') // host-wide guide
243
- writeFileSync(join(localFrags, 'iapeer-memory.md'), 'mem-index\n') // per-peer index
244
- writeFileSync(join(globalFrags, 'other-primitive.md'), 'other-frag\n')
245
- writeFileSync(join(localFrags, 'notes.txt'), 'ignored') // non-.md, must be ignored
246
-
247
- // Layer 3 — registry at the global root (deliberately UNSORTED on disk)
248
- const index = {
249
- version: 2,
250
- peers: [
251
- { personality: 'boris', runtime: 'claude', runtimes: ['claude'], description: 'PM', intelligence: 'artificial', cwd: '/p/boris' },
252
- { personality: 'arthur', runtime: 'telegram', runtimes: ['telegram', 'claude'], description: 'owner', intelligence: 'natural', cwd: '/p/arthur' },
253
- ],
254
- }
255
- writeFileSync(join(globalRoot, 'peers-profiles.json'), JSON.stringify(index))
256
- })
257
- afterAll(() => rmSync(root, { recursive: true, force: true }))
258
-
259
- function gather() {
260
- return gatherPromptInput({
261
- personality: 'iapeer',
262
- description: 'impl',
263
- cwd,
264
- platform: 'darwin',
265
- osVersion: '15.5',
266
- user: 'u',
267
- hostname: 'h',
268
- today: '2026-06-06',
269
- globalRoot,
270
- })
271
- }
272
-
273
- test('layer 2: IAPEER.md read as global + local', () => {
274
- const input = gather()
275
- expect(input.globalDoctrine).toBe('GLOBAL DOCTRINE\n')
276
- expect(input.peerDoctrine).toBe('LOCAL ROLE\n')
277
- })
278
-
279
- test('layer 4: IAPEER.md excluded; other domains discovered, sorted, non-.md ignored', () => {
280
- const input = gather()
281
- expect((input.pluginDomains ?? []).map(d => d.domain)).toEqual(['AAA', 'MARKER', 'MERGEMIND', 'ZZZ'])
282
- })
283
-
284
- test('layer 4: per-domain global/local halves resolved correctly, each with its display path', () => {
285
- const input = gather()
286
- const byName = Object.fromEntries((input.pluginDomains ?? []).map(d => [d.domain, d]))
287
- expect(byName.MERGEMIND).toEqual({
288
- domain: 'MERGEMIND',
289
- global: 'mm-global\n',
290
- globalPath: join(globalRoot, 'MERGEMIND.md'),
291
- local: 'mm-local\n',
292
- localPath: join(localRoot, 'MERGEMIND.md'),
293
- })
294
- expect(byName.AAA).toEqual({ domain: 'AAA', global: 'aaa-global-only\n', globalPath: join(globalRoot, 'AAA.md') })
295
- expect(byName.ZZZ).toEqual({ domain: 'ZZZ', local: 'zzz-local-only\n', localPath: join(localRoot, 'ZZZ.md') })
296
- expect(byName.MARKER).toEqual({ domain: 'MARKER', local: '', localPath: join(localRoot, 'MARKER.md') }) // present but empty
297
- })
298
-
299
- test('layer 5: fragments/ subdir scanned, paired by stem (guide global + index local), sorted', () => {
300
- const input = gather()
301
- const frags = input.promptFragments ?? []
302
- expect(frags.map(f => f.domain)).toEqual(['iapeer-memory', 'other-primitive'])
303
- const byName = Object.fromEntries(frags.map(f => [f.domain, f]))
304
- const gfrag = join(globalRoot, 'fragments')
305
- const lfrag = join(localRoot, 'fragments')
306
- expect(byName['iapeer-memory']).toEqual({
307
- domain: 'iapeer-memory',
308
- global: 'mem-guide\n',
309
- globalPath: join(gfrag, 'iapeer-memory.md'),
310
- local: 'mem-index\n',
311
- localPath: join(lfrag, 'iapeer-memory.md'),
312
- })
313
- expect(byName['other-primitive']).toEqual({ domain: 'other-primitive', global: 'other-frag\n', globalPath: join(gfrag, 'other-primitive.md') })
314
- // the non-.md notes.txt never became a fragment
315
- expect(frags.map(f => f.domain)).not.toContain('notes')
316
- })
317
-
318
- test('layer 5: fragments are NOT confused with the root-level Layer-4 domains', () => {
319
- const input = gather()
320
- // the fragments dir itself is a directory, not a `.md` file → never a Layer-4 domain
321
- expect((input.pluginDomains ?? []).map(d => d.domain)).not.toContain('fragments')
322
- })
323
-
324
- test('layer 3: registry projected to EXACTLY 5 fields, sorted by personality', () => {
325
- const input = gather()
326
- expect((input.peers ?? []).map(p => p.personality)).toEqual(['arthur', 'boris'])
327
- for (const p of input.peers ?? []) {
328
- expect(Object.keys(p).sort()).toEqual(['description', 'intelligence', 'personality', 'runtime', 'runtimes'])
329
- }
330
- expect((input.peers ?? [])[0]).toEqual({
331
- personality: 'arthur',
332
- runtime: 'telegram',
333
- runtimes: ['telegram', 'claude'],
334
- description: 'owner',
335
- intelligence: 'natural',
336
- })
337
- })
338
-
339
- test('CHECKPOINT — the assembled prompt of a real peer contains all 4 layers, in order', () => {
340
- const prompt = composeSystemPrompt(gather())
341
- // Layer 1
342
- expect(prompt).toContain('---\npersonality: "iapeer"')
343
- // Layer 2
344
- expect(prompt).toContain('GLOBAL DOCTRINE')
345
- expect(prompt).toContain('LOCAL ROLE')
346
- // Layer 3 (with descriptions)
347
- expect(prompt).toContain('## Known peers')
348
- expect(prompt).toContain('- arthur — owner')
349
- expect(prompt).toContain('- boris — PM')
350
- // Layer 4 (presence-marker file excluded, others per-file with path markers, sorted)
351
- expect(prompt).toContain('aaa-global-only')
352
- expect(prompt).toContain('zzz-local-only')
353
- // global half then a fresh marker then the local half (each its own marked section)
354
- expect(prompt).toContain(`<!-- ${join(globalRoot, 'MERGEMIND.md')} -->\nmm-global`)
355
- expect(prompt).toContain(`mm-global\n\n<!-- ${join(localRoot, 'MERGEMIND.md')} -->\nmm-local`)
356
- // Layer 5 (fragments: guide+index paired as separate marked sections, global-only fragment)
357
- expect(prompt).toContain(`<!-- ${join(globalRoot, 'fragments', 'iapeer-memory.md')} -->\nmem-guide`)
358
- expect(prompt).toContain(`mem-guide\n\n<!-- ${join(localRoot, 'fragments', 'iapeer-memory.md')} -->\nmem-index`)
359
- expect(prompt).toContain('other-frag')
360
-
361
- const order = [
362
- prompt.indexOf('personality: "iapeer"'),
363
- prompt.indexOf('LOCAL ROLE'),
364
- prompt.indexOf('## Known peers'),
365
- prompt.indexOf('aaa-global-only'),
366
- prompt.indexOf('mem-guide'), // Layer 5 — after every Layer-4 domain
367
- ]
368
- expect(order).toEqual([...order].sort((a, b) => a - b))
369
- expect(prompt.indexOf('zzz-local-only')).toBeLessThan(prompt.indexOf('mem-guide'))
370
- // marker file produced no content and no blank-block artifact
371
- expect(prompt).not.toContain('MARKER')
372
- expect(prompt).not.toMatch(/\n{4,}/)
373
- })
374
-
375
- test('absent IAPEER.md → peerDoctrine "" (does not throw); empty dirs → no domains/peers', () => {
376
- const bareCwd = join(root, 'bare')
377
- const bareGlobal = join(root, 'bareglobal', '.iapeer')
378
- mkdirSync(join(bareCwd, '.iapeer'), { recursive: true })
379
- mkdirSync(bareGlobal, { recursive: true })
380
- const input = gatherPromptInput({
381
- personality: 'bare', description: '', cwd: bareCwd,
382
- platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
383
- globalRoot: bareGlobal,
384
- })
385
- expect(input.peerDoctrine).toBe('')
386
- expect(input.globalDoctrine).toBeUndefined()
387
- expect(input.pluginDomains).toEqual([])
388
- expect(input.promptFragments).toEqual([]) // no fragments/ subdir → empty layer
389
- expect(input.peers).toEqual([])
390
- // composes to just the YAML block (no doctrine, no layers); the per-file
391
- // assembly normalizes the single section's trailing newlines to exactly one.
392
- const prompt = composeSystemPrompt(input)
393
- expect(prompt).not.toContain('## Known peers')
394
- expect(prompt).not.toContain('<!--') // no files → no markers
395
- expect(prompt.endsWith('---\n')).toBe(true)
396
- expect(prompt.endsWith('---\n\n')).toBe(false)
397
- })
398
-
399
- test('doctrine excluded from Layer 4 case-INSENSITIVELY; .MD ext picked up (verify edge-lens fix)', () => {
400
- // A separate tree so a lowercase iapeer.md cannot collide with the uppercase
401
- // IAPEER.md of the main fixture on a case-insensitive volume.
402
- const ciCwd = join(root, 'ci')
403
- const ciLocal = join(ciCwd, '.iapeer')
404
- const ciGlobal = join(root, 'ciglobal', '.iapeer')
405
- mkdirSync(ciLocal, { recursive: true })
406
- mkdirSync(ciGlobal, { recursive: true })
407
- writeFileSync(join(ciLocal, 'iapeer.md'), 'lowercase doctrine\n') // doctrine, lowercase name
408
- writeFileSync(join(ciLocal, 'OTHER.md'), 'other domain\n')
409
- writeFileSync(join(ciLocal, 'UP.MD'), 'upper ext domain\n') // uppercase extension
410
-
411
- const input = gatherPromptInput({
412
- personality: 'ci', description: '', cwd: ciCwd,
413
- platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
414
- globalRoot: ciGlobal, peers: [],
415
- })
416
- const domains = (input.pluginDomains ?? []).map(d => d.domain)
417
- // The doctrine (whatever its on-disk case) must NEVER be a Layer-4 domain —
418
- // before the fix, on a case-sensitive FS 'iapeer' would leak in here.
419
- expect(domains).not.toContain('iapeer')
420
- expect(domains).toContain('OTHER')
421
- expect(domains).toContain('UP') // .MD upper-ext now included
422
-
423
- // And the doctrine text is never duplicated: at most once (Layer 2 on a
424
- // case-insensitive FS; zero on a case-sensitive FS where IAPEER.md is absent).
425
- const prompt = composeSystemPrompt(input)
426
- expect(prompt.split('lowercase doctrine').length - 1).toBeLessThanOrEqual(1)
427
- })
428
-
429
- test('gatherPromptInput honors a passed-in peers projection without a second registry read', () => {
430
- // Point globalRoot at a dir with NO peers-profiles.json — if gather tried to
431
- // read the registry it would just get []. We pass peers explicitly and expect
432
- // them through (sorted), proving the thread-through path works.
433
- const provided: PublicPeerSummary[] = [
434
- { personality: 'zeta', runtime: 'claude', runtimes: ['claude'], description: 'z', intelligence: 'artificial' },
435
- { personality: 'alpha', runtime: 'codex', runtimes: ['codex'], description: 'a', intelligence: 'artificial' },
436
- ]
437
- const input = gatherPromptInput({
438
- personality: 'x', description: '', cwd: join(root, 'noreg'),
439
- platform: 'darwin', osVersion: '1', user: 'u', hostname: 'h', today: '2026-06-06',
440
- globalRoot: join(root, 'noreg-global'), peers: provided,
441
- })
442
- expect((input.peers ?? []).map(p => p.personality)).toEqual(['alpha', 'zeta']) // sorted, not mutated input
443
- expect(provided.map(p => p.personality)).toEqual(['zeta', 'alpha']) // caller array untouched
444
- })
445
- })
@@ -1,145 +0,0 @@
1
- // composeSystemPrompt — the YAML identity block (Layer 1) stays byte-for-byte the
2
- // claude-start.sh:264-290 jq layout (the durable golden); the file-sourced layers
3
- // (2/4/5) are now rendered PER FILE, each prefixed by a `<!-- <path> -->` marker.
4
- // This test pins BOTH: the jq-equivalent YAML prefix, and the marker format /
5
- // empty-file-drops rules of the per-file assembly.
6
-
7
- import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
8
- import { spawnSync } from 'child_process'
9
- import { mkdtempSync, rmSync, writeFileSync } from 'fs'
10
- import { tmpdir } from 'os'
11
- import { join } from 'path'
12
- import { composeSystemPrompt } from './composeSystemPrompt.ts'
13
- import type { ComposePromptInput } from './types.ts'
14
-
15
- let dir: string
16
- let jqProg: string
17
-
18
- // The jq program verbatim from claude-start.sh:274-284 — the YAML identity block.
19
- const JQ_PROGRAM = `"---",
20
- "personality: \\($personality | @json)",
21
- "description: \\($description | @json)",
22
- "peer-cwd: \\($peer_cwd | @json)",
23
- "platform: \\($platform | @json)",
24
- "os_version: \\($os_version | @json)",
25
- "user: \\($user | @json)",
26
- "hostname: \\($hostname | @json)",
27
- "today: \\($today | @json)",
28
- "---",
29
- ""`
30
-
31
- function shq(v: string): string {
32
- return `'${v.replace(/'/g, `'\\''`)}'`
33
- }
34
-
35
- beforeAll(() => {
36
- dir = mkdtempSync(join(tmpdir(), 'iapeer-csp-'))
37
- jqProg = join(dir, 'prog.jq')
38
- writeFileSync(jqProg, JQ_PROGRAM)
39
- })
40
- afterAll(() => rmSync(dir, { recursive: true, force: true }))
41
-
42
- /** The jq YAML identity block alone (no doctrine appended) — the byte-for-byte
43
- * Layer-1 golden every composed prompt must START with. */
44
- function jqYamlBlock(input: ComposePromptInput): string {
45
- const script = `set -e
46
- jq -n --raw-output \
47
- --arg personality ${shq(input.personality)} \
48
- --arg description ${shq(input.description)} \
49
- --arg peer_cwd ${shq(input.cwd)} \
50
- --arg platform ${shq(input.platform)} \
51
- --arg os_version ${shq(input.osVersion)} \
52
- --arg user ${shq(input.user)} \
53
- --arg hostname ${shq(input.hostname)} \
54
- --arg today ${shq(input.today)} \
55
- -f ${shq(jqProg)}`
56
- const r = spawnSync('bash', ['-c', script], { encoding: 'utf8' })
57
- if (r.status !== 0) throw new Error(`bash reference failed: ${r.stderr}`)
58
- return r.stdout
59
- }
60
-
61
- const base = {
62
- personality: 'arthur',
63
- cwd: '/Users/macmini/Peers/arthur',
64
- platform: 'darwin',
65
- osVersion: '15.5',
66
- user: 'macmini',
67
- hostname: 'macmini',
68
- today: '2026-06-06',
69
- }
70
-
71
- // ─────────────────────────────────────────────────────────────────────────────
72
- // Layer 1 — the YAML identity block is byte-for-byte the jq layout
73
- // ─────────────────────────────────────────────────────────────────────────────
74
-
75
- describe('composeSystemPrompt — Layer 1 YAML block is byte-for-byte the jq layout', () => {
76
- const cases: Array<{ name: string; input: ComposePromptInput }> = [
77
- { name: 'basic', input: { ...base, description: 'owner', peerDoctrine: 'Be helpful.\n' } },
78
- { name: 'hostile description: colon + quote + newline', input: { ...base, description: 'a: "b"\nc', peerDoctrine: 'doc\n' } },
79
- { name: 'unicode + emoji description', input: { ...base, description: 'Артур 🦊 №2', peerDoctrine: 'док\n' } },
80
- { name: 'description with backslash and slash', input: { ...base, description: 'a\\b/c', peerDoctrine: 'x\n' } },
81
- { name: 'empty description', input: { ...base, description: '', peerDoctrine: 'x\n' } },
82
- ]
83
- for (const c of cases) {
84
- test(c.name, () => {
85
- const yaml = jqYamlBlock(c.input)
86
- // the composed prompt opens with the exact jq YAML block (the `---…---\n\n`)
87
- expect(composeSystemPrompt(c.input).startsWith(yaml)).toBe(true)
88
- })
89
- }
90
- })
91
-
92
- // ─────────────────────────────────────────────────────────────────────────────
93
- // Layer 2 — IAPEER.md doctrine is rendered per-file with a path marker
94
- // ─────────────────────────────────────────────────────────────────────────────
95
-
96
- describe('composeSystemPrompt — per-file path markers (Layer 2 doctrine)', () => {
97
- const input: ComposePromptInput = {
98
- ...base,
99
- description: 'owner',
100
- peerDoctrine: 'Be the implementer.\n',
101
- peerDoctrinePath: '~/Peers/arthur/.iapeer/IAPEER.md',
102
- globalDoctrine: 'Host rules.\n',
103
- globalDoctrinePath: '~/.iapeer/IAPEER.md',
104
- }
105
-
106
- test('each doctrine file is prefixed by its <!-- path --> marker, global before local', () => {
107
- const out = composeSystemPrompt(input)
108
- expect(out).toContain('<!-- ~/.iapeer/IAPEER.md -->\nHost rules.')
109
- expect(out).toContain('<!-- ~/Peers/arthur/.iapeer/IAPEER.md -->\nBe the implementer.')
110
- expect(out.indexOf('<!-- ~/.iapeer/IAPEER.md -->')).toBeLessThan(
111
- out.indexOf('<!-- ~/Peers/arthur/.iapeer/IAPEER.md -->'),
112
- )
113
- })
114
-
115
- test('marker sits AFTER the YAML block (one blank line between)', () => {
116
- const out = composeSystemPrompt(input)
117
- expect(out).toContain('---\n\n<!-- ~/.iapeer/IAPEER.md -->')
118
- })
119
-
120
- test('a path-less content (pure-renderer caller) renders WITHOUT a marker', () => {
121
- const out = composeSystemPrompt({ ...base, description: 'd', peerDoctrine: 'no marker here\n' })
122
- expect(out).toContain('no marker here')
123
- expect(out).not.toContain('<!--')
124
- })
125
-
126
- test('an EMPTY doctrine file contributes no section and no dangling marker', () => {
127
- const out = composeSystemPrompt({
128
- ...base,
129
- description: 'd',
130
- peerDoctrine: 'real\n',
131
- peerDoctrinePath: '~/p/IAPEER.md',
132
- globalDoctrine: '', // present but empty → dropped
133
- globalDoctrinePath: '~/.iapeer/IAPEER.md',
134
- })
135
- expect(out).not.toContain('~/.iapeer/IAPEER.md') // empty global marker absent
136
- expect(out).toContain('<!-- ~/p/IAPEER.md -->\nreal')
137
- })
138
-
139
- test('output ends with exactly one newline; no 3+ blank-line gaps', () => {
140
- const out = composeSystemPrompt(input)
141
- expect(out.endsWith('\n')).toBe(true)
142
- expect(out.endsWith('\n\n')).toBe(false)
143
- expect(out).not.toMatch(/\n{3,}/)
144
- })
145
- })
@@ -1,63 +0,0 @@
1
- // Spawn-flip cutover Block 2, Ф0b-2 slice 2 — hosted composer-occupancy DETECTION.
2
- //
3
- // paneLogComposerOccupied reads a supervisor-HOSTED session's pane-log model and runs the burn-in-
4
- // validated composerOccupancyFromModel (REUSED from the leaf, not reimplemented). These suites are
5
- // boris's slice-2 acceptance: the guard FIRES on a busy-composer model-state and stays cold on an
6
- // empty / dim-ghost composer — proven by injecting real rendered frames into a pane-log. The end-to-
7
- // end hold (attached human + warm-deliver respects the guard) is validated with the attach client in
8
- // Ф0b-3; here we bind the DETECTOR. Bun-native @xterm is loaded dynamically by the function under test.
9
- import { afterEach, describe, expect, test } from 'bun:test'
10
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
11
- import { tmpdir } from 'node:os'
12
- import { join } from 'node:path'
13
- import { paneLogComposerOccupied } from './readyGateModel.ts'
14
-
15
- const COLS = 80
16
- const ROWS = 24
17
-
18
- let dir: string | null = null
19
- function paneLogWith(frame: string): string {
20
- dir = mkdtempSync(join(tmpdir(), 'iapeer-occ-'))
21
- const log = join(dir, 'pane.log')
22
- writeFileSync(log, Buffer.from(frame, 'utf8')) // raw bytes — preserve the multibyte ❯ / › glyphs
23
- return log
24
- }
25
- afterEach(() => {
26
- if (dir) rmSync(dir, { recursive: true, force: true })
27
- dir = null
28
- })
29
-
30
- describe('paneLogComposerOccupied — guard FIRES on a busy hosted composer (slice-2 acceptance)', () => {
31
- test('claude ❯ with human text → BUSY (true)', async () => {
32
- const log = paneLogWith('\x1b[24;1H❯ the quick brown fox')
33
- expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(true)
34
- })
35
-
36
- test('codex › with human text → BUSY (true)', async () => {
37
- const log = paneLogWith('\x1b[24;1H› reviewer note')
38
- expect(await paneLogComposerOccupied(log, COLS, ROWS, 'codex')).toBe(true)
39
- })
40
- })
41
-
42
- describe('paneLogComposerOccupied — guard stays COLD on a free composer', () => {
43
- test('claude ❯ empty → free (false)', async () => {
44
- const log = paneLogWith('\x1b[24;1H❯ ')
45
- expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
46
- })
47
-
48
- test('dim-ghost placeholder (grey246 + dim) → NOT human → free (false)', async () => {
49
- const log = paneLogWith('\x1b[24;1H❯ \x1b[2m\x1b[38;5;246mTry "fix typecheck errors"\x1b[0m')
50
- expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
51
- })
52
-
53
- test('no prompt glyph rendered → free (false)', async () => {
54
- const log = paneLogWith('\x1b[24;1Hjust some output, no composer')
55
- expect(await paneLogComposerOccupied(log, COLS, ROWS, 'claude')).toBe(false)
56
- })
57
- })
58
-
59
- describe('paneLogComposerOccupied — uncertainty is conservative (BUSY)', () => {
60
- test('missing pane-log → BUSY (true): never paste into a maybe-occupied composer', async () => {
61
- expect(await paneLogComposerOccupied(join(tmpdir(), 'iapeer-occ-nonexistent.log'), COLS, ROWS, 'claude')).toBe(true)
62
- })
63
- })