@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 +1 -1
- package/src/cli/cli.test.ts +18 -21
- package/src/cli/index.ts +0 -30
- package/src/enable/index.ts +8 -56
- package/src/enable/provisionCommand.ts +5 -2
- package/src/lifecycle/index.ts +14 -2
- package/src/onboard/index.ts +3 -3
- package/src/provision/index.ts +10 -39
- package/src/provision/provision.test.ts +7 -8
- package/src/status/index.ts +7 -36
- package/src/status/status.test.ts +8 -12
- package/src/enable/memoryPlugin.test.ts +0 -197
- package/src/enable/memoryPlugin.ts +0 -210
package/package.json
CHANGED
package/src/cli/cli.test.ts
CHANGED
|
@@ -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 (
|
|
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('
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
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
|
package/src/enable/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
402
|
-
*
|
|
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
|
|
476
|
-
//
|
|
477
|
-
//
|
|
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 |
|
|
12
|
-
// ref-counting of host-global surfaces is the
|
|
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
|
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/onboard/index.ts
CHANGED
|
@@ -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
|
|
128
|
-
*
|
|
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
|
|
145
|
+
* isMarketplaceRegistered for the generic capability-enable path. */
|
|
146
146
|
export function isMarketplaceRegisteredAs(
|
|
147
147
|
runtime: OnboardRuntime,
|
|
148
148
|
name: string,
|
package/src/provision/index.ts
CHANGED
|
@@ -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.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
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('
|
|
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
|
|
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)
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
286
|
+
expect(w).toContain("re-run the provider's provision") // provider-side repair, no core verb named
|
|
288
287
|
})
|
|
289
288
|
})
|
package/src/status/index.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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 (
|
|
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
|
-
*
|
|
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
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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:
|
|
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
|
-
}
|