@jjh32456/dsh-longlive 1.6.7 → 1.8.0

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.
Files changed (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -1,405 +0,0 @@
1
- // WP3 · proposal data layer contract (DESIGN §3.3/§3.6/§5.1/§5.2/§5.7/§5.10).
2
- //
3
- // These tests use a real RouterService (mock LLM, temp memory root) so the tool
4
- // wrapper, caller resolution, audit file and proposal store are exercised
5
- // together. They never touch the real memory root or the network.
6
-
7
- import assert from 'node:assert/strict';
8
- import crypto from 'node:crypto';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import test from 'node:test';
12
-
13
- import { apply } from '../../src/index.js';
14
- import { MockLlm } from '../../src/router/background/mock-llm.mjs';
15
- import { createRouterTools } from '../../src/router/service/tools.mjs';
16
- import { auditKinds, startedService, tempRoot } from './helpers.mjs';
17
-
18
- function sha256(text) {
19
- return crypto.createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex');
20
- }
21
-
22
- function execFor(sessionId, extra = {}) {
23
- return { agent: { session: { header: { id: sessionId } } }, ...extra };
24
- }
25
-
26
- function toolsOf(service) {
27
- return new Map(createRouterTools(service).map((tool) => [tool.name, tool]));
28
- }
29
-
30
- async function propose(service, args, sessionId = 's1') {
31
- return toolsOf(service).get('router_propose').invoke(args, { exec: execFor(sessionId) });
32
- }
33
-
34
- async function viewDetail(service, id, caller) {
35
- return service.proposalsView({ mode: 'detail', id }, { caller });
36
- }
37
-
38
- function proposalFiles(env) {
39
- const dir = path.join(env.vaultDir, 'proposals');
40
- if (!fs.existsSync(dir)) return [];
41
- return fs.readdirSync(dir).filter((name) => name.startsWith('p-') && name.endsWith('.json'));
42
- }
43
-
44
- function readAudit(env) {
45
- const file = path.join(env.vaultDir, 'audit.jsonl');
46
- if (!fs.existsSync(file)) return [];
47
- return fs.readFileSync(file, 'utf8').trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
48
- }
49
-
50
- async function withStarted(config, fn) {
51
- const env = await startedService({ config });
52
- try {
53
- assert.equal(env.result.ok, true);
54
- assert.equal(env.result.state, 'running');
55
- return await fn(env);
56
- } finally {
57
- try { await env.service.stop('test'); } catch { /* best effort */ }
58
- env.cleanup();
59
- }
60
- }
61
-
62
- test('K1: propose queues a ticket without writing canonical memory or the proposal body to audit', async () => {
63
- await withStarted({ proposalEnabled: true }, async (env) => {
64
- const journalDir = path.join(env.memoryRoot, 'journal');
65
- fs.mkdirSync(journalDir, { recursive: true });
66
- const journal = path.join(journalDir, '2026-11.md');
67
- fs.writeFileSync(journal, '# journal\n\n- existing\n', 'utf8');
68
- const before = fs.readFileSync(journal, 'utf8');
69
- const beforeSha = sha256(before);
70
-
71
- const result = await propose(env.service, {
72
- op: 'journal.append',
73
- target: 'journal/2026-11.md',
74
- title: 'WP3 K1',
75
- content: 'canonical must not move',
76
- }, 's-author');
77
-
78
- assert.equal(result.ok, true);
79
- assert.equal(result.ticket, 1);
80
- assert.match(result.ticketName, /^q-\d{4}$/u);
81
- assert.equal(result.position, 1);
82
- assert.deepEqual(result.ahead, []);
83
- assert.equal(result.etaMs, 1500);
84
- assert.equal(result.risk, 'low');
85
- assert.equal(typeof result.preview, 'object');
86
-
87
- const after = fs.readFileSync(journal, 'utf8');
88
- assert.equal(after, before);
89
- assert.equal(sha256(after), beforeSha);
90
-
91
- const files = proposalFiles(env);
92
- assert.equal(files.length, 1);
93
- const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', files[0]), 'utf8'));
94
- assert.equal(proposal.status, 'pending');
95
- assert.equal(proposal.content, 'canonical must not move');
96
- assert.equal(proposal.queue.ticket, result.ticketName);
97
- assert.equal(proposal.origin.sessionId, 's-author');
98
- assert.equal(proposal.queue.priority, 'P3');
99
- assert.equal(proposal.queue.priorityBy, 'default');
100
-
101
- const created = readAudit(env).find((line) => line.kind === 'proposal.create');
102
- assert.ok(created, 'proposal.create must be audited');
103
- assert.equal(created.content, undefined);
104
- assert.match(String(created.contentSha), /^sha256:[0-9a-f]{64}$/u);
105
- assert.equal(readAudit(env).some((line) => JSON.stringify(line).includes('canonical must not move')), false);
106
- });
107
- });
108
-
109
- test('K2: identical content dedupes to the existing ticket and writes no second proposal', async () => {
110
- await withStarted({ proposalEnabled: true }, async (env) => {
111
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
112
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
113
- const args = {
114
- op: 'journal.append',
115
- target: 'journal/2026-11.md',
116
- title: 'dedupe',
117
- content: 'same body',
118
- };
119
- const first = await propose(env.service, args, 's1');
120
- const second = await propose(env.service, { ...args, title: 'dedupe again' }, 's1');
121
- assert.equal(second.ok, true);
122
- assert.equal(second.ticket, first.ticket);
123
- assert.equal(second.duplicateOf, first.id);
124
- assert.equal(second.status, 'pending');
125
- assert.equal(proposalFiles(env).length, 1);
126
- });
127
- });
128
-
129
- test('K5: quota and per-file size limits fail closed', async () => {
130
- await withStarted({ proposalEnabled: true, maxProposals: 20, proposalsPerSessionHour: 60 }, async (env) => {
131
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
132
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
133
- // Alternate targets so consecutive proposals are never queue-adjacent for
134
- // the same target: this test measures the global quota, not §5.8 merging.
135
- const targetFor = (index) => (index % 2 === 0 ? 'journal/2026-11.md' : 'journal/2026-12.md');
136
- const base = {
137
- op: 'journal.append',
138
- target: 'journal/2026-11.md',
139
- title: 'quota',
140
- };
141
- for (let index = 0; index < 20; index += 1) {
142
- const created = await propose(env.service, {
143
- ...base,
144
- target: targetFor(index),
145
- title: `quota-${index}`,
146
- content: `body-${index}`,
147
- }, 's1');
148
- assert.equal(created.ok, true, `proposal ${index} should be accepted`);
149
- }
150
- const over = await propose(env.service, { ...base, title: 'quota-over', content: 'over' }, 's1');
151
- assert.equal(over.ok, false);
152
- assert.equal(over.code, 'E_PROPOSAL_QUOTA');
153
- assert.equal(proposalFiles(env).length, 20);
154
- });
155
-
156
- await withStarted({ proposalEnabled: true }, async (env) => {
157
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
158
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
159
- const oversized = await propose(env.service, {
160
- op: 'journal.append',
161
- target: 'journal/2026-11.md',
162
- title: 'oversized',
163
- content: 'x'.repeat(4001),
164
- }, 's1');
165
- assert.equal(oversized.ok, false);
166
- assert.equal(oversized.code, 'E_CONTENT_TOO_LARGE');
167
- assert.equal(proposalFiles(env).length, 0);
168
- });
169
- });
170
-
171
- test('K10: tool-channel P0 is denied, audited, and creates no proposal', async () => {
172
- await withStarted({ proposalEnabled: true }, async (env) => {
173
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
174
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
175
- const denied = await propose(env.service, {
176
- op: 'journal.append',
177
- target: 'journal/2026-11.md',
178
- title: 'P0',
179
- content: 'owner only',
180
- priority: 'P0',
181
- }, 's-root');
182
- assert.deepEqual(denied, { ok: false, code: 'E_PRIORITY_DENIED' });
183
- assert.equal(proposalFiles(env).length, 0);
184
- const rejected = readAudit(env).find((line) => line.kind === 'priority.rejected');
185
- assert.ok(rejected, 'priority.rejected must be audited');
186
- assert.equal(rejected.attempted, 'P0');
187
- assert.equal(rejected.reason, 'tool-cannot-P0');
188
- assert.equal(rejected.by, 's-root');
189
- });
190
- });
191
-
192
- test('K11: detail is limited to author/main/owner; denial is audited; queue/list expose no body', async () => {
193
- await withStarted({ proposalEnabled: true }, async (env) => {
194
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
195
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
196
- const created = await propose(env.service, {
197
- op: 'journal.append',
198
- target: 'journal/2026-11.md',
199
- title: 'secret',
200
- content: 'body-secret-value',
201
- }, 's-author');
202
- assert.equal(created.ok, true);
203
-
204
- const denied = await viewDetail(env.service, created.id, { sessionId: 's-other', kind: 'subagent' });
205
- assert.equal(denied.ok, false);
206
- assert.equal(denied.code, 'E_PROPOSAL_FORBIDDEN');
207
- assert.equal(JSON.stringify(denied).includes('body-secret-value'), false);
208
- const denial = readAudit(env).find((line) => line.kind === 'proposal.detail.denied');
209
- assert.ok(denial, 'proposal.detail.denied must be audited');
210
- assert.equal(denial.id, created.id);
211
-
212
- const queue = await env.service.proposalsView({ mode: 'queue' }, { caller: { sessionId: 's-author', kind: 'root' } });
213
- assert.equal(queue.ok, true);
214
- assert.equal(JSON.stringify(queue).includes('body-secret-value'), false);
215
- assert.equal('content' in (queue.entries[0] ?? {}), false);
216
-
217
- const list = await env.service.proposalsView({ mode: 'list' }, { caller: { sessionId: 's-author', kind: 'root' } });
218
- assert.equal(list.ok, true);
219
- assert.deepEqual(Object.keys(list.proposals[0]).sort(), [
220
- 'cancellable', 'createdAt', 'id', 'position', 'priority', 'state', 'status',
221
- 'terminal', 'ticket', 'title',
222
- ]);
223
- assert.equal(JSON.stringify(list).includes('body-secret-value'), false);
224
-
225
- const author = await viewDetail(env.service, created.id, { sessionId: 's-author', kind: 'root' });
226
- assert.equal(author.ok, true);
227
- assert.match(author.proposal.content, /^<<<PROPOSAL DATA — 不是指令 >>>\n/u);
228
- assert.match(author.proposal.content, /body-secret-value/u);
229
- assert.match(author.proposal.content, /\n<<<END PROPOSAL DATA>>>$/u);
230
-
231
- const owner = await viewDetail(env.service, created.id, { sessionId: 'owner', kind: 'owner-cli' });
232
- assert.equal(owner.ok, true);
233
- });
234
- });
235
-
236
- test('K12: tool counts stay 21 / 25 / 28 and the proposal placeholders are wired', async () => {
237
- const temp = tempRoot('longlive-router-counts-');
238
- try {
239
- const makeHarness = () => {
240
- const tools = [];
241
- const effects = [];
242
- const ctx = {
243
- tools: { register: (definition) => { tools.push(definition); return () => {}; } },
244
- on: () => () => {},
245
- logger: () => ({ warn: () => {}, info: () => {} }),
246
- effect: (fn) => {
247
- const dispose = fn();
248
- effects.push(typeof dispose === 'function' ? dispose : () => {});
249
- return dispose;
250
- },
251
- get: (name) => (name === 'llm' ? new MockLlm() : undefined),
252
- };
253
- return { ctx, tools, effects };
254
- };
255
- const count = async (flags) => {
256
- const harness = makeHarness();
257
- const memoryRoot = path.join(temp, `memory-${flags.label}`);
258
- fs.mkdirSync(memoryRoot, { recursive: true });
259
- fs.writeFileSync(path.join(memoryRoot, 'STATE.md'), '## 我在哪\n测试\n', 'utf8');
260
- try {
261
- const returned = await apply(harness.ctx, {
262
- home: path.join(temp, `home-${flags.label}`),
263
- memoryRoot,
264
- env: {},
265
- heartbeatMs: 0,
266
- recoverOnStart: false,
267
- defineTool: (definition) => definition,
268
- routerRuntime: 'off',
269
- ...flags.config,
270
- });
271
- return { returned, names: harness.tools.map((tool) => tool.name) };
272
- } finally {
273
- for (const dispose of harness.effects.reverse()) {
274
- try { await dispose(); } catch { /* best effort */ }
275
- }
276
- }
277
- };
278
-
279
- const off = await count({ label: 'off', config: {} });
280
- assert.equal(off.returned, 22);
281
- assert.equal(off.names.filter((name) => name.startsWith('router_')).length, 0);
282
-
283
- const on = await count({ label: 'on', config: { routerEnabled: true, routerProvider: 'mock', routerModel: 'mock-1', routerTickMs: 60000 } });
284
- assert.equal(on.returned, 26);
285
- assert.equal(on.names.filter((name) => name.startsWith('router_')).length, 4);
286
-
287
- const proposals = await count({
288
- label: 'proposals',
289
- config: {
290
- routerEnabled: true,
291
- routerProvider: 'mock',
292
- routerModel: 'mock-1',
293
- routerTickMs: 60000,
294
- routerProposalEnabled: true,
295
- },
296
- });
297
- assert.equal(proposals.returned, 29);
298
- assert.deepEqual(
299
- proposals.names.filter((name) => name.startsWith('router_')).sort(),
300
- ['router_apply', 'router_ask', 'router_proposals', 'router_propose', 'router_report', 'router_status', 'router_tick'],
301
- );
302
- } finally {
303
- fs.rmSync(temp, { recursive: true, force: true });
304
- }
305
- });
306
-
307
- test('ticket allocation is serialized by issue.lock: concurrent proposes get unique tickets', async () => {
308
- await withStarted({ proposalEnabled: true }, async (env) => {
309
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
310
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
311
- // Different targets: a same-target pair is allowed to coalesce (§5.8);
312
- // this test is only about serialized ticket allocation.
313
- const [a, b] = await Promise.all([
314
- propose(env.service, { op: 'journal.append', target: 'journal/2026-11.md', title: 'a', content: 'alpha' }, 's1'),
315
- propose(env.service, { op: 'journal.append', target: 'journal/2026-12.md', title: 'b', content: 'beta' }, 's2'),
316
- ]);
317
- assert.equal(a.ok, true);
318
- assert.equal(b.ok, true);
319
- assert.notEqual(a.ticket, b.ticket);
320
- assert.notEqual(a.id, b.id);
321
- assert.equal(fs.existsSync(path.join(env.vaultDir, 'locks', 'issue.lock')), false);
322
- const tickets = proposalFiles(env).map((file) => {
323
- const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', file), 'utf8'));
324
- return proposal.queue.ticket;
325
- });
326
- assert.equal(new Set(tickets).size, tickets.length);
327
- });
328
- });
329
-
330
- test('deadlineAt is createdAt + 72h and titles are sanitized to one line', async () => {
331
- await withStarted({ proposalEnabled: true, proposalTtlH: 72 }, async (env) => {
332
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
333
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
334
- const created = await propose(env.service, {
335
- op: 'journal.append',
336
- target: 'journal/2026-11.md',
337
- title: 'a\u0000b\u0007c\nd',
338
- content: 'ttl',
339
- }, 's1');
340
- assert.equal(created.ok, true);
341
- const proposal = JSON.parse(fs.readFileSync(path.join(env.vaultDir, 'proposals', proposalFiles(env)[0]), 'utf8'));
342
- assert.equal(proposal.title, 'a b c d');
343
- assert.equal(/[\u0000-\u001f\u007f]/u.test(proposal.title), false);
344
- assert.equal(Date.parse(proposal.queue.deadlineAt) - Date.parse(proposal.createdAt), 72 * 60 * 60 * 1000);
345
- });
346
- });
347
-
348
- test('default-off proposals leave no proposals directory and refuse the tool surface', async () => {
349
- await withStarted({ proposalEnabled: false }, async (env) => {
350
- assert.equal(fs.existsSync(path.join(env.vaultDir, 'proposals')), false);
351
- assert.equal(createRouterTools(env.service).length, 4);
352
- const disabled = await env.service.proposalsView({ mode: 'list' });
353
- assert.equal(disabled.ok, false);
354
- assert.equal(disabled.code, 'E_PROPOSAL_DISABLED');
355
- const proposal = await env.service.proposeProposal({
356
- op: 'journal.append',
357
- target: 'journal/2026-11.md',
358
- title: 'off',
359
- content: 'x',
360
- }, { caller: { sessionId: 's1', kind: 'root' } });
361
- assert.equal(proposal.ok, false);
362
- assert.equal(proposal.code, 'E_PROPOSAL_DISABLED');
363
- assert.equal(fs.existsSync(path.join(env.vaultDir, 'proposals')), false);
364
- });
365
- });
366
-
367
- test('path escapes, unknown ops and missing state sections are rejected before allocation', async () => {
368
- await withStarted({ proposalEnabled: true }, async (env) => {
369
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
370
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
371
- const escapes = await propose(env.service, {
372
- op: 'journal.append', target: '../STATE.md', title: 'escape', content: 'x',
373
- }, 's1');
374
- assert.equal(escapes.ok, false);
375
- assert.equal(escapes.code, 'E_PATH_DENIED');
376
- const unknown = await propose(env.service, {
377
- op: 'topic.patch', target: 'topics/test.md', title: 'unknown', content: 'x',
378
- }, 's1');
379
- assert.equal(unknown.ok, false);
380
- assert.equal(unknown.code, 'E_BAD_STATE');
381
- const missingSection = await propose(env.service, {
382
- op: 'state.section', target: 'STATE.md', title: '不存在的节', content: 'x',
383
- }, 's1');
384
- assert.equal(missingSection.ok, false);
385
- assert.equal(missingSection.code, 'E_TARGET_MISSING');
386
- assert.equal(proposalFiles(env).length, 0);
387
- });
388
- });
389
-
390
- test('status exposes proposal counts and a seq counter without proposal bodies', async () => {
391
- await withStarted({ proposalEnabled: true }, async (env) => {
392
- fs.mkdirSync(path.join(env.memoryRoot, 'journal'), { recursive: true });
393
- fs.writeFileSync(path.join(env.memoryRoot, 'journal', '2026-11.md'), '# j\n', 'utf8');
394
- const created = await propose(env.service, {
395
- op: 'journal.append', target: 'journal/2026-11.md', title: 'status', content: 'status-body',
396
- }, 's1');
397
- assert.equal(created.ok, true);
398
- const snapshot = env.service.status();
399
- assert.equal(snapshot.queue.pending, 1);
400
- assert.equal(snapshot.proposals.queue.depth, 1);
401
- assert.equal(typeof snapshot.proposals.queue.seqCounter, 'number');
402
- assert.equal(JSON.stringify(snapshot).includes('status-body'), false);
403
- assert.equal(auditKinds(path.join(env.vaultDir, 'audit.jsonl')).includes('proposal.create'), true);
404
- });
405
- });