projectkeeper 0.0.0-stage → 1.0.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 (276) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +21 -0
  3. package/README.md +283 -2
  4. package/README.zh-CN.md +285 -0
  5. package/THIRD_PARTY_NOTICES.md +53 -0
  6. package/bin/pk.mjs +6 -0
  7. package/bin/postinstall.mjs +5 -0
  8. package/bin/strip-types.mjs +28 -0
  9. package/package.json +68 -4
  10. package/patches/README.md +111 -0
  11. package/patches/pi-ai-0.85.0.ts +122 -0
  12. package/patches/pi-ai-0.87.1.ts +122 -0
  13. package/scripts/apply-pi-streaming-patch.ts +86 -0
  14. package/skills/clerk/coverage/SKILL.md +35 -0
  15. package/skills/clerk/cross-check/SKILL.md +118 -0
  16. package/skills/clerk/deepen-dispatch/SKILL.md +69 -0
  17. package/skills/clerk/lane-follow-up/SKILL.md +94 -0
  18. package/skills/clerk/lane-plan/SKILL.md +107 -0
  19. package/skills/clerk/lane-slot/SKILL.md +113 -0
  20. package/skills/clerk/lane-topic/SKILL.md +116 -0
  21. package/skills/clerk/orientation/SKILL.md +160 -0
  22. package/skills/clerk/reconcile/SKILL.md +77 -0
  23. package/skills/clerk/skeleton-dispatch/SKILL.md +43 -0
  24. package/skills/clerk/spot-check/SKILL.md +52 -0
  25. package/skills/clerk/synthesis/SKILL.md +65 -0
  26. package/skills/table-follow-up/SKILL.md +42 -0
  27. package/src/cli.ts +319 -0
  28. package/src/codemap/README.md +13 -0
  29. package/src/codemap/engines.ts +102 -0
  30. package/src/codemap/facts.ts +504 -0
  31. package/src/context/agent-entry.ts +232 -0
  32. package/src/context/ask.ts +97 -0
  33. package/src/context/assemble.ts +1058 -0
  34. package/src/context/code-source.ts +103 -0
  35. package/src/context/k-briefs.ts +114 -0
  36. package/src/context/lineage.ts +100 -0
  37. package/src/context/object-brief.ts +300 -0
  38. package/src/context/owner-words.ts +191 -0
  39. package/src/context/pack-fixture.ts +342 -0
  40. package/src/context/project-rules.ts +160 -0
  41. package/src/intake/intake.ts +392 -0
  42. package/src/intake/material-rules.ts +166 -0
  43. package/src/intake/removal.ts +316 -0
  44. package/src/intake/skipped.ts +72 -0
  45. package/src/keeper/adjustment.ts +565 -0
  46. package/src/keeper/bounds/boundary.ts +509 -0
  47. package/src/keeper/bounds/command.ts +1665 -0
  48. package/src/keeper/bounds/paths.ts +164 -0
  49. package/src/keeper/bounds/reads.ts +718 -0
  50. package/src/keeper/bounds/shell-write-guard.ts +521 -0
  51. package/src/keeper/capabilities.ts +136 -0
  52. package/src/keeper/clerk-steps.ts +151 -0
  53. package/src/keeper/clerk-tools.ts +1424 -0
  54. package/src/keeper/conversation.ts +253 -0
  55. package/src/keeper/evidence.ts +662 -0
  56. package/src/keeper/fake-provider.ts +132 -0
  57. package/src/keeper/held.ts +15 -0
  58. package/src/keeper/history-tools.ts +163 -0
  59. package/src/keeper/jobs/answering.ts +53 -0
  60. package/src/keeper/key-check.ts +105 -0
  61. package/src/keeper/ledger-tools.ts +295 -0
  62. package/src/keeper/link-check.ts +185 -0
  63. package/src/keeper/merge.ts +260 -0
  64. package/src/keeper/note-origin.ts +56 -0
  65. package/src/keeper/numbers-check.ts +102 -0
  66. package/src/keeper/open-in-pi.ts +57 -0
  67. package/src/keeper/organize/absence.ts +266 -0
  68. package/src/keeper/organize/carried-on.ts +214 -0
  69. package/src/keeper/organize/clerk-coverage.ts +293 -0
  70. package/src/keeper/organize/clerk-prompts.ts +132 -0
  71. package/src/keeper/organize/clerk.ts +1395 -0
  72. package/src/keeper/organize/coverage-gate.ts +19 -0
  73. package/src/keeper/organize/coverage-tools.ts +765 -0
  74. package/src/keeper/organize/entries.ts +153 -0
  75. package/src/keeper/organize/entry-gate.ts +289 -0
  76. package/src/keeper/organize/fill-tools.ts +718 -0
  77. package/src/keeper/organize/follow-up.ts +280 -0
  78. package/src/keeper/organize/generation-check.ts +504 -0
  79. package/src/keeper/organize/graph.ts +230 -0
  80. package/src/keeper/organize/lane-tools.ts +289 -0
  81. package/src/keeper/organize/looked-tools.ts +54 -0
  82. package/src/keeper/organize/materials.ts +243 -0
  83. package/src/keeper/organize/owner-lines.ts +493 -0
  84. package/src/keeper/organize/place-tools.ts +216 -0
  85. package/src/keeper/organize/placement-inference.ts +1045 -0
  86. package/src/keeper/organize/placement-time.ts +320 -0
  87. package/src/keeper/organize/placing.ts +821 -0
  88. package/src/keeper/organize/positioning.ts +33 -0
  89. package/src/keeper/organize/readiness.ts +47 -0
  90. package/src/keeper/organize/reading.ts +401 -0
  91. package/src/keeper/organize/round-blocks.ts +65 -0
  92. package/src/keeper/organize/round-news.ts +164 -0
  93. package/src/keeper/organize/round-open.ts +586 -0
  94. package/src/keeper/organize/schedule-state.ts +39 -0
  95. package/src/keeper/organize/schedule.ts +137 -0
  96. package/src/keeper/organize/service.ts +409 -0
  97. package/src/keeper/organize/skills.ts +116 -0
  98. package/src/keeper/organize/slots.ts +85 -0
  99. package/src/keeper/organize/stage-gate.ts +175 -0
  100. package/src/keeper/organize/stage-tools.ts +471 -0
  101. package/src/keeper/organize/standing-notes.ts +141 -0
  102. package/src/keeper/organize/table-follow-up.ts +330 -0
  103. package/src/keeper/organize/takeover-clerk.ts +832 -0
  104. package/src/keeper/organize/takeover-state.ts +103 -0
  105. package/src/keeper/organize/takeover.ts +216 -0
  106. package/src/keeper/organize/update-pending.ts +535 -0
  107. package/src/keeper/organize/workbench-placement.ts +92 -0
  108. package/src/keeper/own-keys.ts +114 -0
  109. package/src/keeper/owner-text.ts +165 -0
  110. package/src/keeper/project-folder.ts +334 -0
  111. package/src/keeper/prompts.ts +101 -0
  112. package/src/keeper/references.ts +177 -0
  113. package/src/keeper/relook/compose.ts +120 -0
  114. package/src/keeper/relook/prompts.ts +36 -0
  115. package/src/keeper/roles.ts +259 -0
  116. package/src/keeper/round-tools.ts +227 -0
  117. package/src/keeper/route.ts +148 -0
  118. package/src/keeper/rules.ts +97 -0
  119. package/src/keeper/runtime.ts +1858 -0
  120. package/src/keeper/scope-tools.ts +194 -0
  121. package/src/keeper/sendback-text.ts +83 -0
  122. package/src/keeper/step-timing.ts +207 -0
  123. package/src/keeper/testing/clerk-script.ts +212 -0
  124. package/src/keeper/testing/streaming-arguments.ts +57 -0
  125. package/src/keeper/tools.ts +2340 -0
  126. package/src/keeper/utterance-tools.ts +64 -0
  127. package/src/keeper/verbatim.ts +397 -0
  128. package/src/ledger/adapters.ts +186 -0
  129. package/src/ledger/arrangements.ts +257 -0
  130. package/src/ledger/blame.ts +58 -0
  131. package/src/ledger/code-engine-child.ts +69 -0
  132. package/src/ledger/code-engine.ts +548 -0
  133. package/src/ledger/code.ts +607 -0
  134. package/src/ledger/docs.ts +294 -0
  135. package/src/ledger/git-read.ts +321 -0
  136. package/src/ledger/index.ts +1850 -0
  137. package/src/ledger/item-range.ts +16 -0
  138. package/src/ledger/lines.ts +328 -0
  139. package/src/ledger/numbering.ts +269 -0
  140. package/src/ledger/ranges.ts +74 -0
  141. package/src/ledger/rebuild-worker.ts +15 -0
  142. package/src/ledger/rebuild.ts +387 -0
  143. package/src/ledger/repo-scan.ts +181 -0
  144. package/src/ledger/schema.ts +450 -0
  145. package/src/ledger/sessions.ts +134 -0
  146. package/src/ledger/text-scan.ts +466 -0
  147. package/src/ledger/time.ts +185 -0
  148. package/src/ledger/ts-symbols.ts +180 -0
  149. package/src/ledger/views.ts +377 -0
  150. package/src/mcp/server.ts +127 -0
  151. package/src/mcp/tools.ts +187 -0
  152. package/src/model/ids.ts +23 -0
  153. package/src/model/k-types.ts +912 -0
  154. package/src/model/time.ts +85 -0
  155. package/src/model/types.ts +1382 -0
  156. package/src/model/views-k.ts +707 -0
  157. package/src/model/vocab.ts +269 -0
  158. package/src/process/analysis.ts +79 -0
  159. package/src/process/breakpoint-candidates.ts +383 -0
  160. package/src/process/breakpoints.ts +746 -0
  161. package/src/process/claim-check.ts +295 -0
  162. package/src/process/delivery-links.ts +52 -0
  163. package/src/process/facts.ts +487 -0
  164. package/src/process/generations.ts +138 -0
  165. package/src/process/index.ts +243 -0
  166. package/src/process/integration.ts +128 -0
  167. package/src/process/placement.ts +107 -0
  168. package/src/process/plan.ts +276 -0
  169. package/src/process/sendbacks.ts +244 -0
  170. package/src/process/text.ts +185 -0
  171. package/src/process/units.ts +543 -0
  172. package/src/process/unnumbered.ts +431 -0
  173. package/src/process/work.ts +884 -0
  174. package/src/scope/candidates.ts +86 -0
  175. package/src/scope/decisions.ts +229 -0
  176. package/src/scope/discover.ts +741 -0
  177. package/src/scope/ignore.ts +126 -0
  178. package/src/scope/ignored-place.ts +93 -0
  179. package/src/scope/skip.ts +161 -0
  180. package/src/scope/toolchain.ts +180 -0
  181. package/src/scope/worktree.ts +187 -0
  182. package/src/server/api.ts +549 -0
  183. package/src/server/app.ts +595 -0
  184. package/src/server/drafts-api.ts +170 -0
  185. package/src/server/folders-api.ts +243 -0
  186. package/src/server/graph-view.ts +639 -0
  187. package/src/server/http.ts +224 -0
  188. package/src/server/k-agent-api.ts +56 -0
  189. package/src/server/k-rounds-d99.ts +91 -0
  190. package/src/server/k-views.ts +405 -0
  191. package/src/server/keeper-page.ts +446 -0
  192. package/src/server/keys-api.ts +239 -0
  193. package/src/server/project-folder-api.ts +104 -0
  194. package/src/server/vendor.ts +14 -0
  195. package/src/server/workbench-content.ts +561 -0
  196. package/src/sources/anchor.ts +129 -0
  197. package/src/sources/files.ts +267 -0
  198. package/src/sources/gitobs.ts +41 -0
  199. package/src/sources/history.ts +231 -0
  200. package/src/sources/scan-worker.ts +10 -0
  201. package/src/sources/sessions/locate.ts +292 -0
  202. package/src/sources/sessions/read.ts +346 -0
  203. package/src/sources/sessions/scope.ts +42 -0
  204. package/src/sources/sessions/utterances.ts +223 -0
  205. package/src/sources/watch.ts +215 -0
  206. package/src/store/json-file.ts +50 -0
  207. package/src/store/paths.ts +39 -0
  208. package/src/store/project-store.ts +295 -0
  209. package/src/store/versions.ts +172 -0
  210. package/src/store/workspace.ts +216 -0
  211. package/src/table/check.ts +333 -0
  212. package/src/table/follow-up-tools.ts +30 -0
  213. package/src/table/follow-up.ts +344 -0
  214. package/src/table/import.ts +610 -0
  215. package/src/table/project-home.ts +61 -0
  216. package/src/table/sessions.ts +168 -0
  217. package/src/table/takeover.ts +483 -0
  218. package/src/util/git-check.ts +29 -0
  219. package/src/util/git.ts +411 -0
  220. package/src/util/own-ts.ts +15 -0
  221. package/src/util/paths.ts +225 -0
  222. package/template/FILLING-IN.md +148 -0
  223. package/template/examples/papertrail.json +747 -0
  224. package/template/projectkeeper.schema.json +1001 -0
  225. package/ui/app.js +1135 -0
  226. package/ui/failures.js +82 -0
  227. package/ui/flyout.js +120 -0
  228. package/ui/folder-chooser.js +264 -0
  229. package/ui/glossary.js +156 -0
  230. package/ui/graph-fit.js +157 -0
  231. package/ui/graph-material.js +61 -0
  232. package/ui/graph-palette.js +109 -0
  233. package/ui/graph-tools.js +62 -0
  234. package/ui/graph.js +1712 -0
  235. package/ui/index.html +31 -0
  236. package/ui/k/code-view.js +230 -0
  237. package/ui/k/draft-view.js +93 -0
  238. package/ui/k/k-views.css +224 -0
  239. package/ui/k/round-tree.js +450 -0
  240. package/ui/k/scope-k.js +159 -0
  241. package/ui/k-data.js +118 -0
  242. package/ui/k-fold.js +106 -0
  243. package/ui/k-process.css +302 -0
  244. package/ui/k-process.js +934 -0
  245. package/ui/keeper-activity.js +175 -0
  246. package/ui/keeper-view.js +801 -0
  247. package/ui/keeper.css +101 -0
  248. package/ui/key-order.js +62 -0
  249. package/ui/list-fold.js +67 -0
  250. package/ui/markdown.d.ts +21 -0
  251. package/ui/markdown.js +472 -0
  252. package/ui/placement.d.ts +92 -0
  253. package/ui/placement.js +445 -0
  254. package/ui/popover-place.d.ts +16 -0
  255. package/ui/popover-place.js +89 -0
  256. package/ui/popover.js +198 -0
  257. package/ui/project-folder.js +37 -0
  258. package/ui/shell-state.js +40 -0
  259. package/ui/strip-rows.js +171 -0
  260. package/ui/styles.css +492 -0
  261. package/ui/theme-pick.js +36 -0
  262. package/ui/theme.js +74 -0
  263. package/ui/themes/LICENSE.md +13 -0
  264. package/ui/themes/README.md +44 -0
  265. package/ui/themes/TOKENS.md +291 -0
  266. package/ui/themes/a1/board.css +1557 -0
  267. package/ui/themes/a1/pk.css +392 -0
  268. package/ui/themes/b1/board.css +1163 -0
  269. package/ui/themes/b1/pk.css +218 -0
  270. package/ui/themes/d2/board.css +518 -0
  271. package/ui/themes/d2/pk.css +243 -0
  272. package/ui/themes/guard.css +73 -0
  273. package/ui/themes/i1/board.css +776 -0
  274. package/ui/themes/i1/pk.css +223 -0
  275. package/ui/themes/manifest.json +13 -0
  276. package/ui/views.js +2796 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Minimal HTTP layer: JSON routes, server-sent events, static files. Bound to 127.0.0.1, and answering only requests
