wicked-crew 0.7.18 → 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 -4
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +198 -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,59 @@ 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
|
+
/** 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
|
+
}
|
|
90
143
|
// Closed facet vocabularies for the `GET /governance/rules` browse filters (wiki-mgmt) — the
|
|
91
144
|
// wire contract's `RuleBrowseQuery`. Sets, not zod: the query is otherwise free-form and only
|
|
92
145
|
// these three facets have a vocabulary to enforce.
|
|
@@ -213,6 +266,13 @@ export const OpenPathSchema = z.object({
|
|
|
213
266
|
path: z.string().min(1),
|
|
214
267
|
runId: z.string().min(1).optional(),
|
|
215
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();
|
|
216
276
|
/**
|
|
217
277
|
* A SKIN-OWNED settings key (crew#323): one lowercase segment under the `studio.` namespace,
|
|
218
278
|
* e.g. `studio.appearance`, `studio.notifications`. The daemon never interprets these values —
|
|
@@ -283,10 +343,10 @@ runtime = {}) {
|
|
|
283
343
|
});
|
|
284
344
|
const deliverExec = runtime.deliverExec ?? runDeliverScript;
|
|
285
345
|
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
|
|
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;
|
|
290
350
|
/** Repo root for a repo ref, from the registry — shared by the reprovision path below. */
|
|
291
351
|
const repoRootOf = async (repoRef) => (await adapter.listRepos()).find((r) => r.id === repoRef)?.root_path;
|
|
292
352
|
// The run-DTO joins (DES-UX-001 §8.2/§8.3, DES-UX-002 §7.2): `project_id` from the membership
|
|
@@ -2650,7 +2710,7 @@ runtime = {}) {
|
|
|
2650
2710
|
// fault (502), kept distinct from a client mistake (400: a bad `state`/`id`, or estate's own
|
|
2651
2711
|
// -32602 invalid-params) — the FINDING-031 posture that a malformed filter is refused, never
|
|
2652
2712
|
// silently honored as a different request.
|
|
2653
|
-
const
|
|
2713
|
+
const estateUpstreamError = (reply, err) => {
|
|
2654
2714
|
if (err instanceof EstateMcpError && err.code === -32602) {
|
|
2655
2715
|
return reply.code(400).send({ error: err.message });
|
|
2656
2716
|
}
|
|
@@ -2678,10 +2738,10 @@ runtime = {}) {
|
|
|
2678
2738
|
if (state !== undefined)
|
|
2679
2739
|
args.state = state;
|
|
2680
2740
|
try {
|
|
2681
|
-
return (await
|
|
2741
|
+
return (await estateTool('proposal.list', args));
|
|
2682
2742
|
}
|
|
2683
2743
|
catch (err) {
|
|
2684
|
-
return
|
|
2744
|
+
return estateUpstreamError(reply, err);
|
|
2685
2745
|
}
|
|
2686
2746
|
});
|
|
2687
2747
|
// POST /proposals/:id/approve → proposal.approve →
|
|
@@ -2697,10 +2757,10 @@ runtime = {}) {
|
|
|
2697
2757
|
// §5.2). Policy→steering routing is OUT OF SCOPE here: a `handed_off` outcome is passed
|
|
2698
2758
|
// through as-is for a later steering-write to consume; memory proposals return `promoted` and
|
|
2699
2759
|
// need nothing more.
|
|
2700
|
-
return (await
|
|
2760
|
+
return (await estateTool('proposal.approve', { id }));
|
|
2701
2761
|
}
|
|
2702
2762
|
catch (err) {
|
|
2703
|
-
return
|
|
2763
|
+
return estateUpstreamError(reply, err);
|
|
2704
2764
|
}
|
|
2705
2765
|
});
|
|
2706
2766
|
// POST /proposals/:id/reject → proposal.reject → { ok: true }.
|
|
@@ -2710,10 +2770,136 @@ runtime = {}) {
|
|
|
2710
2770
|
return reply.code(400).send({ error: '`id` is required' });
|
|
2711
2771
|
}
|
|
2712
2772
|
try {
|
|
2713
|
-
return (await
|
|
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;
|
|
2714
2900
|
}
|
|
2715
2901
|
catch (err) {
|
|
2716
|
-
return
|
|
2902
|
+
return estateUpstreamError(reply, err);
|
|
2717
2903
|
}
|
|
2718
2904
|
});
|
|
2719
2905
|
// ── Projects (DES-PROJECT-001) — the 9-route experience-plane surface ────────
|