Skip to content

Commit 27a2684

Browse files
committed
feat(agent-workspace): persist operator diagnostics reports via sidecar
1 parent 2ded132 commit 27a2684

11 files changed

Lines changed: 625 additions & 1 deletion

docs/brainstorms/2026-04-16-mainline-ci-stabilization-and-m7-direction-requirements.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,30 @@ Deliverables:
258258
- `npm test -- src/agent_workspace.runtime.behavior.test.ts --runInBand`
259259
- `npm run test:agent-workspace:contracts`
260260

261+
### M7.7 (Now): Operator Route/Index Persistence and CI Evidence Gate Hardening (Lane Ops Bridge)
262+
263+
Deliverables:
264+
265+
- add sidecar route/index persistence for agent workspace diagnostics reports.
266+
- add runtime persistence bridge and harden CI evidence gates to fail fast on route/wiring drift.
267+
268+
#### M7.7 Progress Note (2026-04-16)
269+
270+
- [Done] expanded `src/server.ts` with diagnostics report persistence routes:
271+
- `POST /api/knowledge/operator/agent-workspace-diagnostics/report`,
272+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/index`,
273+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/latest`,
274+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/report?reportId=...`.
275+
- [Done] expanded `src/frontend/agent_workspace_runtime.js` with `persistDiagnosticsReport()` to export and persist diagnostics via sidecar route.
276+
- [Done] expanded evidence coverage:
277+
- `src/server.migration.test.ts` now asserts persist/index/latest/read-by-id flow and runtime-data file persistence,
278+
- `src/knowledge.api.contract.test.ts` asserts operator diagnostics route contract presence,
279+
- `src/agent_workspace.verification.contract.test.ts` + `scripts/verify-agent-workspace-runtime.js` assert runtime gate hardening.
280+
- [Done] verification evidence:
281+
- `npm test -- src/server.migration.test.ts --runInBand --testNamePattern "agent workspace diagnostics report"`
282+
- `npm run test:agent-workspace:contracts`
283+
- `npm run verify:agent-workspace:runtime`
284+
261285
## Success Criteria
262286

263287
- CI failure mode that previously blocked the three agent-workspace suites is eliminated on mainline.
@@ -267,4 +291,4 @@ Deliverables:
267291

268292
## Next Step
269293

270-
Proceed to `/prompts:ce-plan` using this document as the source for `M7.7` decomposition (operator route/index persistence and CI evidence gate hardening), while preserving M7 lane boundary constraints.
294+
Proceed to `/prompts:ce-plan` using this document as the source for `M7.8` decomposition (operator runbook replay triage and bounded retention governance), while preserving M7 lane boundary constraints.

docs/diataxis/en/explanation/development-progress-dashboard.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,25 @@ Execution anchor:
331331
- `npm test -- src/agent_workspace.runtime.behavior.test.ts --runInBand`
332332
- `npm run test:agent-workspace:contracts`
333333

334+
## Latest Mainline Increment (2026-04-16 M7.7 Operator Route/Index Persistence and CI Gate Hardening Lane)
335+
336+
- Added sidecar persistence routes in `src/server.ts` for operator diagnostics report storage and retrieval:
337+
- `POST /api/knowledge/operator/agent-workspace-diagnostics/report`,
338+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/index`,
339+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/latest`,
340+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/report?reportId=...`.
341+
- Added runtime bridge surface in `src/frontend/agent_workspace_runtime.js`:
342+
- `persistDiagnosticsReport()` now exports snapshot/trend/index and persists it through the sidecar route.
343+
- Added executable evidence for route/index persistence:
344+
- `src/server.migration.test.ts` now validates persist -> index -> latest -> read-by-id flow and file persistence in `runtime_data/agent_workspace_diagnostics/`.
345+
- Hardened runtime CI evidence gate:
346+
- `scripts/verify-agent-workspace-runtime.js` now asserts diagnostics persistence route wiring and runtime `persistDiagnosticsReport` surface,
347+
- `src/agent_workspace.verification.contract.test.ts` and `src/knowledge.api.contract.test.ts` now fail fast on route/gate drift.
348+
- Verification evidence:
349+
- `npm test -- src/server.migration.test.ts --runInBand --testNamePattern \"agent workspace diagnostics report\"`
350+
- `npm run test:agent-workspace:contracts`
351+
- `npm run verify:agent-workspace:runtime`
352+
334353
## Mainline vs Working-Branch Snapshot (2026-04-14)
335354

336355
| Capability Slice | Working Branch (`feat/learning-multi-tutor-adapter`) | Mainline (`origin/main`) | Integration Status |

docs/diataxis/zh/explanation/development-progress-dashboard.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,25 @@
333333
- `npm test -- src/agent_workspace.runtime.behavior.test.ts --runInBand`
334334
- `npm run test:agent-workspace:contracts`
335335

