@agfpd/iapeer 0.2.32 → 0.2.34

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.
@@ -97,15 +97,28 @@ describe('composeSystemPrompt — layer 4 (other domains)', () => {
97
97
  expect(out).not.toMatch(/\n{4,}/) // no 3+ blank lines = no empty block injected
98
98
  })
99
99
 
100
- test('within a domain, global precedes local (general → specific)', () => {
100
+ test('within a domain, global precedes local (general → specific), each its own section', () => {
101
101
  const out = composeSystemPrompt({
102
102
  ...base,
103
103
  pluginDomains: [dom({ domain: 'MM', global: 'mm-global', local: 'mm-local' })],
104
104
  })
105
- expect(out).toContain('mm-global\nmm-local')
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')
106
108
  expect(out.indexOf('mm-global')).toBeLessThan(out.indexOf('mm-local'))
107
109
  })
108
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
+
109
122
  test('layer 4 sits after layer 3; domains kept in the order given', () => {
110
123
  const out = composeSystemPrompt({
111
124
  ...base,
@@ -156,7 +169,7 @@ describe('composeSystemPrompt — layer 5 (doctrine fragments)', () => {
156
169
  ...base,
157
170
  promptFragments: [frag({ domain: 'MEM', global: 'mem-guide', local: 'mem-index' })],
158
171
  })
159
- expect(out).toContain('mem-guide\nmem-index')
172
+ expect(out).toContain('mem-guide\n\nmem-index') // per-file: separate sections
160
173
  expect(out.indexOf('mem-guide')).toBeLessThan(out.indexOf('mem-index'))
161
174
  })
162
175
 
@@ -268,13 +281,19 @@ describe('gatherPromptInput — FS discovery of all 4 layers', () => {
268
281
  expect((input.pluginDomains ?? []).map(d => d.domain)).toEqual(['AAA', 'MARKER', 'MERGEMIND', 'ZZZ'])
269
282
  })
270
283
 
271
- test('layer 4: per-domain global/local halves resolved correctly', () => {
284
+ test('layer 4: per-domain global/local halves resolved correctly, each with its display path', () => {
272
285
  const input = gather()
273
286
  const byName = Object.fromEntries((input.pluginDomains ?? []).map(d => [d.domain, d]))
274
- expect(byName.MERGEMIND).toEqual({ domain: 'MERGEMIND', global: 'mm-global\n', local: 'mm-local\n' })
275
- expect(byName.AAA).toEqual({ domain: 'AAA', global: 'aaa-global-only\n' })
276
- expect(byName.ZZZ).toEqual({ domain: 'ZZZ', local: 'zzz-local-only\n' })
277
- expect(byName.MARKER).toEqual({ domain: 'MARKER', local: '' }) // present but empty
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
278
297
  })
279
298
 
280
299
  test('layer 5: fragments/ subdir scanned, paired by stem (guide global + index local), sorted', () => {
@@ -282,8 +301,16 @@ describe('gatherPromptInput — FS discovery of all 4 layers', () => {
282
301
  const frags = input.promptFragments ?? []
283
302
  expect(frags.map(f => f.domain)).toEqual(['iapeer-memory', 'other-primitive'])
284
303
  const byName = Object.fromEntries(frags.map(f => [f.domain, f]))
285
- expect(byName['iapeer-memory']).toEqual({ domain: 'iapeer-memory', global: 'mem-guide\n', local: 'mem-index\n' })
286
- expect(byName['other-primitive']).toEqual({ domain: 'other-primitive', global: 'other-frag\n' })
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') })
287
314
  // the non-.md notes.txt never became a fragment
288
315
  expect(frags.map(f => f.domain)).not.toContain('notes')
289
316
  })
@@ -320,12 +347,15 @@ describe('gatherPromptInput — FS discovery of all 4 layers', () => {
320
347
  expect(prompt).toContain('## Known peers')
321
348
  expect(prompt).toContain('- arthur — owner')
322
349
  expect(prompt).toContain('- boris — PM')
323
- // Layer 4 (markers excluded, others merged, sorted)
350
+ // Layer 4 (presence-marker file excluded, others per-file with path markers, sorted)
324
351
  expect(prompt).toContain('aaa-global-only')
325
- expect(prompt).toContain('mm-global\nmm-local')
326
352
  expect(prompt).toContain('zzz-local-only')
327
- // Layer 5 (fragments: guide+index paired, global-only fragment)
328
- expect(prompt).toContain('mem-guide\nmem-index')
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`)
329
359
  expect(prompt).toContain('other-frag')
330
360
 
331
361
  const order = [
@@ -357,10 +387,13 @@ describe('gatherPromptInput — FS discovery of all 4 layers', () => {
357
387
  expect(input.pluginDomains).toEqual([])
358
388
  expect(input.promptFragments).toEqual([]) // no fragments/ subdir → empty layer
359
389
  expect(input.peers).toEqual([])
360
- // composes to just the YAML block (+ empty doctrine), no extra layers
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.
361
392
  const prompt = composeSystemPrompt(input)
362
393
  expect(prompt).not.toContain('## Known peers')
363
- expect(prompt.endsWith('---\n\n')).toBe(true)
394
+ expect(prompt).not.toContain('<!--') // no files → no markers
395
+ expect(prompt.endsWith('---\n')).toBe(true)
396
+ expect(prompt.endsWith('---\n\n')).toBe(false)
364
397
  })
365
398
 
366
399
  test('doctrine excluded from Layer 4 case-INSENSITIVELY; .MD ext picked up (verify edge-lens fix)', () => {
@@ -1,8 +1,8 @@
1
- // Golden-equivalence test: composeSystemPrompt MUST produce byte-for-byte the
2
- // same output as the real jq doctrine-merge in claude-start.sh:264-290. Each
3
- // case runs the ACTUAL jq pipeline (extracted verbatim) and the TS function,
4
- // then byte-compares. This is the durable form of the Ф3 adversarial-verify
5
- // (which caught a 1-byte divergence on the empty-global-file case).
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
6
 
7
7
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
8
8
  import { spawnSync } from 'child_process'
@@ -15,7 +15,7 @@ import type { ComposePromptInput } from './types.ts'
15
15
  let dir: string
16
16
  let jqProg: string
17
17
 
18
- // The jq program verbatim from claude-start.sh:274-284.
18
+ // The jq program verbatim from claude-start.sh:274-284 — the YAML identity block.
19
19
  const JQ_PROGRAM = `"---",
20
20
  "personality: \\($personality | @json)",
21
21
  "description: \\($description | @json)",
@@ -39,16 +39,9 @@ beforeAll(() => {
39
39
  })
40
40
  afterAll(() => rmSync(dir, { recursive: true, force: true }))
41
41
 
42
- /** Run the exact bash/jq pipeline; `globalDoctrine` undefined = file absent. */
43
- function bashReference(input: ComposePromptInput): string {
44
- const peerPath = join(dir, 'peer.md')
45
- writeFileSync(peerPath, input.peerDoctrine)
46
- let globalBlock = ''
47
- if (input.globalDoctrine !== undefined) {
48
- const gp = join(dir, 'global.md')
49
- writeFileSync(gp, input.globalDoctrine)
50
- globalBlock = `if [ -f ${shq(gp)} ]; then cat ${shq(gp)}; printf "\\n"; fi`
51
- }
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 {
52
45
  const script = `set -e
53
46
  jq -n --raw-output \
54
47
  --arg personality ${shq(input.personality)} \
@@ -59,9 +52,7 @@ jq -n --raw-output \
59
52
  --arg user ${shq(input.user)} \
60
53
  --arg hostname ${shq(input.hostname)} \
61
54
  --arg today ${shq(input.today)} \
62
- -f ${shq(jqProg)}
63
- ${globalBlock}
64
- cat ${shq(peerPath)}`
55
+ -f ${shq(jqProg)}`
65
56
  const r = spawnSync('bash', ['-c', script], { encoding: 'utf8' })
66
57
  if (r.status !== 0) throw new Error(`bash reference failed: ${r.stderr}`)
67
58
  return r.stdout
@@ -77,22 +68,78 @@ const base = {
77
68
  today: '2026-06-06',
78
69
  }
79
70
 
80
- const cases: Array<{ name: string; input: ComposePromptInput }> = [
81
- { name: 'basic, no global', input: { ...base, description: 'owner', peerDoctrine: 'Be helpful.\n' } },
82
- { name: 'present non-empty global', input: { ...base, description: 'owner', peerDoctrine: 'peer\n', globalDoctrine: 'GLOBAL\n' } },
83
- { name: 'EMPTY-but-present global (the 1-byte divergence the verify caught)', input: { ...base, description: 'owner', peerDoctrine: 'peer', globalDoctrine: '' } },
84
- { name: 'hostile description: colon + quote + newline', input: { ...base, description: 'a: "b"\nc', peerDoctrine: 'doc\n' } },
85
- { name: 'unicode + emoji description', input: { ...base, description: 'Артур 🦊 №2', peerDoctrine: 'док\n', globalDoctrine: 'гл\n' } },
86
- { name: 'multiline peer doctrine, no trailing newline', input: { ...base, description: 'd', peerDoctrine: 'line1\nline2' } },
87
- { name: 'description with backslash and slash', input: { ...base, description: 'a\\b/c', peerDoctrine: 'x\n' } },
88
- ]
89
-
90
- describe('composeSystemPrompt golden-equivalence vs claude-start.sh jq', () => {
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
+ ]
91
83
  for (const c of cases) {
92
84
  test(c.name, () => {
93
- const ref = bashReference(c.input)
94
- const got = composeSystemPrompt(c.input)
95
- expect(got).toBe(ref)
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)
96
88
  })
97
89
  }
98
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
+ })
@@ -55,10 +55,33 @@ function jqJson(value: string): string {
55
55
  .replace(new RegExp(String.fromCharCode(0x00), 'g'), '\\u0000')
56
56
  }
57
57
 
58
+ /** A per-file section: a path marker line, then the file content. The marker is an
59
+ * HTML comment — inert in rendered markdown yet plainly visible in the raw system-
60
+ * prompt the model reads (verified: the model's context surfaces HTML comments) —
61
+ * so a reader (human or agent) can tell, per file, exactly which path each chunk of
62
+ * the assembled prompt came from. No marker when `path` is absent (a pure-renderer
63
+ * caller, or the synthesized Layer-1 YAML / Layer-3 registry which are not files). */
64
+ function markedSection(path: string | undefined, content: string): string {
65
+ return path ? `<!-- ${path} -->\n${content}` : content
66
+ }
67
+
68
+ /** Trailing-newline-trimmed length test: a 0-byte or newline-only file is "empty"
69
+ * and contributes no section (and so no dangling marker). */
70
+ function hasContent(s: string | undefined): s is string {
71
+ return s !== undefined && s.replace(/\n+$/, '').length > 0
72
+ }
73
+
58
74
  /**
59
- * Compose the merged system prompt (YAML identity block + optional global
60
- * doctrine + per-peer doctrine), GOLDEN byte-for-byte equivalent to the
61
- * claude-start.sh:264-290 jq pipeline.
75
+ * Compose the merged system prompt: the YAML identity block (Layer 1) followed by
76
+ * every file-sourced layer rendered PER FILE — each injected `.md` prefixed by a
77
+ * path marker (`<!-- <path> -->`). Layers, general → specific:
78
+ * 1. YAML identity block (synthesized, no marker) — byte-for-byte the jq layout.
79
+ * 2. IAPEER.md doctrine: global then local, each its own marked section.
80
+ * 3. Normalized peer registry (generated, no marker).
81
+ * 4. Other `<DOMAIN>.md` at the .iapeer root — each half (global, local) marked.
82
+ * 5. fragments/ `*.md` — each half marked. Most-volatile, sits LAST.
83
+ * Empty files contribute nothing (no section, no marker). Sections are joined by a
84
+ * single blank line and the output ends with exactly one newline.
62
85
  */
63
86
  export function composeSystemPrompt(input: ComposePromptInput): string {
64
87
  // The eight identity lines, in the bash's exact key order. Keys verbatim:
@@ -77,38 +100,37 @@ export function composeSystemPrompt(input: ComposePromptInput): string {
77
100
  // jq's final "" array element → the bare blank line after the closing '---'.
78
101
  '\n'
79
102
 
80
- // Global doctrine sits between the YAML block and the per-peer doctrine so
81
- // per-peer rules override global (specific over general). Bash gates on file
82
- // EXISTENCE (`[ -f "$GLOBAL_DOCTRINE" ]`), NOT non-emptiness — a present but
83
- // EMPTY global file still emits `cat "" + printf "\n"` = "\n". So the contract
84
- // is: a present file → globalDoctrine is the string (possibly ""), absent file
85
- // → undefined. Guard on `!== undefined` (NOT truthiness) to match bash exactly;
86
- // an empty "" then yields "" + "\n" = "\n" (the 1-byte golden divergence the
87
- // adversarial verify caught). The trailing "\n" mirrors the unconditional
88
- // `printf "\n"` after `cat "$GLOBAL"`.
89
- const global = input.globalDoctrine !== undefined ? input.globalDoctrine + '\n' : ''
103
+ // The YAML identity block is the only Layer-1 section (synthesized, no marker).
104
+ const sections: string[] = [yaml]
105
+
106
+ // Layer 2 — IAPEER.md doctrine, GLOBAL then LOCAL (general → specific), each its
107
+ // OWN marked file-section. The bash existence-gate (present-but-empty global → a
108
+ // bare "\n") is dropped deliberately: an empty doctrine file now contributes no
109
+ // section (and no dangling marker), like every other empty file. IAPEER.md carries
110
+ // the only FIXED semantics in the assembly (host = general peer rules, local = this
111
+ // peer's rules — the CLAUDE.md analogue); its meaning is conveyed by the well-known
112
+ // filename + path, so the marker stays a bare path like every other file's.
113
+ if (hasContent(input.globalDoctrine)) {
114
+ sections.push(markedSection(input.globalDoctrinePath, input.globalDoctrine))
115
+ }
116
+ if (hasContent(input.peerDoctrine)) {
117
+ sections.push(markedSection(input.peerDoctrinePath, input.peerDoctrine))
118
+ }
90
119
 
91
- // Layers 1+2: YAML block + global doctrine + per-peer doctrine verbatim
92
- // (`cat "$DOCTRINE"`), no trailing newline added. BYTE-IDENTICAL to the legacy
93
- // jq output — the golden test pins this exactly.
94
- const layer12 = yaml + global + input.peerDoctrine
120
+ // Layer 3 — normalized peer registry (generated from the index, not a file → no
121
+ // marker). Empty list → nothing.
122
+ const registry = renderRegistry(input.peers ?? [])
123
+ if (registry.length > 0) sections.push(registry)
95
124
 
96
- // Layer 3 (registry), Layer 4 (operator/plugin domains) and Layer 5 (primitive
97
- // doctrine fragments) are appended ONLY when they have content. Layer 5 sits LAST
98
- // — it is the most volatile (machine-regenerated on every source change), so it
99
- // never disturbs the stable prefix (YAML + doctrine + registry + domains) above it.
100
- const layer3 = renderRegistry(input.peers ?? [])
101
- const layer4 = renderMergedBlocks(input.pluginDomains ?? [])
102
- const layer5 = renderMergedBlocks(input.promptFragments ?? [])
103
- const sections = [layer12, layer3, layer4, layer5].filter(section => section.length > 0)
125
+ // Layers 4 (other <DOMAIN>.md) then 5 (fragments/, most-volatile, LAST) — each
126
+ // file half (global, local) is its own marked section. Empty halves drop out.
127
+ pushMarkedBlocks(sections, input.pluginDomains ?? [])
128
+ pushMarkedBlocks(sections, input.promptFragments ?? [])
104
129
 
105
- // ONE section (no registry, no extra domains) → the legacy bytes, UNTOUCHED —
106
- // the golden test pins layer12 exactly (peerDoctrine verbatim, incl. its own
107
- // trailing-newline-or-not). MULTIPLE sections → normalize every seam to exactly
108
- // one blank line: strip each section's trailing newlines, join with '\n\n', end
109
- // with a single '\n'. This makes the output robust to whether the source files
110
- // carried trailing newlines (a present-but-newline-only tail never widens a gap).
111
- if (sections.length === 1) return sections[0]
130
+ // Normalize every seam to exactly one blank line: strip each section's trailing
131
+ // newlines, join with '\n\n', end with a single '\n'. Robust to whether source
132
+ // files carried trailing newlines. A bare-doctrine peer is now [YAML, local
133
+ // IAPEER.md] → two sections, so it always carries its doctrine's path marker.
112
134
  return sections.map(section => section.replace(/\n+$/, '')).join('\n\n') + '\n'
113
135
  }
114
136
 
@@ -132,32 +154,23 @@ function renderRegistry(peers: readonly PublicPeerSummary[]): string {
132
154
  }
133
155
 
134
156
  // ─────────────────────────────────────────────────────────────────────────────
135
- // Layers 4 & 5 — merged global/local blocks. Shared renderer:
157
+ // Layers 4 & 5 — per-file marked sections:
136
158
  // • Layer 4: every non-IAPEER `<DOMAIN>.md` pair at the .iapeer/ root (operator /
137
159
  // plugin user-settings; SPAWNER_INSTRUCTIONS.md, … flow in organically).
138
160
  // • Layer 5: every `<STEM>.md` pair in .iapeer/fragments/ (primitive-owned,
139
161
  // machine-regenerated doctrine fragments — iapeer-memory's guide + note index).
140
- // Both merge general → specific (global then local). The merge is ORGANIC: stems
141
- // are used only for stable ordering (done by the gatherer), never emitted. An empty
142
- // file is a presence marker (Канал B) and contributes no text — so a fragment dir
143
- // that drops to a 0-byte placeholder, or whose file vanishes mid-rotation, yields
144
- // nothing here rather than a dangling separator.
162
+ // Each block contributes its GLOBAL half then its LOCAL half (general → specific),
163
+ // EACH as its own marked file-section (a 0-byte / newline-only file → no section,
164
+ // no dangling marker). Blocks are appended in the order given (the gatherer sorts
165
+ // by filename for determinism); the stem (`domain`) is used only for that ordering,
166
+ // never emitted.
145
167
  // ─────────────────────────────────────────────────────────────────────────────
146
168
 
147
- function renderMergedBlocks(blocks: readonly PromptDomainBlock[]): string {
148
- const out: string[] = []
169
+ function pushMarkedBlocks(sections: string[], blocks: readonly PromptDomainBlock[]): void {
149
170
  for (const b of blocks) {
150
- // Per stem: global then local (general → specific), each trimmed of its
151
- // trailing newlines so a 0-byte marker (or a newline-only file) drops out and
152
- // the spacing is uniform. Halves of one stem are kept tight (single '\n');
153
- // distinct stems are separated by a blank line below.
154
- const halves = [b.global, b.local]
155
- .filter((s): s is string => s !== undefined)
156
- .map(s => s.replace(/\n+$/, ''))
157
- .filter(s => s.length > 0)
158
- if (halves.length > 0) out.push(halves.join('\n'))
171
+ if (hasContent(b.global)) sections.push(markedSection(b.globalPath, b.global))
172
+ if (hasContent(b.local)) sections.push(markedSection(b.localPath, b.local))
159
173
  }
160
- return out.join('\n\n')
161
174
  }
162
175
 
163
176
  // ─────────────────────────────────────────────────────────────────────────────
@@ -218,16 +231,28 @@ function listDomainFiles(root: string): string[] {
218
231
  return listMdFiles(root).filter(name => name.toLowerCase() !== doctrineLower)
219
232
  }
220
233
 
234
+ /** Display path for a marker: an absolute path with a leading HOME abbreviated to
235
+ * `~` (the form a human scanning the assembled prompt expects). HOME unset / path
236
+ * outside HOME → the absolute path verbatim. */
237
+ function displayPath(absPath: string, env: NodeJS.ProcessEnv): string {
238
+ const home = env.HOME?.trim()
239
+ if (home && (absPath === home || absPath.startsWith(home + '/'))) {
240
+ return '~' + absPath.slice(home.length)
241
+ }
242
+ return absPath
243
+ }
244
+
221
245
  /** Build a list of merged global/local blocks (Layer 4 OR Layer 5) from the
222
246
  * union of `*.md` stems present in the global and/or local dir, sorted by
223
247
  * filename (plain codepoint order) for a deterministic prompt. `lister` selects
224
248
  * the candidate `.md` files — Layer 4 excludes the IAPEER.md doctrine, Layer 5
225
- * takes every fragment. A read that races a rotation (file removed between listdir
226
- * and read) → that half is undefined and drops out; a 0-byte file → '' and is
227
- * dropped by the renderer. */
249
+ * takes every fragment. Each present half carries its display path for the marker.
250
+ * A read that races a rotation (file removed between listdir and read) → that half
251
+ * is undefined and drops out; a 0-byte file → '' and is dropped by the renderer. */
228
252
  function gatherMergedBlocks(
229
253
  globalDir: string,
230
254
  localDir: string,
255
+ env: NodeJS.ProcessEnv,
231
256
  lister: (dir: string) => string[] = listMdFiles,
232
257
  ): PromptDomainBlock[] {
233
258
  const names = new Set<string>([...lister(globalDir), ...lister(localDir)])
@@ -235,8 +260,14 @@ function gatherMergedBlocks(
235
260
  const block: PromptDomainBlock = { domain: name.slice(0, -3) } // strip ".md"
236
261
  const global = readFileIfPresent(join(globalDir, name))
237
262
  const local = readFileIfPresent(join(localDir, name))
238
- if (global !== undefined) block.global = global
239
- if (local !== undefined) block.local = local
263
+ if (global !== undefined) {
264
+ block.global = global
265
+ block.globalPath = displayPath(join(globalDir, name), env)
266
+ }
267
+ if (local !== undefined) {
268
+ block.local = local
269
+ block.localPath = displayPath(join(localDir, name), env)
270
+ }
240
271
  return block
241
272
  })
242
273
  }
@@ -246,16 +277,19 @@ export function gatherPromptInput(opts: GatherPromptOptions): ComposePromptInput
246
277
  const globalRoot = opts.globalRoot ?? resolveGlobalRoot(env)
247
278
  const localRoot = join(opts.cwd, IAPEER_DIR)
248
279
 
249
- // Layer 2 — IAPEER.md merge. Global is existence-gated (undefined when absent,
250
- // '' when present-but-empty — golden parity). Local defaults to '' when absent.
251
- const globalDoctrine = readFileIfPresent(join(globalRoot, IAPEER_DOCTRINE_FILE))
252
- const peerDoctrine = readFileIfPresent(join(localRoot, IAPEER_DOCTRINE_FILE)) ?? ''
280
+ // Layer 2 — IAPEER.md merge. Each half carries its display path for the marker.
281
+ // Existence-gated: undefined when absent, '' when present-but-empty (an empty
282
+ // doctrine now contributes no section — no bare-"\n" legacy quirk).
283
+ const globalDoctrineFile = join(globalRoot, IAPEER_DOCTRINE_FILE)
284
+ const peerDoctrineFile = join(localRoot, IAPEER_DOCTRINE_FILE)
285
+ const globalDoctrine = readFileIfPresent(globalDoctrineFile)
286
+ const peerDoctrine = readFileIfPresent(peerDoctrineFile) ?? ''
253
287
 
254
288
  // Layer 4 — union of every non-IAPEER domain present globally and/or locally,
255
289
  // sorted by filename for a deterministic prompt. Empty (0-byte) files stay as
256
290
  // presence markers — readFileIfPresent returns '', and the renderer drops empty
257
291
  // halves, so a marker contributes no text/separator.
258
- const pluginDomains = gatherMergedBlocks(globalRoot, localRoot, listDomainFiles)
292
+ const pluginDomains = gatherMergedBlocks(globalRoot, localRoot, env, listDomainFiles)
259
293
 
260
294
  // Layer 5 — doctrine fragments: every `*.md` in the fragments/ subdir of both
261
295
  // roots, merged per stem (global guide → per-peer specifics). Primitive-owned and
@@ -264,6 +298,7 @@ export function gatherPromptInput(opts: GatherPromptOptions): ComposePromptInput
264
298
  const promptFragments = gatherMergedBlocks(
265
299
  join(globalRoot, FRAGMENTS_DIR),
266
300
  join(localRoot, FRAGMENTS_DIR),
301
+ env,
267
302
  )
268
303
 
269
304
  // Layer 3 — peers projected through the shared normalizer, sorted by personality
@@ -285,7 +320,10 @@ export function gatherPromptInput(opts: GatherPromptOptions): ComposePromptInput
285
320
  hostname: opts.hostname,
286
321
  today: opts.today,
287
322
  peerDoctrine,
288
- ...(globalDoctrine !== undefined ? { globalDoctrine } : {}),
323
+ peerDoctrinePath: displayPath(peerDoctrineFile, env),
324
+ ...(globalDoctrine !== undefined
325
+ ? { globalDoctrine, globalDoctrinePath: displayPath(globalDoctrineFile, env) }
326
+ : {}),
289
327
  peers,
290
328
  pluginDomains,
291
329
  promptFragments,
@@ -42,14 +42,21 @@ export type { PublicPeerSummary } from '../registry/index.ts'
42
42
  /** One merged block: the global + local halves of a single stem (either may be
43
43
  * absent), used for BOTH Layer 4 (`<DOMAIN>.md` at the .iapeer/ root) and Layer 5
44
44
  * (`<STEM>.md` in .iapeer/fragments/). `domain` is the filename stem, used only
45
- * for stable ordering — it is NOT emitted (the merge is organic). Within a block
46
- * global precedes local (general → specific). */
45
+ * for stable ordering — it is NOT emitted. Within a block global precedes local
46
+ * (general → specific), and each half is rendered as its OWN per-file section
47
+ * prefixed by a path marker (`globalPath` / `localPath`) — the assembly is now
48
+ * per-file, not an organic stem-merge. */
47
49
  export interface PromptDomainBlock {
48
50
  domain: string
49
51
  /** Global half content (general): ~/.iapeer/<DOMAIN>.md or ~/.iapeer/fragments/<STEM>.md. */
50
52
  global?: string
51
53
  /** Local half content (specific — overrides global): <cwd>/.iapeer/… counterpart. */
52
54
  local?: string
55
+ /** Display path for the global half's marker (HOME abbreviated to ~). Absent →
56
+ * no marker (pure-renderer callers). */
57
+ globalPath?: string
58
+ /** Display path for the local half's marker. */
59
+ localPath?: string
53
60
  }
54
61
 
55
62
  export interface ComposePromptInput {
@@ -64,10 +71,14 @@ export interface ComposePromptInput {
64
71
  today: string
65
72
  /** Layer 2 local: <cwd>/.iapeer/IAPEER.md content. '' when absent. */
66
73
  peerDoctrine: string
74
+ /** Display path for the local IAPEER.md marker (HOME abbreviated to ~). */
75
+ peerDoctrinePath?: string
67
76
  /** Layer 2 global, OPTIONAL: ~/.iapeer/IAPEER.md content (sits between the YAML
68
77
  * block and the per-peer doctrine so per-peer overrides global). Existence-
69
78
  * gated: a present-but-empty file → '', an absent file → undefined. */
70
79
  globalDoctrine?: string
80
+ /** Display path for the global IAPEER.md marker. */
81
+ globalDoctrinePath?: string
71
82
  /** Layer 3: the normalized peer registry. Empty/omitted → the layer emits
72
83
  * nothing (and the output stays byte-identical to the legacy YAML+doctrine). */
73
84
  peers?: PublicPeerSummary[]
@@ -654,7 +654,7 @@ export function classifyGoneSession(sock: string): { death: 'server-dead' | 'ses
654
654
  // proves the server is alive and merely lost this session.
655
655
  return tmux(sock, 'list-sessions').ok
656
656
  ? { death: 'session-gone', reason: 'session gone, tmux server alive (exit cause should be in exits.log)' }
657
- : { death: 'server-dead', reason: 'tmux server dead — stale socket (SIGKILL/OOM class; check exits.log ev=server-exit canary record)' }
657
+ : { death: 'server-dead', reason: 'tmux server dead — stale socket (SIGKILL class — OOM only if a matching JetsamEvent report exists; check exits.log ev=server-exit canary record)' }
658
658
  }
659
659
 
660
660
  // ─────────────────────────────────────────────────────────────────────────────
@@ -790,6 +790,13 @@ export interface WakeResult {
790
790
  * deliberate halt, distinct from offline/wake-failure). The sender is told the
791
791
  * peer is stopped, not that delivery failed transiently. */
792
792
  stopped?: boolean
793
+ /** Was `task` actually delivered by this wake? true on the launch path (the
794
+ * task rode as the boot first-message); FALSE on the idempotent live-session
795
+ * fast path — a CONCURRENT sender's boot won the lock and only ITS envelope
796
+ * was delivered; this caller's task was NOT. The caller (routeSend) must then
797
+ * deliver the envelope itself via the live path, or the second sender's
798
+ * message is silently lost behind a false ok (contract forbids silent loss). */
799
+ taskDelivered?: boolean
793
800
  }
794
801
 
795
802
  export interface WakeDeps {
@@ -890,7 +897,9 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
890
897
  // wake already brought it up) — no second spawn.
891
898
  if (sessionAlive(sock, identity)) {
892
899
  writeSessionState(cfg, { identity, runtime, personality: args.personality, cwd, wokeAt: Date.now() })
893
- return { status: 'READY', woke: false, runtime, process_address: identity }
900
+ // taskDelivered:false — the winning concurrent wake delivered ITS OWN task as
901
+ // the boot first-message; THIS caller's task was not delivered by anything.
902
+ return { status: 'READY', woke: false, runtime, process_address: identity, taskDelivered: false }
894
903
  }
895
904
  if (!existsSync(cwd)) {
896
905
  return { status: 'FAILED', woke: false, runtime, reason: `peer cwd does not exist: ${cwd}` }
@@ -997,7 +1006,7 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
997
1006
  // ring — a successful wake means this is NOT a tight crash loop. Best-effort.
998
1007
  writeTopic(cfg, identity, args.topic?.trim() ?? '')
999
1008
  trimDeaths(cfg, identity, cfg.crashLoopWindowSecs, Date.now())
1000
- return { status: 'READY', woke: true, runtime, process_address: identity }
1009
+ return { status: 'READY', woke: true, runtime, process_address: identity, taskDelivered: true }
1001
1010
  })
1002
1011
  }
1003
1012