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,212 @@
1
+ /**
2
+ * A scripted clerk for the fake provider (fake-provider.ts): it plays a round of the clerk method the way D99 and D103 run
3
+ * it — the main agent through its stages in one session, its lanes each in a session of their own, then the synthesis in
4
+ * a session of its own after the main agent's handover, then the independent spot-check — so a test can drive the planner,
5
+ * the runtime and pi end to end without a model. What the main agent, each lane and the synthesis write is the test's
6
+ * (`ClerkScript`); the moves between stages are the script's:
7
+ * First usable orientation (briefs) → skeleton (the test's extras, then the lanes in one call) → reconcile → handover
8
+ * Deepen orientation (briefs) → dig (the lanes in one call) → coverage (check, account for every listed group)
9
+ * → cross-check → handover (again, with why, when a Full deepening refuses it for candidates with no result)
10
+ * Follow up orientation → cross-check (skipping the rest, with why) → handover
11
+ * The handover is `pk_stage({ to: "synthesis", handover })`: the main agent's session ends there. The synthesis job then
12
+ * makes the test's writes for the stage `synthesis` and writes the round's Result.
13
+ * The main agent reads its progress off its own session: a stage entered is a `pk_stage` call that did not fail, a
14
+ * brief is a `pk_write_round_doc` that did not fail. A continued session (a restart, a run again) goes on from there.
15
+ */
16
+ import type { LaneKind, RoundKind } from '../../model/k-types.ts';
17
+ import { callResults, taskText, type Planner, type ToolCall } from '../fake-provider.ts';
18
+ import { slotsUnheld } from '../organize/slots.ts';
19
+
20
+ type Msg = Parameters<Planner>[1][number];
21
+
22
+ /** One lane the main agent sends, with its brief. */
23
+ export interface ScriptedLane {
24
+ readonly name: string;
25
+ readonly kind: LaneKind;
26
+ readonly slots: readonly string[];
27
+ readonly brief: string;
28
+ }
29
+
30
+ /** What the test decides: the lanes of each round, and what the main agent, the lanes and the spot-check write. */
31
+ export interface ClerkScript {
32
+ /** The lanes the main agent sends in a round of this kind (in the skeleton of a first usable round, in a dig). */
33
+ lanes(round: RoundKind): readonly ScriptedLane[];
34
+ /**
35
+ * In a dig, the lanes the main agent sends first (by name); it then tries to leave the dig — the gate of the four kinds
36
+ * of question refuses it when one has no lane (E148 D-g) — and sends the rest. By default it sends them all at once.
37
+ */
38
+ firstBatch?(round: RoundKind): readonly string[];
39
+ /** A lane's writes, in its first turn, before its Report. */
40
+ laneWork?(name: string, task: string): readonly ToolCall[];
41
+ /**
42
+ * The main agent's writes in a stage, once, before it moves on (e.g. `pk_fill_from_table` in the skeleton); for the
43
+ * stage `synthesis`, the synthesis job's writes, once, before it writes the round's Result (D103).
44
+ */
45
+ stageWork?(round: RoundKind, stage: string, task: string, messages: readonly Msg[]): readonly ToolCall[];
46
+ /** The main agent's handover to the synthesis; by default a plain one with its three parts. */
47
+ handover?(round: RoundKind): { readonly settled: string; readonly open: string; readonly first: string };
48
+ /** The spot-check's writes; by default it records one sample item Right, and confirms nothing. */
49
+ spotCheck?(task: string): readonly ToolCall[];
50
+ }
51
+
52
+ /** A refused call, or one a restart or a stop cut short (pi fills its result in with "No result provided"). */
53
+ const ERROR = /^(?:ERROR|No result provided)/;
54
+ /** A tool's calls in this session that did not fail. */
55
+ export const succeeded = (messages: readonly Msg[], tool: string) => callResults(messages as Msg[], tool).filter((c) => !ERROR.test(c.result));
56
+ const idOf = (result: string): string | null => /"id": "([^"]+)"/.exec(result)?.[1] ?? null;
57
+
58
+ export const MAIN_TASK = /^Task: the main agent of this round/m;
59
+ const LANE_TASK = /^Task: a lane of this round — (.+?) \((slot|plan|topic|follow-up)\)/m;
60
+ export const SYNTHESIS_TASK = /^Task: the synthesis of this round/m;
61
+ const SPOT_TASK = /^Task: the independent spot-check/m;
62
+ export const laneNameOf = (task: string): string | null => LANE_TASK.exec(task)?.[1] ?? null;
63
+ export const roundKindOf = (task: string): RoundKind | null => (/This is (?:takeover|Follow up) round \d+ \((First usable|Deepen|Follow up)\)/.exec(task)?.[1] as RoundKind | undefined) ?? null;
64
+ /** Which job a task is: the main agent, a lane (by name), the synthesis, the spot-check, the session drafts, or none of them. */
65
+ export function jobOf(task: string): { readonly kind: 'main' | 'lane' | 'synthesis' | 'spot-check' | 'session-drafts' | null; readonly lane: string | null } {
66
+ if (MAIN_TASK.test(task)) return { kind: 'main', lane: null };
67
+ if (SYNTHESIS_TASK.test(task)) return { kind: 'synthesis', lane: null };
68
+ const lane = laneNameOf(task);
69
+ if (lane) return { kind: 'lane', lane };
70
+ if (SPOT_TASK.test(task)) return { kind: 'spot-check', lane: null };
71
+ if (/^Task: session drafts/m.test(task)) return { kind: 'session-drafts', lane: null };
72
+ return { kind: null, lane: null };
73
+ }
74
+
75
+ const stage = (to: string, why?: string): ToolCall => ({ name: 'pk_stage', args: { to, ...(why ? { why } : {}) } });
76
+ /** A plain handover with its three parts (D103), for a script that gives none of its own. */
77
+ export const HANDOVER = { settled: 'The lanes are joined into one trunk; every placement the records give is written.', open: 'Nothing is left open that a record settles.', first: 'The lane reports, then the work items.' } as const;
78
+ /** The handover call: the main agent's last. */
79
+ const handOver = (script: ClerkScript, round: RoundKind, why?: string): ToolCall => ({ name: 'pk_stage', args: { to: 'synthesis', ...(why ? { why } : {}), handover: script.handover?.(round) ?? HANDOVER } });
80
+ const brief = (l: ScriptedLane): ToolCall => ({ name: 'pk_write_round_doc', args: { kind: 'Brief', path: l.name, title: l.name, markdown: l.brief } });
81
+
82
+ /** The main agent's next calls, from what its session shows it has done. */
83
+ function mainTurn(script: ClerkScript, task: string, messages: readonly Msg[]): ToolCall[] | string {
84
+ const round = roundKindOf(task);
85
+ if (!round) return 'The main agent cannot tell which round this is.';
86
+ const entered = succeeded(messages, 'pk_stage').map((c) => String(c.args.to));
87
+ const at = entered[entered.length - 1] ?? 'orientation';
88
+ const lanes = script.lanes(round);
89
+ const briefs = succeeded(messages, 'pk_write_round_doc').filter((c) => c.args.kind === 'Brief').map((c) => ({ path: String(c.args.path), id: idOf(c.result) }));
90
+ const sent = succeeded(messages, 'pk_send_lanes').length > 0;
91
+ // The test's writes for this stage, once each: a call is made when no call of that tool with the same arguments was.
92
+ const made = (c: ToolCall): boolean => callResults(messages as Msg[], c.name).some((r) => JSON.stringify(r.args) === JSON.stringify(c.args));
93
+ const extras = (s: string): ToolCall[] => [...(script.stageWork?.(round, s, task, messages) ?? [])].filter((c) => !made(c));
94
+ const doneExtras = (s: string): boolean => extras(s).length === 0;
95
+ const sentNames = new Set(succeeded(messages, 'pk_send_lanes').flatMap((c) => ((c.args.lanes as { name: string }[] | undefined) ?? []).map((l) => l.name)));
96
+ // DB: a first usable round's skeleton accounts for every slot of the workbench — held by a lane, or one line on why not.
97
+ const empty = round === 'First usable' ? slotsUnheld(lanes.flatMap((l) => l.slots), []).map((u) => ({ slot: u.slot, why: `The project has nothing for it: no document of it states ${u.holds.split(':')[0]}.` })) : [];
98
+ const send = (which: readonly ScriptedLane[] = lanes): ToolCall => ({ name: 'pk_send_lanes', args: { lanes: which.map((l) => ({ name: l.name, kind: l.kind, briefDocId: briefs.find((b) => b.path === l.name)?.id ?? '', slots: [...l.slots] })), ...(empty.length ? { empty } : {}) } });
99
+ // D103: the main agent's session ends with its handover; the synthesis is a job of its own (`synthesisTurn`).
100
+ const handedOver = (): string => `The ${round} round is handed over to the synthesis.`;
101
+ if (round === 'Follow up') {
102
+ if (at === 'orientation') return doneExtras('orientation') ? [stage('cross-check', 'Nothing in the document chain changed and nothing needs digging')] : extras('orientation');
103
+ if (at === 'cross-check') return doneExtras('cross-check') ? [handOver(script, round)] : extras('cross-check');
104
+ return handedOver();
105
+ }
106
+ if (at === 'orientation') {
107
+ if (!doneExtras('orientation')) return extras('orientation');
108
+ const missing = lanes.filter((l) => !briefs.some((b) => b.path === l.name));
109
+ return [...missing.map(brief), stage(round === 'First usable' ? 'skeleton' : 'dig', lanes.length ? undefined : 'Nothing to dig this round')];
110
+ }
111
+ if (round === 'First usable') {
112
+ if (at === 'skeleton') {
113
+ if (!doneExtras('skeleton')) return extras('skeleton');
114
+ if (lanes.length && !sent) return [send()];
115
+ return [stage('reconcile')];
116
+ }
117
+ if (at === 'reconcile') return doneExtras('reconcile') ? [handOver(script, round)] : extras('reconcile');
118
+ return handedOver();
119
+ }
120
+ // Deepen.
121
+ if (at === 'dig') {
122
+ if (!lanes.length) return [stage('coverage')];
123
+ const first = script.firstBatch?.(round);
124
+ if (!sent) return [send(first ? lanes.filter((l) => first.includes(l.name)) : lanes)];
125
+ const rest = lanes.filter((l) => !sentNames.has(l.name));
126
+ if (!rest.length) return [stage('coverage')];
127
+ // Some lanes are still unsent: try to leave the dig first, and send the rest once the gate refuses it.
128
+ const refused = callResults(messages as Msg[], 'pk_stage').some((c) => c.args.to === 'coverage' && ERROR.test(c.result));
129
+ return refused ? [send(rest)] : [stage('coverage')];
130
+ }
131
+ if (at === 'coverage') {
132
+ // The check once the lanes are back; every group it lists accounted for, in one turn; then on to the cross-check.
133
+ const checks = succeeded(messages, 'pk_coverage_check');
134
+ if (!checks.length) return [{ name: 'pk_coverage_check', args: {} }];
135
+ const accounted = callResults(messages as Msg[], 'pk_account_material');
136
+ const listed = JSON.parse(checks[checks.length - 1]!.result) as { untouched: { group: string; read: 'none' | 'part' }[]; settled: boolean };
137
+ if (!listed.settled && !accounted.length) {
138
+ return listed.untouched.map((g) => ({ name: 'pk_account_material', args: { group: g.group, outcome: g.read === 'part' ? 'part' : 'not needed', why: g.read === 'part' ? 'The part read answers this round’s questions' : 'Nothing this round asks rests on it' } }));
139
+ }
140
+ const tried = callResults(messages as Msg[], 'pk_stage').filter((c) => c.args.to === 'cross-check').length;
141
+ return tried < 3 ? [stage('cross-check')] : 'The coverage stays open: the cross-check was refused.';
142
+ }
143
+ if (at === 'cross-check') return doneExtras('cross-check') ? [toSynthesis(script, round, messages)] : extras('cross-check');
144
+ return handedOver();
145
+ }
146
+
147
+ /**
148
+ * The handover to the synthesis; when a Full deepening refused it for candidates with no result (CD), again with why — the
149
+ * way a main agent leaves candidates it could not settle to the next round.
150
+ */
151
+ function toSynthesis(script: ClerkScript, round: RoundKind, messages: readonly Msg[]): ToolCall {
152
+ const refused = callResults(messages as Msg[], 'pk_stage').some((c) => c.args.to === 'synthesis' && /no result yet/.test(c.result));
153
+ return handOver(script, round, refused ? 'The candidates still without a result are left to the next round: no lane of this round covered them.' : undefined);
154
+ }
155
+
156
+ /**
157
+ * The synthesis (D103), in its own session: the test's writes for the stage `synthesis`, once each, then the round's
158
+ * Result, then its summary. It reads its progress off its own session, so a continued one goes on from there.
159
+ */
160
+ function synthesisTurn(script: ClerkScript, task: string, messages: readonly Msg[]): ToolCall[] | string {
161
+ const round = roundKindOf(task);
162
+ if (!round) return 'The synthesis cannot tell which round this is.';
163
+ const made = (c: ToolCall): boolean => callResults(messages as Msg[], c.name).some((r) => JSON.stringify(r.args) === JSON.stringify(c.args));
164
+ const extras = [...(script.stageWork?.(round, 'synthesis', task, messages) ?? [])].filter((c) => !made(c));
165
+ if (extras.length) return extras;
166
+ if (!succeeded(messages, 'pk_write_round_doc').some((c) => c.args.kind === 'Result')) return [{ name: 'pk_write_round_doc', args: { kind: 'Result', title: 'Result of the round', markdown: `# Result\n\nThe ${round} round is on the workbench.` } }];
167
+ return `The ${round} round is done.`;
168
+ }
169
+
170
+ /** A lane: its writes and its Report in one turn, then its summary. */
171
+ function laneTurn(script: ClerkScript, name: string, task: string, messages: readonly Msg[], afterTool: boolean): ToolCall[] | string {
172
+ if (afterTool || succeeded(messages, 'pk_write_round_doc').some((c) => c.args.kind === 'Report')) return `The ${name} lane wrote its slots and its report.`;
173
+ return [...(script.laneWork?.(name, task) ?? []), { name: 'pk_write_round_doc', args: { kind: 'Report', title: `Report: ${name}`, markdown: `# Report: ${name}\n\n## Present now\n- What the ${name} lane found, cited.\n` } }];
174
+ }
175
+
176
+ /**
177
+ * The spot-check: what the test says, or — the round's final re-check (D103) — every target it is given to check in full
178
+ * (the notes, what the synthesis wrote, the Result) and one sample item, each recorded Right; then its document.
179
+ */
180
+ function spotTurn(script: ClerkScript, task: string, messages: readonly Msg[]): ToolCall[] | string {
181
+ if (succeeded(messages, 'pk_write_round_doc').some((c) => c.args.kind === 'Spot check')) return 'The spot-check is recorded.';
182
+ if (callResults(messages as Msg[], 'pk_record_spot_check').length === 0) {
183
+ const own = script.spotCheck?.(task);
184
+ if (own) return [...own];
185
+ const TARGET = /^- (\w+) ([a-z]+_[0-9a-z_-]{4,}):/gm;
186
+ const sampleAt = task.indexOf('=== The sample');
187
+ const fullAt = task.indexOf('=== Checked in full');
188
+ const full = fullAt < 0 ? [] : [...task.slice(fullAt, sampleAt < 0 ? undefined : sampleAt).matchAll(TARGET)];
189
+ const sample = sampleAt < 0 ? [] : [...task.slice(sampleAt).matchAll(TARGET)].slice(0, 1);
190
+ const targets = [...full, ...sample].map((m) => ({ collection: m[1]!, id: m[2]! }));
191
+ return [{ name: 'pk_record_spot_check', args: { checked: (targets.length ? targets : [{ collection: 'threads', id: 'thread_none' }]).map((target) => ({ target, kind: target.collection === 'notes' ? 'note' : target.collection === 'roundDocs' ? 'the Result’s claims' : 'progress', verdict: 'Right' })) } }];
192
+ }
193
+ return [{ name: 'pk_write_round_doc', args: { kind: 'Spot check', title: 'Spot check', markdown: '# Spot check\n\nChecked what the block lists.' } }];
194
+ }
195
+
196
+ /**
197
+ * The fake provider's planner for a scripted clerk. `route` may take a request first (return a value to answer it), for
198
+ * a test's own jobs.
199
+ */
200
+ export function clerkPlanner(script: ClerkScript, route?: (task: string, messages: readonly Msg[], afterTool: boolean) => ToolCall[] | string | undefined): Planner {
201
+ return (prompt, messages, afterTool = false) => {
202
+ const task = taskText(messages) || prompt;
203
+ const own = route?.(task, messages, afterTool);
204
+ if (own !== undefined) return own;
205
+ const job = jobOf(task);
206
+ if (job.kind === 'main') return mainTurn(script, task, messages);
207
+ if (job.kind === 'lane') return laneTurn(script, job.lane!, task, messages, afterTool);
208
+ if (job.kind === 'synthesis') return synthesisTurn(script, task, messages);
209
+ if (job.kind === 'spot-check') return spotTurn(script, task, messages);
210
+ return afterTool ? '' : 'Nothing to do (scripted clerk).';
211
+ };
212
+ }
@@ -0,0 +1,57 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { ModelRuntime } from '@earendil-works/pi-coding-agent';
3
+ import type { CredentialStore } from '@earendil-works/pi-ai';
4
+ import { FAKE_MODEL, startFakeProvider } from '../fake-provider.ts';
5
+
6
+ const noCredentials: CredentialStore = {
7
+ async read() { return undefined; },
8
+ async list() { return []; },
9
+ async modify(_providerId, fn) { return fn(undefined); },
10
+ async delete() {},
11
+ };
12
+
13
+ /** Keeper's real ModelRuntime/provider path, loopback HTTP only, synthetic in-memory credentials. */
14
+ export async function measureToolStream(inputs: string[], chunkSize = 20) {
15
+ const fake = await startFakeProvider(() => inputs.map((rawArguments) => ({ name: 'synthetic_note', args: {}, rawArguments })), { toolArgumentChunkSize: chunkSize });
16
+ const provider = 'ao-local-test';
17
+ const runtime = await ModelRuntime.create({ credentials: noCredentials, modelsPath: null, refreshOnCreate: false });
18
+ runtime.registerProvider(provider, {
19
+ name: 'AO local test', baseUrl: fake.url, api: 'openai-completions', apiKey: 'local-fake-key',
20
+ models: [{ ...FAKE_MODEL, api: 'openai-completions', baseUrl: fake.url }],
21
+ });
22
+ const model = runtime.getModel(provider, FAKE_MODEL.id);
23
+ if (!model) throw new Error('AO local test model registration failed');
24
+ let maxDelayMs = 0;
25
+ let last = performance.now();
26
+ const timer = setInterval(() => {
27
+ const now = performance.now();
28
+ maxDelayMs = Math.max(maxDelayMs, now - last - 10);
29
+ last = now;
30
+ }, 10);
31
+ const started = performance.now();
32
+ let deltas = 0;
33
+ const wire = inputs.map(() => '');
34
+ const ended: string[] = [];
35
+ try {
36
+ const events = runtime.streamSimple(model, {
37
+ messages: [{ role: 'user', content: 'Emit the synthetic tool calls.', timestamp: 0 }],
38
+ }, { apiKey: 'local-fake-key', maxTokens: 100000, maxRetries: 0 });
39
+ for await (const event of events) {
40
+ if (event.type === 'toolcall_delta') { deltas++; wire[event.contentIndex] += event.delta; }
41
+ if (event.type === 'toolcall_end') ended.push(JSON.stringify(event.toolCall.arguments));
42
+ }
43
+ const message = await events.result();
44
+ const elapsedMs = performance.now() - started;
45
+ // Let the overdue timer fire even when the stream completed in one microtask burst.
46
+ await delay(20);
47
+ if (message.stopReason === 'error' || message.stopReason === 'aborted') throw new Error(message.errorMessage);
48
+ return { elapsedMs, maxDelayMs, deltas, wire, ended, message };
49
+ } finally {
50
+ clearInterval(timer);
51
+ fake.close();
52
+ }
53
+ }
54
+
55
+ export function syntheticArguments(bytes: number): string {
56
+ return JSON.stringify({ note: 'x'.repeat(bytes - 11) });
57
+ }