@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -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
- });