wicked-crew 0.7.17 → 0.7.19
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 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +266 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/core/estate-mcp-client.d.ts +75 -0
- package/dist/core/estate-mcp-client.d.ts.map +1 -0
- package/dist/core/estate-mcp-client.js +254 -0
- package/dist/core/estate-mcp-client.js.map +1 -0
- package/dist/interactive/chat-events.d.ts +2 -1
- package/dist/interactive/chat-events.d.ts.map +1 -1
- package/dist/interactive/chat-events.js +13 -3
- package/dist/interactive/chat-events.js.map +1 -1
- package/dist/interactive/draft-events.d.ts +26 -1
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +56 -3
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +2 -1
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +12 -4
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/studio/assets/{index-DDpidE4C.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 +215 -27
- package/endpoint-manifest.json +67 -1
- package/package.json +3 -3
- package/dist/studio/assets/index-MVBENzHL.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,6 +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>;
|
|
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>;
|
|
250
267
|
/** Seat sign-in presence probe (seat sign-in) — injectable so route tests never read the
|
|
251
268
|
* developer's real dotfiles. Defaults to the file/env heuristic in seat-signin.ts. */
|
|
252
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":"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;AAmBnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAWrD,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAYtF,OAAO,EAAe,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAGzE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EACL,aAAa,EAOd,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAGL,KAAK,WAAW,EAEhB,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AA+J7C;;;;;;;;;;;;GAYG;AAIH,eAAO,MAAM,YAAY;;;;;;;;IAQvB;+FAC2F;;IAE3F;;;;;;+CAM2C;;IAE3C;;0FAEsF;;IAEtF;;;;6FAIyF;;IAEzF;;wDAEoD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASpD,CAAC;AAEH,eAAO,MAAM,UAAU;;;;;;;;;EAGZ,CAAC;AAEZ;;yEAEyE;AACzE,eAAO,MAAM,cAAc;;;;;;EAEhB,CAAC;AAgBZ,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;EAQpB,CAAC;AAOZ,4FAA4F;AAC5F,eAAO,MAAM,cAAc;;;;;;;;;EAGhB,CAAC;AAEZ;;;gGAGgG;AAChG,eAAO,MAAM,kBAAkB;;;;;;EAEpB,CAAC;AAwBZ,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B;oGACgG;IAChG,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;;sDAEkD;IAClD,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB;uGACmG;IACnG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;qBAEiB;IACjB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;4DAEwD;IACxD,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C;;oGAEgG;IAChG,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD;;;yBAGqB;IACrB,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxE;;;;gGAI4F;IAC5F,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC;;2FAEuF;IACvF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;wGAEoG;IACpG,mBAAmB,CAAC,EAAE,qBAAqB,CAAC;IAC5C,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;;4CAIwC;IACxC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF;2FACuF;IACvF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC1E;;0DAEsD;IACtD,kBAAkB,CAAC,EAAE,qBAAqB,CAAC;IAC3C;;gFAE4E;IAC5E,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,cAAc,CAAC;IAC3D;4FACwF;IACxF,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;mGAC+F;IAC/F,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,eAAe,EACpB,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,SAAS,EACpB,gBAAgB,EAAE,gBAAgB,EAIlC,YAAY,GAAE,WAA+B,EAG7C,QAAQ,GAAE,iBAAqF,EAK/F,QAAQ,GAAE,YAA0D,EAGpE,OAAO,GAAE,WAAgB,GACxB,IAAI,CA2vFN"}
|
package/dist/api/routes.js
CHANGED
|
@@ -14,6 +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 { callEstateTool, EstateMcpError } from '../core/estate-mcp-client.js';
|
|
17
18
|
import { SeatHealthTracker } from './seat-health.js';
|
|
18
19
|
import { applyWorkerConfigRoot, signedInHeuristic } from './seat-signin.js';
|
|
19
20
|
import { allowedRootsFor, isInsideRoot, openWithSystemDefault } from './open-path.js';
|
|
@@ -86,6 +87,59 @@ function invalidBody(err, what) {
|
|
|
86
87
|
: what;
|
|
87
88
|
return { error, details: err.issues };
|
|
88
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
|
+
/** The broad-browse recall token budget for `GET /memory` when the caller passes no `limit` — larger
|
|
111
|
+
* than estate's own 2000 default so a browse returns a meaningful slice, not a conversational one. */
|
|
112
|
+
const MEMORY_BROWSE_TOKEN_BUDGET = 8000;
|
|
113
|
+
/**
|
|
114
|
+
* Shape one estate `memory.recall` item (`{ memory_id, scope, content, tier, score }`) into the wire
|
|
115
|
+
* {@link MemoryItem}. estate recall does NOT surface per-item facets, so `facets` defaults to `{}`
|
|
116
|
+
* (mapped defensively so a future estate that DOES carry them rides through); `score` is dropped
|
|
117
|
+
* when absent. String fields missing on the wire degrade to `''` rather than throwing on one item.
|
|
118
|
+
*/
|
|
119
|
+
function shapeMemoryItem(raw) {
|
|
120
|
+
const r = typeof raw === 'object' && raw !== null ? raw : {};
|
|
121
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
122
|
+
const item = {
|
|
123
|
+
id: str(r['memory_id']),
|
|
124
|
+
content: str(r['content']),
|
|
125
|
+
tier: str(r['tier']),
|
|
126
|
+
scope: str(r['scope']),
|
|
127
|
+
facets: isStringRecord(r['facets']) ? r['facets'] : {},
|
|
128
|
+
};
|
|
129
|
+
if (typeof r['score'] === 'number')
|
|
130
|
+
item.score = r['score'];
|
|
131
|
+
return item;
|
|
132
|
+
}
|
|
133
|
+
/** Shape the estate `memory.coverage` result (`{ total, by_tier, by_kind }`) into the wire
|
|
134
|
+
* {@link MemoryCoverageResponse}. A shape estate never produces is an UPSTREAM fault — thrown as an
|
|
135
|
+
* {@link EstateMcpError} (→ 502), never silently coerced to zeroes. */
|
|
136
|
+
function shapeMemoryCoverage(raw) {
|
|
137
|
+
const r = typeof raw === 'object' && raw !== null ? raw : undefined;
|
|
138
|
+
if (r === undefined || typeof r['total'] !== 'number' || !isNumberRecord(r['by_tier']) || !isNumberRecord(r['by_kind'])) {
|
|
139
|
+
throw new EstateMcpError('memory.coverage returned an unexpected shape');
|
|
140
|
+
}
|
|
141
|
+
return { total: r['total'], by_tier: r['by_tier'], by_kind: r['by_kind'] };
|
|
142
|
+
}
|
|
89
143
|
// Closed facet vocabularies for the `GET /governance/rules` browse filters (wiki-mgmt) — the
|
|
90
144
|
// wire contract's `RuleBrowseQuery`. Sets, not zod: the query is otherwise free-form and only
|
|
91
145
|
// these three facets have a vocabulary to enforce.
|
|
@@ -212,6 +266,13 @@ export const OpenPathSchema = z.object({
|
|
|
212
266
|
path: z.string().min(1),
|
|
213
267
|
runId: z.string().min(1).optional(),
|
|
214
268
|
}).strict();
|
|
269
|
+
/** `POST /memory/retire` body (DES-MEM-FACETED-001). `scope_prefix` is REQUIRED and, after trim,
|
|
270
|
+
* non-empty (enforced in the route so the 400 names the total-wipe guard): estate `memory.erase`
|
|
271
|
+
* is subtree-scoped and refuses an empty prefix. `.strict()` for the FINDING-031 posture — an
|
|
272
|
+
* unknown field is a 400, never silently ignored into a broader erase than the caller sent. */
|
|
273
|
+
export const RetireMemorySchema = z.object({
|
|
274
|
+
scope_prefix: z.string(),
|
|
275
|
+
}).strict();
|
|
215
276
|
/**
|
|
216
277
|
* A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
|
|
217
278
|
* e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
|
|
@@ -282,6 +343,10 @@ runtime = {}) {
|
|
|
282
343
|
});
|
|
283
344
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
284
345
|
const reprovisionWorktree = runtime.reprovisionWorktree ?? gitReprovisionWorktree;
|
|
346
|
+
// The estate MCP client behind `/proposals*` AND `/memory*` (DES-MEM-FACETED-001) — one seam. The
|
|
347
|
+
// default is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no
|
|
348
|
+
// process is ever spawned.
|
|
349
|
+
const estateTool = runtime.callEstateTool ?? callEstateTool;
|
|
285
350
|
/** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
|
|
286
351
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
287
352
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
@@ -2636,6 +2701,207 @@ runtime = {}) {
|
|
|
2636
2701
|
});
|
|
2637
2702
|
return { settings };
|
|
2638
2703
|
});
|
|
2704
|
+
// ── Memory proposal queue (DES-MEM-FACETED-001 §5.0) ─────────────────────────
|
|
2705
|
+
// The estate proposal queue over HTTP, so a studio operator can review the learnings workers
|
|
2706
|
+
// propose and approve/reject them. The queue tools live ONLY on the estate MCP (JSON-RPC over
|
|
2707
|
+
// stdio, estate #162), reached through `proposalTool` — a spawn-per-call `wicked-estate-mcp`
|
|
2708
|
+
// client, NON-`--readonly` (this is the OPERATOR surface: it lists AND mutates), with
|
|
2709
|
+
// WICKED_MEMORY_DB pinned to the operator global store. A queue/estate failure is an UPSTREAM
|
|
2710
|
+
// fault (502), kept distinct from a client mistake (400: a bad `state`/`id`, or estate's own
|
|
2711
|
+
// -32602 invalid-params) — the FINDING-031 posture that a malformed filter is refused, never
|
|
2712
|
+
// silently honored as a different request.
|
|
2713
|
+
const estateUpstreamError = (reply, err) => {
|
|
2714
|
+
if (err instanceof EstateMcpError && err.code === -32602) {
|
|
2715
|
+
return reply.code(400).send({ error: err.message });
|
|
2716
|
+
}
|
|
2717
|
+
return reply.code(502).send({ error: message(err) });
|
|
2718
|
+
};
|
|
2719
|
+
// GET /proposals?kind_type=&state= → proposal.list → { proposals: Proposal[] }.
|
|
2720
|
+
app.get(`${V}/proposals`, { config: { manifest: { responseType: 'ListProposalsResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2721
|
+
const q = req.query;
|
|
2722
|
+
const rawOf = (v) => Array.isArray(v) ? v[0] : v;
|
|
2723
|
+
const rawKind = rawOf(q.kind_type);
|
|
2724
|
+
const rawState = rawOf(q.state);
|
|
2725
|
+
// Fail-loud: a PRESENT-but-blank param (`?state=` / `?state=%20`) is a client error, NOT a
|
|
2726
|
+
// silent "no filter" — else a client accidentally issues a broader query than intended.
|
|
2727
|
+
if (rawKind !== undefined && rawKind.trim() === '') {
|
|
2728
|
+
return reply.code(400).send({ error: '`kind_type` must not be empty' });
|
|
2729
|
+
}
|
|
2730
|
+
if (rawState !== undefined && !['pending', 'approved', 'rejected'].includes(rawState.trim())) {
|
|
2731
|
+
return reply.code(400).send({ error: '`state` must be one of pending|approved|rejected' });
|
|
2732
|
+
}
|
|
2733
|
+
const kindType = rawKind?.trim() || undefined;
|
|
2734
|
+
const state = rawState?.trim() || undefined;
|
|
2735
|
+
const args = {};
|
|
2736
|
+
if (kindType !== undefined)
|
|
2737
|
+
args.kind_type = kindType;
|
|
2738
|
+
if (state !== undefined)
|
|
2739
|
+
args.state = state;
|
|
2740
|
+
try {
|
|
2741
|
+
return (await estateTool('proposal.list', args));
|
|
2742
|
+
}
|
|
2743
|
+
catch (err) {
|
|
2744
|
+
return estateUpstreamError(reply, err);
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
// POST /proposals/:id/approve → proposal.approve →
|
|
2748
|
+
// { outcome:'promoted', active_id } — a MEMORY proposal, now an active memory (complete);
|
|
2749
|
+
// { outcome:'handed_off', payload } — a POLICY proposal, returned VERBATIM.
|
|
2750
|
+
app.post(`${V}/proposals/:id/approve`, { config: { manifest: { responseType: 'ApproveProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2751
|
+
const { id } = req.params;
|
|
2752
|
+
if (id.trim() === '') {
|
|
2753
|
+
return reply.code(400).send({ error: '`id` is required' });
|
|
2754
|
+
}
|
|
2755
|
+
try {
|
|
2756
|
+
// TODO: route handed_off policy payload to steering (crew steering-write, DES-MEM-FACETED-001
|
|
2757
|
+
// §5.2). Policy→steering routing is OUT OF SCOPE here: a `handed_off` outcome is passed
|
|
2758
|
+
// through as-is for a later steering-write to consume; memory proposals return `promoted` and
|
|
2759
|
+
// need nothing more.
|
|
2760
|
+
return (await estateTool('proposal.approve', { id }));
|
|
2761
|
+
}
|
|
2762
|
+
catch (err) {
|
|
2763
|
+
return estateUpstreamError(reply, err);
|
|
2764
|
+
}
|
|
2765
|
+
});
|
|
2766
|
+
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
2767
|
+
app.post(`${V}/proposals/:id/reject`, { config: { manifest: { responseType: 'RejectProposalResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2768
|
+
const { id } = req.params;
|
|
2769
|
+
if (id.trim() === '') {
|
|
2770
|
+
return reply.code(400).send({ error: '`id` is required' });
|
|
2771
|
+
}
|
|
2772
|
+
try {
|
|
2773
|
+
return (await estateTool('proposal.reject', { id }));
|
|
2774
|
+
}
|
|
2775
|
+
catch (err) {
|
|
2776
|
+
return estateUpstreamError(reply, err);
|
|
2777
|
+
}
|
|
2778
|
+
});
|
|
2779
|
+
// ── Memory management (governed-knowledge surface, DES-MEM-FACETED-001) ───────
|
|
2780
|
+
// Browse + retire the EXISTING operator memory store — the studio counterpart to /proposals:
|
|
2781
|
+
// proposals DECIDE learnings not yet stored; these MANAGE what already is. The estate memory
|
|
2782
|
+
// tools live ONLY on the estate MCP (memory.recall / memory.coverage / memory.erase), reached
|
|
2783
|
+
// through the SAME `estateTool` seam and `estateUpstreamError` ladder as /proposals — a client
|
|
2784
|
+
// mistake (non-integer limit, malformed facets JSON, empty retire scope_prefix, or estate's own
|
|
2785
|
+
// -32602 invalid-params) is a 400; an upstream estate/transport fault, or a malformed estate
|
|
2786
|
+
// response, is a 502.
|
|
2787
|
+
//
|
|
2788
|
+
// GRANULARITY, stated honestly (the estate contract, not a convenience wrapper):
|
|
2789
|
+
// • BROWSE is query-based. estate has no "list all"; `memory.recall` returns a token-budgeted,
|
|
2790
|
+
// relevance-ranked slice. Broad browse = an empty `query` + `scope_prefix` (subtree filter,
|
|
2791
|
+
// "" = every memory) + a large token budget. `limit` IS that token budget — estate exposes no
|
|
2792
|
+
// row-count cap — so it bounds the SIZE of the slice, not a memory count.
|
|
2793
|
+
// • recall returns { memory_id, scope, content, tier, score } and does NOT surface per-item
|
|
2794
|
+
// facets, so MemoryItem.facets is always {} (mapped defensively for a future estate).
|
|
2795
|
+
// • RETIRE is SUBTREE-scoped, never per-id. estate exposes NO per-memory delete: `memory.erase`
|
|
2796
|
+
// hard-deletes EVERY memory whose scope equals or descends from `scope_prefix`, and refuses an
|
|
2797
|
+
// empty prefix (a total-wipe guard). So retire takes a `scope_prefix` and reports how many
|
|
2798
|
+
// memories the subtree wipe removed — the UI must show the operator the subtree, not one row.
|
|
2799
|
+
// GET /memory?query=&scope=&scope_prefix=&facets=<json>&limit= → memory.recall → { memories }.
|
|
2800
|
+
app.get(`${V}/memory`, { config: { manifest: { responseType: 'ListMemoriesResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2801
|
+
const q = req.query;
|
|
2802
|
+
// `query` defaults to "" (broad browse). scope / scope_prefix forward verbatim WHEN PRESENT —
|
|
2803
|
+
// a blank value is meaningful to estate (root / whole-subtree), so it is not dropped.
|
|
2804
|
+
const args = { query: firstQueryValue(q.query) ?? '' };
|
|
2805
|
+
const scope = firstQueryValue(q.scope);
|
|
2806
|
+
if (scope !== undefined)
|
|
2807
|
+
args.scope = scope;
|
|
2808
|
+
const scopePrefix = firstQueryValue(q.scope_prefix);
|
|
2809
|
+
if (scopePrefix !== undefined)
|
|
2810
|
+
args.scope_prefix = scopePrefix;
|
|
2811
|
+
// `facets` is the recall `intent` tuple (axis→value). Present-but-blank is a client error
|
|
2812
|
+
// (fail-loud, never a silent no-filter — FINDING-031); a present value must parse to a JSON
|
|
2813
|
+
// object of string values (estate further validates each axis and answers -32602 → 400).
|
|
2814
|
+
const rawFacets = firstQueryValue(q.facets);
|
|
2815
|
+
if (rawFacets !== undefined) {
|
|
2816
|
+
if (rawFacets.trim() === '') {
|
|
2817
|
+
return reply.code(400).send({ error: '`facets` must not be empty' });
|
|
2818
|
+
}
|
|
2819
|
+
let parsedFacets;
|
|
2820
|
+
try {
|
|
2821
|
+
parsedFacets = JSON.parse(rawFacets);
|
|
2822
|
+
}
|
|
2823
|
+
catch {
|
|
2824
|
+
return reply.code(400).send({ error: '`facets` must be a JSON object of axis:value strings' });
|
|
2825
|
+
}
|
|
2826
|
+
if (!isStringRecord(parsedFacets)) {
|
|
2827
|
+
return reply.code(400).send({ error: '`facets` must be a JSON object of axis:value strings' });
|
|
2828
|
+
}
|
|
2829
|
+
args.intent = parsedFacets;
|
|
2830
|
+
}
|
|
2831
|
+
// `limit` is the recall token budget (estate has no row-count cap). Omitted/blank ⇒ the broad
|
|
2832
|
+
// browse budget; a present value must be a positive integer (mirrors GET /audit's `limit`).
|
|
2833
|
+
const rawLimit = firstQueryValue(q.limit);
|
|
2834
|
+
let tokenBudget = MEMORY_BROWSE_TOKEN_BUDGET;
|
|
2835
|
+
if (rawLimit !== undefined && rawLimit.trim() !== '') {
|
|
2836
|
+
const n = Number(rawLimit);
|
|
2837
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
2838
|
+
return reply.code(400).send({ error: '`limit` must be a positive integer (the recall token budget)' });
|
|
2839
|
+
}
|
|
2840
|
+
tokenBudget = n;
|
|
2841
|
+
}
|
|
2842
|
+
args.token_budget = tokenBudget;
|
|
2843
|
+
try {
|
|
2844
|
+
const raw = await estateTool('memory.recall', args);
|
|
2845
|
+
const items = typeof raw === 'object' && raw !== null ? raw['items'] : undefined;
|
|
2846
|
+
if (!Array.isArray(items)) {
|
|
2847
|
+
throw new EstateMcpError('memory.recall returned an unexpected shape');
|
|
2848
|
+
}
|
|
2849
|
+
const body = { memories: items.map(shapeMemoryItem) };
|
|
2850
|
+
return body;
|
|
2851
|
+
}
|
|
2852
|
+
catch (err) {
|
|
2853
|
+
return estateUpstreamError(reply, err);
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
// GET /memory/coverage?scope_prefix= → memory.coverage → { total, by_tier, by_kind }.
|
|
2857
|
+
app.get(`${V}/memory/coverage`, { config: { manifest: { responseType: 'MemoryCoverageResponse', statusCodes: [200, 400, 502] } } }, async (req, reply) => {
|
|
2858
|
+
const q = req.query;
|
|
2859
|
+
const scopePrefix = firstQueryValue(q.scope_prefix);
|
|
2860
|
+
const args = {};
|
|
2861
|
+
if (scopePrefix !== undefined)
|
|
2862
|
+
args.scope_prefix = scopePrefix;
|
|
2863
|
+
try {
|
|
2864
|
+
return shapeMemoryCoverage(await estateTool('memory.coverage', args));
|
|
2865
|
+
}
|
|
2866
|
+
catch (err) {
|
|
2867
|
+
return estateUpstreamError(reply, err);
|
|
2868
|
+
}
|
|
2869
|
+
});
|
|
2870
|
+
// POST /memory/retire { scope_prefix } → memory.erase → { erased }. SUBTREE-scoped (no per-id
|
|
2871
|
+
// delete in estate); an empty scope_prefix is refused at the route (a 400 BEFORE any spawn) — the
|
|
2872
|
+
// same total-wipe guard estate enforces with -32602.
|
|
2873
|
+
app.post(`${V}/memory/retire`, {
|
|
2874
|
+
config: {
|
|
2875
|
+
manifest: {
|
|
2876
|
+
requestType: 'RetireMemoryBody',
|
|
2877
|
+
responseType: 'RetireMemoryResponse',
|
|
2878
|
+
statusCodes: [200, 400, 502],
|
|
2879
|
+
},
|
|
2880
|
+
},
|
|
2881
|
+
}, async (req, reply) => {
|
|
2882
|
+
const parsed = RetireMemorySchema.safeParse(req.body);
|
|
2883
|
+
if (!parsed.success) {
|
|
2884
|
+
return reply.code(400).send(invalidBody(parsed.error, 'Invalid retire request'));
|
|
2885
|
+
}
|
|
2886
|
+
const scopePrefix = parsed.data.scope_prefix.trim();
|
|
2887
|
+
if (scopePrefix === '') {
|
|
2888
|
+
return reply.code(400).send({
|
|
2889
|
+
error: '`scope_prefix` must not be empty — memory.erase is subtree-scoped (no per-id delete) and refuses a total wipe',
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
try {
|
|
2893
|
+
const raw = await estateTool('memory.erase', { scope_prefix: scopePrefix });
|
|
2894
|
+
const deleted = typeof raw === 'object' && raw !== null ? raw['deleted_count'] : undefined;
|
|
2895
|
+
if (typeof deleted !== 'number') {
|
|
2896
|
+
throw new EstateMcpError('memory.erase returned an unexpected shape');
|
|
2897
|
+
}
|
|
2898
|
+
const body = { erased: deleted };
|
|
2899
|
+
return body;
|
|
2900
|
+
}
|
|
2901
|
+
catch (err) {
|
|
2902
|
+
return estateUpstreamError(reply, err);
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2639
2905
|
// ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
|
|
2640
2906
|
registerProjectRoutes(app, adapter, { ...projects, settings: projectSettings }, security);
|
|
2641
2907
|
// ── Campaigns (crew#342 + TH-9) — the engine's durable Run-DAG scheduler over REST ──────────
|