3
+ * made to that local address by the workbench's own pages or by a program on this machine (`refusal` below).
4
+ */
5
+ import { createReadStream, existsSync, statSync } from 'node:fs';
6
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
7
+ import { extname, join, normalize, resolve } from 'node:path';
8
+
9
+ export interface RouteContext {
10
+ readonly req: IncomingMessage;
11
+ readonly res: ServerResponse;
12
+ readonly params: Record<string, string>;
13
+ readonly query: URLSearchParams;
14
+ readonly body: unknown;
15
+ }
16
+ export type Handler = (ctx: RouteContext) => unknown | Promise<unknown>;
17
+
18
+ interface Route { method: string; pattern: RegExp; keys: string[]; handler: Handler }
19
+
20
+ export class HttpError extends Error {
21
+ readonly status: number;
22
+ constructor(status: number, message: string) { super(message); this.status = status; }
23
+ }
24
+
25
+ const MIME: Record<string, string> = {
26
+ '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
27
+ '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml',
28
+ '.png': 'image/png', '.ico': 'image/x-icon', '.md': 'text/markdown; charset=utf-8', '.txt': 'text/plain; charset=utf-8',
29
+ '.woff2': 'font/woff2', '.map': 'application/json',
30
+ };
31
+
32
+ // ───────────────────────── who is answered ─────────────────────────
33
+ //
34
+ // Listening on 127.0.0.1 keeps other machines out. It does not keep out a web page open in this machine's browser: the
35
+ // browser will send a request to a local port for any page that asks. Its same-origin rule hides the answer from that
36
+ // page, but the request has been carried out by then, and a page whose own name is made to resolve to 127.0.0.1 ("DNS
37
+ // rebinding") counts as the same origin and reads the answers too. So every request, to a route or to a file, is
38
+ // checked before anything is done with it. There is no login: a program running on this machine as the user is
39
+ // answered, as docs/privacy.md says. There is no setting that widens this.
40
+
41
+ /** The names a browser on this machine can reach the workbench by. A name of the user's own (a hosts-file alias, a
42
+ * proxy in front) is not among them: nothing tells such a name from one an outside page made up. */
43
+ const LOCAL_NAMES = ['127.0.0.1', 'localhost', '[::1]'];
44
+
45
+ /** `Host` as the workbench's own address is written for the port a request arrived on (a browser leaves `:80` out). */
46
+ function localHosts(port: number | undefined): string[] {
47
+ return LOCAL_NAMES.flatMap((name) => (port === 80 ? [name, `${name}:80`] : [`${name}:${port}`]));
48
+ }
49
+
50
+ const one = (value: string | string[] | undefined): string | undefined => (Array.isArray(value) ? value[0] : value);
51
+
52
+ /**
53
+ * Why a request is not answered, or null when it is. In order:
54
+ *
55
+ * - `Host` has to be the workbench's own local address with the port the request arrived on. A page reaching this
56
+ * port under a name of its own sends that name. HTTP/1.0 may send no `Host` at all: refused too.
57
+ * - `Origin`, when a request carries one, has to be that same address: the page that made the request was served
58
+ * from here. Browsers send it with every request another origin's script makes and with every POST; a program that
59
+ * is not a browser (`pk`, a test, curl) sends none.
60
+ * - `Sec-Fetch-Site`, which browsers add themselves and a page cannot set, has to be `same-origin` (a page of the
61
+ * workbench) or `none` (the address typed, a bookmark, a link opened from another program). `same-site` is
62
+ * another origin as well — another port of localhost — and is refused like `cross-site`, which covers what carries
63
+ * no `Origin`: an image, a script or a frame another page points here. One thing from elsewhere is answered: a
64
+ * link followed to a page of the workbench, in a tab of its own; that gives the other page nothing to read and
65
+ * starts nothing. The API is never answered that way, and neither is a frame.
66
+ * - A request with a body says it is `application/json`. A page can post to another origin without the browser
67
+ * asking first only as a form or as plain text, so this refuses such a post even where the headers above are absent.
68
+ */
69
+ export function refusal(req: IncomingMessage, pathname: string | null): { status: 403 | 415; message: string } | null {
70
+ const port = req.socket.localPort;
71
+ const notHere = { status: 403 as const, message: `The ProjectKeeper workbench answers only at its own local address, http://127.0.0.1:${port}/, and only to its own pages.` };
72
+ const host = one(req.headers.host)?.toLowerCase();
73
+ if (host === undefined || !localHosts(port).includes(host)) return notHere;
74
+ const origin = one(req.headers.origin);
75
+ if (origin !== undefined && origin.toLowerCase() !== `http://${host}`) return notHere;
76
+ const site = one(req.headers['sec-fetch-site'])?.toLowerCase();
77
+ if (site !== undefined && site !== 'same-origin' && site !== 'none') {
78
+ const method = (req.method ?? 'GET').toUpperCase();
79
+ const followedLink = (method === 'GET' || method === 'HEAD') && one(req.headers['sec-fetch-mode']) === 'navigate' && one(req.headers['sec-fetch-dest']) === 'document';
80
+ if (!followedLink || pathname === null || pathname === '/api' || pathname.startsWith('/api/')) return notHere;
81
+ }
82
+ const hasBody = Number(req.headers['content-length'] ?? 0) > 0 || req.headers['transfer-encoding'] !== undefined;
83
+ if (hasBody && (one(req.headers['content-type']) ?? '').split(';')[0]!.trim().toLowerCase() !== 'application/json') {
84
+ return { status: 415, message: 'A request with a body must be sent as application/json.' };
85
+ }
86
+ return null;
87
+ }
88
+
89
+ export class SseHub {
90
+ private readonly clients = new Set<ServerResponse>();
91
+
92
+ attach(res: ServerResponse): void {
93
+ res.writeHead(200, {
94
+ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive',
95
+ 'X-Accel-Buffering': 'no',
96
+ });
97
+ res.write(': connected\n\n');
98
+ this.clients.add(res);
99
+ const ping = setInterval(() => { if (!res.writableEnded) res.write(': ping\n\n'); }, 25_000);
100
+ ping.unref?.();
101
+ res.on('close', () => { clearInterval(ping); this.clients.delete(res); });
102
+ }
103
+
104
+ broadcast(event: string, data: unknown): void {
105
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
106
+ for (const client of this.clients) {
107
+ if (!client.writableEnded) client.write(payload);
108
+ }
109
+ }
110
+
111
+ get size(): number { return this.clients.size; }
112
+ }
113
+
114
+ export class HttpApp {
115
+ private readonly routes: Route[] = [];
116
+ private readonly statics: { prefix: string; dir: string }[] = [];
117
+ readonly sse = new SseHub();
118
+ private server: Server | null = null;
119
+
120
+ route(method: string, path: string, handler: Handler): void {
121
+ const keys: string[] = [];
122
+ const pattern = new RegExp('^' + path.replace(/\/:([A-Za-z_]+)/g, (_m, key: string) => { keys.push(key); return '/([^/]+)'; }) + '/?$');
123
+ this.routes.push({ method, pattern, keys, handler });
124
+ }
125
+
126
+ static(prefix: string, dir: string): void {
127
+ this.statics.push({ prefix, dir: resolve(dir) });
128
+ }
129
+
130
+ private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
131
+ // A request line that is no path (`GET //`) is not a URL; it is answered as that, after the check of who asks, and
132
+ // never thrown from here, where nothing would catch it and the process would end.
133
+ let url: URL | null = null;
134
+ try { url = new URL(req.url ?? '/', 'http://127.0.0.1'); } catch { /* said below */ }
135
+ const method = (req.method ?? 'GET').toUpperCase();
136
+ const refused = refusal(req, url?.pathname ?? null) ?? (url ? null : { status: 400, message: 'The request names no path.' });
137
+ if (refused || !url) {
138
+ // Said in plain text: the one who reads it is a person who opened the workbench under another name. Nothing of
139
+ // the request is repeated in it, and the connection is not kept for a body that was not read.
140
+ res.writeHead(refused?.status ?? 400, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store', Connection: 'close' });
141
+ res.end(`${refused?.message ?? 'The request names no path.'}\n`);
142
+ return;
143
+ }
144
+ try {
145
+ for (const route of this.routes) {
146
+ if (route.method !== method) continue;
147
+ const match = route.pattern.exec(url.pathname);
148
+ if (!match) continue;
149
+ const params: Record<string, string> = {};
150
+ route.keys.forEach((key, i) => { params[key] = decodeURIComponent(match[i + 1] ?? ''); });
151
+ const body = method === 'GET' || method === 'HEAD' ? undefined : await readBody(req);
152
+ const result = await route.handler({ req, res, params, query: url.searchParams, body });
153
+ if (res.writableEnded || res.headersSent) return;
154
+ if (result === undefined) { res.writeHead(204); res.end(); return; }
155
+ const json = JSON.stringify(result);
156
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
157
+ res.end(json);
158
+ return;
159
+ }
160
+ if (method === 'GET' || method === 'HEAD') {
161
+ for (const s of this.statics) {
162
+ if (!url.pathname.startsWith(s.prefix)) continue;
163
+ const rel = decodeURIComponent(url.pathname.slice(s.prefix.length)) || 'index.html';
164
+ const file = normalize(join(s.dir, rel));
165
+ if (!file.startsWith(s.dir)) { res.writeHead(403); res.end(); return; }
166
+ const target = existsSync(file) && statSync(file).isDirectory() ? join(file, 'index.html') : file;
167
+ if (!existsSync(target) || !statSync(target).isFile()) continue;
168
+ res.writeHead(200, { 'Content-Type': MIME[extname(target).toLowerCase()] ?? 'application/octet-stream', 'Cache-Control': 'no-cache' });
169
+ if (method === 'HEAD') { res.end(); return; }
170
+ createReadStream(target).pipe(res);
171
+ return;
172
+ }
173
+ }
174
+ res.writeHead(404, { 'Content-Type': 'application/json' });
175
+ res.end(JSON.stringify({ error: `Not found: ${method} ${url.pathname}` }));
176
+ } catch (error) {
177
+ const status = error instanceof HttpError ? error.status : 500;
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ if (!res.headersSent) {
180
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
181
+ res.end(JSON.stringify({ error: message }));
182
+ } else if (!res.writableEnded) {
183
+ res.end();
184
+ }
185
+ if (status === 500) console.error(`[http] ${method} ${url.pathname}: ${error instanceof Error ? error.stack : String(error)}`);
186
+ }
187
+ }
188
+
189
+ /** Always on 127.0.0.1: the address is not a parameter, an option or a setting. */
190
+ listen(port: number): Promise<{ port: number; close: () => Promise<void> }> {
191
+ return new Promise((resolvePromise, reject) => {
192
+ const server = createServer((req, res) => { void this.handle(req, res); });
193
+ server.on('error', reject);
194
+ server.listen(port, '127.0.0.1', () => {
195
+ this.server = server;
196
+ const address = server.address();
197
+ const actualPort = typeof address === 'object' && address ? address.port : port;
198
+ resolvePromise({ port: actualPort, close: () => new Promise((r) => server.close(() => r())) });
199
+ });
200
+ });
201
+ }
202
+ }
203
+
204
+ function readBody(req: IncomingMessage): Promise<unknown> {
205
+ return new Promise((resolvePromise, reject) => {
206
+ const chunks: Buffer[] = [];
207
+ req.on('data', (c: Buffer) => chunks.push(c));
208
+ req.on('end', () => {
209
+ const text = Buffer.concat(chunks).toString('utf8');
210
+ if (!text.trim()) { resolvePromise(undefined); return; }
211
+ try { resolvePromise(JSON.parse(text)); } catch { reject(new HttpError(400, 'Body is not valid JSON')); }
212
+ });
213
+ req.on('error', reject);
214
+ });
215
+ }
216
+
217
+ export function requireString(value: unknown, name: string): string {
218
+ if (typeof value !== 'string' || !value.trim()) throw new HttpError(400, `${name} is required`);
219
+ return value.trim();
220
+ }
221
+
222
+ export function optionalString(value: unknown): string | null {
223
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
224
+ }
@@ -0,0 +1,56 @@
1
+ /** Read-only increment K material for `pk get` and `pk options` (Spec §7.10). */
2
+ import type { App } from './app.ts';
3
+ import { HttpError, type HttpApp } from './http.ts';
4
+ import { processView } from './k-views.ts';
5
+ import { breakpointBrief, patchBrief, sendBackBrief, territoryBrief } from '../context/k-briefs.ts';
6
+ import { redactCredentials } from '../sources/anchor.ts';
7
+ import type { ProjectStore } from '../store/project-store.ts';
8
+
9
+ function objectName(store: ProjectStore, id: string): string {
10
+ return store.threads.get(id)?.title ?? store.reference.get(id)?.name ?? store.nodes.get(id)?.label
11
+ ?? store.territories.get(id)?.name ?? store.patches.get(id)?.title ?? id;
12
+ }
13
+
14
+ const safe = (text: string) => redactCredentials(text).text;
15
+
16
+ export function registerKAgentRoutes(http: HttpApp, app: App): void {
17
+ http.route('GET', '/api/projects/:id/k-briefs/:oid', ({ params }) => {
18
+ const project = app.project(params.id!);
19
+ const store = app.store(project.id);
20
+ const id = params.oid!;
21
+ const b = store.breakpoints.get(id);
22
+ if (b) {
23
+ const view = processView(store, project, app.kEngines).breakpoints.find((item) => item.id === id)!;
24
+ return { text: breakpointBrief(view, objectName(store, view.targetId)) };
25
+ }
26
+ const sb = store.sendbacks.get(id);
27
+ if (sb) {
28
+ const view = processView(store, project, app.kEngines).sendBacks.find((item) => item.id === id)!;
29
+ return { text: sendBackBrief(view, objectName(store, view.targetId)) };
30
+ }
31
+ const patch = store.patches.get(id);
32
+ if (patch) return { text: patchBrief(patch, (oid) => objectName(store, oid)) };
33
+ const territory = store.territories.get(id);
34
+ if (territory) {
35
+ const code = app.kEngines.ledger?.code(store, project) ?? null;
36
+ const view = code?.territories.find((item) => item.id === id);
37
+ const ledger = code && view ? { view, version: code.version } : null;
38
+ // CodeTerritory has no Occurred field. Use its first trace when kept, or the first observation retained in the
39
+ // saved record, and never claim `updatedAt` is a commit or a date in the code's history.
40
+ const firstSeen = (store.traceFor('territories', id, Number.MAX_SAFE_INTEGER)[0]?.at ?? territory.updatedAt).slice(0, 10) || 'unknown';
41
+ return { text: territoryBrief(territory, (oid) => objectName(store, oid), ledger, firstSeen) };
42
+ }
43
+ throw new HttpError(404, `No breakpoint, send-back, semantic patch or code territory has the id ${id}`);
44
+ });
45
+
46
+ http.route('GET', '/api/projects/:id/k-options', ({ params }) => {
47
+ const project = app.project(params.id!);
48
+ const store = app.store(project.id);
49
+ const view = processView(store, project, app.kEngines);
50
+ return {
51
+ breakpoints: view.breakpoints.filter((b) => b.lit).map((b) => ({ id: b.id, kind: b.kind, targetId: b.targetId, targetName: safe(objectName(store, b.targetId)) })),
52
+ sendBacks: view.sendBacks.filter((s) => s.lit).map((s) => ({ id: s.id, stage: s.stage, to: s.to, what: safe(s.what), targetId: s.targetId, targetName: safe(objectName(store, s.targetId)) })),
53
+ sixThings: view.sixThings.map((s) => ({ thing: s.thing, objects: s.objectIds.map((id) => ({ id, name: safe(objectName(store, id)) })) })),
54
+ };
55
+ });
56
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A round run by the main agent (D99; Spec §3.3, §3.10, §6.9) as the Keeper view's round tree shows it: the main agent with
3
+ * the stage it is in and each stage's time, the lanes it sent — what each answers, the slots it writes, its status, what it
4
+ * read, its brief and its report — the coverage check with each account, and the missing steps the lanes looked for and
5
+ * the spot-check confirmed. Pure reads of the round record, its jobs, its documents and the breakpoints.
6
+ */
7
+ import type { ProjectStore } from '../store/project-store.ts';
8
+ import type { KeeperJob, Usage } from '../model/types.ts';
9
+ import type { ClerkRound, RoundDoc } from '../model/k-types.ts';
10
+ import type { RoundCoverageView, RoundDocRef, RoundLaneView, RoundMainView, RoundView } from '../model/views-k.ts';
11
+
12
+ /** A round the main agent runs: it has a stage (null before orientation) or a stage log; a round from before D99 has neither. */
13
+ export const isMainAgentRound = (r: ClerkRound): boolean => r.stage !== undefined || r.stageLog !== undefined;
14
+
15
+ const docRef = (d: RoundDoc): RoundDocRef => ({ id: d.id, kind: d.kind, title: d.title, path: d.path, at: d.at });
16
+
17
+ /** The question a lane answers, as its brief opens: the first lines that are not headings. */
18
+ export function questionOf(markdown: string, lines = 3): string | null {
19
+ const text = markdown.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/^#{1,6}\s/.test(l) && !/^[-*_]{3,}$/.test(l)).slice(0, lines);
20
+ return text.length ? text.join('\n') : null;
21
+ }
22
+
23
+ /**
24
+ * What jobs read, each material once, from their steps' recorded reads (`JobStep.reads`): a file as it stands, a version
25
+ * of a file at a commit, a commit, a session. A search reads nothing; a step recorded before reads were kept counts none.
26
+ */
27
+ export function readsOf(jobs: Iterable<KeeperJob>): RoundLaneView['read'] {
28
+ const files = new Set<string>(), versions = new Set<string>(), commits = new Set<string>(), sessions = new Set<string>();
29
+ for (const j of jobs) for (const s of j.steps) for (const r of s.reads ?? []) {
30
+ if (r.session) sessions.add(r.session);
31
+ else if (r.path && r.rev) versions.add(`${r.path.toLowerCase()}@${r.rev.toLowerCase().slice(0, 12)}`);
32
+ else if (r.path) files.add(r.path.toLowerCase());
33
+ else if (r.rev) commits.add(r.rev.toLowerCase().slice(0, 12));
34
+ }
35
+ return { files: files.size, versions: versions.size, commits: commits.size, sessions: sessions.size };
36
+ }
37
+
38
+ /** A job and every job under it (the investigations a lane sent). */
39
+ export function withSent(store: ProjectStore, job: KeeperJob): KeeperJob[] {
40
+ const kids = store.jobs.filter((c) => c.parentJobId === job.id);
41
+ return [job, ...kids.flatMap((k) => withSent(store, k))];
42
+ }
43
+
44
+ /** The parts of `RoundView` a main-agent round adds; `usageOf` sums a set of jobs as the round's usage does. */
45
+ export function mainAgentParts(store: ProjectStore, r: ClerkRound, jobs: readonly KeeperJob[], usageOf: (jobs: readonly KeeperJob[]) => Usage): Pick<RoundView, 'main' | 'lanes' | 'coverage' | 'missing'> {
46
+ const docs = store.roundDocs.filter((d) => d.roundId === r.id);
47
+ const mainJob = jobs.filter((j) => j.step!.kind === 'main').sort((a, b) => a.queuedAt.localeCompare(b.queuedAt)).pop() ?? null;
48
+ const log = r.stageLog ?? [];
49
+ // D103: once handed over, the main agent is in no stage any more; its Handover document opens from the tree.
50
+ const handoverDoc = r.handover ? store.roundDocs.get(r.handover.docId) : undefined;
51
+ const main: RoundMainView = {
52
+ jobId: mainJob?.id ?? null, stage: r.stage ?? null,
53
+ stages: log.map((e, i) => ({ stage: e.stage, startedAt: e.startedAt, endedAt: e.endedAt, timing: e.timing, current: i === log.length - 1 && e.endedAt === null && e.stage === r.stage })),
54
+ ...(r.handover ? { handover: { at: r.handover.at, doc: handoverDoc ? docRef(handoverDoc) : null } } : {}),
55
+ };
56
+ // The lanes the round records, in the order sent; a lane job the record does not name yet (sent a moment ago) after them.
57
+ const recorded = r.lanes ?? [];
58
+ const laneJobs = jobs.filter((j) => j.step!.kind === 'lane');
59
+ const extra = laneJobs.filter((j) => !recorded.some((l) => l.jobId === j.id || l.name === j.step!.path));
60
+ const lanes: RoundLaneView[] = [
61
+ ...recorded.map((l) => ({ lane: l, job: laneJobs.find((j) => j.id === l.jobId) ?? store.jobs.get(l.jobId) ?? null })),
62
+ ...extra.map((j) => ({ lane: null, job: j })),
63
+ ].map(({ lane, job }) => {
64
+ const name = lane?.name ?? job!.step!.path ?? job!.scope.label;
65
+ const brief = (lane ? store.roundDocs.get(lane.briefDocId) : undefined) ?? docs.find((d) => d.kind === 'Brief' && d.path === name && d.jobId !== job?.id);
66
+ const report = (lane?.reportDocId ? store.roundDocs.get(lane.reportDocId) : undefined)
67
+ ?? docs.filter((d) => d.kind === 'Report' && (d.path === name || (job !== null && d.jobId === job.id))).sort((a, b) => a.at.localeCompare(b.at)).pop();
68
+ const sent = job ? withSent(store, job) : [];
69
+ return {
70
+ name, kind: lane?.kind ?? job!.step!.lane?.kind ?? 'topic', slots: lane?.slots ?? job!.step!.lane?.slots ?? [],
71
+ stage: lane?.stage ?? null, sentAt: lane?.sentAt ?? job?.queuedAt ?? null,
72
+ question: brief ? questionOf(brief.markdown) : null,
73
+ jobId: job?.id ?? lane?.jobId ?? null, status: job?.status ?? 'Queued', model: job?.model ?? null,
74
+ startedAt: job?.startedAt ?? null, endedAt: job?.endedAt ?? null, timing: job?.timing ?? null,
75
+ usage: sent.length ? usageOf(sent) : null,
76
+ brief: brief ? docRef(brief) : null, report: report ? docRef(report) : null,
77
+ read: readsOf(sent),
78
+ };
79
+ });
80
+ const c = r.coverage ?? null;
81
+ const coverage: RoundCoverageView | null = c ? { at: c.at, settled: c.settled, untouched: c.untouched ?? null, accounted: c.accounted } : null;
82
+ const bps = store.breakpoints.all();
83
+ const missing = { looked: bps.filter((b) => b.looked?.roundId === r.id).length, checked: bps.filter((b) => b.checked?.roundId === r.id).length };
84
+ return { main, lanes, coverage, missing };
85
+ }
86
+
87
+ /** The main job's own documents, less the briefs of its lanes: those stand under each lane. */
88
+ export function mainDocsOf(docs: readonly RoundDocRef[], lanes: readonly RoundLaneView[]): RoundDocRef[] {
89
+ const briefs = new Set(lanes.flatMap((l) => (l.brief ? [l.brief.id] : [])));
90
+ return docs.filter((d) => !briefs.has(d.id));
91
+ }