@agfpd/iapeer 0.2.12 → 0.2.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -368,6 +368,7 @@ const USAGE = `usage: iapeer <verb> [args]
368
368
  interrupt <peer> [runtime] interrupt the current turn (Escape) — context intact
369
369
  compact <peer> [runtime] compact the peer's context (/compact)
370
370
  self-fresh (agent self-call) mark /new eager-fresh + self-kill — the daemon relaunches fresh
371
+ native-memory <off|on> (--peer <p> | --all) gate/restore runtimes' native memory (canonized lever; контракт «Слот памяти»)
371
372
  `
372
373
 
373
374
  export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.env): Promise<number> {
@@ -422,6 +423,37 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
422
423
  out(`memory: ${mem.state}${mem.detail ? ` — ${mem.detail}` : ''} (slot: ${memLabel})\n`)
423
424
  return r.marketplaces.some(m => m.state === 'failed') || infraFailed ? 1 : 0
424
425
  }
426
+ case 'native-memory': {
427
+ // The canonized runtime-memory lever (контракт «Слот памяти» §Native-память):
428
+ // off = explicit disable merged into the peer's runtime config files; on =
429
+ // remove the key (restore the runtime's own default). Consumers: the operator
430
+ // and the memory provider's install-time sweep (`--all`). NOT slot-gated here —
431
+ // an explicit operator/provider action; only the BIRTH-time hook is slot-gated.
432
+ const state = positionals[0]
433
+ if (state !== 'off' && state !== 'on') return usage(errOut)
434
+ const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
435
+ if (flags.all !== true && !peerName) return usage(errOut)
436
+ const { applyNativeMemory } = await import('../launch/nativeMemory.ts')
437
+ const index = readPeersIndex({ env })
438
+ const targets = flags.all === true ? index.peers : index.peers.filter(p => p.personality === peerName)
439
+ if (targets.length === 0) {
440
+ errOut(`peer "${peerName ?? ''}" is not in the iapeer peers index\n`)
441
+ return 1
442
+ }
443
+ let failed = false
444
+ for (const p of targets) {
445
+ const outcomes = applyNativeMemory(p.cwd, p.runtimes, state)
446
+ if (outcomes.length === 0) {
447
+ out(`${p.personality}: no claude/codex runtime — skipped\n`)
448
+ continue
449
+ }
450
+ for (const o of outcomes) {
451
+ out(`${p.personality} (${o.runtime}): ${o.state}${o.detail ? ` — ${o.detail}` : ''}\n`)
452
+ if (o.state === 'failed') failed = true
453
+ }
454
+ }
455
+ return failed ? 1 : 0
456
+ }
425
457
  case 'status': {
426
458
  // Host snapshot (контракт «Слот памяти» §status): version + daemon health +
427
459
  // the memory-slot line. Exit 1 iff the daemon is unhealthy (usable as a
package/src/index.ts CHANGED
@@ -28,5 +28,7 @@ export * from './onboard/index.ts'
28
28
  // Memory slot — the declarative provider slot (контракт «Слот памяти»): status read + onboard step.
29
29
  export * from './status/index.ts'
30
30
  export * from './onboard/memory.ts'
31
+ // Native runtime-memory levers (slot contract §Native-память): canonized forms + verb mechanics.
32
+ export * from './launch/nativeMemory.ts'
31
33
  export { composeSystemPrompt, gatherPromptInput } from './launch/composeSystemPrompt.ts'
32
34
  export type { GatherPromptOptions } from './launch/composeSystemPrompt.ts'
@@ -0,0 +1,150 @@
1
+ // Native runtime-memory levers — the canonized forms (slot contract §Native-
2
+ // память). The HARD requirement pinned here: NO-CLOBBER — both target files
3
+ // carry foreign blocks (plugin enables, statusline wrappers) that every lever
4
+ // write must preserve byte-meaningfully.
5
+
6
+ import { afterEach, describe, expect, test } from 'bun:test'
7
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
8
+ import { tmpdir } from 'os'
9
+ import { join } from 'path'
10
+ import {
11
+ applyNativeMemory,
12
+ claudeSettingsPath,
13
+ codexProjectConfigPath,
14
+ codexGlobalConfigPath,
15
+ preTrustCodexCwd,
16
+ } from './nativeMemory.ts'
17
+
18
+ const dirs: string[] = []
19
+ function mkTmp(): string {
20
+ const d = mkdtempSync(join(tmpdir(), 'iapeer-natmem-'))
21
+ dirs.push(d)
22
+ return d
23
+ }
24
+ afterEach(() => {
25
+ while (dirs.length) rmSync(dirs.pop()!, { recursive: true, force: true })
26
+ })
27
+
28
+ describe('claude lever (settings.json merge)', () => {
29
+ test('absent file → created with ONLY the lever key', () => {
30
+ const cwd = mkTmp()
31
+ const [o] = applyNativeMemory(cwd, ['claude'], 'off')
32
+ expect(o?.state).toBe('written')
33
+ expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual({ autoMemoryEnabled: false })
34
+ })
35
+
36
+ test('NO-CLOBBER: foreign keys (plugins, statusline) survive off AND on', () => {
37
+ const cwd = mkTmp()
38
+ mkdirSync(join(cwd, '.claude'), { recursive: true })
39
+ const foreign = { enabledPlugins: { 'MergeMind@agfpd': true }, statusLine: { type: 'command', command: 'x' } }
40
+ writeFileSync(claudeSettingsPath(cwd), JSON.stringify(foreign))
41
+ applyNativeMemory(cwd, ['claude'], 'off')
42
+ const afterOff = JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))
43
+ expect(afterOff).toEqual({ ...foreign, autoMemoryEnabled: false })
44
+ const [on] = applyNativeMemory(cwd, ['claude'], 'on')
45
+ expect(on?.state).toBe('written')
46
+ expect(JSON.parse(readFileSync(claudeSettingsPath(cwd), 'utf8'))).toEqual(foreign) // key REMOVED, default restored
47
+ })
48
+
49
+ test('idempotent: already-false → already; on with no key → already', () => {
50
+ const cwd = mkTmp()
51
+ applyNativeMemory(cwd, ['claude'], 'off')
52
+ expect(applyNativeMemory(cwd, ['claude'], 'off')[0]?.state).toBe('already')
53
+ applyNativeMemory(cwd, ['claude'], 'on')
54
+ expect(applyNativeMemory(cwd, ['claude'], 'on')[0]?.state).toBe('already')
55
+ })
56
+
57
+ test('non-object settings.json → failed (refuses to clobber), file untouched', () => {
58
+ const cwd = mkTmp()
59
+ mkdirSync(join(cwd, '.claude'), { recursive: true })
60
+ writeFileSync(claudeSettingsPath(cwd), '"just a string"')
61
+ const [o] = applyNativeMemory(cwd, ['claude'], 'off')
62
+ expect(o?.state).toBe('failed')
63
+ expect(readFileSync(claudeSettingsPath(cwd), 'utf8')).toBe('"just a string"')
64
+ })
65
+ })
66
+
67
+ describe('codex lever (config.toml [features] merge)', () => {
68
+ test('absent file → [features] section with the lever line', () => {
69
+ const cwd = mkTmp()
70
+ const [o] = applyNativeMemory(cwd, ['codex'], 'off')
71
+ expect(o?.state).toBe('written')
72
+ expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toBe('[features]\nmemories = false\n')
73
+ })
74
+
75
+ test('NO-CLOBBER: plugin blocks survive; [features] appended after them (fleet-file shape)', () => {
76
+ const cwd = mkTmp()
77
+ mkdirSync(join(cwd, '.codex'), { recursive: true })
78
+ const fleet = '[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true\n\n[plugins."MergeMind@agfpd"]\nenabled = true\n'
79
+ writeFileSync(codexProjectConfigPath(cwd), fleet)
80
+ applyNativeMemory(cwd, ['codex'], 'off')
81
+ const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
82
+ expect(text).toContain('[plugins."Inter-Agent-Protocol@agfpd"]\nenabled = true')
83
+ expect(text).toContain('[plugins."MergeMind@agfpd"]\nenabled = true')
84
+ expect(text).toContain('[features]\nmemories = false')
85
+ })
86
+
87
+ test('existing [features] WITHOUT memories (followed by another section) → inserted INSIDE the section', () => {
88
+ const cwd = mkTmp()
89
+ mkdirSync(join(cwd, '.codex'), { recursive: true })
90
+ writeFileSync(codexProjectConfigPath(cwd), '[features]\nother = true\n\n[plugins."X@y"]\nenabled = true\n')
91
+ applyNativeMemory(cwd, ['codex'], 'off')
92
+ const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
93
+ // the lever line lands in [features], NOT in [plugins."X@y"]
94
+ expect(text.indexOf('memories = false')).toBeLessThan(text.indexOf('[plugins."X@y"]'))
95
+ expect(text).toContain('other = true')
96
+ })
97
+
98
+ test('memories = true → replaced with false; already false → already', () => {
99
+ const cwd = mkTmp()
100
+ mkdirSync(join(cwd, '.codex'), { recursive: true })
101
+ writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = true\n')
102
+ expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('written')
103
+ expect(readFileSync(codexProjectConfigPath(cwd), 'utf8')).toContain('memories = false')
104
+ expect(applyNativeMemory(cwd, ['codex'], 'off')[0]?.state).toBe('already')
105
+ })
106
+
107
+ test('on → the memories line is REMOVED (default restored), section + foreign lines kept', () => {
108
+ const cwd = mkTmp()
109
+ mkdirSync(join(cwd, '.codex'), { recursive: true })
110
+ writeFileSync(codexProjectConfigPath(cwd), '[features]\nmemories = false\nother = true\n')
111
+ const [o] = applyNativeMemory(cwd, ['codex'], 'on')
112
+ expect(o?.state).toBe('written')
113
+ const text = readFileSync(codexProjectConfigPath(cwd), 'utf8')
114
+ expect(text).not.toContain('memories')
115
+ expect(text).toContain('other = true')
116
+ expect(applyNativeMemory(cwd, ['codex'], 'on')[0]?.state).toBe('already')
117
+ })
118
+ })
119
+
120
+ describe('applyNativeMemory runtime dispatch', () => {
121
+ test('both runtimes → both levers; router-only runtimes → no outcomes', () => {
122
+ const cwd = mkTmp()
123
+ const both = applyNativeMemory(cwd, ['claude', 'codex'], 'off')
124
+ expect(both.map(o => o.runtime).sort()).toEqual(['claude', 'codex'])
125
+ expect(applyNativeMemory(mkTmp(), ['telegram', 'notifier'], 'off')).toEqual([])
126
+ })
127
+ })
128
+
129
+ describe('preTrustCodexCwd (birth-time trust, codex global config)', () => {
130
+ test('appends a [projects] block once; NO-CLOBBER of existing content; idempotent', () => {
131
+ const home = mkTmp()
132
+ const env = { HOME: home } as NodeJS.ProcessEnv
133
+ mkdirSync(join(home, '.codex'), { recursive: true })
134
+ const existing = '[projects."/already/trusted"]\ntrust_level = "trusted"\n'
135
+ writeFileSync(codexGlobalConfigPath(env), existing)
136
+ const first = preTrustCodexCwd('/peers/newborn', env)
137
+ expect(first.state).toBe('written')
138
+ const text = readFileSync(codexGlobalConfigPath(env), 'utf8')
139
+ expect(text).toContain(existing.trim())
140
+ expect(text).toContain('[projects."/peers/newborn"]\ntrust_level = "trusted"')
141
+ expect(preTrustCodexCwd('/peers/newborn', env).state).toBe('already')
142
+ })
143
+
144
+ test('absent global config → created with just the block', () => {
145
+ const home = mkTmp()
146
+ const env = { HOME: home } as NodeJS.ProcessEnv
147
+ expect(preTrustCodexCwd('/peers/first', env).state).toBe('written')
148
+ expect(readFileSync(codexGlobalConfigPath(env), 'utf8')).toBe('[projects."/peers/first"]\ntrust_level = "trusted"\n')
149
+ })
150
+ })
@@ -0,0 +1,185 @@
1
+ // Native runtime-memory levers (контракт «Слот памяти» §Native-память рантаймов
2
+ // при занятом слоте, agreed 10.06). The CORE canonizes the lever FORMS — runtime
3
+ // knowledge lives in the runtime layer, like deliveryMarkers/buildArgv:
4
+ //
5
+ // claude: merge `"autoMemoryEnabled": false` into `<cwd>/.claude/settings.json`
6
+ // codex: merge `memories = false` into the `[features]` section of
7
+ // `<cwd>/.codex/config.toml`
8
+ //
9
+ // Both files CARRY FOREIGN BLOCKS (plugin enables, statusline wrappers, …) —
10
+ // every write here is a NO-CLOBBER merge (parse/patch + atomic temp+rename),
11
+ // never a rewrite. Forms verified live on the fleet (12/12 + behavioral smoke
12
+ // 09.06; codex reads project-local config when the cwd is TRUSTED — see
13
+ // preTrustCodexCwd).
14
+ //
15
+ // `off` writes the explicit disable; `on` REMOVES the key (restores the
16
+ // runtime's own default rather than baking an explicit enable — the core has
17
+ // no opinion beyond "parallel store off while a memory provider owns the host").
18
+ //
19
+ // Consumers: the operator verb `iapeer native-memory <off|on>`, the provider's
20
+ // install-time sweep (calls the verb), and provisionPeer's birth-time hook
21
+ // (slot-gated, see provision/index.ts).
22
+
23
+ import { existsSync, mkdirSync, readFileSync, realpathSync } from 'fs'
24
+ import { dirname, join } from 'path'
25
+ import { homedir } from 'os'
26
+ import type { Runtime } from '../core/constants.ts'
27
+ import { writeFileAtomic } from '../storage/index.ts'
28
+
29
+ export type NativeMemoryState = 'off' | 'on'
30
+
31
+ export interface LeverOutcome {
32
+ runtime: 'claude' | 'codex'
33
+ path: string
34
+ state: 'written' | 'already' | 'failed'
35
+ detail?: string
36
+ }
37
+
38
+ // ─── claude: <cwd>/.claude/settings.json — JSON merge ───────────────────────
39
+
40
+ export function claudeSettingsPath(cwd: string): string {
41
+ return join(cwd, '.claude', 'settings.json')
42
+ }
43
+
44
+ function applyClaudeLever(cwd: string, state: NativeMemoryState): LeverOutcome {
45
+ const path = claudeSettingsPath(cwd)
46
+ try {
47
+ let obj: Record<string, unknown> = {}
48
+ if (existsSync(path)) {
49
+ const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown
50
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
51
+ return { runtime: 'claude', path, state: 'failed', detail: 'settings.json is not a JSON object — refusing to clobber' }
52
+ }
53
+ obj = raw as Record<string, unknown>
54
+ }
55
+ if (state === 'off') {
56
+ if (obj.autoMemoryEnabled === false) return { runtime: 'claude', path, state: 'already' }
57
+ obj.autoMemoryEnabled = false
58
+ } else {
59
+ if (!('autoMemoryEnabled' in obj)) return { runtime: 'claude', path, state: 'already' }
60
+ delete obj.autoMemoryEnabled // restore the runtime's own default
61
+ }
62
+ mkdirSync(dirname(path), { recursive: true })
63
+ writeFileAtomic(path, `${JSON.stringify(obj, null, 2)}\n`)
64
+ return { runtime: 'claude', path, state: 'written' }
65
+ } catch (e) {
66
+ return { runtime: 'claude', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
67
+ }
68
+ }
69
+
70
+ // ─── codex: <cwd>/.codex/config.toml — section-scoped line merge ─────────────
71
+
72
+ export function codexProjectConfigPath(cwd: string): string {
73
+ return join(cwd, '.codex', 'config.toml')
74
+ }
75
+
76
+ /** Locate the `[features]` section bounds: [startLine, endLine) of its BODY
77
+ * (after the header, up to the next `[…]` header or EOF). null = no section. */
78
+ function featuresBounds(lines: string[]): { header: number; start: number; end: number } | null {
79
+ const header = lines.findIndex(l => l.trim() === '[features]')
80
+ if (header < 0) return null
81
+ let end = lines.length
82
+ for (let i = header + 1; i < lines.length; i++) {
83
+ if (/^\s*\[/.test(lines[i]!)) {
84
+ end = i
85
+ break
86
+ }
87
+ }
88
+ return { header, start: header + 1, end }
89
+ }
90
+
91
+ function applyCodexLever(cwd: string, state: NativeMemoryState): LeverOutcome {
92
+ const path = codexProjectConfigPath(cwd)
93
+ try {
94
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
95
+ const lines = text.length ? text.split('\n') : []
96
+ const bounds = featuresBounds(lines)
97
+ const memRe = /^\s*memories\s*=/
98
+ if (state === 'off') {
99
+ if (bounds) {
100
+ const idx = lines.slice(bounds.start, bounds.end).findIndex(l => memRe.test(l))
101
+ if (idx >= 0) {
102
+ const abs = bounds.start + idx
103
+ if (/^\s*memories\s*=\s*false\s*$/.test(lines[abs]!)) return { runtime: 'codex', path, state: 'already' }
104
+ lines[abs] = 'memories = false'
105
+ } else {
106
+ lines.splice(bounds.start, 0, 'memories = false')
107
+ }
108
+ } else {
109
+ if (lines.length && lines[lines.length - 1]!.trim() !== '') lines.push('')
110
+ lines.push('[features]', 'memories = false')
111
+ }
112
+ } else {
113
+ if (!bounds) return { runtime: 'codex', path, state: 'already' }
114
+ const idx = lines.slice(bounds.start, bounds.end).findIndex(l => memRe.test(l))
115
+ if (idx < 0) return { runtime: 'codex', path, state: 'already' }
116
+ lines.splice(bounds.start + idx, 1) // restore the runtime's own default
117
+ }
118
+ mkdirSync(dirname(path), { recursive: true })
119
+ const outText = lines.join('\n')
120
+ writeFileAtomic(path, outText.endsWith('\n') ? outText : `${outText}\n`)
121
+ return { runtime: 'codex', path, state: 'written' }
122
+ } catch (e) {
123
+ return { runtime: 'codex', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
124
+ }
125
+ }
126
+
127
+ // ─── codex trust (birth-time only) ───────────────────────────────────────────
128
+
129
+ /** The codex GLOBAL config (`~/.codex/config.toml`) — where trust lives as
130
+ * `[projects."<path>"] trust_level = "trusted"` blocks. */
131
+ export function codexGlobalConfigPath(env: NodeJS.ProcessEnv = process.env): string {
132
+ const home = env.HOME?.trim() || homedir()
133
+ return join(home, '.codex', 'config.toml')
134
+ }
135
+
136
+ /**
137
+ * Pre-trust a NEWBORN codex peer's cwd by appending its `[projects."<cwd>"]`
138
+ * block to the codex global config (no-clobber: only when no block for this
139
+ * path exists). Closes the contract requirement «рычаг действует с ПЕРВОЙ
140
+ * сессии» DETERMINISTICALLY: codex reads project-local config only for a
141
+ * trusted cwd, and without pre-trust the first session's trust is granted
142
+ * mid-boot by the adapter's dialog auto-accept — whether the project config is
143
+ * (re)read after that grant within the same run is codex-internal. Birth-time
144
+ * only — existing peers are already trusted through their boot history.
145
+ */
146
+ export function preTrustCodexCwd(cwd: string, env: NodeJS.ProcessEnv = process.env): LeverOutcome {
147
+ const path = codexGlobalConfigPath(env)
148
+ try {
149
+ // Codex keys trust on the RESOLVED real path (live fact, 10.06 acceptance: the
150
+ // /tmp → /private/tmp symlink made a literal-path entry MISS and left the lever
151
+ // inert — trust entries written by codex itself are all /private/tmp/...).
152
+ let real = cwd
153
+ try {
154
+ real = realpathSync(cwd)
155
+ } catch {
156
+ /* cwd not on disk yet → keep as given */
157
+ }
158
+ const text = existsSync(path) ? readFileSync(path, 'utf8') : ''
159
+ if (text.includes(`[projects."${real}"]`)) return { runtime: 'codex', path, state: 'already' }
160
+ const block = `${text.length && !text.endsWith('\n') ? '\n' : ''}${text.trim().length ? '\n' : ''}[projects."${real}"]\ntrust_level = "trusted"\n`
161
+ mkdirSync(dirname(path), { recursive: true })
162
+ writeFileAtomic(path, text + block)
163
+ return { runtime: 'codex', path, state: 'written' }
164
+ } catch (e) {
165
+ return { runtime: 'codex', path, state: 'failed', detail: e instanceof Error ? e.message : String(e) }
166
+ }
167
+ }
168
+
169
+ // ─── public entry ────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Apply the native-memory lever for one peer cwd across its declared runtimes
173
+ * (∩ {claude, codex} — other runtimes carry no native memory). Idempotent;
174
+ * every outcome is reported (a 'failed' lever never throws).
175
+ */
176
+ export function applyNativeMemory(
177
+ cwd: string,
178
+ runtimes: readonly Runtime[],
179
+ state: NativeMemoryState,
180
+ ): LeverOutcome[] {
181
+ const out: LeverOutcome[] = []
182
+ if (runtimes.includes('claude')) out.push(applyClaudeLever(cwd, state))
183
+ if (runtimes.includes('codex')) out.push(applyCodexLever(cwd, state))
184
+ return out
185
+ }
@@ -91,6 +91,40 @@ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<Provisi
91
91
  runtimeBin,
92
92
  warn: opts.warn,
93
93
  })
