feat(#956): add MemPalace memory capability (ADR-857 feature plug-in) (#1201)

* feat(#956): add MemPalace memory capability (ADR-857 feature plug-in)

Adds an opt-in, default-resilient ADR-857 feature capability that wires
MemPalace (local-first memory: MCP server + CLI) into the GSD loop:
deliberate recall before discuss/plan and verbatim + temporal-KG capture
at phase boundaries. Three memory modes (augment default; kg_backend and
replace forward-declared). Master gate mempalace.enabled (default off);
every hook onError:skip, zero gates; absent/disabled MemPalace => loop
unchanged. Transport is rendered-markdown only — MemPalace runs
out-of-process, no third-party code in gsd-core (ADR-857 §7).

Capability: capabilities/mempalace/ (manifest + 2 fragments), skills
commands/gsd/mempalace-{recall,capture}.md, agent
agents/gsd-mempalace-curator.md. Registration: ns-context router,
utility cluster, KNOWN_SKILLS, help full.md, model-catalog, copilot
install list, size baselines; regenerated capability-registry +
inventory manifest. ship:post wired into ship.md (wire-on-demand).

HELD on #1196: this capability also declares hooks at discuss:pre and
discuss:post, which are structurally un-wireable until the host-loop
conformance model covers the discuss phase (discuss-phase.md is not in
HOST_LOOP_FILES). The phase6-capstone-conformance gate therefore fails
on exactly those two orphaned points by design — see #1196. Once #1196
lands, rebase onto next and the gate goes green with no further change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#956): backfill changeset PR number (#1201)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-14 02:09:10 -04:00
committed by GitHub
parent b1e8a74708
commit a375c4b354
29 changed files with 1117 additions and 31 deletions

View File

@@ -585,7 +585,8 @@ describe('ship:post — real registry empty-resolution + resilience to registry
}
});
it('[happy] resolveLoopHooks with real capability-registry at ship:post returns empty activeHooks and well-formed byLoopPoint entry', () => {
it('[happy] resolveLoopHooks with real capability-registry at ship:post returns empty activeHooks (mempalace step inactive by default) and well-formed byLoopPoint entry with 1 step', () => {
// mempalace.enabled defaults to false → step not activated with empty config
const resolved = resolveLoopHooks({ point: 'ship:post', registry: realRegistry, config: {} });
assert.strictEqual(resolved.point, 'ship:post');
assert.ok(Array.isArray(resolved.activeHooks));
@@ -593,7 +594,8 @@ describe('ship:post — real registry empty-resolution + resilience to registry
const entry = realRegistry.byLoopPoint['ship:post'];
assert.ok(entry, 'ship:post must be present in real registry byLoopPoint');
assert.strictEqual(entry.steps.length, 0, 'ship:post must have 0 steps');
assert.strictEqual(entry.steps.length, 1, 'ship:post must have 1 step (mempalace curator)');
assert.strictEqual(entry.steps[0].capId, 'mempalace', 'ship:post step must be from mempalace');
assert.strictEqual(entry.contributions.length, 0, 'ship:post must have 0 contributions');
assert.strictEqual(entry.gates.length, 0, 'ship:post must have 0 gates');
});
@@ -665,12 +667,13 @@ describe('ship:post — real registry empty-resolution + resilience to registry
}
});
it('[empty-resolution] ship:post byLoopPoint entry exists in the real registry with all three arrays empty — no capability has silently self-registered here', () => {
it('[happy] ship:post byLoopPoint entry exists in the real registry with 1 step (mempalace), 0 contributions, 0 gates', () => {
const entry = realRegistry.byLoopPoint['ship:post'];
assert.ok(entry, 'ship:post must be present in real registry byLoopPoint');
assert.strictEqual(entry.steps.length, 0, 'ship:post must have 0 steps — accidental registration guard');
assert.strictEqual(entry.contributions.length, 0, 'ship:post must have 0 contributions — accidental registration guard');
assert.strictEqual(entry.gates.length, 0, 'ship:post must have 0 gates — accidental registration guard');
assert.strictEqual(entry.steps.length, 1, 'ship:post must have 1 step (mempalace curator step)');
assert.strictEqual(entry.steps[0].capId, 'mempalace', 'ship:post step must be from mempalace capability');
assert.strictEqual(entry.contributions.length, 0, 'ship:post must have 0 contributions');
assert.strictEqual(entry.gates.length, 0, 'ship:post must have 0 gates');
});
});
@@ -715,14 +718,13 @@ describe('CLI contract — missing/invalid point argument (shared across all 6 e
// SECTION 8: Parametric empty-point sweep across all 6 points (E2E regression guard)
// ─────────────────────────────────────────────────────────────────────────────
describe('Parametric E2E sweep — all 6 empty points return correct envelope shape via real registry', () => {
describe('Parametric E2E sweep — 5 empty points return correct envelope shape via real registry (ship:post excluded — mempalace registers 1 step there)', () => {
const EMPTY_POINTS = [
'discuss:pre',
'discuss:post',
'execute:pre',
'execute:wave:pre',
'verify:pre',
'ship:post',
];
for (const point of EMPTY_POINTS) {