wicked-crew 0.7.18 → 0.7.20
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/dist/api/routes.d.ts +17 -4
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +215 -12
- package/dist/api/routes.js.map +1 -1
- package/dist/core/estate-mcp-client.d.ts +7 -2
- package/dist/core/estate-mcp-client.d.ts.map +1 -1
- package/dist/core/estate-mcp-client.js +7 -2
- package/dist/core/estate-mcp-client.js.map +1 -1
- package/dist/studio/assets/{index-Cm_7U3Ys.css → index-Dz6mXrD5.css} +1 -1
- package/dist/studio/assets/index-dRfKAWIp.js +537 -0
- package/dist/studio/index.html +2 -2
- package/dist/studio/testid-inventory.json +146 -50
- package/endpoint-manifest.json +34 -1
- package/package.json +2 -2
- package/dist/studio/assets/index-Dmv8_LV-.js +0 -537
package/dist/api/routes.d.ts
CHANGED
|
@@ -193,6 +193,17 @@ export declare const OpenPathSchema: z.ZodObject<{
|
|
|
193
193
|
path: string;
|
|
194
194
|
runId?: string | undefined;
|
|
195
195
|
}>;
|
|
196
|
+
/** `POST /memory/retire` body (DES-MEM-FACETED-001). `scope_prefix` is REQUIRED and, after trim,
|
|
197
|
+
* non-empty (enforced in the route so the 400 names the total-wipe guard): estate `memory.erase`
|
|
198
|
+
* is subtree-scoped and refuses an empty prefix. `.strict()` for the FINDING-031 posture — an
|
|
199
|
+
* unknown field is a 400, never silently ignored into a broader erase than the caller sent. */
|
|
200
|
+
export declare const RetireMemorySchema: z.ZodObject<{
|
|
201
|
+
scope_prefix: z.ZodString;
|
|
202
|
+
}, "strict", z.ZodTypeAny, {
|
|
203
|
+
scope_prefix: string;
|
|
204
|
+
}, {
|
|
205
|
+
scope_prefix: string;
|
|
206
|
+
}>;
|
|
196
207
|
/** The actor/audit deps (task #88) — threaded from `createServer`. */
|
|
197
208
|
export interface SecurityDeps {
|
|
198
209
|
audit: AuditLog;
|
|
@@ -247,10 +258,12 @@ export interface RuntimeDeps {
|
|
|
247
258
|
* Injectable so route tests never shell out to git; defaults to {@link gitReprovisionWorktree}. */
|
|
248
259
|
reprovisionWorktree?: WorktreeReprovisioner;
|
|
249
260
|
openWithOs?: (target: string) => Promise<void>;
|
|
250
|
-
/** The estate-MCP
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
|
|
261
|
+
/** The estate-MCP client behind `/proposals*` AND `/memory*` (DES-MEM-FACETED-001) — one seam for
|
|
262
|
+
* every operator-facing estate tool (`proposal.*` + `memory.recall`/`memory.coverage`/
|
|
263
|
+
* `memory.erase`). Injectable so route tests never spawn `wicked-estate-mcp`; defaults to the
|
|
264
|
+
* real spawn-per-call client (NON-`--readonly`, `WICKED_MEMORY_DB` pinned to the operator global
|
|
265
|
+
* store — memory.erase is a WRITE). */
|
|
266
|
+
callEstateTool?: (tool: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
254
267
|
/** Seat sign-in presence probe (seat sign-in) — injectable so route tests never read the
|
|
255
268
|
* developer's real dotfiles. Defaults to the file/env heuristic in seat-signin.ts. */
|
|
256
269
|
signedIn?: (seatKey: string, workerConfigRoot?: string) => boolean | null;
|
package/dist/api/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,OAAO,EAAmD,WAAW,EAA4E,MAAM,oBAAoB,CAAC;AAG5K,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
|
package/dist/api/routes.js
CHANGED
|
@@ -14,7 +14,7 @@ import { buildAcceptanceView, resolveRunWorkflow } from '../qe/acceptance.js';
|
|
|
14
14
|
import { buildEvidenceBundle, coreUnitId, evidenceFilename } from './evidence.js';
|
|
15
15
|
import { outputUnavailableReason, resolveUnit, unitKeysFor } from './unit-output.js';
|
|
16
16
|
import { execCapped, ExecOutputTooLarge } from '../core/exec.js';
|
|
17
|
-
import {
|
|
17
|
+
import { callEstateTool, EstateMcpError } from '../core/estate-mcp-client.js';
|
|
18
18
|
import { SeatHealthTracker } from './seat-health.js';
|
|
19
19
|
import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
20
20
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
@@ -87,6 +87,58 @@ function invalidBody(err, what) {
|
|
|
87
87
|
: what;
|
|
88
88
|
return { error, details: err.issues };
|
|
89
89
|
}
|
|
90
|
+
/** First value of a possibly-repeated query param, WITHOUT trimming — `''` is PRESERVED because a
|
|
91
|
+
* blank `scope`/`scope_prefix` is meaningful to estate (root / whole-subtree), unlike an omitted
|
|
92
|
+
* one. Returns undefined only when the param is absent. */
|
|
93
|
+
function firstQueryValue(v) {
|
|
94
|
+
return Array.isArray(v) ? v[0] : v;
|
|
95
|
+
}
|
|
96
|
+
/** A JSON object whose every value is a string (a facet/intent tuple, axis→value). */
|
|
97
|
+
function isStringRecord(v) {
|
|
98
|
+
return (typeof v === 'object' &&
|
|
99
|
+
v !== null &&
|
|
100
|
+
!Array.isArray(v) &&
|
|
101
|
+
Object.values(v).every((x) => typeof x === 'string'));
|
|
102
|
+
}
|
|
103
|
+
/** A JSON object whose every value is a number (a coverage breakdown map). */
|
|
104
|
+
function isNumberRecord(v) {
|
|
105
|
+
return (typeof v === 'object' &&
|
|
106
|
+
v !== null &&
|
|
107
|
+
!Array.isArray(v) &&
|
|
108
|
+
Object.values(v).every((x) => typeof x === 'number'));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Shape one estate `memory.list` item (`{ memory_id, scope, content, tier, facets, created_at }`)
|
|
112
|
+
* into the wire {@link MemoryItem}. `memory.list` (the management browse) surfaces per-item
|
|
113
|
+
* `facets` — the reason the surface can filter by facet at all; `score` is absent (list is not
|
|
114
|
+
* relevance-ranked) and simply omitted. String fields missing on the wire degrade to `''` rather
|
|
115
|
+
* than throwing on one item. (The same shape parses a `memory.recall` item unchanged — recall
|
|
116
|
+
* carries no facets, so `facets` degrades to `{}`.)
|
|
117
|
+
*/
|
|
118
|
+
function shapeMemoryItem(raw) {
|
|
119
|
+
const r = typeof raw === 'object' && raw !== null ? raw : {};
|
|
120
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
121
|
+
const item = {
|
|
122
|
+
id: str(r['memory_id']),
|
|
123
|
+
content: str(r['content']),
|
|
124
|
+
tier: str(r['tier']),
|
|
125
|
+
scope: str(r['scope']),
|
|
126
|
+
facets: isStringRecord(r['facets']) ? r['facets'] : {},
|
|
127
|
+
};
|
|
128
|
+
if (typeof r['score'] === 'number')
|
|
129
|
+
item.score = r['score'];
|
|
130
|
+
return item;
|
|
131
|
+
}
|
|
132
|
+
/** Shape the estate `memory.coverage` result (`{ total, by_tier, by_kind }`) into the wire
|
|
133
|
+
* {@link MemoryCoverageResponse}. A shape estate never produces is an UPSTREAM fault — thrown as an
|
|
134
|
+
* {@link EstateMcpError} (→ 502), never silently coerced to zeroes. */
|
|
135
|
+
function shapeMemoryCoverage(raw) {
|
|
136
|
+
const r = typeof raw === 'object' && raw !== null ? raw : undefined;
|
|
137
|
+
if (r === undefined || typeof r['total'] !== 'number' || !isNumberRecord(r['by_tier']) || !isNumberRecord(r['by_kind'])) {
|
|
138
|
+
throw new EstateMcpError('memory.coverage returned an unexpected shape');
|
|
139
|
+
}
|
|
140
|
+
return { total: r['total'], by_tier: r['by_tier'], by_kind: r['by_kind'] };
|
|
141
|
+
}
|
|
90
142
|
// Closed facet vocabularies for the `GET /governance/rules` browse filters (wiki-mgmt) — the
|
|
91
143
|
// wire contract's `RuleBrowseQuery`. Sets, not zod: the query is otherwise free-form and only
|
|
92
144
|
// these three facets have a vocabulary to enforce.
|
|
@@ -213,6 +265,13 @@ export const OpenPathSchema = z.object({
|
|
|
213
265
|
path: z.string().min(1),
|
|
214
266
|
runId: z.string().min(1).optional(),
|
|
215
267
|
}).strict();
|
|
268
|
+
/** `POST /memory/retire` body (DES-MEM-FACETED-001). `scope_prefix` is REQUIRED and, after trim,
|
|
269
|
+
* non-empty (enforced in the route so the 400 names the total-wipe guard): estate `memory.erase`
|
|
270
|
+
* is subtree-scoped and refuses an empty prefix. `.strict()` for the FINDING-031 posture — an
|
|
271
|
+
* unknown field is a 400, never silently ignored into a broader erase than the caller sent. */
|
|
272
|
+
export const RetireMemorySchema = z.object({
|
|
273
|
+
scope_prefix: z.string(),
|
|
274
|
+
}).strict();
|
|
216
275
|
/**
|
|
217
276
|
* A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
|
|
218
277
|
* e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
|
|
@@ -283,10 +342,10 @@ runtime = {}) {
|
|
|
283
342
|
});
|
|
284
343
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
285
344
|
const reprovisionWorktree = runtime.reprovisionWorktree ?? gitReprovisionWorktree;
|
|
286
|
-
// The estate
|
|
287
|
-
// is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
288
|
-
// is ever spawned.
|
|
289
|
-
const
|
|
345
|
+
// The estate MCP client behind `/proposals*` AND `/memory*` (DES-MEM-FACETED-001) — one seam. The
|
|
346
|
+
// default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
347
|
+
// process is ever spawned.
|
|
348
|
+
const estateTool = runtime.callEstateTool ?? callEstateTool;
|
|
290
349
|
/** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
|
|
291
350
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
292
351
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
@@ -2650,7 +2709,7 @@ runtime = {}) {
|
|
|
2650
2709
|
// fault (502), kept distinct from a client mistake (400: a bad `state`/`id`, or estate's own
|
|
2651
2710
|
// -32602 invalid-params) — the FINDING-031 posture that a malformed filter is refused, never
|
|
2652
2711
|
// silently honored as a different request.
|
|
2653
|
-
const
|
|
2712
|
+
const estateUpstreamError = (reply, err) => {
|
|
2654
2713
|
if (err instanceof EstateMcpError && err.code === -32602) {
|
|
2655
2714
|
return reply.code(400).send({ error: err.message });
|
|
2656
2715
|
}
|
|
@@ -2678,10 +2737,10 @@ runtime = {}) {
|
|
|
2678
2737
|
if (state !== undefined)
|
|
2679
2738
|
args.state = state;
|
|
2680
2739
|
try {
|
|
2681
|
-
return (await
|
|
2740
|
+
return (await estateTool('proposal.list', args));
|
|
2682
2741
|
}
|
|
2683
2742
|
catch (err) {
|
|
2684
|
-
return
|
|
2743
|
+
return estateUpstreamError(reply, err);
|
|
2685
2744
|
}
|
|
2686
2745
|
});
|
|
2687
2746
|
// POST /proposals/:id/approve → proposal.approve →
|
|
@@ -2697,10 +2756,10 @@ runtime = {}) {
|
|
|
2697
2756
|
// §5.2). Policy→steering routing is OUT OF SCOPE here: a `handed_off` outcome is passed
|
|
2698
2757
|
// through as-is for a later steering-write to consume; memory proposals return `promoted` and
|
|
2699
2758
|
// need nothing more.
|
|
2700
|
-
return (await
|
|
2759
|
+
return (await estateTool('proposal.approve', { id }));
|
|
2701
2760
|
}
|
|
2702
2761
|
catch (err) {
|
|
2703
|
-
return
|
|
2762
|
+
return estateUpstreamError(reply, err);
|
|
2704
2763
|
}
|
|
2705
2764
|
});
|
|
2706
2765
|
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
@@ -2710,10 +2769,154 @@ runtime = {}) {
|
|
|
2710
2769
|
return reply.code(400).send({ error: '`id` is required' });
|
|
2711
2770
|
}
|
|
2712
2771
|
try {
|
|
2713
|
-
return (await
|
|
2772
|
+
return (await estateTool('proposal.reject', { id }));
|
|
2773
|
+
}
|
|
2774
|
+
catch (err) {
|
|
2775
|
+
return estateUpstreamError(reply, err);
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
// ── Memory management (governed-knowledge surface, DES-MEM-FACETED-001) ───────
|
|
2779
|
+
// Browse + retire the EXISTING operator memory store — the studio counterpart to /proposals:
|
|
2780
|
+
// proposals DECIDE learnings not yet stored; these MANAGE what already is. The estate memory
|
|
2781
|
+
// tools live ONLY on the estate MCP (memory.list / memory.coverage / memory.erase), reached
|
|
2782
|
+
// through the SAME `estateTool` seam and `estateUpstreamError` ladder as /proposals — a client
|
|
2783
|
+
// mistake (non-integer limit, malformed facets JSON, empty retire scope_prefix, or estate's own
|
|
2784
|
+
// -32602 invalid-params) is a 400; an upstream estate/transport fault, or a malformed estate
|
|
2785
|
+
// response, is a 502.
|
|
2786
|
+
//
|
|
2787
|
+
// GRANULARITY, stated honestly (the estate contract, not a convenience wrapper):
|
|
2788
|
+
// • BROWSE lists the COMPLETE in-scope set. `memory.list` (DES-MEM-FACETED-001) returns every
|
|
2789
|
+
// memory under `scope_prefix` ("" / omitted = all), each carrying its facets — NOT
|
|
2790
|
+
// relevance-ranked or facet-intent-filtered. This is why an operator can see faceted memories
|
|
2791
|
+
// at all: `memory.recall` retrieves nothing for an empty query and EXCLUDES faceted memories
|
|
2792
|
+
// under empty intent, so it can never inventory the store. `query`/`facets`/`limit` are then
|
|
2793
|
+
// applied HERE as post-filters over that complete set (see below) — filtering, not retrieval.
|
|
2794
|
+
// • list returns { memory_id, scope, content, tier, facets, created_at }; facets ride through to
|
|
2795
|
+
// MemoryItem.facets, powering the surface's facet filter.
|
|
2796
|
+
// • RETIRE is SUBTREE-scoped, never per-id. estate exposes NO per-memory delete: `memory.erase`
|
|
2797
|
+
// hard-deletes EVERY memory whose scope equals or descends from `scope_prefix`, and refuses an
|
|
2798
|
+
// empty prefix (a total-wipe guard). So retire takes a `scope_prefix` and reports how many
|
|
2799
|
+
// memories the subtree wipe removed — the UI must show the operator the subtree, not one row.
|
|
2800
|
+
// GET /memory?query=&scope_prefix=&facets=<json>&limit= → memory.list + post-filter → { memories }.
|
|
2801
|
+
app.get(`${V}/memory`, { config: { manifest: { responseType: 'ListMemoriesResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2802
|
+
const q = req.query;
|
|
2803
|
+
// `scope_prefix` is the ONLY estate-side argument to memory.list (a subtree filter; blank /
|
|
2804
|
+
// omitted = every memory). Forward verbatim WHEN PRESENT — a blank value is meaningful
|
|
2805
|
+
// (whole-subtree), so it is not dropped.
|
|
2806
|
+
const args = {};
|
|
2807
|
+
const scopePrefix = firstQueryValue(q.scope_prefix);
|
|
2808
|
+
if (scopePrefix !== undefined)
|
|
2809
|
+
args.scope_prefix = scopePrefix;
|
|
2810
|
+
// `facets` narrows the COMPLETE list crew-side (after the estate fetch) to memories carrying
|
|
2811
|
+
// every given axis:value (a management filter, NOT a recall intent). Present-but-blank is a
|
|
2812
|
+
// client error
|
|
2813
|
+
// (fail-loud, never a silent no-filter — FINDING-031); a present value must parse to a JSON
|
|
2814
|
+
// object of string values.
|
|
2815
|
+
let facetFilter;
|
|
2816
|
+
const rawFacets = firstQueryValue(q.facets);
|
|
2817
|
+
if (rawFacets !== undefined) {
|
|
2818
|
+
if (rawFacets.trim() === '') {
|
|
2819
|
+
return reply.code(400).send({ error: '`facets` must not be empty' });
|
|
2820
|
+
}
|
|
2821
|
+
let parsedFacets;
|
|
2822
|
+
try {
|
|
2823
|
+
parsedFacets = JSON.parse(rawFacets);
|
|
2824
|
+
}
|
|
2825
|
+
catch {
|
|
2826
|
+
return reply.code(400).send({ error: '`facets` must be a JSON object of axis:value strings' });
|
|
2827
|
+
}
|
|
2828
|
+
if (!isStringRecord(parsedFacets)) {
|
|
2829
|
+
return reply.code(400).send({ error: '`facets` must be a JSON object of axis:value strings' });
|
|
2830
|
+
}
|
|
2831
|
+
facetFilter = parsedFacets;
|
|
2832
|
+
}
|
|
2833
|
+
// `limit` caps the returned ROW count over the complete set (memory.list has no cap of its
|
|
2834
|
+
// own). Omitted/blank ⇒ every matching memory; a present value must be a positive integer.
|
|
2835
|
+
// NOTE: a management browse deliberately fetches the COMPLETE in-scope set before filtering —
|
|
2836
|
+
// the operator memory store is operator-scale, and the studio surface filters that set
|
|
2837
|
+
// client-side, so a server-side cap would hide memories from the filter. Server-side paging
|
|
2838
|
+
// for a store large enough to need it is a future enhancement, not this surface's contract.
|
|
2839
|
+
const rawLimit = firstQueryValue(q.limit);
|
|
2840
|
+
let limit;
|
|
2841
|
+
if (rawLimit !== undefined && rawLimit.trim() !== '') {
|
|
2842
|
+
const n = Number(rawLimit);
|
|
2843
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
2844
|
+
return reply.code(400).send({ error: '`limit` must be a positive integer' });
|
|
2845
|
+
}
|
|
2846
|
+
limit = n;
|
|
2847
|
+
}
|
|
2848
|
+
// `query`, when present, is a case-insensitive CONTENT substring filter over the complete set
|
|
2849
|
+
// — a management search that finds faceted memories too (unlike relevance recall).
|
|
2850
|
+
const query = (firstQueryValue(q.query) ?? '').trim().toLowerCase();
|
|
2851
|
+
try {
|
|
2852
|
+
const raw = await estateTool('memory.list', args);
|
|
2853
|
+
const items = typeof raw === 'object' && raw !== null ? raw['items'] : undefined;
|
|
2854
|
+
if (!Array.isArray(items)) {
|
|
2855
|
+
throw new EstateMcpError('memory.list returned an unexpected shape');
|
|
2856
|
+
}
|
|
2857
|
+
let memories = items.map(shapeMemoryItem);
|
|
2858
|
+
if (query !== '')
|
|
2859
|
+
memories = memories.filter((m) => m.content.toLowerCase().includes(query));
|
|
2860
|
+
if (facetFilter !== undefined) {
|
|
2861
|
+
const pairs = Object.entries(facetFilter);
|
|
2862
|
+
memories = memories.filter((m) => pairs.every(([k, v]) => m.facets[k] === v));
|
|
2863
|
+
}
|
|
2864
|
+
if (limit !== undefined)
|
|
2865
|
+
memories = memories.slice(0, limit);
|
|
2866
|
+
const body = { memories };
|
|
2867
|
+
return body;
|
|
2868
|
+
}
|
|
2869
|
+
catch (err) {
|
|
2870
|
+
return estateUpstreamError(reply, err);
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
// GET /memory/coverage?scope_prefix= → memory.coverage → { total, by_tier, by_kind }.
|
|
2874
|
+
app.get(`${V}/memory/coverage`, { config: { manifest: { responseType: 'MemoryCoverageResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2875
|
+
const q = req.query;
|
|
2876
|
+
const scopePrefix = firstQueryValue(q.scope_prefix);
|
|
2877
|
+
const args = {};
|
|
2878
|
+
if (scopePrefix !== undefined)
|
|
2879
|
+
args.scope_prefix = scopePrefix;
|
|
2880
|
+
try {
|
|
2881
|
+
return shapeMemoryCoverage(await estateTool('memory.coverage', args));
|
|
2882
|
+
}
|
|
2883
|
+
catch (err) {
|
|
2884
|
+
return estateUpstreamError(reply, err);
|
|
2885
|
+
}
|
|
2886
|
+
});
|
|
2887
|
+
// POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (no per-id
|
|
2888
|
+
// delete in estate); an empty scope_prefix is refused at the route (a 400 BEFORE any spawn) — the
|
|
2889
|
+
// same total-wipe guard estate enforces with -32602.
|
|
2890
|
+
app.post(`${V}/memory/retire`, {
|
|
2891
|
+
config: {
|
|
2892
|
+
manifest: {
|
|
2893
|
+
requestType: 'RetireMemoryBody',
|
|
2894
|
+
responseType: 'RetireMemoryResponse',
|
|
2895
|
+
statusCodes: [200, 400, 502],
|
|
2896
|
+
},
|
|
2897
|
+
},
|
|
2898
|
+
}, async (req, reply) => {
|
|
2899
|
+
const parsed = RetireMemorySchema.safeParse(req.body);
|
|
2900
|
+
if (!parsed.success) {
|
|
2901
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid retire request'));
|
|
2902
|
+
}
|
|
2903
|
+
const scopePrefix = parsed.data.scope_prefix.trim();
|
|
2904
|
+
if (scopePrefix === '') {
|
|
2905
|
+
return reply.code(400).send({
|
|
2906
|
+
error: '`scope_prefix` must not be empty — memory.erase is subtree-scoped (no per-id delete) and refuses a total wipe',
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
try {
|
|
2910
|
+
const raw = await estateTool('memory.erase', { scope_prefix: scopePrefix });
|
|
2911
|
+
const deleted = typeof raw === 'object' && raw !== null ? raw['deleted_count'] : undefined;
|
|
2912
|
+
if (typeof deleted !== 'number') {
|
|
2913
|
+
throw new EstateMcpError('memory.erase returned an unexpected shape');
|
|
2914
|
+
}
|
|
2915
|
+
const body = { erased: deleted };
|
|
2916
|
+
return body;
|
|
2714
2917
|
}
|
|
2715
2918
|
catch (err) {
|
|
2716
|
-
return
|
|
2919
|
+
return estateUpstreamError(reply, err);
|
|
2717
2920
|
}
|
|
2718
2921
|
});
|
|
2719
2922
|
// ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
|