94
+ // Birth-time native-memory lever (контракт «Слот памяти» §Native-память рантаймов):
95
+ // an OCCUPIED memory slot gates the AUTOMATIC lever at peer birth — zero window of
96
+ // a parallel, uncurated native store (the criterion). Empty slot → native memory is
97
+ // legitimate, untouched. Best-effort with a LOUD warn (a lever hiccup must not kill
98
+ // the provision — the operator verb `iapeer native-memory off` is the repair).
99
+ try {
100
+ const { readMemoryProvider } = await import('../status/index.ts')
101
+ if (readMemoryProvider(env)) {
102
+ const { applyNativeMemory, preTrustCodexCwd } = await import('../launch/nativeMemory.ts')
103
+ for (const o of applyNativeMemory(cwd, profile.runtimes, 'off')) {
104
+ if (o.state === 'failed') {
105
+ opts.warn?.(
106
+ `native-memory lever (${o.runtime}) FAILED for "${profile.personality}": ${o.detail} — ` +
107
+ `the peer may accumulate a parallel native store; repair: iapeer native-memory off --peer ${profile.personality}`,
108
+ )
109
+ }
110
+ }
111
+ // Codex reads project-local config only for a TRUSTED cwd — pre-trust the
112
+ // newborn so the lever acts from the FIRST session (contract requirement),
113
+ // not from whenever the boot dialog's trust grant takes effect.
114
+ if (profile.runtimes.includes('codex')) {
115
+ const t = preTrustCodexCwd(cwd, env)
116
+ if (t.state === 'failed') {
117
+ opts.warn?.(
118
+ `codex pre-trust FAILED for "${profile.personality}": ${t.detail} — ` +
119
+ `the lever may be inert until the first boot's trust dialog`,
120
+ )
121
+ }
122
+ }
123
+ }
124
+ } catch (e) {
125
+ opts.warn?.(`native-memory birth-time hook failed: ${e instanceof Error ? e.message : String(e)}`)
126
+ }
127
+
94
128
  // wake_policy:"ephemeral" sanity (M2 edge cases — warn, don't refuse: the policy