336+
## 主线最新增量(2026-04-16 M7.7 运维路由/索引持久化与 CI 门禁加固链路)
337+
338+
- 已在 `src/server.ts` 增加运维诊断报告持久化与读取路由:
339+
- `POST /api/knowledge/operator/agent-workspace-diagnostics/report`,
340+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/index`,
341+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/latest`,
342+
- `GET /api/knowledge/operator/agent-workspace-diagnostics/report?reportId=...`
343+
- 已在 `src/frontend/agent_workspace_runtime.js` 增加运行时桥接接口:
344+
- `persistDiagnosticsReport()` 导出 snapshot/trend/index 并通过 sidecar 路由持久化。
345+
- 已补可执行证据覆盖 route/index 持久化闭环:
346+
- `src/server.migration.test.ts` 覆盖 persist -> index -> latest -> read-by-id 链路,并验证 `runtime_data/agent_workspace_diagnostics/` 文件落盘。
347+
- 已加固 runtime CI 证据门禁:
348+
- `scripts/verify-agent-workspace-runtime.js` 新增诊断持久化路由与 runtime `persistDiagnosticsReport` 接线断言,
349+
- `src/agent_workspace.verification.contract.test.ts``src/knowledge.api.contract.test.ts` 新增 fail-fast 契约断言,防止路由/门禁漂移。
350+
- 验证证据:
351+
- `npm test -- src/server.migration.test.ts --runInBand --testNamePattern \"agent workspace diagnostics report\"`
352+
- `npm run test:agent-workspace:contracts`
353+
- `npm run verify:agent-workspace:runtime`
354+
336355
## 主线 vs 工作分支快照(2026-04-14)
337356

338357
| 能力切片 | 工作分支(`feat/learning-multi-tutor-adapter`| 主线(`origin/main`| 集成状态 |

scripts/verify-agent-workspace-runtime.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,27 @@ function verifyAgentWorkspaceRuntime(repoRoot = path.resolve(__dirname, '..')) {
8787
assert(fs.existsSync(contractFilePath), `Missing contract file: ${contractFilePath}`);
8888

8989
const serverSource = readText(serverPath);
90+
const runtimeSource = readText(runtimeFilePath);
9091
assert(
9192
serverSource.includes('/api/knowledge/conversation'),
9293
'Missing /api/knowledge/conversation runtime route in src/server.ts'
9394
);
95+
assert(
96+
serverSource.includes('/api/knowledge/operator/agent-workspace-diagnostics/report'),
97+
'Missing diagnostics report persistence route in src/server.ts'
98+
);
99+
assert(
100+
serverSource.includes('/api/knowledge/operator/agent-workspace-diagnostics/index'),
101+
'Missing diagnostics report index route in src/server.ts'
102+
);
103+
assert(
104+
serverSource.includes('/api/knowledge/operator/agent-workspace-diagnostics/latest'),
105+
'Missing diagnostics report latest route in src/server.ts'
106+
);
107+
assert(
108+
runtimeSource.includes('persistDiagnosticsReport'),
109+
'Missing persistDiagnosticsReport runtime surface in src/frontend/agent_workspace_runtime.js'
110+
);
94111

95112
runNpm(repoRoot, ['run', 'test:agent-workspace:contracts']);
96113

@@ -101,6 +118,8 @@ function verifyAgentWorkspaceRuntime(repoRoot = path.resolve(__dirname, '..')) {
101118
'frontend runtime shell exists',
102119
'frontend contract module exists',
103120
'conversation route wiring exists',
121+
'diagnostics report persistence routes exist',
122+
'runtime diagnostics persistence surface exists',
104123
'agent workspace contract test suite passes',
105124
],
106125
};

src/agent_workspace.runtime.behavior.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type AgentRuntimeModule = {
4646
};
4747
};
4848
exportDiagnosticsReport: (options?: { format?: string }) => string | Record<string, unknown>;
49+
persistDiagnosticsReport: (options?: { endpoint?: string; source?: string }) => Promise<Record<string, unknown>>;
4950
};
5051
};
5152

@@ -1603,6 +1604,20 @@ describe('agent workspace runtime behavior', () => {
16031604
divergencePaths: [{ id: 'divergence-1' }],
16041605
},
16051606
}),
1607+
})
1608+
.mockResolvedValueOnce({
1609+
ok: true,
1610+
status: 200,
1611+
json: async () => ({
1612+
success: true,
1613+
result: {
1614+
metadata: {
1615+
reportId: 'awd-1',
1616+
source: 'agent-workspace-runtime',
1617+
},
1618+
indexCount: 1,
1619+
},
1620+
}),
16061621
});
16071622
(global as unknown as Record<string, unknown>).fetch = fetchMock;
16081623

