@agfpd/iapeer 0.2.41 → 0.2.42

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.41",
3
+ "version": "0.2.42",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,9 +40,12 @@ async function register(personality: string, runtime = 'claude', intelligence: '
40
40
  await upsertPeer({ personality, runtime, cwd: `/tmp/${personality}`, intelligence }, { rootDir: root })
41
41
  }
42
42
 
43
- // ─── v1.2 provision-инверсия at the CLI joints (memory-plugin printer + remove) ───
43
+ // ─── v1.2 provision-инверсия at the CLI joints (remove → unprovision) ─────────
44
+ // NB: the memory-plugin verb was REMOVED 11.06 (ADR-017 — the plugin form is
45
+ // retired ecosystem-wide; fleet sweeps are the provider's side). The core's
46
+ // remaining v1.2 joints are birth (provision.test.ts) and remove (below).
44
47
 
45
- describe('memory-plugin / remove with a v1.2 provision slot', () => {
48
+ describe('remove with a v1.2 provision slot', () => {
46
49
  function declareV12Slot(): string {
47
50
  const journal = join(root, 'journal.txt')
48
51
  const script = join(root, 'fake-provider.sh')
@@ -61,36 +64,30 @@ describe('memory-plugin / remove with a v1.2 provision slot', () => {
61
64
  return journal
62
65
  }
63
66
 
64
- test('memory-plugin on --peer: printer survives plugin=null (v1.2 path), exit 0 (live-caught 11.06 smoke)', async () => {
65
- declareV12Slot()
66
- await register('alpha')
67
- let captured = ''
68
- const origWrite = process.stdout.write
69
- process.stdout.write = ((s: string | Uint8Array) => {
70
- captured += typeof s === 'string' ? s : Buffer.from(s).toString('utf8')
71
- return true
72
- }) as typeof process.stdout.write
73
- try {
74
- const code = await runCli(['memory-plugin', 'on', '--peer', 'alpha'], env())
75
- expect(code).toBe(0)
76
- expect(captured).toContain('provision: provider command')
77
- expect(captured).toContain('alpha (claude): ok')
78
- } finally {
79
- process.stdout.write = origWrite
80
- }
67
+ test('memory-plugin verb is GONE — unknown verb exits with usage (removal 11.06)', async () => {
68
+ const code = await runCli(['memory-plugin', 'on', '--all'], env())
69
+ expect(code).toBe(2) // usage exit — the verb no longer exists
81
70
  })
82
71
 
83
- test('remove: unprovision runs with occasion=remove BEFORE the purge, outcome reported', async () => {
72
+ test('remove: unprovision runs with occasion=remove BEFORE the purge, outcome reported; cmdlog dir dies with the record', async () => {
84
73
  const journal = declareV12Slot()
85
74
  await register('beta')
75
+ // identity-keyed tmux command-log dir (0.2.40) — must die with the registry
76
+ // record (the leak class noted at the 0.2.40 acceptance)
77
+ const { mkdirSync: mkd } = await import('fs')
78
+ const cmdDir = join(root, 'logs', 'iapeer', 'tmux-cmdlog', 'claude-beta')
79
+ mkd(cmdDir, { recursive: true })
80
+ writeFileSync(join(cmdDir, 'tmux-server-1.log'), 'x')
86
81
  const o = await removePeerCli('beta', { env: env() })
87
82
  expect(o.action).toBe('removed')
88
83
  expect(o.unprovision).toEqual(['claude:ok'])
89
- const { readFileSync } = await import('fs')
84
+ const { readFileSync, existsSync: ex } = await import('fs')
90
85
  const j = readFileSync(journal, 'utf8')
91
86
  expect(j).toContain('unprovision-peer')
92
87
  expect(j).toContain('--cwd\n/tmp/beta')
93
88
  expect(j).toContain('remove')
89
+ expect(ex(cmdDir)).toBe(false)
90
+ expect(o.purgedState).toContain('tmux-cmdlog/claude-beta')
94
91
  })
95
92
  })
96
93
 
package/src/cli/index.ts CHANGED
@@ -571,7 +571,6 @@ const USAGE = `usage: iapeer <verb> [args]
571
571
  self-fresh (agent self-call) mark /new eager-fresh + self-kill — the daemon relaunches fresh
572
572
  self-done (agent self-call, ephemeral) silent finish: arm own quiet-reap, wake no one
573
573
  native-memory <off|on> (--peer <p> | --all) gate/restore runtimes' native memory (canonized lever; контракт «Слот памяти»)
574
- memory-plugin <on|off> (--peer <p> | --all) install/remove the slot-declared provider plugin (claude per-peer, codex host-global)
575
574
  trust-hooks <hooks.json> [--check] pre-seed codex hooks trust for a file-form hooks.json (no modal); --check = drift report
576
575
  `
577
576
 
@@ -702,35 +701,6 @@ export async function runCli(argv: string[], env: NodeJS.ProcessEnv = process.en
702
701
  }
703
702
  return failed ? 1 : 0
704
703
  }
705
- case 'memory-plugin': {
706
- // Slot-derived provider-plugin install/removal (контракт «Слот памяти»
707
- // §Плагин провайдера; agreed iapeer-memory + boris 10.06). The plugin
708
- // identity comes from the slot declaration's v1.1 plugin block — empty
709
- // slot / v1 declaration → explicit refusal (generic `enable` exists for
710
- // arbitrary plugins). Forms live in enable/ (ONE home); codex off is
711
- // host-global (only --all removes; --peer reports it explicitly).
712
- const state = positionals[0]
713
- if (state !== 'on' && state !== 'off') return usage(errOut)
714
- const peerName = typeof flags.peer === 'string' ? flags.peer : undefined
715
- if (flags.all !== true && !peerName) return usage(errOut)
716
- const { memoryPluginApply } = await import('../enable/memoryPlugin.ts')
717
- const r = memoryPluginApply(state, { peer: peerName, all: flags.all === true, env })
718
- if (r.error) {
719
- errOut(`memory-plugin: ${r.error}\n`)
720
- return 1
721
- }
722
- // v1.2: a provision-declaring slot routes through the provider's commands —
723
- // r.plugin is null then (there is no marketplace plugin in play to print).
724
- out(
725
- r.plugin
726
- ? `plugin: ${r.plugin.name}@${r.plugin.marketplace} (provider-declared)\n`
727
- : `provision: provider command (declaration v1.2, occasion=${state === 'on' ? 'sweep-on' : flags.all === true ? 'off-all' : 'off-peer'})\n`,
728
- )
729
- for (const o of r.outcomes) {
730
- out(`${o.personality} (${o.runtime}): ${o.state}${o.detail ? ` — ${o.detail}` : ''}\n`)
731
- }
732
- return r.ok ? 0 : 1
733
- }
734
704
  case 'trust-hooks': {
735
705
  // Deterministic pre-seed of the codex hooks trust state for a FILE-form
736
706
  // hooks.json (user- or project-local layer) — the headless replacement for
@@ -398,8 +398,9 @@ function enableRuntimeWithRetry(
398
398
  * The registry-FREE enable core: install per-runtime (claude project-scope IN cwd /
399
399
  * codex global) + enable + `setup` ONLY if declared — with marketplace ensure
400
400
  * (marketplaceRef given + not registered → add) and the stale-snapshot retry.
401
- * Consumers: enableCapability (peer-resolved verb), provisionPeer's birth-time
402
- * memory-plugin hook, and the `memory-plugin` verb — ONE home of the install forms.
401
+ * Consumer: enableCapability (the peer-resolved generic `iapeer enable` verb) —
402
+ * ONE home of the install forms. (The memory-plugin birth-hook and verb consumers
403
+ * were removed 11.06 — the provider plugin form is retired, ADR-017.)
403
404
  */
404
405
  export function enableCapabilityForCwd(o: CwdEnableOptions): EnableResult {
405
406
  const env = o.env ?? process.env
@@ -472,58 +473,9 @@ export function enableCapability(opts: EnableOptions): EnableResult {
472
473
  }
473
474
 
474
475
  // ─────────────────────────────────────────────────────────────────────────────
475
- // Removal forms (контракт §Плагин провайдера, off-direction) — verb names
476
- // verified live 10.06: claude `plugin uninstall <id> --scope project` (run in the
477
- // peer cwd), codex `plugin remove <id>` (host-global).
476
+ // Removal forms — REMOVED 11.06 with the memory-plugin verb (their only caller;
477
+ // ADR-017 retired the provider plugin form). Historical verb names, verified
478
+ // live 10.06, for a future generic `disable`: claude `plugin uninstall <id>
479
+ // --scope project` (run in the peer cwd), codex `plugin remove <id>`
480
+ // (host-global). See git history ≤v0.2.41 for the implementations.
478
481
  // ─────────────────────────────────────────────────────────────────────────────
479
-
480
- export type RuntimeRemoveState = 'removed' | 'absent' | 'runtime-missing' | 'failed'
481
-
482
- export interface RuntimeRemoveResult {
483
- runtime: CapabilityRuntime
484
- state: RuntimeRemoveState
485
- detail?: string
486
- }
487
-
488
- /** claude: uninstall the peer-scoped (projectPath-keyed) entry — fleet-safe by the
489
- * same key enable uses; a peer without an entry is 'absent' (idempotent). */
490
- export function removeClaudeForCwd(
491
- plugin: string,
492
- marketplace: string,
493
- peerCwd: string,
494
- env: NodeJS.ProcessEnv = process.env,
495
- ): RuntimeRemoveResult {
496
- const bin = claudeBin(env)
497
- if (!isExecutable(bin, env)) return { runtime: 'claude', state: 'runtime-missing' }
498
- const id = `${plugin}@${marketplace}`
499
- const existing = claudeEntry(bin, plugin, marketplace, peerCwd, env)
500
- if (!existing) return { runtime: 'claude', state: 'absent' }
501
- const r = spawnSync(bin, ['plugin', 'uninstall', id, '--scope', 'project'], {
502
- cwd: peerCwd,
503
- encoding: 'utf8',
504
- env: env as Record<string, string>,
505
- })
506
- const after = claudeEntry(bin, plugin, marketplace, peerCwd, env)
507
- if (after) {
508
- return { runtime: 'claude', state: 'failed', detail: (r.stderr || r.stdout || `exit ${r.status}`).trim() }
509
- }
510
- return { runtime: 'claude', state: 'removed' }
511
- }
512
-
513
- /** codex: remove the HOST-GLOBAL plugin (codex has no project scope) — callers gate
514
- * this on an explicit fleet-wide intent (`memory-plugin off --all`). */
515
- export function removeCodexGlobal(
516
- plugin: string,
517
- marketplace: string,
518
- env: NodeJS.ProcessEnv = process.env,
519
- ): RuntimeRemoveResult {
520
- const bin = codexBin(env)
521
- if (!isExecutable(bin, env)) return { runtime: 'codex', state: 'runtime-missing' }
522
- const id = `${plugin}@${marketplace}`
523
- if (codexState(bin, id, env) === 'absent') return { runtime: 'codex', state: 'absent' }
524
- const r = spawnSync(bin, ['plugin', 'remove', id], { encoding: 'utf8', env: env as Record<string, string> })
525
- if (codexState(bin, id, env) !== 'absent') {
526
- return { runtime: 'codex', state: 'failed', detail: (r.stderr || r.stdout || `exit ${r.status}`).trim() }
527
- }
528
- return { runtime: 'codex', state: 'removed' }
529
- }
@@ -8,8 +8,11 @@
8
8
  // 2. `command` is ABSOLUTE (parser refuses relative — launchd minimal PATH).
9
9
  // 3. Timeout 120 s; best-effort semantics live at the CALL SITES (LOUD warn,
10
10
  // the birth/remove flow continues) — this module only reports structurally.
11
- // 4. {occasion} vocabulary: birth | sweep-on | off-peer | off-all | remove —
12
- // ref-counting of host-global surfaces is the PROVIDER's business.
11
+ // 4. {occasion} vocabulary (финален, contract §Provision): birth | sweep-on |
12
+ // off-peer | off-all | remove — ref-counting of host-global surfaces is the
13
+ // PROVIDER's business. Since the memory-plugin verb removal (11.06, ADR-017)
14
+ // the CORE emits only `birth` and `remove`; the sweep occasions remain part
15
+ // of the provider-command interface (their CLI may use them internally).
13
16
  // Idempotency is the provider's obligation by construction; the provider holds
14
17
  // its own lock against concurrent provisions.
15
18
 
@@ -26,7 +26,7 @@ import {
26
26
  } from '../core/constants.ts'
27
27
  import { buildProcessAddress, buildSocketPath, parseSessionName } from '../core/socket.ts'
28
28
  import { err, ok, type Result } from '../core/errors.ts'
29
- import { capCmdLogs } from '../launch/cmdlog.ts'
29
+ import { capCmdLogs, cmdLogDirFor } from '../launch/cmdlog.ts'
30
30
  import { resolveGlobalRoot } from '../storage/index.ts'
31
31
  import { readPeerProfile, resolveIdentity } from '../identity/index.ts'
32
32
  import {
@@ -422,7 +422,7 @@ export function purgeIdentityState(cfg: LifecycleConfig, identity: string): stri
422
422
  try {
423
423
  entries = readdirSync(cfg.stateDir)
424
424
  } catch {
425
- return removed // no state dir → nothing to purge
425
+ entries = [] // no state dir → no markers; the cmdlog purge below still runs
426
426
  }
427
427
  for (const name of entries) {
428
428
  if (!name.startsWith(`${identity}.`)) continue
@@ -433,6 +433,18 @@ export function purgeIdentityState(cfg: LifecycleConfig, identity: string): stri
433
433
  /* best-effort — a locked/vanished entry must not fail the remove */
434
434
  }
435
435
  }
436
+ // tmux command-log dir (0.2.40 observability) — identity-keyed like the state
437
+ // markers above, so it must die with the registry record too (the leak class
438
+ // noted at the 0.2.40 acceptance: a removed peer left its cmdlog dir forever).
439
+ try {
440
+ const cmdDir = cmdLogDirFor(cfg.eventLogDir, identity)
441
+ if (existsSync(cmdDir)) {
442
+ rmSync(cmdDir, { recursive: true, force: true })
443
+ removed.push(`tmux-cmdlog/${identity}`)
444
+ }
445
+ } catch {
446
+ /* best-effort */
447
+ }
436
448
  return removed
437
449
  }
438
450
 
@@ -124,8 +124,8 @@ export function isAgfpdInList(listOutput: string): boolean {
124
124
  }
125
125
 
126
126
  /** Register a marketplace for this runtime (`<runtime> plugin marketplace add <ref>`).
127
- * Exported for the memory-plugin install path (контракт §Плагин провайдера: a
128
- * third-party provider's marketplaceRef must self-register on a fresh host). */
127
+ * Exported for the generic capability-enable path (a third-party marketplaceRef
128
+ * must self-register on a fresh host). */
129
129
  export function registerMarketplace(
130
130
  runtime: OnboardRuntime,
131
131
  env: NodeJS.ProcessEnv,
@@ -142,7 +142,7 @@ export function registerMarketplace(
142
142
  }
143
143
 
144
144
  /** Is a marketplace (by name/ref) registered for this runtime? Generalized form of
145
- * isMarketplaceRegistered for the memory-plugin install path. */
145
+ * isMarketplaceRegistered for the generic capability-enable path. */
146
146
  export function isMarketplaceRegisteredAs(
147
147
  runtime: OnboardRuntime,
148
148
  name: string,
@@ -125,11 +125,11 @@ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<Provisi
125
125
  // v1.2 — ADR-009 inversion; решение Артура 10.06, схема сверена с
126
126
  // iapeer-memory 11.06): an occupied slot WITH a provision block shells into
127
127
  // the PROVIDER's command per agentic runtime (occasion=birth) — the core
128
- // knows nothing about surface forms. PRECEDENCE: provision WINS over the
129
- // deprecated v1.1 plugin block, with NO runtime fallback to plugin on
130
- // failure (masking a provision refusal would hide a broken provider —
131
- // LOUD warn + repair-hint instead). Runs AFTER preTrustCodexCwd above, so
132
- // the provider's per-peer codex surfaces land in an already-trusted cwd.
128
+ // knows nothing about surface forms. A failure is NEVER masked (a broken
129
+ // provider must not hide) — LOUD warn; repair is provider-side (the core is
130
+ // provider-agnostic and names no command). Runs AFTER preTrustCodexCwd
131
+ // above, so the provider's per-peer codex surfaces land in an already-
132
+ // trusted cwd.
133
133
  if (slot.provision) {
134
134
  const { runProvisionCommand } = await import('../enable/provisionCommand.ts')
135
135
  const { appendLifecycleEvent } = await import('../lifecycle/eventlog.ts')
@@ -161,44 +161,15 @@ export async function provisionPeer(opts: ProvisionPeerOptions): Promise<Provisi
161
161
  opts.warn?.(
162
162
  `memory provision (${rt}) ${o.state.toUpperCase()} for "${profile.personality}"` +
163
163
  `${o.detail ? `: ${o.detail}` : ''} — память пира не подключена; ` +
164
- `repair: iapeer memory-plugin on --peer ${profile.personality}`,
164
+ `repair: re-run the provider's provision for this peer (the provider's own CLI — verify/repair/init)`,
165
165
  )
166
166
  }
167
167
  }
168
168
  }
169
- // Provider PLUGIN auto-install (контракт §Плагин провайдера, declaration
170
- // v1.1, DEPRECATED by §Provision; agreed iapeer-memory + boris 10.06): an
171
- // occupied slot WITH a plugin block and WITHOUT a provision block installs
172
- // the provider's marketplace plugin into the newborn's scope (claude
173
- // project-scope in cwd; codex host-global, idempotent) — «создал пира —
174
- // память работает». A v1 declaration (no block) installs nothing.
175
- // Best-effort with a LOUD warn; repair = `iapeer memory-plugin on --peer`.
176
- else if (slot.plugin) {
177
- const { enableCapabilityForCwd } = await import('../enable/index.ts')
178
- const agentic = profile.runtimes.filter((r): r is 'claude' | 'codex' => r === 'claude' || r === 'codex')
179
- if (agentic.length > 0) {
180
- const r = enableCapabilityForCwd({
181
- plugin: slot.plugin.name,
182
- marketplace: slot.plugin.marketplace,
183
- marketplaceRef: slot.plugin.marketplaceRef,
184
- cwd,
185
- personality: profile.personality,
186
- runtimes: agentic,
187
- env,
188
- })
189
- for (const rr of r.runtimes) {
190
- if (rr.state === 'failed') {
191
- opts.warn?.(
192
- `memory-plugin install (${rr.runtime}) FAILED for "${profile.personality}": ${rr.detail} — ` +
193
- `repair: iapeer memory-plugin on --peer ${profile.personality}`,
194
- )
195
- }
196
- }
197
- if (r.setup === 'failed') {
198
- opts.warn?.(`memory-plugin setup FAILED for "${profile.personality}": ${r.setupDetail ?? ''}`)
199
- }
200
- }
201
- }
169
+ // NB: the v1.1 plugin auto-install branch (an occupied slot WITH a `plugin`
170
+ // block) was REMOVED 11.06 — the plugin form is retired ecosystem-wide
171
+ // (provider's ADR-017; contract §Плагин провайдера is a tombstone). A v1.2
172
+ // declaration without a provision block provisions nothing here.
202
173
  }
203
174
  } catch (e) {
204
175
  opts.warn?.(`memory birth-time hook failed: ${e instanceof Error ? e.message : String(e)}`)
@@ -199,7 +199,7 @@ describe('provisionPeer birth-time native-memory lever', () => {
199
199
  expect(existsSync(join(cwd, '.codex', 'config.toml'))).toBe(false)
200
200
  })
201
201
 
202
- test('slot with v1.1 plugin block → birth hook attempts the plugin install; runtime-missing host stays silent and provision SUCCEEDS', async () => {
202
+ test('LEGACY v1.1 plugin block in the declaration → ignored entirely (form removed 11.06, ADR-017); native lever still applies', async () => {
203
203
  const root = mkTmp()
204
204
  const env = envFor(root)
205
205
  mkdirSync(join(root, 'iapeer'), { recursive: true })
@@ -210,21 +210,20 @@ describe('provisionPeer birth-time native-memory lever', () => {
210
210
  package: '@agfpd/iapeer-memory',
211
211
  version: '0.1.0',
212
212
  registeredAt: 'x',
213
+ // a stale v1.1 declaration on a legacy host — the core no longer parses it
213
214
  plugin: { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' },
214
215
  }),
215
216
  )
216
217
  const cwd = join(root, 'wplug')
217
218
  const warns: string[] = []
218
- // HOME-scoped claude bin + no codex on PATH → both runtimes 'runtime-missing':
219
- // a clean skip (host without the runtime), NOT a failure warn, and never a throw.
220
219
  const r = await provisionPeer({ cwd, runtime: 'claude', env, warn: m => warns.push(m) })
221
220
  expect(r.personality).toBe('wplug')
222
- expect(warns.filter(w => w.includes('memory-plugin install'))).toEqual([])
221
+ expect(warns).toEqual([]) // no install attempt, no warn — the block is unknown noise
223
222
  // the native lever still applied (same slot-gated block)
224
223
  expect(JSON.parse(readFileSync(join(cwd, '.claude', 'settings.json'), 'utf8')).autoMemoryEnabled).toBe(false)
225
224
  })
226
225
 
227
- test('v1.2: slot with provision command → birth shells into it per runtime (occasion=birth), plugin path NOT taken', async () => {
226
+ test('v1.2: slot with provision command → birth shells into it per runtime (occasion=birth); a legacy plugin block alongside is inert', async () => {
228
227
  const root = mkTmp()
229
228
  const env = envFor(root)
230
229
  mkdirSync(join(root, 'iapeer'), { recursive: true })
@@ -240,7 +239,7 @@ describe('provisionPeer birth-time native-memory lever', () => {
240
239
  package: '@agfpd/iapeer-memory',
241
240
  version: '0.2.0',
242
241
  registeredAt: 'x',
243
- // BOTH blocks declared → provision must WIN (precedence, no plugin attempt)
242
+ // legacy plugin block alongside (stale declaration) — must be inert
244
243
  plugin: { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' },
245
244
  provision: {
246
245
  command: script,
@@ -258,7 +257,7 @@ describe('provisionPeer birth-time native-memory lever', () => {
258
257
  expect(j.slice(1, 9)).toEqual(['--cwd', cwd, '--runtime', 'claude', '--personality', 'wprov', '--occasion', 'birth'])
259
258
  })
260
259
 
261
- test('v1.2: provision command FAILS → LOUD warn with repair-hint, provision of the peer still SUCCEEDS (best-effort)', async () => {
260
+ test('v1.2: provision command FAILS → LOUD warn (provider-side repair), provision of the peer still SUCCEEDS (best-effort)', async () => {
262
261
  const root = mkTmp()
263
262
  const env = envFor(root)
264
263
  mkdirSync(join(root, 'iapeer'), { recursive: true })
@@ -284,6 +283,6 @@ describe('provisionPeer birth-time native-memory lever', () => {
284
283
  expect(w).toBeDefined()
285
284
  expect(w).toContain('FAILED')
286
285
  expect(w).toContain('vault offline')
287
- expect(w).toContain('memory-plugin on --peer wfail') // repair-hint
286
+ expect(w).toContain("re-run the provider's provision") // provider-side repair, no core verb named
288
287
  })
289
288
  })
@@ -17,21 +17,10 @@ import { iapeerBinPath } from '../install/index.ts'
17
17
  /** The slot-declaration filename in the storage root (next to peers-profiles.json). */
18
18
  export const MEMORY_PROVIDER_FILE = 'memory-provider.json'
19
19
 
20
- /** The provider's marketplace plugin (declaration v1.1, контракт §Плагин
21
- * провайдера — agreed with iapeer-memory + boris 10.06). Declares WHAT the core
22
- * auto-installs into a newborn peer's scope (and what `memory-plugin` targets).
23
- * All three fields are required when the block is present; an invalid block is
24
- * treated as ABSENT (fail-open, like the whole file) = v1 behavior, no install. */
25
- export interface MemoryProviderPlugin {
26
- /** Plugin id in the marketplace (forms `<name>@<marketplace>` for installs). */
27
- name: string
28
- /** Marketplace NAME the plugin id keys on (e.g. "agfpd"). */
29
- marketplace: string
30
- /** Source ref for `plugin marketplace add` when the marketplace is not yet
31
- * registered on the host (owner/repo or URL) — the third-party-provider path;
32
- * for the distribution default it matches what onboard already adds. */
33
- marketplaceRef: string
34
- }
20
+ // NB: the v1.1 `plugin` block (MemoryProviderPlugin + its parser) was REMOVED
21
+ // 11.06 (provider's ADR-017 retired the plugin form ecosystem-wide; contract
22
+ // §Плагин провайдера is a tombstone). An unknown `plugin` key in a legacy
23
+ // declaration is simply ignored — the same fail-open as any unknown block.
35
24
 
36
25
  /** The provider's provision command (declaration v1.2, контракт §Provision
37
26
  * провайдера — ADR-009 inversion, решение Артура 10.06 + дизайн-консенсус
@@ -58,30 +47,14 @@ export interface MemoryProvider {
58
47
  /** Optional liveness proxy: an absolute path whose mtime the provider's daemon
59
48
  * refreshes. status reports its age; the core takes NO action on staleness. */
60
49
  heartbeat?: string
61
- /** Optional marketplace-plugin declaration (v1.1) — see MemoryProviderPlugin.
62
- * DEPRECATED by v1.2: when `provision` is also present, provision WINS and
63
- * the plugin block is ignored (no runtime fallback — an explicit precedence,
64
- * agreed iapeer-memory 11.06). */
65
- plugin?: MemoryProviderPlugin
66
- /** Optional v1.2 provision command (peer-birth / sweep-on joints). */
50
+ /** Optional v1.2 provision command (the peer-birth joint). */
67
51
  provision?: MemoryProviderProvision
68
- /** Optional v1.2 unprovision command (off-peer / off-all / remove joints). */
52
+ /** Optional v1.2 unprovision command (the remove joint). */
69
53
  unprovision?: MemoryProviderProvision
70
54
  }
71
55
 
72
- /** Parse the optional v1.1 `plugin` block; anything short of three non-empty
73
- * strings → undefined (treated as absent — v1 declaration, core installs nothing). */
74
- function parsePluginBlock(raw: unknown): MemoryProviderPlugin | undefined {
75
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined
76
- const o = raw as Record<string, unknown>
77
- if (typeof o.name !== 'string' || !o.name.trim()) return undefined
78
- if (typeof o.marketplace !== 'string' || !o.marketplace.trim()) return undefined
79
- if (typeof o.marketplaceRef !== 'string' || !o.marketplaceRef.trim()) return undefined
80
- return { name: o.name.trim(), marketplace: o.marketplace.trim(), marketplaceRef: o.marketplaceRef.trim() }
81
- }
82
-
83
56
  /** Parse an optional v1.2 `provision`/`unprovision` block. Fail-open like the
84
- * plugin block: anything short of an ABSOLUTE command string + an array of
57
+ * whole file: anything short of an ABSOLUTE command string + an array of
85
58
  * strings → undefined (treated as absent). A relative command is INVALID by
86
59
  * contract (launchd minimal PATH would resolve it differently per caller). */
87
60
  function parseProvisionBlock(raw: unknown): MemoryProviderProvision | undefined {
@@ -108,7 +81,6 @@ export function readMemoryProvider(env: NodeJS.ProcessEnv = process.env): Memory
108
81
  if (typeof o.provider !== 'string' || !o.provider.trim()) return null
109
82
  if (typeof o.package !== 'string' || !o.package.trim()) return null
110
83
  if (typeof o.version !== 'string' || !o.version.trim()) return null
111
- const plugin = parsePluginBlock(o.plugin)
112
84
  const provision = parseProvisionBlock(o.provision)
113
85
  const unprovision = parseProvisionBlock(o.unprovision)
114
86
  return {
@@ -117,7 +89,6 @@ export function readMemoryProvider(env: NodeJS.ProcessEnv = process.env): Memory
117
89
  version: o.version.trim(),
118
90
  registeredAt: typeof o.registeredAt === 'string' ? o.registeredAt : '',
119
91
  ...(typeof o.heartbeat === 'string' && o.heartbeat.trim() ? { heartbeat: o.heartbeat.trim() } : {}),
120
- ...(plugin ? { plugin } : {}),
121
92
  ...(provision ? { provision } : {}),
122
93
  ...(unprovision ? { unprovision } : {}),
123
94
  }
@@ -62,23 +62,19 @@ describe('readMemoryProvider (slot declaration, fail-open)', () => {
62
62
  }
63
63
  })
64
64
 
65
- test('v1.1 plugin block: parsed when complete; INVALID block = absent (v1 behavior), declaration still valid', () => {
65
+ test('LEGACY v1.1 plugin block (any shape) → ignored as unknown noise; the declaration itself stays valid (form removed 11.06, ADR-017)', () => {
66
66
  const root = mkTmp()
67
67
  const env = envFor(root)
68
- const plugin = { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' }
69
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, plugin }))
70
- expect(readMemoryProvider(env)?.plugin).toEqual(plugin)
71
- // incomplete / wrong-shape blocks → treated as ABSENT, the declaration itself stays valid
72
- for (const bad of [
73
- { name: 'x', marketplace: 'agfpd' }, // missing marketplaceRef
74
- { name: '', marketplace: 'agfpd', marketplaceRef: 'r' }, // empty name
75
- 'iapeer-memory', // not an object
76
- ['x'], // array
68
+ for (const legacy of [
69
+ { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' }, // a complete v1.1 block
70
+ { name: 'x', marketplace: 'agfpd' },
71
+ 'iapeer-memory',
72
+ ['x'],
77
73
  ]) {
78
- writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, plugin: bad }))
74
+ writeFileSync(memoryProviderPath(env), JSON.stringify({ ...VALID, plugin: legacy }))
79
75
  const p = readMemoryProvider(env)
80
76
  expect(p).not.toBeNull()
81
- expect(p?.plugin).toBeUndefined()
77
+ expect((p as Record<string, unknown> | null)?.plugin).toBeUndefined() // not parsed at all
82
78
  }
83
79
  })
84
80
 
@@ -1,197 +0,0 @@
1
- // memory-plugin — slot-derived gating + target resolution + codex host-global
2
- // semantics (контракт §Плагин провайдера). Install forms are NOT exercised here
3
- // (they spawn runtime CLIs); the bins are pointed at /nonexistent so every
4
- // runtime resolves 'runtime-missing' — the orchestration around them is what
5
- // these tests pin.
6
-
7
- import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
8
- import { mkdtempSync, rmSync, writeFileSync } from 'fs'
9
- import { tmpdir } from 'os'
10
- import { join } from 'path'
11
- import { upsertPeer } from '../registry/index.ts'
12
- import { memoryProviderPath } from '../status/index.ts'
13
- import { memoryPluginApply } from './memoryPlugin.ts'
14
-
15
- let root: string
16
- function env(): NodeJS.ProcessEnv {
17
- const e: NodeJS.ProcessEnv = {
18
- ...process.env,
19
- IAPEER_ROOT: root,
20
- IAPEER_CLAUDE_BIN: '/nonexistent/claude',
21
- IAPEER_CODEX_BIN: '/nonexistent/codex',
22
- }
23
- delete e.PEER_PERSONALITY
24
- delete e.PEER_IDENTITY
25
- delete e.PEER_RUNTIME
26
- return e
27
- }
28
-
29
- beforeEach(() => {
30
- root = mkdtempSync(join(tmpdir(), 'iapeer-memplugin-'))
31
- })
32
- afterEach(() => {
33
- rmSync(root, { recursive: true, force: true })
34
- })
35
-
36
- const DECLARATION = {
37
- provider: 'iapeer-memory',
38
- package: '@agfpd/iapeer-memory',
39
- version: '0.1.0',
40
- registeredAt: '2026-06-10T00:00:00Z',
41
- }
42
- const PLUGIN = { name: 'iapeer-memory', marketplace: 'agfpd', marketplaceRef: 'agfpd/agfpd-marketplace' }
43
-
44
- function declareSlot(withPlugin: boolean): void {
45
- writeFileSync(memoryProviderPath(env()), JSON.stringify(withPlugin ? { ...DECLARATION, plugin: PLUGIN } : DECLARATION))
46
- }
47
-
48
- describe('memory-plugin gating (slot-derived)', () => {
49
- test('EMPTY slot → explicit refusal, no outcomes', () => {
50
- const r = memoryPluginApply('on', { all: true, env: env() })
51
- expect(r.ok).toBe(false)
52
- expect(r.error).toContain('EMPTY')
53
- expect(r.outcomes).toEqual([])
54
- })
55
-
56
- test('v1 declaration (no plugin block) → explicit refusal pointing at generic enable', () => {
57
- declareSlot(false)
58
- const r = memoryPluginApply('on', { all: true, env: env() })
59
- expect(r.ok).toBe(false)
60
- expect(r.error).toContain('declares no plugin block')
61
- })
62
-
63
- test('unknown --peer → explicit refusal', async () => {
64
- declareSlot(true)
65
- const r = memoryPluginApply('on', { peer: 'ghost', env: env() })
66
- expect(r.ok).toBe(false)
67
- expect(r.error).toContain('not in the iapeer peers index')
68
- })
69
-
70
- test('on --all: plugin derived from the slot; runtime-missing host → clean per-runtime outcomes, ok', async () => {
71
- declareSlot(true)
72
- await upsertPeer({ personality: 'alpha', runtime: 'claude', cwd: join(root, 'alpha'), intelligence: 'artificial' }, { rootDir: root })
73
- await upsertPeer({ personality: 'tim', runtime: 'notifier', cwd: join(root, 'tim'), intelligence: 'absent' }, { rootDir: root })
74
- const r = memoryPluginApply('on', { all: true, env: env() })
75
- expect(r.plugin).toEqual(PLUGIN)
76
- expect(r.ok).toBe(true) // runtime-missing is a skip, not a failure
77
- const alpha = r.outcomes.find(o => o.personality === 'alpha')
78
- expect(alpha?.state).toBe('runtime-missing')
79
- // a peer with NO agentic runtime is skipped explicitly
80
- const tim = r.outcomes.find(o => o.personality === 'tim')
81
- expect(tim?.state).toBe('skipped-no-agentic-runtime')
82
- })
83
-
84
- test('off --peer on a codex peer → explicit host-global message, NOT a silent skip', async () => {
85
- declareSlot(true)
86
- await upsertPeer({ personality: 'cx', runtime: 'codex', cwd: join(root, 'cx'), intelligence: 'artificial' }, { rootDir: root })
87
- const r = memoryPluginApply('off', { peer: 'cx', env: env() })
88
- const o = r.outcomes.find(x => x.personality === 'cx' && x.runtime === 'codex')
89
- expect(o?.state).toBe('skipped-host-global')
90
- expect(o?.detail).toContain('--all')
91
- })
92
-
93
- test('off --all: codex host-global remove runs ONCE across the fleet', async () => {
94
- declareSlot(true)
95
- await upsertPeer({ personality: 'cx1', runtime: 'codex', cwd: join(root, 'cx1'), intelligence: 'artificial' }, { rootDir: root })
96
- await upsertPeer({ personality: 'cx2', runtime: 'codex', cwd: join(root, 'cx2'), intelligence: 'artificial' }, { rootDir: root })
97
- const r = memoryPluginApply('off', { all: true, env: env() })
98
- const codexOutcomes = r.outcomes.filter(x => x.runtime === 'codex')
99
- // one real attempt (runtime-missing on this sandbox) + one 'already' dedupe
100
- expect(codexOutcomes.filter(x => x.state === 'runtime-missing').length).toBe(1)
101
- expect(codexOutcomes.filter(x => x.state === 'already').length).toBe(1)
102
- })
103
- })
104
-
105
- // ─── v1.2 provision-инверсия (контракт §Provision провайдера, ADR-009) ───────
106
-
107
- import { chmodSync, readFileSync, existsSync } from 'fs'
108
-
109
- /** Declare a v1.2 slot whose provision/unprovision are fake journaling scripts. */
110
- function declareProvisionSlot(o: { provision?: boolean; unprovision?: boolean; alsoPlugin?: boolean; failing?: boolean }): {
111
- journal: string
112
- } {
113
- const journal = join(root, 'journal.txt')
114
- const script = join(root, 'fake-provider.sh')
115
- writeFileSync(
116
- script,
117
- o.failing ? `#!/bin/sh\necho provider broken >&2\nexit 7\n` : `#!/bin/sh\nprintf '%s\\n' "$@" >> '${journal}'\n`,
118
- )
119
- chmodSync(script, 0o755)
120
- const block = (occ: string) => ({
121
- command: script,
122
- args: [occ, '--cwd', '{cwd}', '--runtime', '{runtime}', '--personality', '{personality}', '--occasion', '{occasion}'],
123
- })
124
- writeFileSync(
125
- memoryProviderPath(env()),
126
- JSON.stringify({
127
- ...DECLARATION,
128
- ...(o.alsoPlugin ? { plugin: PLUGIN } : {}),
129
- ...(o.provision !== false ? { provision: block('provision-peer') } : {}),
130
- ...(o.unprovision !== false ? { unprovision: block('unprovision-peer') } : {}),
131
- }),
132
- )
133
- return { journal }
134
- }
135
-
136
- describe('memory-plugin v1.2 provision routing', () => {
137
- test('on --peer: provision command invoked per agentic runtime with occasion=sweep-on, substituted argv', async () => {
138
- const { journal } = declareProvisionSlot({})
139
- await upsertPeer({ personality: 'alpha', runtime: 'claude', cwd: join(root, 'alpha'), intelligence: 'artificial' }, { rootDir: root })
140
- const r = memoryPluginApply('on', { peer: 'alpha', env: env() })
141
- expect(r.ok).toBe(true)
142
- expect(r.outcomes).toEqual([{ personality: 'alpha', runtime: 'claude', state: 'ok', detail: undefined }])
143
- const j = readFileSync(journal, 'utf8').split('\n')
144
- expect(j[0]).toBe('provision-peer')
145
- expect(j.slice(1, 9)).toEqual(['--cwd', join(root, 'alpha'), '--runtime', 'claude', '--personality', 'alpha', '--occasion', 'sweep-on'])
146
- })
147
-
148
- test('PRECEDENCE: slot with BOTH blocks routes to provision; the plugin path is never touched', async () => {
149
- const { journal } = declareProvisionSlot({ alsoPlugin: true })
150
- await upsertPeer({ personality: 'beta', runtime: 'claude', cwd: join(root, 'beta'), intelligence: 'artificial' }, { rootDir: root })
151
- const r = memoryPluginApply('on', { peer: 'beta', env: env() })
152
- expect(r.ok).toBe(true)
153
- // plugin path on this sandbox would yield 'runtime-missing' (bins at /nonexistent);
154
- // 'ok' from the journaling script proves the provision command ran instead.
155
- expect(r.outcomes[0]?.state).toBe('ok')
156
- expect(readFileSync(journal, 'utf8')).toContain('provision-peer')
157
- })
158
-
159
- test('provision failure → ok=false with detail, NO fallback to the plugin path', async () => {
160
- declareProvisionSlot({ failing: true, alsoPlugin: true })
161
- await upsertPeer({ personality: 'gamma', runtime: 'claude', cwd: join(root, 'gamma'), intelligence: 'artificial' }, { rootDir: root })
162
- const r = memoryPluginApply('on', { peer: 'gamma', env: env() })
163
- expect(r.ok).toBe(false)
164
- expect(r.outcomes[0]?.state).toBe('failed')
165
- expect(r.outcomes[0]?.detail).toContain('provider broken')
166
- })
167
-
168
- test('off --peer → unprovision occasion=off-peer; off --all → off-all; codex has NO host-global special-casing', async () => {
169
- const { journal } = declareProvisionSlot({})
170
- await upsertPeer({ personality: 'cx', runtime: 'codex', cwd: join(root, 'cx'), intelligence: 'artificial' }, { rootDir: root })
171
- const r1 = memoryPluginApply('off', { peer: 'cx', env: env() })
172
- expect(r1.ok).toBe(true)
173
- expect(r1.outcomes[0]).toEqual({ personality: 'cx', runtime: 'codex', state: 'ok', detail: undefined })
174
- const r2 = memoryPluginApply('off', { all: true, env: env() })
175
- expect(r2.ok).toBe(true)
176
- const j = readFileSync(journal, 'utf8')
177
- expect(j).toContain('off-peer')
178
- expect(j).toContain('off-all')
179
- expect(j).toContain('unprovision-peer')
180
- })
181
-
182
- test('missing direction block → explicit refusal (no silent skip, no plugin fallback)', async () => {
183
- declareProvisionSlot({ unprovision: false, alsoPlugin: true })
184
- await upsertPeer({ personality: 'delta', runtime: 'claude', cwd: join(root, 'delta'), intelligence: 'artificial' }, { rootDir: root })
185
- const r = memoryPluginApply('off', { peer: 'delta', env: env() })
186
- expect(r.ok).toBe(false)
187
- expect(r.error).toContain('declares no unprovision command')
188
- })
189
-
190
- test('non-agentic peer → explicit skip (provision never invoked)', async () => {
191
- const { journal } = declareProvisionSlot({})
192
- await upsertPeer({ personality: 'tim2', runtime: 'notifier', cwd: join(root, 'tim2'), intelligence: 'absent' }, { rootDir: root })
193
- const r = memoryPluginApply('on', { peer: 'tim2', env: env() })
194
- expect(r.outcomes[0]?.state).toBe('skipped-no-agentic-runtime')
195
- expect(existsSync(journal)).toBe(false)
196
- })
197
- })
@@ -1,210 +0,0 @@
1
- // memory-plugin — the slot-derived operator verb (контракт «Слот памяти»
2
- // §Плагин провайдера — авто-установка при рождении; agreed iapeer-memory + boris
3
- // 10.06). By analogy with native-memory: `iapeer memory-plugin <on|off>
4
- // (--peer <p> | --all)`. The plugin identity is DERIVED from the slot
5
- // declaration's v1.1 `plugin` block — an empty slot or a v1 declaration without
6
- // the block is an EXPLICIT refusal (for arbitrary plugins the generic `iapeer
7
- // enable` exists). Install/removal forms live in enable/index.ts (the ONE home);
8
- // this module only orchestrates targets and the codex host-global semantics.
9
- //
10
- // Consumers: the operator, the provider's init-sweep (`on --all` after writing
11
- // the declaration), and the provider's uninstall (`off --all` BEFORE deleting
12
- // the declaration — agreed AUTO-removal: a dead provider's plugin actively
13
- // errors in every session, unlike the native-memory restore asymmetry).
14
-
15
- import { appendLifecycleEvent } from '../lifecycle/eventlog.ts'
16
- import { findPeer, readPeersIndex } from '../registry/index.ts'
17
- import { readMemoryProvider, type MemoryProviderPlugin, type MemoryProviderProvision } from '../status/index.ts'
18
- import { pluginLogsDir } from '../storage/index.ts'
19
- import {
20
- enableCapabilityForCwd,
21
- removeClaudeForCwd,
22
- removeCodexGlobal,
23
- type CapabilityRuntime,
24
- } from './index.ts'
25
- import { runProvisionCommand, type ProvisionOccasion } from './provisionCommand.ts'
26
-
27
- export interface MemoryPluginOutcome {
28
- personality: string
29
- runtime: CapabilityRuntime | 'none'
30
- /** enable states (on) | remove states (off) | orchestration-level skips. */
31
- state: string
32
- detail?: string
33
- }
34
-
35
- export interface MemoryPluginResult {
36
- ok: boolean
37
- /** The derived plugin (null when the slot refused — see error). */
38
- plugin: MemoryProviderPlugin | null
39
- /** Refusal reason (empty slot / v1 declaration / no targets). */
40
- error?: string
41
- outcomes: MemoryPluginOutcome[]
42
- }
43
-
44
- export interface MemoryPluginOptions {
45
- peer?: string
46
- all?: boolean
47
- env?: NodeJS.ProcessEnv
48
- }
49
-
50
- /**
51
- * Apply the slot-declared provider plugin across targets. `on` = install per the
52
- * canonical forms (claude project-scope per peer cwd; codex host-global once —
53
- * idempotent). `off` = claude per-peer project-scope uninstall; codex is
54
- * HOST-GLOBAL, so off with `--peer` on a codex runtime reports an explicit
55
- * «host-global — use --all» (no silent skip, сверка boris) and the global remove
56
- * runs only under `--all` (once).
57
- */
58
- export function memoryPluginApply(state: 'on' | 'off', opts: MemoryPluginOptions = {}): MemoryPluginResult {
59
- const env = opts.env ?? process.env
60
- const slot = readMemoryProvider(env)
61
- if (!slot) {
62
- return { ok: false, plugin: null, error: 'memory slot is EMPTY — no provider declared, nothing to install', outcomes: [] }
63
- }
64
- // v1.2 PRECEDENCE (контракт §Provision провайдера): a provision-declaring slot
65
- // routes BOTH verb directions through the provider's commands; the deprecated
66
- // v1.1 plugin block is ignored entirely (no fallback — masking a provision
67
- // failure would hide a broken provider behind a stale plugin install).
68
- if (slot.provision || slot.unprovision) {
69
- return memoryProvisionApply(state, slot.provision, slot.unprovision, slot.provider, opts, env)
70
- }
71
- if (!slot.plugin) {
72
- return {
73
- ok: false,
74
- plugin: null,
75
- error: `provider "${slot.provider}" declares no plugin block (declaration v1) — for arbitrary plugins use \`iapeer enable\``,
76
- outcomes: [],
77
- }
78
- }
79
- const plugin = slot.plugin
80
- const index = readPeersIndex({ env })
81
- const targets = opts.all === true ? index.peers : opts.peer ? (p => (p ? [p] : []))(findPeer(index, opts.peer)) : []
82
- if (targets.length === 0) {
83
- return {
84
- ok: false,
85
- plugin,
86
- error: opts.peer ? `peer "${opts.peer}" is not in the iapeer peers index` : 'no targets — pass --peer <p> or --all',
87
- outcomes: [],
88
- }
89
- }
90
-
91
- const outcomes: MemoryPluginOutcome[] = []
92
- let codexHandledGlobally = false
93
- for (const p of targets) {
94
- const agentic = p.runtimes.filter((r): r is CapabilityRuntime => r === 'claude' || r === 'codex')
95
- if (agentic.length === 0) {
96
- outcomes.push({ personality: p.personality, runtime: 'none', state: 'skipped-no-agentic-runtime' })
97
- continue
98
- }
99
- if (state === 'on') {
100
- const r = enableCapabilityForCwd({
101
- plugin: plugin.name,
102
- marketplace: plugin.marketplace,
103
- marketplaceRef: plugin.marketplaceRef,
104
- cwd: p.cwd,
105
- personality: p.personality,
106
- runtimes: agentic,
107
- env,
108
- })
109
- for (const rr of r.runtimes) {
110
- outcomes.push({ personality: p.personality, runtime: rr.runtime, state: rr.state, detail: rr.detail })
111
- }
112
- if (r.setup !== 'absent') {
113
- outcomes.push({ personality: p.personality, runtime: agentic[0]!, state: `setup-${r.setup}`, detail: r.setupDetail })
114
- }
115
- } else {
116
- for (const rt of agentic) {
117
- if (rt === 'claude') {
118
- const rr = removeClaudeForCwd(plugin.name, plugin.marketplace, p.cwd, env)
119
- outcomes.push({ personality: p.personality, runtime: 'claude', state: rr.state, detail: rr.detail })
120
- } else {
121
- // codex plugin is HOST-GLOBAL: per-peer off is impossible — explicit
122
- // message (сверка boris), the actual remove runs ONCE under --all.
123
- if (opts.all !== true) {
124
- outcomes.push({
125
- personality: p.personality,
126
- runtime: 'codex',
127
- state: 'skipped-host-global',
128
- detail: 'codex plugin is host-global — use `memory-plugin off --all`',
129
- })
130
- } else if (!codexHandledGlobally) {
131
- codexHandledGlobally = true
132
- const rr = removeCodexGlobal(plugin.name, plugin.marketplace, env)
133
- outcomes.push({ personality: p.personality, runtime: 'codex', state: rr.state, detail: rr.detail })
134
- } else {
135
- outcomes.push({ personality: p.personality, runtime: 'codex', state: 'already', detail: 'host-global remove already done this run' })
136
- }
137
- }
138
- }
139
- }
140
- }
141
- const failed = outcomes.some(o => o.state === 'failed' || o.state === 'setup-failed')
142
- return { ok: !failed, plugin, outcomes }
143
- }
144
-
145
- /**
146
- * v1.2 verb routing through the provider's provision/unprovision commands
147
- * (occasion: on → sweep-on; off --peer → off-peer; off --all → off-all). One
148
- * invocation per target peer × agentic runtime — NO host-global special-casing
149
- * here: ref-counting of host-global surfaces (codex) moved to the PROVIDER with
150
- * the inversion (contract requirement 4). A direction whose command block is
151
- * absent is an explicit refusal (never silently skipped, never plugin-fallback).
152
- */
153
- function memoryProvisionApply(
154
- state: 'on' | 'off',
155
- provision: MemoryProviderProvision | undefined,
156
- unprovision: MemoryProviderProvision | undefined,
157
- provider: string,
158
- opts: MemoryPluginOptions,
159
- env: NodeJS.ProcessEnv,
160
- ): MemoryPluginResult {
161
- const block = state === 'on' ? provision : unprovision
162
- if (!block) {
163
- return {
164
- ok: false,
165
- plugin: null,
166
- error:
167
- `provider "${provider}" declares no ${state === 'on' ? 'provision' : 'unprovision'} command ` +
168
- `(declaration v1.2 requires both directions) — fix the provider's declaration`,
169
- outcomes: [],
170
- }
171
- }
172
- const index = readPeersIndex({ env })
173
- const targets = opts.all === true ? index.peers : opts.peer ? (p => (p ? [p] : []))(findPeer(index, opts.peer)) : []
174
- if (targets.length === 0) {
175
- return {
176
- ok: false,
177
- plugin: null,
178
- error: opts.peer ? `peer "${opts.peer}" is not in the iapeer peers index` : 'no targets — pass --peer <p> or --all',
179
- outcomes: [],
180
- }
181
- }
182
- const occasion: ProvisionOccasion = state === 'on' ? 'sweep-on' : opts.all === true ? 'off-all' : 'off-peer'
183
- const outcomes: MemoryPluginOutcome[] = []
184
- for (const p of targets) {
185
- const agentic = p.runtimes.filter((r): r is CapabilityRuntime => r === 'claude' || r === 'codex')
186
- if (agentic.length === 0) {
187
- outcomes.push({ personality: p.personality, runtime: 'none', state: 'skipped-no-agentic-runtime' })
188
- continue
189
- }
190
- for (const rt of agentic) {
191
- const o = runProvisionCommand({ block, cwd: p.cwd, runtime: rt, personality: p.personality, occasion, env })
192
- appendLifecycleEvent(
193
- pluginLogsDir('iapeer', { env }),
194
- {
195
- ev: 'memory-provision',
196
- identity: `${rt}-${p.personality}`,
197
- occasion,
198
- state: o.state,
199
- exit: o.exitCode ?? undefined,
200
- ms: o.durationMs,
201
- detail: o.detail,
202
- },
203
- { env },
204
- )
205
- outcomes.push({ personality: p.personality, runtime: rt, state: o.state, detail: o.detail })
206
- }
207
- }
208
- const failed = outcomes.some(o => o.state !== 'ok' && o.state !== 'skipped-no-agentic-runtime')
209
- return { ok: !failed, plugin: null, outcomes }
210
- }