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.
@@ -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 proposal-queue client behind `/proposals*` (DES-MEM-FACETED-001 §5.0).
251
- * Injectable so route tests never spawn `wicked-estate-mcp`; defaults to the real spawn-per-call
252
- * client (NON-`--readonly`, `WICKED_MEMORY_DB` pinned to the operator global store). */
253
- callEstateProposalTool?: (tool: string, args: Record<string, unknown>) => Promise<unknown>;
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;
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"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"}
@@ -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 { callEstateProposalTool, EstateMcpError } from '../core/estate-mcp-client.js';
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 proposal-queue client behind `/proposals*` (DES-MEM-FACETED-001 §5.0). The default
287
- // is the real spawn-per-call `wicked-estate-mcp` client; route tests inject a stub so no process
288
- // is ever spawned.
289
- const proposalTool = runtime.callEstateProposalTool ?? callEstateProposalTool;
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 proposalUpstreamError = (reply, err) => {
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 proposalTool('proposal.list', args));
2740
+ return (await estateTool('proposal.list', args));
2682
2741
  }
2683
2742
  catch (err) {
2684
- return proposalUpstreamError(reply, err);
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 proposalTool('proposal.approve', { id }));
2759
+ return (await estateTool('proposal.approve', { id }));
2701
2760
  }
2702
2761
  catch (err) {
2703
- return proposalUpstreamError(reply, err);
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 proposalTool('proposal.reject', { id }));
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 proposalUpstreamError(reply, err);
2919
+ return estateUpstreamError(reply, err);
2717
2920
  }
2718
2921
  });
2719
2922
  // ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────