@@ -1706,5 +1721,35 @@ describe('agent workspace runtime behavior', () => {
17061721
}),
17071722
])
17081723
);
1724+
1725+
const persistResult = await runtime.persistDiagnosticsReport();
1726+
expect(persistResult).toEqual(
1727+
expect.objectContaining({
1728+
metadata: expect.objectContaining({
1729+
reportId: 'awd-1',
1730+
}),
1731+
indexCount: 1,
1732+
})
1733+
);
1734+
const persistCall = fetchMock.mock.calls[3];
1735+
expect(persistCall[0]).toBe('/api/knowledge/operator/agent-workspace-diagnostics/report');
1736+
expect(persistCall[1]).toEqual(
1737+
expect.objectContaining({
1738+
method: 'POST',
1739+
})
1740+
);
1741+
const persistedBody = JSON.parse(String(persistCall[1].body));
1742+
expect(persistedBody.source).toBe('agent-workspace-runtime');
1743+
expect(persistedBody.report).toEqual(
1744+
expect.objectContaining({
1745+
snapshot: expect.objectContaining({
1746+
conversationRequests: 2,
1747+
}),
1748+
trend: expect.objectContaining({
1749+
replayCandidateRate: 0.5,
1750+
}),
1751+
index: expect.any(Object),
1752+
})
1753+
);
17091754
});
17101755
});

src/agent_workspace.runtime.integration.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('agent workspace runtime integration baseline', () => {
3131
expect(runtimeSource).toContain('/api/knowledge/mastery/misconceptions');
3232
expect(runtimeSource).toContain('/api/knowledge/quality/snapshot');
3333
expect(runtimeSource).toContain('/api/knowledge/memory/policy');
34+
expect(runtimeSource).toContain('/api/knowledge/operator/agent-workspace-diagnostics/report');
3435
expect(runtimeSource).toContain('focusOnNode');
3536
expect(runtimeSource).toContain('enterFocusMode');
3637
expect(runtimeSource).toContain('agent-workspace-path-docked');
@@ -41,6 +42,7 @@ describe('agent workspace runtime integration baseline', () => {
4142
expect(runtimeSource).toContain('getDiagnosticsTrendSnapshot');
4243
expect(runtimeSource).toContain('getDiagnosticsIndexSnapshot');
4344
expect(runtimeSource).toContain('exportDiagnosticsReport');
45+
expect(runtimeSource).toContain('persistDiagnosticsReport');
4446
expect(runtimeSource).toContain('DIAGNOSTIC_RUNBOOK_LINKS');
4547
expect(runtimeSource).toContain('__noteConnectionAgentWorkspaceRuntimeInstance');
4648
});

src/agent_workspace.verification.contract.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ describe('agent workspace verification script contracts', () => {
4444
const tauriSource = fs.readFileSync(tauriVerifyPath, 'utf8');
4545

4646
expect(runtimeSource).toContain('verifyAgentWorkspaceRuntime');
47+
expect(runtimeSource).toContain('/api/knowledge/operator/agent-workspace-diagnostics/report');
48+
expect(runtimeSource).toContain('/api/knowledge/operator/agent-workspace-diagnostics/index');
49+
expect(runtimeSource).toContain('/api/knowledge/operator/agent-workspace-diagnostics/latest');
50+
expect(runtimeSource).toContain('persistDiagnosticsReport');
4751
expect(browserSource).toContain('verifyAgentWorkspaceBrowser');
4852
expect(tauriSource).toContain('verifyAgentWorkspaceTauri');
4953
});

src/frontend/agent_workspace_runtime.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,17 @@
688688
return report;
689689
}
690690

691+
async function persistDiagnosticsReport(options = {}) {
692+
const endpoint = trimString(options && options.endpoint)
693+
|| '/api/knowledge/operator/agent-workspace-diagnostics/report';
694+
const source = trimString(options && options.source) || 'agent-workspace-runtime';
695+
const report = exportDiagnosticsReport();
696+
return requestJson(endpoint, {
697+
source,
698+
report,
699+
});
700+
}
701+
691702
function resolveUserId() {
692703
const fallback = trimString(options.defaultUserId) || 'agent_user_default';
693704
const candidate = trimString(dom.userIdInput && dom.userIdInput.value);
@@ -1455,6 +1466,7 @@
14551466
getDiagnosticsTrendSnapshot,
14561467
getDiagnosticsIndexSnapshot,
14571468
exportDiagnosticsReport,
1469+
persistDiagnosticsReport,
14581470
_state: state,
14591471
};
14601472
}