95
129
  // lives in the hand-editable local profile, provision just surfaces the mismatch):
96
130
  // • + interfaces.telegram: ephemeral WINS in resolveWakeMode (explicit policy
@@ -158,3 +158,44 @@ describe('provisionPeer ephemeral warnings', () => {
158
158
  expect(warns.some(w => /ephemeral/.test(w) && /inert|launchd/i.test(w))).toBe(true)
159
159
  })
160
160
  })
161
+
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+ // Birth-time native-memory lever (slot contract §Native-память): an OCCUPIED
164
+ // slot gates the automatic lever at provision; empty slot → untouched.
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+
167
+ describe('provisionPeer birth-time native-memory lever', () => {
168
+ test('slot OCCUPIED → claude+codex levers written + codex cwd pre-trusted', async () => {
169
+ const root = mkTmp()
170
+ const env = envFor(root)
171
+ // occupy the slot (as the provider's init would)
172
+ mkdirSync(join(root, 'iapeer'), { recursive: true })
173
+ writeFileSync(
174
+ join(root, 'iapeer', 'memory-provider.json'),
175
+ JSON.stringify({ provider: 'iapeer-memory', package: '@agfpd/iapeer-memory', version: '0.1.0', registeredAt: 'x' }),
176
+ )
177
+ const cwd = join(root, 'wnat')
178
+ mkdirSync(join(cwd, '.iapeer'), { recursive: true })
179
+ writeFileSync(
180
+ peerProfilePath(cwd),
181
+ JSON.stringify({ personality: 'wnat', runtime: 'claude', runtimes: ['claude', 'codex'] }),
182
+ )
183
+ await provisionPeer({ cwd, runtime: 'claude', env })
184
+ expect(JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8')).autoMemoryEnabled).toBe(false)
185
+ expect(readFileSync(join(cwd, '.codex', 'config.toml'), 'utf8')).toContain('memories = false')
186
+ // codex newborn pre-trusted in the GLOBAL codex config (HOME-scoped → temp root).
187
+ // Codex keys trust on the RESOLVED real path (macOS /var → /private/var), so the
188
+ // entry must carry realpath(cwd) — the literal-path miss was a live-caught bug.
189
+ const { realpathSync } = await import('fs')
190
+ expect(readFileSync(join(root, '.codex', 'config.toml'), 'utf8')).toContain(`[projects."${realpathSync(cwd)}"]`)
191
+ })
192
+
193
+ test('slot EMPTY → native memory untouched (legitimate without a provider)', async () => {
194
+ const root = mkTmp()
195
+ const env = envFor(root)
196
+ const cwd = join(root, 'wbare')
197
+ await provisionPeer({ cwd, runtime: 'claude', env })
198
+ expect(existsSync(join(cwd, '.claude', 'settings.json'))).toBe(false)
199
+ expect(existsSync(join(cwd, '.codex', 'config.toml'))).toBe(false)
200
+ })
201
+ })