@wenrwa/marketplace-cli 0.5.23 → 0.5.24
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/__tests__/bridge-context-writer.test.js +87 -0
- package/dist/__tests__/bridge-local-context-writer.test.js +40 -0
- package/dist/__tests__/bridge-memory-brief.test.d.ts +1 -0
- package/dist/__tests__/bridge-memory-brief.test.js +107 -0
- package/dist/__tests__/bridge-memory-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-memory-pointer.test.js +92 -0
- package/dist/bridge/contextWriter.d.ts +13 -0
- package/dist/bridge/contextWriter.js +15 -3
- package/dist/bridge/localContextWriter.d.ts +7 -0
- package/dist/bridge/localContextWriter.js +15 -3
- package/dist/bridge/memoryBrief.d.ts +41 -0
- package/dist/bridge/memoryBrief.js +63 -0
- package/dist/bridge/memoryPointer.d.ts +49 -0
- package/dist/bridge/memoryPointer.js +62 -0
- package/dist/bridge/relayClient.d.ts +11 -1
- package/dist/bridge/relayClient.js +20 -6
- package/dist/bridge/server.d.ts +4 -1
- package/dist/bridge/server.js +21 -10
- package/dist/commands/bridge.js +21 -2
- package/dist/index.js +0 -0
- package/package.json +2 -2
|
@@ -192,3 +192,90 @@ describe('writeContext — path safety (writes nothing outside the workspace)',
|
|
|
192
192
|
expect(files.size).toBe(0);
|
|
193
193
|
});
|
|
194
194
|
});
|
|
195
|
+
/**
|
|
196
|
+
* wmr-01 — the memory pointer rides the same managed region as the work-graph
|
|
197
|
+
* pointer, gated the same way (the caller decides; absence is byte-identical
|
|
198
|
+
* to before). The no-op proof below is the load-bearing one: every agent on
|
|
199
|
+
* every attach gets this file, so a regression here is a regression for the
|
|
200
|
+
* whole fleet.
|
|
201
|
+
*/
|
|
202
|
+
describe('writeContext — workspace memory pointer (wmr-01)', () => {
|
|
203
|
+
const { MEMORY_POINTER } = require('../bridge/memoryPointer');
|
|
204
|
+
it('writes the memory pointer into the region when the caller says it is available', () => {
|
|
205
|
+
const { files, deps } = makeFs();
|
|
206
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: '', memoryAvailable: true }, deps);
|
|
207
|
+
expect(files.get(CLAUDE)).toContain(MEMORY_POINTER);
|
|
208
|
+
});
|
|
209
|
+
it('NO-OP PROOF: without the flag the output is byte-identical to before', () => {
|
|
210
|
+
const withFlag = makeFs();
|
|
211
|
+
const withoutFlag = makeFs();
|
|
212
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'pushed content' }, withoutFlag.deps);
|
|
213
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'pushed content', memoryAvailable: false }, withFlag.deps);
|
|
214
|
+
expect(withFlag.files.get(CLAUDE)).toBe(withoutFlag.files.get(CLAUDE));
|
|
215
|
+
expect(withoutFlag.files.get(CLAUDE)).not.toContain('Workspace memory');
|
|
216
|
+
});
|
|
217
|
+
it('coexists with the work-graph pointer and the arena\'s pushed content', () => {
|
|
218
|
+
const { files, deps } = makeFs();
|
|
219
|
+
(0, contextWriter_1.writeContext)({
|
|
220
|
+
workspaceId: 'ws-7',
|
|
221
|
+
content: 'pushed content',
|
|
222
|
+
workGraphAvailable: true,
|
|
223
|
+
memoryAvailable: true,
|
|
224
|
+
}, deps);
|
|
225
|
+
const written = files.get(CLAUDE);
|
|
226
|
+
expect(written).toContain(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
227
|
+
expect(written).toContain(MEMORY_POINTER);
|
|
228
|
+
expect(written).toContain('pushed content');
|
|
229
|
+
// All of it inside the managed fence — nothing leaks into the user's own file.
|
|
230
|
+
const body = written.slice(written.indexOf(contextWriter_1.CONTEXT_START_MARKER), written.indexOf(contextWriter_1.CONTEXT_END_MARKER));
|
|
231
|
+
expect(body).toContain(MEMORY_POINTER);
|
|
232
|
+
});
|
|
233
|
+
it('keeps the region alive on the pointer alone, with no pushed content', () => {
|
|
234
|
+
// Same rule wgcp-01 established: a pointer is reason enough to keep the
|
|
235
|
+
// region — it must not vanish just because the arena pushed nothing.
|
|
236
|
+
const { files, deps } = makeFs();
|
|
237
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: '', memoryAvailable: true }, deps);
|
|
238
|
+
expect(files.get(CLAUDE)).toContain(contextWriter_1.CONTEXT_START_MARKER);
|
|
239
|
+
});
|
|
240
|
+
it('is idempotent — re-writing the same inputs does not duplicate the pointer', () => {
|
|
241
|
+
const { files, deps } = makeFs();
|
|
242
|
+
const input = { workspaceId: 'ws-7', content: 'x', memoryAvailable: true };
|
|
243
|
+
(0, contextWriter_1.writeContext)(input, deps);
|
|
244
|
+
(0, contextWriter_1.writeContext)(input, deps);
|
|
245
|
+
const written = files.get(CLAUDE);
|
|
246
|
+
expect(written.split('## Workspace memory')).toHaveLength(2);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
/**
|
|
250
|
+
* wmr-03 — the brief reaching the hosted context file. The failing-fetch case
|
|
251
|
+
* is the load-bearing one: PRD decision D2 is that memory can never stop a
|
|
252
|
+
* terminal from opening.
|
|
253
|
+
*/
|
|
254
|
+
describe('writeContext — the memory brief (wmr-03)', () => {
|
|
255
|
+
const { MEMORY_POINTER } = require('../bridge/memoryPointer');
|
|
256
|
+
const BRIEF = 'WORKSPACE MEMORY BRIEF (1 entries, 0 flagged)\n- deploy: prod deploys from master';
|
|
257
|
+
it('writes the brief into the managed region', () => {
|
|
258
|
+
const { files, deps } = makeFs();
|
|
259
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: '', memoryAvailable: true, memoryBrief: BRIEF }, deps);
|
|
260
|
+
expect(files.get(CLAUDE)).toContain(BRIEF);
|
|
261
|
+
});
|
|
262
|
+
it('a failed fetch (null brief) still writes the instruction', () => {
|
|
263
|
+
const { files, deps } = makeFs();
|
|
264
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: '', memoryAvailable: true, memoryBrief: null }, deps);
|
|
265
|
+
const written = files.get(CLAUDE);
|
|
266
|
+
expect(written).toContain(MEMORY_POINTER);
|
|
267
|
+
expect(written).toContain(contextWriter_1.CONTEXT_START_MARKER);
|
|
268
|
+
});
|
|
269
|
+
it('NO-OP PROOF: a null brief is byte-identical to the wmr-01 output', () => {
|
|
270
|
+
const withBrief = makeFs();
|
|
271
|
+
const without = makeFs();
|
|
272
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'x', memoryAvailable: true }, without.deps);
|
|
273
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'x', memoryAvailable: true, memoryBrief: null }, withBrief.deps);
|
|
274
|
+
expect(withBrief.files.get(CLAUDE)).toBe(without.files.get(CLAUDE));
|
|
275
|
+
});
|
|
276
|
+
it('a brief without the gate is never written — the gate still governs', () => {
|
|
277
|
+
const { files, deps } = makeFs();
|
|
278
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'x', memoryAvailable: false, memoryBrief: BRIEF }, deps);
|
|
279
|
+
expect(files.get(CLAUDE)).not.toContain(BRIEF);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -150,3 +150,43 @@ describe('writeLocalContext — work graph pointer (wgcp-01)', () => {
|
|
|
150
150
|
expect(fs.readFileSync(contextPath, 'utf-8')).toBe('## files\n- a.ts');
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
|
+
/**
|
|
154
|
+
* wmr-04 — two-lane parity. A local agent gets the same memory section a
|
|
155
|
+
* hosted one does, in the lane's own bridge-owned file. The standing mandate
|
|
156
|
+
* is that a feature ships for both lanes or is scoped out in writing; memory
|
|
157
|
+
* was never scoped out.
|
|
158
|
+
*/
|
|
159
|
+
describe('writeLocalContext — workspace memory (wmr-04)', () => {
|
|
160
|
+
const { MEMORY_POINTER } = require('../bridge/memoryPointer');
|
|
161
|
+
const BRIEF = 'WORKSPACE MEMORY BRIEF (1 entries, 0 flagged)\n- deploy: prod deploys from master';
|
|
162
|
+
it('writes the instruction when memory is available', () => {
|
|
163
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04-'));
|
|
164
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: root, content: '', memoryAvailable: true }, realDeps());
|
|
165
|
+
const written = fs.readFileSync(path.join(root, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8');
|
|
166
|
+
expect(written).toContain(MEMORY_POINTER);
|
|
167
|
+
});
|
|
168
|
+
it('writes the brief alongside it', () => {
|
|
169
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04-'));
|
|
170
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: root, content: '', memoryAvailable: true, memoryBrief: BRIEF }, realDeps());
|
|
171
|
+
expect(fs.readFileSync(path.join(root, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8')).toContain(BRIEF);
|
|
172
|
+
});
|
|
173
|
+
it('NO-OP PROOF: without the flag the file is byte-identical to before', () => {
|
|
174
|
+
const a = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04a-'));
|
|
175
|
+
const b = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04b-'));
|
|
176
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: a, content: 'pushed' }, realDeps());
|
|
177
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: b, content: 'pushed', memoryAvailable: false, memoryBrief: BRIEF }, realDeps());
|
|
178
|
+
expect(fs.readFileSync(path.join(b, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8')).toBe(fs.readFileSync(path.join(a, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8'));
|
|
179
|
+
});
|
|
180
|
+
it('a failed fetch still writes the instruction', () => {
|
|
181
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04-'));
|
|
182
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: root, content: '', memoryAvailable: true, memoryBrief: null }, realDeps());
|
|
183
|
+
expect(fs.readFileSync(path.join(root, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8')).toContain(MEMORY_POINTER);
|
|
184
|
+
});
|
|
185
|
+
it('never touches the user\'s own CLAUDE.md', () => {
|
|
186
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'wmr04-'));
|
|
187
|
+
const usersOwn = path.join(root, 'CLAUDE.md');
|
|
188
|
+
fs.writeFileSync(usersOwn, 'the user wrote this');
|
|
189
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot: root, content: '', memoryAvailable: true, memoryBrief: BRIEF }, realDeps());
|
|
190
|
+
expect(fs.readFileSync(usersOwn, 'utf-8')).toBe('the user wrote this');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* TDD — wmr-02: bridge/memoryBrief.ts. The brief fetcher never breaks a
|
|
5
|
+
* terminal: every failure mode (non-200, network error, malformed body,
|
|
6
|
+
* missing token, timeout) must resolve null, never throw — and every test
|
|
7
|
+
* here asserts against a stubbed transport what request was ACTUALLY
|
|
8
|
+
* SENT (url, method, Authorization header), not just that a canned
|
|
9
|
+
* fixture comes back.
|
|
10
|
+
*/
|
|
11
|
+
const memoryBrief_1 = require("../bridge/memoryBrief");
|
|
12
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
13
|
+
function fakeFetch(handler) {
|
|
14
|
+
return async (url, init) => {
|
|
15
|
+
const { ok, status, body } = handler(url, init);
|
|
16
|
+
return { ok, status, json: async () => body };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe('fetchMemoryBrief', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
process.env.WENRWA_CONNECT_TOKEN = 'wm_ct_' + 'a'.repeat(32);
|
|
22
|
+
delete process.env.MARKETPLACE_API_URL;
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
process.env = { ...ORIGINAL_ENV };
|
|
26
|
+
});
|
|
27
|
+
it('returns the brief text on a 200 and sends an authenticated GET at the right URL', async () => {
|
|
28
|
+
let seenUrl = '';
|
|
29
|
+
let seenInit = {};
|
|
30
|
+
const fetchImpl = async (url, init) => {
|
|
31
|
+
seenUrl = url;
|
|
32
|
+
seenInit = init;
|
|
33
|
+
return { ok: true, status: 200, json: async () => ({ success: true, brief: 'workspace uses pnpm, not npm' }) };
|
|
34
|
+
};
|
|
35
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-123', { fetchImpl });
|
|
36
|
+
expect(brief).toBe('workspace uses pnpm, not npm');
|
|
37
|
+
expect(seenUrl).toBe('https://api.wenrwa.com/api/v1/workspaces/ws-123/memory/brief');
|
|
38
|
+
expect(seenInit.method).toBe('GET');
|
|
39
|
+
expect(seenInit.headers.Authorization).toBe(`Bearer ${process.env.WENRWA_CONNECT_TOKEN}`);
|
|
40
|
+
});
|
|
41
|
+
it('honours MARKETPLACE_API_URL when set', async () => {
|
|
42
|
+
process.env.MARKETPLACE_API_URL = 'https://api.test.internal/api/v1';
|
|
43
|
+
let seenUrl = '';
|
|
44
|
+
const fetchImpl = async (url) => {
|
|
45
|
+
seenUrl = url;
|
|
46
|
+
return { ok: true, status: 200, json: async () => ({ success: true, brief: 'hello' }) };
|
|
47
|
+
};
|
|
48
|
+
await (0, memoryBrief_1.fetchMemoryBrief)('ws-456', { fetchImpl });
|
|
49
|
+
expect(seenUrl).toBe('https://api.test.internal/api/v1/workspaces/ws-456/memory/brief');
|
|
50
|
+
});
|
|
51
|
+
it('returns null — never throws — on a non-200 response', async () => {
|
|
52
|
+
const fetchImpl = fakeFetch(() => ({ ok: false, status: 500, body: { success: false } }));
|
|
53
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
54
|
+
expect(brief).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
it('returns null — never throws — when the transport throws a network error', async () => {
|
|
57
|
+
const fetchImpl = async () => {
|
|
58
|
+
throw new Error('ECONNREFUSED');
|
|
59
|
+
};
|
|
60
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
61
|
+
expect(brief).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
it('returns null — never throws — on malformed JSON (json() rejects)', async () => {
|
|
64
|
+
const fetchImpl = async () => ({
|
|
65
|
+
ok: true,
|
|
66
|
+
status: 200,
|
|
67
|
+
json: async () => {
|
|
68
|
+
throw new SyntaxError('Unexpected token in JSON');
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
72
|
+
expect(brief).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
it('returns null — never throws — on a well-formed but wrongly-shaped body', async () => {
|
|
75
|
+
const fetchImpl = fakeFetch(() => ({ ok: true, status: 200, body: { success: true } })); // no `brief`
|
|
76
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
77
|
+
expect(brief).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
it('returns null and never calls the transport when the Connect Token is missing', async () => {
|
|
80
|
+
delete process.env.WENRWA_CONNECT_TOKEN;
|
|
81
|
+
const fetchImpl = jest.fn();
|
|
82
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
83
|
+
expect(brief).toBeNull();
|
|
84
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
it('returns null and never calls the transport when the Connect Token is empty', async () => {
|
|
87
|
+
process.env.WENRWA_CONNECT_TOKEN = '';
|
|
88
|
+
const fetchImpl = jest.fn();
|
|
89
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl });
|
|
90
|
+
expect(brief).toBeNull();
|
|
91
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
it('aborts and returns null when the backend hangs past the bounded timeout', async () => {
|
|
94
|
+
let aborted = false;
|
|
95
|
+
const fetchImpl = (_url, init) => new Promise((_resolve, reject) => {
|
|
96
|
+
init?.signal?.addEventListener('abort', () => {
|
|
97
|
+
aborted = true;
|
|
98
|
+
const err = new Error('The operation was aborted');
|
|
99
|
+
err.name = 'AbortError';
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
const brief = await (0, memoryBrief_1.fetchMemoryBrief)('ws-1', { fetchImpl, timeoutMs: 15 });
|
|
104
|
+
expect(aborted).toBe(true);
|
|
105
|
+
expect(brief).toBeNull();
|
|
106
|
+
}, 2000);
|
|
107
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* wmr-01 — telling the agent that workspace memory exists.
|
|
5
|
+
*
|
|
6
|
+
* Workspace Memory shipped complete and held ZERO entries for a month: agents
|
|
7
|
+
* could not write (a Connect Token allowlist omission, fixed in cb5e725) and
|
|
8
|
+
* nothing ever told them the store existed. With writes working, the remaining
|
|
9
|
+
* half is guidance — an agent that is never told `memory_write` exists will
|
|
10
|
+
* never call it, exactly as no agent knew about the work graph before
|
|
11
|
+
* wgcp-01 put a pointer in this same managed region.
|
|
12
|
+
*
|
|
13
|
+
* This is the tracer bullet: STATIC text only, no fetch, cutting the whole
|
|
14
|
+
* path from the constant to the bytes in the context file.
|
|
15
|
+
*
|
|
16
|
+
* Asserts on the IMPORTED runtime value, never this file's own source text —
|
|
17
|
+
* the doc comment above MEMORY_POINTER echoes similar phrasing, so a
|
|
18
|
+
* source-matching guard could pass against the comment instead of the
|
|
19
|
+
* constant (the trap bridge-work-graph-pointer.test.ts already records).
|
|
20
|
+
*/
|
|
21
|
+
const memoryPointer_1 = require("../bridge/memoryPointer");
|
|
22
|
+
describe('MEMORY_POINTER text (wmr-01)', () => {
|
|
23
|
+
it('names the real tool, so the agent has something to actually call', () => {
|
|
24
|
+
// If memory_write is ever renamed, this fails here rather than leaving an
|
|
25
|
+
// agent pointed at a tool that does not exist — the wgcp-01 rule that
|
|
26
|
+
// pointing at unregistered tools is worse than silence.
|
|
27
|
+
expect(memoryPointer_1.MEMORY_POINTER).toContain('memory_write');
|
|
28
|
+
});
|
|
29
|
+
it('is headed like the other managed-region sections', () => {
|
|
30
|
+
expect(memoryPointer_1.MEMORY_POINTER.split('\n')[0]).toBe('## Workspace memory');
|
|
31
|
+
});
|
|
32
|
+
it('says what is worth keeping — a correction, a decision, a non-obvious fact', () => {
|
|
33
|
+
expect(memoryPointer_1.MEMORY_POINTER).toMatch(/correct/i);
|
|
34
|
+
expect(memoryPointer_1.MEMORY_POINTER).toMatch(/decision/i);
|
|
35
|
+
});
|
|
36
|
+
it('says what is NOT worth keeping, so the store does not fill with trivia', () => {
|
|
37
|
+
// Without a stated floor the cheapest thing for a model to record is
|
|
38
|
+
// whatever it just read, which is precisely what the code already says.
|
|
39
|
+
expect(memoryPointer_1.MEMORY_POINTER).toMatch(/already/i);
|
|
40
|
+
});
|
|
41
|
+
it('does not promise the agent that memory is complete or authoritative', () => {
|
|
42
|
+
// Entries carry verdicts and can be stale; the brief renders its own
|
|
43
|
+
// flags. Guidance must not override that framing.
|
|
44
|
+
expect(memoryPointer_1.MEMORY_POINTER).not.toMatch(/do not re-verify/i);
|
|
45
|
+
expect(memoryPointer_1.MEMORY_POINTER).not.toMatch(/authoritative/i);
|
|
46
|
+
});
|
|
47
|
+
it('is a compact block — this rides in every agent\'s context', () => {
|
|
48
|
+
expect(Buffer.byteLength(memoryPointer_1.MEMORY_POINTER, 'utf8')).toBeLessThanOrEqual(600);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* wmr-03 — the workspace's actual memory, rendered beside the instruction.
|
|
53
|
+
*
|
|
54
|
+
* The brief arrives pre-formatted and pre-capped by the backend's
|
|
55
|
+
* brief.service.ts (2048 bytes; tiered pinned → STILL_VALID/ASSERTED →
|
|
56
|
+
* DRIFTED → UNVERIFIED, each with its own flag text). This composer's whole
|
|
57
|
+
* job is to place it and to decide when there is nothing to place — it must
|
|
58
|
+
* never re-rank, re-wrap, re-cap or otherwise reinterpret it, because a second
|
|
59
|
+
* opinion about ranking here would silently diverge from what the Memory panel
|
|
60
|
+
* shows the human for the same workspace.
|
|
61
|
+
*/
|
|
62
|
+
describe('memorySection — placing the brief (wmr-03)', () => {
|
|
63
|
+
const { memorySection, MEMORY_POINTER } = require('../bridge/memoryPointer');
|
|
64
|
+
it('with no brief, is exactly the standing instruction', () => {
|
|
65
|
+
expect(memorySection(null)).toBe(MEMORY_POINTER);
|
|
66
|
+
});
|
|
67
|
+
it('treats an empty or whitespace-only brief as no brief', () => {
|
|
68
|
+
// An empty heading reads as "this workspace knows nothing", which is a
|
|
69
|
+
// different claim from "memory has not been written yet".
|
|
70
|
+
expect(memorySection('')).toBe(MEMORY_POINTER);
|
|
71
|
+
expect(memorySection(' \n ')).toBe(MEMORY_POINTER);
|
|
72
|
+
});
|
|
73
|
+
it('carries the brief verbatim, after the instruction', () => {
|
|
74
|
+
const brief = 'WORKSPACE MEMORY BRIEF (2 entries, 0 flagged)\n- api-shape: GET /foo returns {id}';
|
|
75
|
+
const out = memorySection(brief);
|
|
76
|
+
expect(out).toContain(MEMORY_POINTER);
|
|
77
|
+
expect(out).toContain(brief);
|
|
78
|
+
expect(out.indexOf(MEMORY_POINTER)).toBeLessThan(out.indexOf(brief));
|
|
79
|
+
});
|
|
80
|
+
it('preserves drift flags exactly — an agent acting on a stale fact is the failure this guards', () => {
|
|
81
|
+
const brief = '- old-route: /v1/foo is the endpoint\n⚠ re-verify before relying: refs changed since abc123';
|
|
82
|
+
expect(memorySection(brief)).toContain('⚠ re-verify before relying:');
|
|
83
|
+
});
|
|
84
|
+
it('passes a brief already at the backend cap through unmodified', () => {
|
|
85
|
+
const brief = 'x'.repeat(2048);
|
|
86
|
+
expect(memorySection(brief)).toContain(brief);
|
|
87
|
+
});
|
|
88
|
+
it('does not re-cap or truncate — the backend owns that budget', () => {
|
|
89
|
+
const brief = 'y'.repeat(4096);
|
|
90
|
+
expect(memorySection(brief)).toContain(brief);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -29,4 +29,17 @@ export declare function writeContext(input: {
|
|
|
29
29
|
workspaceId: string;
|
|
30
30
|
content: string;
|
|
31
31
|
workGraphAvailable?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* wmr-01 — whether to carry the workspace-memory guidance. Decided by the
|
|
34
|
+
* caller on the same signal as workGraphAvailable (WENRWA_CONNECT_TOKEN),
|
|
35
|
+
* because it names the same MCP tool registration. Absent/false is
|
|
36
|
+
* byte-identical to pre-wmr-01 output.
|
|
37
|
+
*/
|
|
38
|
+
memoryAvailable?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* wmr-03 — this workspace's composed memory brief, or null when the fetch
|
|
41
|
+
* failed or there is nothing yet. Never blocks the write (PRD D2): a null
|
|
42
|
+
* brief renders the standing instruction alone.
|
|
43
|
+
*/
|
|
44
|
+
memoryBrief?: string | null;
|
|
32
45
|
}, deps: ContextWriterDeps): ContextWriteResult;
|
|
@@ -68,6 +68,7 @@ exports.writeContext = writeContext;
|
|
|
68
68
|
const path = __importStar(require("path"));
|
|
69
69
|
const bundleInstaller_1 = require("./bundleInstaller");
|
|
70
70
|
const workGraphPointer_1 = require("./workGraphPointer");
|
|
71
|
+
const memoryPointer_1 = require("./memoryPointer");
|
|
71
72
|
exports.CONTEXT_START_MARKER = '<!-- HIVEOP:CONTEXT:START -->';
|
|
72
73
|
exports.CONTEXT_END_MARKER = '<!-- HIVEOP:CONTEXT:END -->';
|
|
73
74
|
/** Build the fenced region block for a non-empty body. */
|
|
@@ -100,10 +101,21 @@ function writeContext(input, deps) {
|
|
|
100
101
|
// appears — pointing at unregistered MCP tools is worse than silence). When
|
|
101
102
|
// present, the pointer alone is reason enough to keep the region — it does
|
|
102
103
|
// NOT get removed just because there is no pushed file content this turn.
|
|
104
|
+
//
|
|
105
|
+
// wmr-01 adds the memory pointer beside it under the same rule. Both are
|
|
106
|
+
// assembled as a list of present sections so the joining logic states the
|
|
107
|
+
// shape once — two independent flags would otherwise need four branches to
|
|
108
|
+
// get the blank lines right, and the empty-content case is the one that
|
|
109
|
+
// decides whether the region survives at all.
|
|
103
110
|
const hasFileContent = input.content.trim().length > 0;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
const sections = [];
|
|
112
|
+
if (input.workGraphAvailable)
|
|
113
|
+
sections.push(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
114
|
+
if (input.memoryAvailable)
|
|
115
|
+
sections.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
116
|
+
if (hasFileContent)
|
|
117
|
+
sections.push(input.content);
|
|
118
|
+
const content = sections.length > 0 ? sections.join('\n\n') : input.content;
|
|
107
119
|
const isEmpty = content.trim().length === 0;
|
|
108
120
|
// Locate any existing managed region.
|
|
109
121
|
const startIdx = existing ? existing.indexOf(exports.CONTEXT_START_MARKER) : -1;
|
|
@@ -28,4 +28,11 @@ export declare function writeLocalContext(input: {
|
|
|
28
28
|
projectRoot: string;
|
|
29
29
|
content: string;
|
|
30
30
|
workGraphAvailable?: boolean;
|
|
31
|
+
/** wmr-04 — two-lane parity with contextWriter.ts. Gated by the caller on
|
|
32
|
+
* the local lane's own honest signal (did THIS bridge run register the
|
|
33
|
+
* MCP server), not on a container env var that does not exist here. */
|
|
34
|
+
memoryAvailable?: boolean;
|
|
35
|
+
/** wmr-04 — this workspace's composed brief, or null when the fetch failed
|
|
36
|
+
* or there is nothing yet. Never blocks the write (PRD D2). */
|
|
37
|
+
memoryBrief?: string | null;
|
|
31
38
|
}, deps: LocalContextWriterDeps): LocalContextWriteResult;
|
|
@@ -55,6 +55,7 @@ exports.writeLocalContext = writeLocalContext;
|
|
|
55
55
|
*/
|
|
56
56
|
const path = __importStar(require("path"));
|
|
57
57
|
const workGraphPointer_1 = require("./workGraphPointer");
|
|
58
|
+
const memoryPointer_1 = require("./memoryPointer");
|
|
58
59
|
exports.LOCAL_CONTEXT_DIR = '.hiveop';
|
|
59
60
|
exports.LOCAL_CONTEXT_FILENAME = 'context.md';
|
|
60
61
|
exports.LOCAL_CONTEXT_RELATIVE_PATH = `${exports.LOCAL_CONTEXT_DIR}/${exports.LOCAL_CONTEXT_FILENAME}`;
|
|
@@ -72,10 +73,21 @@ function writeLocalContext(input, deps) {
|
|
|
72
73
|
// to pre-wgcp-01 behavior (never pointing at unregistered MCP tools); when
|
|
73
74
|
// present, the pointer alone is reason enough to write the file, even with
|
|
74
75
|
// no pushed manifest this turn.
|
|
76
|
+
//
|
|
77
|
+
// wmr-04 adds the memory section under the same rule, assembled as a list of
|
|
78
|
+
// present sections so the joining and empty-content logic is stated once —
|
|
79
|
+
// the same shape contextWriter.ts uses, because the two lanes diverging on
|
|
80
|
+
// this would be a two-lane bug nobody would notice until a local pilot
|
|
81
|
+
// reported missing memory.
|
|
75
82
|
const hasFileContent = input.content.trim().length > 0;
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
const sections = [];
|
|
84
|
+
if (input.workGraphAvailable)
|
|
85
|
+
sections.push(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
86
|
+
if (input.memoryAvailable)
|
|
87
|
+
sections.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
88
|
+
if (hasFileContent)
|
|
89
|
+
sections.push(input.content);
|
|
90
|
+
const content = sections.length > 0 ? sections.join('\n\n') : input.content;
|
|
79
91
|
const isEmpty = content.trim().length === 0;
|
|
80
92
|
if (isEmpty) {
|
|
81
93
|
if (!existed)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wmr-02 — bridge-side fetcher for the workspace memory brief
|
|
3
|
+
* (`GET /workspaces/:id/memory/brief`, PRD decision D2 —
|
|
4
|
+
* docs/prd/workspace-memory-read-path.md). Mirrors storageQuotaFetch.ts's
|
|
5
|
+
* shape: injectable fetchImpl (defaults to the global fetch every Node
|
|
6
|
+
* >=18 the CLI already requires ships), ANY failure resolves null, never
|
|
7
|
+
* throws. This is deliberately a separate slice from anything that
|
|
8
|
+
* consumes the brief — proving the failure behaviour before depending on
|
|
9
|
+
* it is the whole point (see the PRD).
|
|
10
|
+
*
|
|
11
|
+
* Its entire contract: never throws, never hangs. On a non-200, network
|
|
12
|
+
* error, malformed/missing body, a missing/empty Connect Token, or a
|
|
13
|
+
* timeout, it resolves null so the caller writes the context region
|
|
14
|
+
* exactly as it would have anyway — memory can never become a single
|
|
15
|
+
* point of failure for opening a terminal.
|
|
16
|
+
*/
|
|
17
|
+
export type FetchLike = (url: string, init?: {
|
|
18
|
+
method?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
status: number;
|
|
24
|
+
json: () => Promise<unknown>;
|
|
25
|
+
}>;
|
|
26
|
+
export interface FetchMemoryBriefOpts {
|
|
27
|
+
/** Injectable transport — tests stub this so nothing hits the network,
|
|
28
|
+
* same discipline as apiClient.ts / storageQuotaFetch.ts. Defaults to
|
|
29
|
+
* the global fetch. */
|
|
30
|
+
fetchImpl?: FetchLike;
|
|
31
|
+
/** Overrides the default bounded timeout, in ms. Tests use this to keep
|
|
32
|
+
* the timeout case fast without waiting out the real default. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fetch the workspace memory brief for `workspaceId`. See the module
|
|
37
|
+
* header for the full failure contract — every branch below returns null
|
|
38
|
+
* rather than propagating an error or letting a slow response block the
|
|
39
|
+
* caller.
|
|
40
|
+
*/
|
|
41
|
+
export declare function fetchMemoryBrief(workspaceId: string, opts?: FetchMemoryBriefOpts): Promise<string | null>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* wmr-02 — bridge-side fetcher for the workspace memory brief
|
|
4
|
+
* (`GET /workspaces/:id/memory/brief`, PRD decision D2 —
|
|
5
|
+
* docs/prd/workspace-memory-read-path.md). Mirrors storageQuotaFetch.ts's
|
|
6
|
+
* shape: injectable fetchImpl (defaults to the global fetch every Node
|
|
7
|
+
* >=18 the CLI already requires ships), ANY failure resolves null, never
|
|
8
|
+
* throws. This is deliberately a separate slice from anything that
|
|
9
|
+
* consumes the brief — proving the failure behaviour before depending on
|
|
10
|
+
* it is the whole point (see the PRD).
|
|
11
|
+
*
|
|
12
|
+
* Its entire contract: never throws, never hangs. On a non-200, network
|
|
13
|
+
* error, malformed/missing body, a missing/empty Connect Token, or a
|
|
14
|
+
* timeout, it resolves null so the caller writes the context region
|
|
15
|
+
* exactly as it would have anyway — memory can never become a single
|
|
16
|
+
* point of failure for opening a terminal.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.fetchMemoryBrief = fetchMemoryBrief;
|
|
20
|
+
const DEFAULT_API_URL = 'https://api.wenrwa.com/api/v1';
|
|
21
|
+
/** Bounded so a hanging backend can never stall a terminal open. */
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
23
|
+
/**
|
|
24
|
+
* Fetch the workspace memory brief for `workspaceId`. See the module
|
|
25
|
+
* header for the full failure contract — every branch below returns null
|
|
26
|
+
* rather than propagating an error or letting a slow response block the
|
|
27
|
+
* caller.
|
|
28
|
+
*/
|
|
29
|
+
async function fetchMemoryBrief(workspaceId, opts = {}) {
|
|
30
|
+
const token = process.env.WENRWA_CONNECT_TOKEN;
|
|
31
|
+
if (!token)
|
|
32
|
+
return null; // missing/empty token — never even attempt the call
|
|
33
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
34
|
+
if (!fetchImpl)
|
|
35
|
+
return null;
|
|
36
|
+
const apiUrl = process.env.MARKETPLACE_API_URL || DEFAULT_API_URL;
|
|
37
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
|
+
const url = `${apiUrl}/workspaces/${encodeURIComponent(workspaceId)}/memory/brief`;
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetchImpl(url, {
|
|
43
|
+
method: 'GET',
|
|
44
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
45
|
+
signal: controller.signal,
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
return null;
|
|
49
|
+
const body = (await res.json());
|
|
50
|
+
if (body?.success !== true || typeof body.brief !== 'string' || body.brief.length === 0) {
|
|
51
|
+
return null; // malformed/missing body — never a fabricated brief
|
|
52
|
+
}
|
|
53
|
+
return body.brief;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// network error, abort/timeout, or json() throwing on malformed JSON —
|
|
57
|
+
// all collapse to the same UNKNOWN outcome, never thrown to the caller.
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wmr-01 — the guidance that tells an agent workspace memory exists at all.
|
|
3
|
+
*
|
|
4
|
+
* Workspace Memory (ADR-0044) shipped complete — store, verdicts, curation UI,
|
|
5
|
+
* MCP tools — and held ZERO entries in production for a month. Two reasons, and
|
|
6
|
+
* only one of them was a bug: agents were structurally forbidden from writing
|
|
7
|
+
* (a Connect Token allowlist omission, fixed in cb5e725), and nothing ever told
|
|
8
|
+
* them the store existed. Fixing the first without the second just swaps a 403
|
|
9
|
+
* for an agent that never calls the tool.
|
|
10
|
+
*
|
|
11
|
+
* Delivered through the SAME managed-region mechanism as the work-graph pointer
|
|
12
|
+
* (wgcp-01, workGraphPointer.ts) — contextWriter.ts for the hosted lane,
|
|
13
|
+
* localContextWriter.ts for local. No new delivery channel, and no image
|
|
14
|
+
* rebuild: `--append-system-prompt` does not exist in this codebase, and the
|
|
15
|
+
* hosted lane launches `claude` from a script baked into the image, so argv is
|
|
16
|
+
* not reachable from here.
|
|
17
|
+
*
|
|
18
|
+
* Gated by the CALLER on `WENRWA_CONNECT_TOKEN`, exactly as the work-graph
|
|
19
|
+
* pointer is: absent means the MCP tools this text names are not registered in
|
|
20
|
+
* this process, and pointing an agent at a tool it does not have is worse than
|
|
21
|
+
* silence.
|
|
22
|
+
*
|
|
23
|
+
* WHAT THIS TEXT HAS TO EARN. It rides in every agent's context on every
|
|
24
|
+
* attach, so it states a floor as well as a prompt: without one, the cheapest
|
|
25
|
+
* thing for a model to record is whatever it just read, which is exactly what
|
|
26
|
+
* the code already says and what nobody needs repeated. It also never tells an
|
|
27
|
+
* agent that memory is settled truth — entries carry verdicts, the brief
|
|
28
|
+
* renders its own `⚠ re-verify before relying:` flags, and the predecessor
|
|
29
|
+
* this epic replaced (lib/agent-memory.ts) failed precisely by prompting
|
|
30
|
+
* "confirmed facts — do not re-verify".
|
|
31
|
+
*/
|
|
32
|
+
export declare const MEMORY_POINTER: string;
|
|
33
|
+
/**
|
|
34
|
+
* wmr-03 — the memory section as it appears in the managed region: the
|
|
35
|
+
* standing instruction, followed by this workspace's brief when there is one.
|
|
36
|
+
*
|
|
37
|
+
* THE BRIEF IS CARRIED VERBATIM. It arrives already composed and already
|
|
38
|
+
* capped by the backend (brief.service.ts: 2048 bytes, tiered pinned →
|
|
39
|
+
* STILL_VALID/ASSERTED → DRIFTED → UNVERIFIED, each rendered with its own flag
|
|
40
|
+
* text). Re-ranking, re-wrapping or re-capping it here would give the agent a
|
|
41
|
+
* different view of the same workspace than the Memory panel gives the human,
|
|
42
|
+
* and the two would drift apart with no test able to see it. The one judgement
|
|
43
|
+
* this function makes is whether there is anything to show at all.
|
|
44
|
+
*
|
|
45
|
+
* `null` — the fetch failed, or the workspace has no memory yet — renders the
|
|
46
|
+
* instruction alone rather than an empty heading. An empty heading reads as
|
|
47
|
+
* "this workspace knows nothing", which is a different and wrong claim.
|
|
48
|
+
*/
|
|
49
|
+
export declare function memorySection(brief: string | null | undefined): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MEMORY_POINTER = void 0;
|
|
4
|
+
exports.memorySection = memorySection;
|
|
5
|
+
/**
|
|
6
|
+
* wmr-01 — the guidance that tells an agent workspace memory exists at all.
|
|
7
|
+
*
|
|
8
|
+
* Workspace Memory (ADR-0044) shipped complete — store, verdicts, curation UI,
|
|
9
|
+
* MCP tools — and held ZERO entries in production for a month. Two reasons, and
|
|
10
|
+
* only one of them was a bug: agents were structurally forbidden from writing
|
|
11
|
+
* (a Connect Token allowlist omission, fixed in cb5e725), and nothing ever told
|
|
12
|
+
* them the store existed. Fixing the first without the second just swaps a 403
|
|
13
|
+
* for an agent that never calls the tool.
|
|
14
|
+
*
|
|
15
|
+
* Delivered through the SAME managed-region mechanism as the work-graph pointer
|
|
16
|
+
* (wgcp-01, workGraphPointer.ts) — contextWriter.ts for the hosted lane,
|
|
17
|
+
* localContextWriter.ts for local. No new delivery channel, and no image
|
|
18
|
+
* rebuild: `--append-system-prompt` does not exist in this codebase, and the
|
|
19
|
+
* hosted lane launches `claude` from a script baked into the image, so argv is
|
|
20
|
+
* not reachable from here.
|
|
21
|
+
*
|
|
22
|
+
* Gated by the CALLER on `WENRWA_CONNECT_TOKEN`, exactly as the work-graph
|
|
23
|
+
* pointer is: absent means the MCP tools this text names are not registered in
|
|
24
|
+
* this process, and pointing an agent at a tool it does not have is worse than
|
|
25
|
+
* silence.
|
|
26
|
+
*
|
|
27
|
+
* WHAT THIS TEXT HAS TO EARN. It rides in every agent's context on every
|
|
28
|
+
* attach, so it states a floor as well as a prompt: without one, the cheapest
|
|
29
|
+
* thing for a model to record is whatever it just read, which is exactly what
|
|
30
|
+
* the code already says and what nobody needs repeated. It also never tells an
|
|
31
|
+
* agent that memory is settled truth — entries carry verdicts, the brief
|
|
32
|
+
* renders its own `⚠ re-verify before relying:` flags, and the predecessor
|
|
33
|
+
* this epic replaced (lib/agent-memory.ts) failed precisely by prompting
|
|
34
|
+
* "confirmed facts — do not re-verify".
|
|
35
|
+
*/
|
|
36
|
+
exports.MEMORY_POINTER = [
|
|
37
|
+
'## Workspace memory',
|
|
38
|
+
'This workspace has a shared memory other agents read: `memory_write` to record something, `memory_recall` to search it.',
|
|
39
|
+
'Worth recording: a correction you were given, a decision and the reason behind it, a non-obvious fact and the file you learned it from.',
|
|
40
|
+
'Not worth recording: anything the code already states, or anything you merely read this session.',
|
|
41
|
+
].join('\n');
|
|
42
|
+
/**
|
|
43
|
+
* wmr-03 — the memory section as it appears in the managed region: the
|
|
44
|
+
* standing instruction, followed by this workspace's brief when there is one.
|
|
45
|
+
*
|
|
46
|
+
* THE BRIEF IS CARRIED VERBATIM. It arrives already composed and already
|
|
47
|
+
* capped by the backend (brief.service.ts: 2048 bytes, tiered pinned →
|
|
48
|
+
* STILL_VALID/ASSERTED → DRIFTED → UNVERIFIED, each rendered with its own flag
|
|
49
|
+
* text). Re-ranking, re-wrapping or re-capping it here would give the agent a
|
|
50
|
+
* different view of the same workspace than the Memory panel gives the human,
|
|
51
|
+
* and the two would drift apart with no test able to see it. The one judgement
|
|
52
|
+
* this function makes is whether there is anything to show at all.
|
|
53
|
+
*
|
|
54
|
+
* `null` — the fetch failed, or the workspace has no memory yet — renders the
|
|
55
|
+
* instruction alone rather than an empty heading. An empty heading reads as
|
|
56
|
+
* "this workspace knows nothing", which is a different and wrong claim.
|
|
57
|
+
*/
|
|
58
|
+
function memorySection(brief) {
|
|
59
|
+
if (!brief || brief.trim().length === 0)
|
|
60
|
+
return exports.MEMORY_POINTER;
|
|
61
|
+
return `${exports.MEMORY_POINTER}\n\n${brief.trim()}`;
|
|
62
|
+
}
|
|
@@ -227,7 +227,17 @@ export interface RelayClientOpts {
|
|
|
227
227
|
}) => {
|
|
228
228
|
action: string;
|
|
229
229
|
claudeMdPath: string | null;
|
|
230
|
-
} | void
|
|
230
|
+
} | void
|
|
231
|
+
/**
|
|
232
|
+
* wmr-03 — may be async: the hosted implementation fetches this
|
|
233
|
+
* workspace's memory brief before writing the region. The frame handler
|
|
234
|
+
* below resolves it rather than becoming async itself, so no other frame
|
|
235
|
+
* type's ordering changes.
|
|
236
|
+
*/
|
|
237
|
+
| Promise<{
|
|
238
|
+
action: string;
|
|
239
|
+
claudeMdPath: string | null;
|
|
240
|
+
} | void>;
|
|
231
241
|
/**
|
|
232
242
|
* cve-06 (ADR-0047 / ADR-0032): read ONE file out of the container's
|
|
233
243
|
* workspace checkout — the hosted-lane counterpart of the local bridge's
|
|
@@ -1274,12 +1274,26 @@ function startRelayClient(opts) {
|
|
|
1274
1274
|
send(socket, { t: 'error', message: 'context-manifest unsupported (no workspace checkouts)' });
|
|
1275
1275
|
break;
|
|
1276
1276
|
}
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1277
|
+
// wmr-03: the implementation may be async (it fetches the memory
|
|
1278
|
+
// brief). Resolved here so the outer message handler stays sync —
|
|
1279
|
+
// making IT async would change ordering for every frame type. A
|
|
1280
|
+
// rejection can only come from a bug in the writer, never from the
|
|
1281
|
+
// brief fetch (which resolves null on any failure, PRD D2), so it
|
|
1282
|
+
// is answered rather than thrown into an unhandled rejection.
|
|
1283
|
+
Promise.resolve(opts.writeContext({ workspaceId: frame.workspaceId, content: frame.content ?? '' }))
|
|
1284
|
+
.then((result) => {
|
|
1285
|
+
send(socket, {
|
|
1286
|
+
t: 'context-written',
|
|
1287
|
+
workspaceId: frame.workspaceId,
|
|
1288
|
+
...(frame.sessionId ? { sessionId: frame.sessionId } : {}),
|
|
1289
|
+
...(result && typeof result === 'object' ? { action: result.action } : {}),
|
|
1290
|
+
});
|
|
1291
|
+
})
|
|
1292
|
+
.catch((err) => {
|
|
1293
|
+
send(socket, {
|
|
1294
|
+
t: 'error',
|
|
1295
|
+
message: `context-manifest failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1296
|
+
});
|
|
1283
1297
|
});
|
|
1284
1298
|
break;
|
|
1285
1299
|
}
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -197,10 +197,13 @@ export interface BridgeServerOpts {
|
|
|
197
197
|
* this is a FILE write, not PTY input (ADR-0028), mirroring the hosted
|
|
198
198
|
* lane's contextWriter.ts/relayClient.ts contract.
|
|
199
199
|
*/
|
|
200
|
+
/** wmr-04 — may be async: the implementation fetches this workspace's memory
|
|
201
|
+
* brief before writing. The frame handler resolves it rather than becoming
|
|
202
|
+
* async itself, so no other frame type's ordering changes. */
|
|
200
203
|
writeLocalContext?: (params: {
|
|
201
204
|
workspaceId: string;
|
|
202
205
|
content: string;
|
|
203
|
-
}) => LocalContextWriteResult | null
|
|
206
|
+
}) => LocalContextWriteResult | null | Promise<LocalContextWriteResult | null>;
|
|
204
207
|
/**
|
|
205
208
|
* fcct-24 — path to the local HiveOp activity JSONL file the activity
|
|
206
209
|
* hooks (registered via a directly-spawned agent's `--settings`, see
|
package/dist/bridge/server.js
CHANGED
|
@@ -874,16 +874,27 @@ function startBridgeServer(opts) {
|
|
|
874
874
|
send(ws, { t: 'error', message: 'context-manifest unsupported (no local writer wired)' });
|
|
875
875
|
break;
|
|
876
876
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
877
|
+
// wmr-04: mirrors the hosted lane — resolved here so the outer
|
|
878
|
+
// handler stays sync. A rejection can only be a bug in the writer;
|
|
879
|
+
// the brief fetch resolves null on any failure (PRD D2).
|
|
880
|
+
Promise.resolve(opts.writeLocalContext({ workspaceId: frame.workspaceId, content: frame.content }))
|
|
881
|
+
.then((result) => {
|
|
882
|
+
if (!result) {
|
|
883
|
+
send(ws, { t: 'error', message: 'unknown local workspace' });
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
send(ws, {
|
|
887
|
+
t: 'context-written',
|
|
888
|
+
workspaceId: frame.workspaceId,
|
|
889
|
+
...(frame.sessionId ? { sessionId: frame.sessionId } : {}),
|
|
890
|
+
action: result.action,
|
|
891
|
+
});
|
|
892
|
+
})
|
|
893
|
+
.catch((err) => {
|
|
894
|
+
send(ws, {
|
|
895
|
+
t: 'error',
|
|
896
|
+
message: `context-manifest failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
897
|
+
});
|
|
887
898
|
});
|
|
888
899
|
break;
|
|
889
900
|
}
|
package/dist/commands/bridge.js
CHANGED
|
@@ -73,6 +73,7 @@ const repoCheckout_1 = require("../bridge/repoCheckout");
|
|
|
73
73
|
const bundleInstaller_1 = require("../bridge/bundleInstaller");
|
|
74
74
|
const agentLaunch_1 = require("../bridge/agentLaunch");
|
|
75
75
|
const contextWriter_1 = require("../bridge/contextWriter");
|
|
76
|
+
const memoryBrief_1 = require("../bridge/memoryBrief");
|
|
76
77
|
const containerFileRelay_1 = require("../bridge/containerFileRelay");
|
|
77
78
|
const claudeTranscriptReader_1 = require("../bridge/claudeTranscriptReader");
|
|
78
79
|
const config_2 = require("../utils/config");
|
|
@@ -714,7 +715,14 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
714
715
|
// into a fenced managed region of /data/repos/<workspaceId>/CLAUDE.md so
|
|
715
716
|
// Claude Code reads it natively next turn. A FILE, never PTY input;
|
|
716
717
|
// path-safe + idempotent; empty content removes the region.
|
|
717
|
-
writeContext: ({ workspaceId, content }) => {
|
|
718
|
+
writeContext: async ({ workspaceId, content }) => {
|
|
719
|
+
// wmr-03 — the workspace's memory brief, fetched at manifest time so
|
|
720
|
+
// the agent has it in context from its next turn. Never blocks the
|
|
721
|
+
// write: fetchMemoryBrief resolves null on any failure (PRD D2), and
|
|
722
|
+
// carries its own bounded timeout so a hanging backend cannot stall a
|
|
723
|
+
// terminal opening.
|
|
724
|
+
const memoryAvailable = Boolean(process.env.WENRWA_CONNECT_TOKEN);
|
|
725
|
+
const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
|
|
718
726
|
const result = (0, contextWriter_1.writeContext)({
|
|
719
727
|
workspaceId,
|
|
720
728
|
content,
|
|
@@ -724,6 +732,10 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
724
732
|
// means the MCP tools this pointer names are not registered here —
|
|
725
733
|
// pointing at tools that do not exist is worse than silence.
|
|
726
734
|
workGraphAvailable: Boolean(process.env.WENRWA_CONNECT_TOKEN),
|
|
735
|
+
// wmr-01 — same gate, same reason: the token is what proves the
|
|
736
|
+
// MCP tools named by the memory pointer are registered here.
|
|
737
|
+
memoryAvailable,
|
|
738
|
+
memoryBrief,
|
|
727
739
|
}, {
|
|
728
740
|
reposBaseDir,
|
|
729
741
|
exists: (p) => fs.existsSync(p),
|
|
@@ -1019,10 +1031,15 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1019
1031
|
// lws-08 (PRD D9) — a context-manifest frame writes ONLY
|
|
1020
1032
|
// `.hiveop/context.md` under the resolved local workspace's project
|
|
1021
1033
|
// root; unknown workspaceId → null (server.ts errors the frame).
|
|
1022
|
-
writeLocalContext: ({ workspaceId, content }) => {
|
|
1034
|
+
writeLocalContext: async ({ workspaceId, content }) => {
|
|
1023
1035
|
const entry = (0, folderRegistry_1.findEntryByWorkspaceId)((0, folderRegistry_1.loadFolderRegistry)(), workspaceId);
|
|
1024
1036
|
if (!entry)
|
|
1025
1037
|
return null;
|
|
1038
|
+
// wmr-04 — same gate as the work-graph pointer on this lane: whether
|
|
1039
|
+
// THIS bridge run actually registered the MCP server. Fetch failures
|
|
1040
|
+
// resolve null and leave the standing instruction in place (PRD D2).
|
|
1041
|
+
const memoryAvailable = mcp.action !== 'claude-not-found';
|
|
1042
|
+
const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
|
|
1026
1043
|
return (0, localContextWriter_1.writeLocalContext)({
|
|
1027
1044
|
projectRoot: entry.path,
|
|
1028
1045
|
content,
|
|
@@ -1034,6 +1051,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
|
|
|
1034
1051
|
// for "are the tools this pointer names actually registered" —
|
|
1035
1052
|
// pointing at tools that do not exist is worse than silence.
|
|
1036
1053
|
workGraphAvailable: mcp.action !== 'claude-not-found',
|
|
1054
|
+
memoryAvailable,
|
|
1055
|
+
memoryBrief,
|
|
1037
1056
|
}, realLocalContextWriterDeps());
|
|
1038
1057
|
},
|
|
1039
1058
|
// lws-06 — best-effort `claude` binary check before spawning an
|
package/dist/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "CLI tool for the Wenrwa Agent Marketplace
|
|
3
|
+
"version": "0.5.24",
|
|
4
|
+
"description": "CLI tool for the Wenrwa Agent Marketplace — monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://marketplace.wenrwa.com",
|