src/knowledge.api.contract.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ describe('Knowledge mastery API contract wiring', () => {
99
const endpoints = [
1010
'/api/knowledge/state',
1111
'/api/knowledge/store-diagnostics',
12+
'/api/knowledge/operator/agent-workspace-diagnostics/index',
13+
'/api/knowledge/operator/agent-workspace-diagnostics/latest',
14+
'/api/knowledge/operator/agent-workspace-diagnostics/report',
1215
'/api/knowledge/store/reload',
1316
'/api/knowledge/ingest',
1417
'/api/knowledge/ingest-diff',

src/server.migration.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,122 @@ describe('server migration settings routes', () => {
420420
expect(response.body.runtime.authToken).toBeUndefined();
421421
});
422422

423+
test('persists agent workspace diagnostics report and exposes index/latest/read-by-id routes', async () => {
424+
const reportPayload = {
425+
snapshot: {
426+
conversationRequests: 2,
427+
replayCandidateTurns: 1,
428+
turnCounts: {
429+
user: 2
430+
},
431+
capabilityEvents: [
432+
{ eventId: 'cap_1', status: 'success' },
433+
{ eventId: 'cap_2', status: 'success' }
434+
],
435+
lastFailure: null
436+
},
437+
trend: {
438+
userTurns: 2
439+
},
440+
index: {
441+
capabilityIndex: {
442+
operationIds: ['build_learning_path']
443+
}
444+
}
445+
};
446+
447+
const createResponse = await requestJson(
448+
port,
449+
'POST',
450+
'/api/knowledge/operator/agent-workspace-diagnostics/report',
451+
{
452+
source: 'server-migration-test',
453+
report: reportPayload
454+
}
455+
);
456+
expect(createResponse.status).toBe(200);
457+
expect(createResponse.body.success).toBe(true);
458+
expect(createResponse.body.result).toEqual(
459+
expect.objectContaining({
460+
indexCount: 1,
461+
metadata: expect.objectContaining({
462+
reportId: expect.stringMatching(/^awd-/),
463+
source: 'server-migration-test',
464+
summary: expect.objectContaining({
465+
conversationRequests: 2,
466+
replayCandidateTurns: 1,
467+
userTurns: 2,
468+
capabilityEvents: 2,
469+
hasLastFailure: false
470+
})
471+
})
472+
})
473+
);
474+
const reportId = createResponse.body.result.metadata.reportId as string;
475+
476+
const indexResponse = await requestJson(
477+
port,
478+
'GET',
479+
'/api/knowledge/operator/agent-workspace-diagnostics/index'
480+
);
481+
expect(indexResponse.status).toBe(200);
482+
expect(indexResponse.body.success).toBe(true);
483+
expect(indexResponse.body.count).toBe(1);
484+
expect(indexResponse.body.index[0]).toEqual(
485+
expect.objectContaining({
486+
reportId,
487+
source: 'server-migration-test'
488+
})
489+
);
490+
491+
const latestResponse = await requestJson(
492+
port,
493+
'GET',
494+
'/api/knowledge/operator/agent-workspace-diagnostics/latest'
495+
);
496+
expect(latestResponse.status).toBe(200);
497+
expect(latestResponse.body.success).toBe(true);
498+
expect(latestResponse.body.latest).toEqual(
499+
expect.objectContaining({
500+
metadata: expect.objectContaining({
501+
reportId,
502+
source: 'server-migration-test'
503+
}),
504+
report: expect.objectContaining({
505+
snapshot: expect.objectContaining({
506+
conversationRequests: 2
507+
})
508+
})
509+
})
510+
);
511+
512+
const byIdResponse = await requestJson(
513+
port,
514+
'GET',
515+
`/api/knowledge/operator/agent-workspace-diagnostics/report?reportId=${encodeURIComponent(reportId)}`
516+
);
517+
expect(byIdResponse.status).toBe(200);
518+
expect(byIdResponse.body.success).toBe(true);
519+
expect(byIdResponse.body.result).toEqual(
520+
expect.objectContaining({
521+
metadata: expect.objectContaining({
522+
reportId
523+
}),
524+
report: expect.objectContaining({
525+
snapshot: expect.objectContaining({
526+
conversationRequests: 2
527+
})
528+
})
529+
})
530+
);
531+
532+
const reportPath = path.join(runtimeDataDir, 'agent_workspace_diagnostics', `${reportId}.json`);
533+
await expect(fs.promises.readFile(reportPath, 'utf8')).resolves.toContain('"conversationRequests": 2');
534+
await expect(
535+
fs.promises.readFile(path.join(runtimeDataDir, 'agent_workspace_diagnostics', 'index.v1.json'), 'utf8')
536+
).resolves.toContain(reportId);
537+
});
538+
423539
test('server runtime path avoids synchronous filesystem APIs', () => {
424540
const serverSourcePath = path.join(__dirname, 'server.ts');
425541
const serverSource = fs.readFileSync(serverSourcePath, 'utf8');

0 commit comments

Comments
 (0)